algorembrant commited on
Commit
b4143a2
·
verified ·
1 Parent(s): e309bbd

Upload 28 files

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ dist-electron/
4
+ *.log
5
+ .DS_Store
FEATURES.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Computer Auditor — Features & Functionality
2
+
3
+ Computer Auditor is a desktop **system inspection dashboard** for Windows, built with **Electron** and a **React + TypeScript** UI. It is styled like a **Task Manager / system manager** console: dark panels, dense tables, and icon-only actions with **hover labels** (custom tooltip plus native `title` for accessibility).
4
+
5
+ The app is designed to **inventory and measure** what is on your machine—storage, processes, services, installed software, network, startup surface, scheduled automation, and optional Windows features—while letting you attach **persistent audit notes** to almost any entity you select.
6
+
7
+ ---
8
+
9
+ ## Platform & runtime
10
+
11
+ - **Hybrid stack**: **React + TypeScript** UI (renderer), **Electron** main (IPC, notes, shell), and a **Python 3** **FastAPI** backend on **127.0.0.1** (dynamic port) for hot paths: directory listing with recursive sizes, folder totals, large-file scans, processes (`psutil`), drives (`psutil`), system/memory snapshot, environment, and network interfaces. Electron **spawns** `python -m uvicorn main:app` with cwd `backend/` and **kills** the process on quit.
12
+ - **Windows-focused (Node)**: registry software list, services, scheduled tasks, DISM snapshot via **PowerShell** / `dism.exe`; Explorer and `taskkill` for actions.
13
+ - **Python setup**: `cd backend && pip install -r requirements.txt`. Set **`PYTHON_PATH`** if `python` is not on `PATH`.
14
+ - **Electron shell**: full local access from main; renderer is isolated (`contextIsolation`, no Node integration).
15
+ - **Development**: `npm run dev` starts Vite and Electron together. **Production**: `npm run build` then `npm start`.
16
+
17
+ ---
18
+
19
+ ## User interface
20
+
21
+ - **Minimal layout**: left **navigation rail** (icons + labels), main **toolbar** (section title, global refresh for live system snapshot, status line), scrollable **content**, and a fixed **notes** strip at the bottom.
22
+ - **React Icons (`react-icons/md`)** for every actionable control; **hover** shows a text label via a small tooltip layer.
23
+ - **No emoji** in the product chrome; data-first tables and monospace paths where helpful.
24
+ - **Auto-refresh**: host memory and core **system snapshot** refresh on a short interval while the app is open (complementing manual refresh).
25
+
26
+ ---
27
+
28
+ ## Overview dashboard
29
+
30
+ - **Host identity**: computer name, signed-in username context from Node.
31
+ - **OS**: platform, kernel release, CPU architecture.
32
+ - **Uptime**: human-readable uptime.
33
+ - **Memory**: used vs total RAM, percentage, bar indicator.
34
+ - **CPU**: logical processor count and model string from `os.cpus()`.
35
+ - **Paths**: home directory and temp directory baselines.
36
+ - **Quick actions** on this screen:
37
+ - Load **logical volumes** (used/free/total per drive letter).
38
+ - **Temp directory audit**: approximate total bytes and file count under temp paths (capped walk for safety/performance; may mark truncation when file cap hit).
39
+
40
+ ---
41
+
42
+ ## Storage & volumes
43
+
44
+ - **Per-volume table**: device id (e.g. `C:`), volume label, used space, free space, total size, utilization bar.
45
+ - **Open in Explorer** for a selected volume root.
46
+ - **Notes** can be keyed to `drive:<letter>` for remediation tracking or change control.
47
+
48
+ ---
49
+
50
+ ## Folders & files
51
+
52
+ - **Path bar**: type or paste any folder; **list contents** with a full recursive **byte total for every subfolder** (same capped walk as aggregate sizing: stops after a maximum **file count** so huge trees do not run forever). Sizes in the table are shown as **decimal KB** (1 KB = 1024 bytes, two fractional digits). If the cap is hit mid-folder, that row shows a **`+` suffix** (strict lower bound, not complete).
53
+ - **Navigation**: parent shortcut, double-click to enter directories, **open in Explorer** for files or folders.
54
+ - **Large file finder**: scan from a chosen root (defaults to current path) for files above a **minimum size in megabytes**; returns top hits sorted by size (result cap). Useful for reclaiming disk space or spotting bulky artifacts.
55
+ - **Recursive folder size**: totals **bytes and file count** for the current path using a **capped** depth-first walk (stops after a maximum file count so system folders cannot lock the app). Shows whether the result was **truncated** due to the cap.
56
+ - **Copy path**: copies the active folder path to the clipboard for tickets, scripts, or shell use (uses the browser clipboard API when available, otherwise **Electron’s clipboard** via IPC so it still works under `file://` loads).
57
+ - **Notes** attach to `path:<fullPath>` keys.
58
+
59
+ ---
60
+
61
+ ## Processes
62
+
63
+ - **Live process list** sourced from WMI/CIM (`Win32_Process`): PID, image name, working set (memory), coarse CPU time estimate (kernel + user time converted to seconds—useful for ordering, not a perf chart), and command line when available.
64
+ - **Filter** by name, PID, or command line substring.
65
+ - **End process**: invokes `taskkill /F` after an explicit confirmation. This is a **destructive** action; data in that process may be lost.
66
+ - **Notes** per `proc:<pid>` (PID can change across reboots; notes remain as historical annotations unless you clear them).
67
+
68
+ ---
69
+
70
+ ## Services
71
+
72
+ - **Windows services** via `Win32_Service`: internal name, display name, state (running/stopped/etc.), start mode (auto/manual/disabled, etc.).
73
+ - **Filtering** across name, display name, and state.
74
+ - **Notes** per `svc:<service name>`.
75
+
76
+ > The app **does not** start/stop/reconfigure services from the UI (that would require elevated privileges and careful safety). It is **audit and documentation** first.
77
+
78
+ ---
79
+
80
+ ## Installed software
81
+
82
+ - **Add/remove programs style inventory** from registry hives (64-bit, 32-bit WOW6432Node, and per-user uninstall keys) via PowerShell `Get-ItemProperty`.
83
+ - Columns include **name, version, publisher, install location, estimated size** (when publishers populate it), and raw uninstall string (for your runbooks).
84
+ - **Open install folder** when a path exists.
85
+ - **Notes** per `app:<display name>` (names are not guaranteed unique if vendors collide; use notes carefully).
86
+
87
+ ---
88
+
89
+ ## Network
90
+
91
+ - **Logical interfaces** from `os.networkInterfaces()`: addresses, address family, internal flag, MAC when present.
92
+ - **Notes** per `net:<interface>:<address>` to document VLANs, VPNs, or static assignments.
93
+
94
+ ---
95
+
96
+ ## Environment
97
+
98
+ - **Full process environment block** as read by the Electron main process (sorted `KEY=value` lines). Useful for spotting **mis-set PATH**, proxy variables, or deployment tooling flags.
99
+ - **Note** slot `env:all` for global remarks about environment hygiene.
100
+
101
+ ---
102
+
103
+ ## Startup items
104
+
105
+ - Scans **per-user** and **all-users** Startup shell folders (shortcuts and files). Lists name, type, and allows **Explorer** jump and **path notes** for each entry.
106
+
107
+ ---
108
+
109
+ ## Scheduled tasks
110
+
111
+ - Enumerates **Task Scheduler** tasks with **name** and **state** via `Get-ScheduledTask` (PowerShell). Handy for **persistence hunting** and automation audits.
112
+ - **Notes** per `task:<task name>`.
113
+
114
+ ---
115
+
116
+ ## Windows optional features (DISM)
117
+
118
+ - Pulls a **truncated text table** from `dism /Online /Get-Features /Format:Table` for an offline-friendly snapshot of **optional Windows features** state. Large output is clipped to keep IPC responsive; use **note** `features:snippet` to record interpretation or follow-up.
119
+
120
+ ---
121
+
122
+ ## Notes system
123
+
124
+ - **Persistent JSON** store under the app’s **Electron `userData`** directory (`auditor-notes.json`).
125
+ - **Key/value** model: keys are semantic strings (`path:…`, `proc:…`, `drive:…`, etc.).
126
+ - **Save**, **clear** (delete key), and **reload** from disk.
127
+ - Selecting an entity from a table or drive row sets the **active note key**; the footer editor binds to that key.
128
+
129
+ ---
130
+
131
+ ## Cross-cutting actions
132
+
133
+ - **Open in Explorer** for paths and volumes (`explorer.exe`).
134
+ - **Open external URL** hook exists in the main process for future deep links (IPC wired).
135
+ - **Status and error lines** in the toolbar for long operations (process list, services, DISM, large scans).
136
+
137
+ ---
138
+
139
+ ## Safety, performance, and limitations
140
+
141
+ - **Heavy filesystem and process enumeration** run in the **Python** process over **HTTP** (FastAPI + `psutil` + threaded `os.walk` / `os.scandir`), so the Electron **main** thread only proxies `fetch` and stays responsive.
142
+ - **Directory walks** and **large-file scans** use **caps** (max files, max depth, max results) so accidental scans of entire drives do not run unbounded; results may be **partial** or **truncated**—always stated where relevant.
143
+ - **Administrator elevation** is **not** assumed. Some counters (certain services, protected paths) may be incomplete without elevation; DISM feature enumeration may require an elevated session on some systems.
144
+ - **Process kill** and **Explorer open** are the main **active** interventions; everything else is **read/audit**.
145
+ - **Non-Windows** platforms are not the target: several modules will return empty or fallback data if ported without adapters.
146
+
147
+ ---
148
+
149
+ ## How it fits together
150
+
151
+ 1. **Python** (`backend/main.py`): FastAPI on localhost; parallel folder sizing via `ThreadPoolExecutor`; process/disk/network/env data via `psutil`.
152
+ 2. **Main process** (`electron/`): starts/stops Python, proxies audit calls with `fetch`, handles notes JSON, PowerShell-only features, Explorer, `taskkill`.
153
+ 3. **Preload** exposes **`window.auditor`** (IPC).
154
+ 4. **Renderer** (`src/`): React + TypeScript—**no** Python or Node builtins.
155
+
156
+ This split keeps the UI responsive while still allowing **deep system visibility** appropriate for a personal or IT **audit workstation** tool.
backend/main.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Computer Auditor — Python backend (FastAPI).
3
+ Hot paths: directory listing with recursive sizes, large-file scan, processes, drives, system, network.
4
+ Binds to 127.0.0.1 only. Started by Electron with: python -m uvicorn main:app --host 127.0.0.1 --port <port>
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import platform
10
+ import socket
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ from concurrent.futures import ThreadPoolExecutor, as_completed
15
+ from typing import Any
16
+
17
+ import psutil
18
+ from fastapi import FastAPI, HTTPException
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from pydantic import BaseModel, Field
21
+
22
+ MAX_FILES_WALK = 200_000
23
+
24
+ app = FastAPI(title="Computer Auditor API", version="1.0.0")
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_credentials=True,
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+
34
+ def measure_folder_bytes(root: str) -> tuple[int, int, bool]:
35
+ """Recursive file byte total under root. Returns (bytes, file_count, truncated)."""
36
+ total = 0
37
+ n = 0
38
+ truncated = False
39
+ for dirpath, _dirnames, filenames in os.walk(root):
40
+ for fn in filenames:
41
+ if n >= MAX_FILES_WALK:
42
+ return total, n, True
43
+ fp = os.path.join(dirpath, fn)
44
+ try:
45
+ total += os.path.getsize(fp)
46
+ n += 1
47
+ except OSError:
48
+ pass
49
+ return total, n, False
50
+
51
+
52
+ class ListDirBody(BaseModel):
53
+ path: str
54
+ max_entries: int = Field(default=800, ge=1, le=5000)
55
+
56
+
57
+ class FolderSizeBody(BaseModel):
58
+ path: str
59
+
60
+
61
+ class LargeFilesBody(BaseModel):
62
+ path: str
63
+ min_bytes: int = Field(ge=0)
64
+ max_results: int = Field(default=80, ge=1, le=500)
65
+
66
+
67
+ @app.get("/health")
68
+ def health() -> dict[str, str]:
69
+ return {"status": "ok"}
70
+
71
+
72
+ @app.post("/api/list_dir")
73
+ def api_list_dir(body: ListDirBody) -> list[dict[str, Any]]:
74
+ try:
75
+ root = os.path.abspath(os.path.normpath(body.path))
76
+ except Exception as e:
77
+ raise HTTPException(400, str(e)) from e
78
+ if not os.path.isdir(root):
79
+ raise HTTPException(400, "not a directory")
80
+
81
+ max_e = min(body.max_entries, 5000)
82
+ try:
83
+ with os.scandir(root) as it:
84
+ raw = list(it)[:max_e]
85
+ except OSError as e:
86
+ return [
87
+ {
88
+ "name": os.path.basename(root),
89
+ "fullPath": root,
90
+ "isDirectory": False,
91
+ "sizeBytes": 0,
92
+ "mtimeMs": 0,
93
+ "error": str(e),
94
+ }
95
+ ]
96
+
97
+ dirs = [e for e in raw if e.is_dir(follow_symlinks=False)]
98
+ file_entries = [e for e in raw if not e.is_dir(follow_symlinks=False)]
99
+
100
+ out: list[dict[str, Any]] = []
101
+
102
+ for e in file_entries:
103
+ try:
104
+ st = e.stat(follow_symlinks=False)
105
+ out.append(
106
+ {
107
+ "name": e.name,
108
+ "fullPath": e.path,
109
+ "isDirectory": False,
110
+ "sizeBytes": st.st_size,
111
+ "mtimeMs": int(st.st_mtime * 1000),
112
+ }
113
+ )
114
+ except OSError as ex:
115
+ out.append(
116
+ {
117
+ "name": e.name,
118
+ "fullPath": e.path,
119
+ "isDirectory": False,
120
+ "sizeBytes": 0,
121
+ "mtimeMs": 0,
122
+ "error": str(ex),
123
+ }
124
+ )
125
+
126
+ workers = min(8, max(1, len(dirs)))
127
+ if dirs:
128
+ with ThreadPoolExecutor(max_workers=workers) as ex:
129
+ future_to_ent = {ex.submit(measure_folder_bytes, d.path): d for d in dirs}
130
+ for fut in as_completed(future_to_ent):
131
+ d = future_to_ent[fut]
132
+ try:
133
+ bytes_total, _n, truncated = fut.result()
134
+ try:
135
+ st = os.stat(d.path)
136
+ mtime_ms = int(st.st_mtime * 1000)
137
+ except OSError:
138
+ mtime_ms = 0
139
+ out.append(
140
+ {
141
+ "name": d.name,
142
+ "fullPath": d.path,
143
+ "isDirectory": True,
144
+ "sizeBytes": bytes_total,
145
+ "mtimeMs": mtime_ms,
146
+ "sizeTruncated": truncated,
147
+ }
148
+ )
149
+ except Exception as ex: # noqa: BLE001
150
+ out.append(
151
+ {
152
+ "name": d.name,
153
+ "fullPath": d.path,
154
+ "isDirectory": True,
155
+ "sizeBytes": 0,
156
+ "mtimeMs": 0,
157
+ "error": str(ex),
158
+ }
159
+ )
160
+
161
+ out.sort(
162
+ key=lambda x: (
163
+ 0 if x.get("isDirectory") else 1,
164
+ -int(x.get("sizeBytes") or 0),
165
+ str(x.get("name") or ""),
166
+ )
167
+ )
168
+ return out
169
+
170
+
171
+ @app.post("/api/folder_size")
172
+ def api_folder_size(body: FolderSizeBody) -> dict[str, Any]:
173
+ try:
174
+ root = os.path.abspath(os.path.normpath(body.path))
175
+ except Exception as e:
176
+ raise HTTPException(400, str(e)) from e
177
+ if not os.path.isdir(root):
178
+ raise HTTPException(400, "not a directory")
179
+ b, n, t = measure_folder_bytes(root)
180
+ return {"bytes": b, "files": n, "truncated": t}
181
+
182
+
183
+ @app.post("/api/large_files")
184
+ def api_large_files(body: LargeFilesBody) -> list[dict[str, Any]]:
185
+ try:
186
+ root = os.path.abspath(os.path.normpath(body.path))
187
+ except Exception as e:
188
+ raise HTTPException(400, str(e)) from e
189
+ if not os.path.isdir(root):
190
+ raise HTTPException(400, "not a directory")
191
+
192
+ cap = min(body.max_results, 500)
193
+ results: list[dict[str, Any]] = []
194
+ for dirpath, _dn, filenames in os.walk(root):
195
+ for fn in filenames:
196
+ if len(results) >= cap:
197
+ break
198
+ fp = os.path.join(dirpath, fn)
199
+ try:
200
+ sz = os.path.getsize(fp)
201
+ if sz >= body.min_bytes:
202
+ results.append({"path": fp, "sizeBytes": sz})
203
+ except OSError:
204
+ pass
205
+ if len(results) >= cap:
206
+ break
207
+ results.sort(key=lambda x: -x["sizeBytes"])
208
+ return results[:cap]
209
+
210
+
211
+ @app.get("/api/processes")
212
+ def api_processes() -> list[dict[str, Any]]:
213
+ rows: list[dict[str, Any]] = []
214
+ for p in psutil.process_iter(
215
+ ["pid", "name", "memory_info", "cpu_times", "cmdline"]
216
+ ):
217
+ try:
218
+ info = p.info
219
+ mi = info.get("memory_info")
220
+ rss = mi.rss if mi else 0
221
+ ct = info.get("cpu_times")
222
+ cpu_s = None
223
+ if ct:
224
+ cpu_s = round(ct.user + ct.system, 2)
225
+ cmd = info.get("cmdline") or []
226
+ cmdline = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
227
+ if len(cmdline) > 8000:
228
+ cmdline = cmdline[:8000] + "…"
229
+ rows.append(
230
+ {
231
+ "pid": int(info["pid"]),
232
+ "name": str(info.get("name") or ""),
233
+ "memoryBytes": int(rss),
234
+ "cpuSeconds": cpu_s,
235
+ "commandLine": cmdline,
236
+ }
237
+ )
238
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
239
+ continue
240
+ return rows
241
+
242
+
243
+ @app.get("/api/drives")
244
+ def api_drives() -> list[dict[str, Any]]:
245
+ out: list[dict[str, Any]] = []
246
+ for part in psutil.disk_partitions(all=False):
247
+ if os.name == "nt" and "cdrom" in (part.opts or "").lower():
248
+ continue
249
+ try:
250
+ u = psutil.disk_usage(part.mountpoint)
251
+ dev = part.device.rstrip("\\/")
252
+ out.append(
253
+ {
254
+ "letter": dev if dev else part.mountpoint,
255
+ "mount": part.mountpoint,
256
+ "label": part.fstype or "",
257
+ "totalBytes": int(u.total),
258
+ "freeBytes": int(u.free),
259
+ "usedBytes": int(u.used),
260
+ }
261
+ )
262
+ except OSError:
263
+ continue
264
+ return out
265
+
266
+
267
+ @app.get("/api/system")
268
+ def api_system() -> dict[str, Any]:
269
+ vm = psutil.virtual_memory()
270
+ boot = psutil.boot_time()
271
+ try:
272
+ user = os.getlogin()
273
+ except OSError:
274
+ user = os.environ.get("USERNAME", os.environ.get("USER", ""))
275
+ cpus = os.cpu_count() or 1
276
+ model = ""
277
+ if platform.system() == "Windows":
278
+ try:
279
+ import ctypes # noqa: PLC0415
280
+
281
+ buf = ctypes.create_unicode_buffer(256)
282
+ if ctypes.windll.kernel32.GetEnvironmentVariableW("PROCESSOR_IDENTIFIER", buf, 256):
283
+ model = buf.value
284
+ except Exception: # noqa: BLE001
285
+ model = platform.processor() or ""
286
+ else:
287
+ model = platform.processor() or ""
288
+
289
+ return {
290
+ "hostname": socket.gethostname(),
291
+ "platform": sys.platform,
292
+ "release": platform.release(),
293
+ "arch": platform.machine(),
294
+ "uptimeSec": time.time() - boot,
295
+ "totalMem": int(vm.total),
296
+ "freeMem": int(vm.available),
297
+ "cpuModel": model,
298
+ "cpuCount": cpus,
299
+ "load1": 0.0,
300
+ "load5": 0.0,
301
+ "load15": 0.0,
302
+ "userInfo": user,
303
+ "homedir": str(os.path.expanduser("~")),
304
+ "tmpdir": tempfile.gettempdir(),
305
+ }
306
+
307
+
308
+ @app.get("/api/network")
309
+ def api_network() -> list[dict[str, Any]]:
310
+ rows: list[dict[str, Any]] = []
311
+ link_fam = getattr(psutil, "AF_LINK", None)
312
+ for name, addrs in psutil.net_if_addrs().items():
313
+ for a in addrs:
314
+ if a.family == socket.AF_INET:
315
+ fam_s = "IPv4"
316
+ elif a.family == socket.AF_INET6:
317
+ fam_s = "IPv6"
318
+ elif link_fam is not None and a.family == link_fam:
319
+ fam_s = "MAC"
320
+ else:
321
+ fam_s = str(a.family)
322
+ addr = a.address
323
+ internal = addr.startswith("127.") or addr == "::1"
324
+ mac = addr if fam_s == "MAC" else None
325
+ rows.append(
326
+ {
327
+ "name": name,
328
+ "address": addr,
329
+ "family": fam_s,
330
+ "internal": internal,
331
+ "mac": mac,
332
+ }
333
+ )
334
+ return rows
335
+
336
+
337
+ @app.get("/api/env")
338
+ def api_env() -> dict[str, str]:
339
+ return dict(os.environ)
340
+
341
+
342
+ if __name__ == "__main__":
343
+ import uvicorn # noqa: PLC0415
344
+
345
+ port = int(os.environ.get("AUDITOR_PY_PORT", "54789"))
346
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")
backend/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.32.0
3
+ psutil>=6.0.0
4
+ pydantic>=2.0.0
dist-electron/audit.js ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getDrivesWin = getDrivesWin;
37
+ exports.listDirectory = listDirectory;
38
+ exports.computeFolderSize = computeFolderSize;
39
+ exports.findLargeFiles = findLargeFiles;
40
+ exports.getProcessesWin = getProcessesWin;
41
+ exports.getServicesWin = getServicesWin;
42
+ exports.getInstalledPrograms = getInstalledPrograms;
43
+ exports.getSystemSnapshot = getSystemSnapshot;
44
+ exports.getNetworkInterfaces = getNetworkInterfaces;
45
+ exports.getEnvSnapshot = getEnvSnapshot;
46
+ exports.getStartupFolders = getStartupFolders;
47
+ exports.getTempAudit = getTempAudit;
48
+ exports.getScheduledTasksSummary = getScheduledTasksSummary;
49
+ exports.openPathInExplorer = openPathInExplorer;
50
+ exports.killProcess = killProcess;
51
+ exports.getWindowsFeaturesSnippet = getWindowsFeaturesSnippet;
52
+ /**
53
+ * Audit logic: hot paths (filesystem, processes, drives, system, network, env) → Python FastAPI.
54
+ * Windows-specific shell/registry work stays in Node.
55
+ */
56
+ const path = __importStar(require("node:path"));
57
+ const os = __importStar(require("node:os"));
58
+ const node_child_process_1 = require("node:child_process");
59
+ const node_util_1 = require("node:util");
60
+ const pythonBackend_1 = require("./pythonBackend");
61
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
62
+ function resolveSafePath(input) {
63
+ const normalized = path.normalize(input);
64
+ return path.resolve(normalized);
65
+ }
66
+ async function getDrivesWin() {
67
+ return (0, pythonBackend_1.pyGet)('/api/drives');
68
+ }
69
+ async function listDirectory(dirPath, options = {}) {
70
+ const root = resolveSafePath(dirPath);
71
+ return (0, pythonBackend_1.pyPost)('/api/list_dir', {
72
+ path: root,
73
+ max_entries: options.maxEntries ?? 800,
74
+ });
75
+ }
76
+ async function computeFolderSize(dirPath) {
77
+ return (0, pythonBackend_1.pyPost)('/api/folder_size', {
78
+ path: resolveSafePath(dirPath),
79
+ });
80
+ }
81
+ async function findLargeFiles(rootPath, minBytes, maxResults) {
82
+ return (0, pythonBackend_1.pyPost)('/api/large_files', {
83
+ path: resolveSafePath(rootPath),
84
+ min_bytes: minBytes,
85
+ max_results: maxResults,
86
+ });
87
+ }
88
+ async function getProcessesWin() {
89
+ return (0, pythonBackend_1.pyGet)('/api/processes');
90
+ }
91
+ async function getServicesWin() {
92
+ const script = `
93
+ Get-CimInstance Win32_Service | Select-Object Name,DisplayName,State,StartMode | ConvertTo-Json -Compress
94
+ `;
95
+ try {
96
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, maxBuffer: 20 * 1024 * 1024, timeout: 120_000 });
97
+ const raw = JSON.parse(stdout.trim() || '[]');
98
+ const arr = Array.isArray(raw) ? raw : [raw];
99
+ return arr.map((s) => ({
100
+ name: String(s.Name ?? ''),
101
+ displayName: String(s.DisplayName ?? ''),
102
+ state: String(s.State ?? ''),
103
+ startType: String(s.StartMode ?? ''),
104
+ }));
105
+ }
106
+ catch {
107
+ return [];
108
+ }
109
+ }
110
+ function getInstalledPrograms() {
111
+ const script = `
112
+ $paths = @(
113
+ 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
114
+ 'HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
115
+ 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
116
+ )
117
+ Get-ItemProperty $paths -ErrorAction SilentlyContinue |
118
+ Where-Object { $_.DisplayName } |
119
+ Select-Object DisplayName, DisplayVersion, Publisher, InstallLocation, UninstallString, EstimatedSize |
120
+ ConvertTo-Json -Compress -Depth 4
121
+ `;
122
+ try {
123
+ const stdout = (0, node_child_process_1.execFileSync)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { encoding: 'utf8', windowsHide: true, maxBuffer: 50 * 1024 * 1024 });
124
+ const raw = JSON.parse(stdout.trim() || '[]');
125
+ const arr = Array.isArray(raw) ? raw : [raw];
126
+ const apps = arr.map((r) => ({
127
+ name: String(r.DisplayName ?? ''),
128
+ version: String(r.DisplayVersion ?? ''),
129
+ publisher: String(r.Publisher ?? ''),
130
+ installLocation: String(r.InstallLocation ?? ''),
131
+ uninstallString: String(r.UninstallString ?? ''),
132
+ estimatedSizeKb: Number(r.EstimatedSize) || 0,
133
+ }));
134
+ const seen = new Set();
135
+ return apps
136
+ .filter((a) => {
137
+ const k = a.name.toLowerCase();
138
+ if (!k || seen.has(k))
139
+ return false;
140
+ seen.add(k);
141
+ return true;
142
+ })
143
+ .sort((a, b) => a.name.localeCompare(b.name));
144
+ }
145
+ catch {
146
+ return [];
147
+ }
148
+ }
149
+ async function getSystemSnapshot() {
150
+ return (0, pythonBackend_1.pyGet)('/api/system');
151
+ }
152
+ async function getNetworkInterfaces() {
153
+ return (0, pythonBackend_1.pyGet)('/api/network');
154
+ }
155
+ async function getEnvSnapshot(keys) {
156
+ const all = await (0, pythonBackend_1.pyGet)('/api/env');
157
+ if (!keys?.length)
158
+ return all;
159
+ const out = {};
160
+ for (const k of keys) {
161
+ const v = all[k];
162
+ if (v !== undefined)
163
+ out[k] = v;
164
+ }
165
+ return out;
166
+ }
167
+ async function getStartupFolders() {
168
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
169
+ const programData = process.env.PROGRAMDATA ?? 'C:\\ProgramData';
170
+ const folders = [
171
+ path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'),
172
+ path.join(programData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'StartUp'),
173
+ ];
174
+ const result = [];
175
+ for (const f of folders) {
176
+ const entries = await listDirectory(f, { maxEntries: 200 });
177
+ result.push({ path: f, entries });
178
+ }
179
+ return result;
180
+ }
181
+ async function getTempAudit() {
182
+ const dirs = [os.tmpdir(), path.join(os.tmpdir(), '..', 'Temp')].map((p) => path.normalize(p));
183
+ const uniq = [...new Set(dirs)];
184
+ const out = [];
185
+ for (const d of uniq) {
186
+ try {
187
+ const r = await computeFolderSize(d);
188
+ out.push({ path: d, ...r });
189
+ }
190
+ catch {
191
+ out.push({ path: d, bytes: 0, files: 0, truncated: false });
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+ async function getScheduledTasksSummary() {
197
+ const script = `
198
+ Get-ScheduledTask | Select-Object TaskName,State | ConvertTo-Json -Compress
199
+ `;
200
+ try {
201
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, maxBuffer: 20 * 1024 * 1024, timeout: 120_000 });
202
+ const raw = JSON.parse(stdout.trim() || '[]');
203
+ const arr = Array.isArray(raw) ? raw : [raw];
204
+ return arr.map((t) => ({
205
+ name: String(t.TaskName ?? ''),
206
+ state: String(t.State ?? ''),
207
+ }));
208
+ }
209
+ catch {
210
+ return [];
211
+ }
212
+ }
213
+ async function openPathInExplorer(p) {
214
+ const resolved = resolveSafePath(p);
215
+ await execFileAsync('explorer.exe', [resolved], { windowsHide: true });
216
+ }
217
+ function killProcess(pid) {
218
+ return new Promise((resolve, reject) => {
219
+ const proc = (0, node_child_process_1.spawn)('taskkill', ['/PID', String(pid), '/F'], { windowsHide: true });
220
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`taskkill exit ${code}`))));
221
+ proc.on('error', reject);
222
+ });
223
+ }
224
+ async function getWindowsFeaturesSnippet() {
225
+ try {
226
+ const { stdout } = await execFileAsync('dism.exe', ['/Online', '/Get-Features', '/Format:Table'], { windowsHide: true, maxBuffer: 5 * 1024 * 1024, timeout: 60_000 });
227
+ return stdout.slice(0, 120_000);
228
+ }
229
+ catch (e) {
230
+ return String(e.message);
231
+ }
232
+ }
dist-electron/fsWorker.js ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * Runs in a worker thread (never blocks the Electron main process).
38
+ * Keep walk caps in sync with audit.ts.
39
+ */
40
+ const node_worker_threads_1 = require("node:worker_threads");
41
+ const fssync = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const FOLDER_WALK_MAX_FILES = 200_000;
44
+ function resolveSafePath(input) {
45
+ return path.resolve(path.normalize(input));
46
+ }
47
+ function measureFolderTree(rootDir, maxFiles = FOLDER_WALK_MAX_FILES) {
48
+ const root = resolveSafePath(rootDir);
49
+ let bytes = 0;
50
+ let files = 0;
51
+ const walk = (d) => {
52
+ let list;
53
+ try {
54
+ list = fssync.readdirSync(d, { withFileTypes: true });
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ for (const ent of list) {
60
+ if (files >= maxFiles)
61
+ return true;
62
+ const p = path.join(d, ent.name);
63
+ try {
64
+ if (ent.isDirectory()) {
65
+ if (walk(p))
66
+ return true;
67
+ }
68
+ else {
69
+ bytes += fssync.statSync(p).size;
70
+ files++;
71
+ }
72
+ }
73
+ catch {
74
+ /* skip */
75
+ }
76
+ }
77
+ return false;
78
+ };
79
+ const truncated = walk(root);
80
+ return { bytes, files, truncated };
81
+ }
82
+ function findLargeFilesSync(rootPath, minBytes, maxResults) {
83
+ const root = resolveSafePath(rootPath);
84
+ const results = [];
85
+ const cap = Math.min(maxResults, 500);
86
+ const walk = (d) => {
87
+ if (results.length >= cap)
88
+ return;
89
+ let list;
90
+ try {
91
+ list = fssync.readdirSync(d, { withFileTypes: true });
92
+ }
93
+ catch {
94
+ return;
95
+ }
96
+ for (const ent of list) {
97
+ if (results.length >= cap)
98
+ return;
99
+ const p = path.join(d, ent.name);
100
+ try {
101
+ if (ent.isDirectory()) {
102
+ walk(p);
103
+ }
104
+ else {
105
+ const st = fssync.statSync(p);
106
+ if (st.size >= minBytes) {
107
+ results.push({ path: p, sizeBytes: st.size });
108
+ }
109
+ }
110
+ }
111
+ catch {
112
+ /* skip */
113
+ }
114
+ }
115
+ };
116
+ walk(root);
117
+ results.sort((a, b) => b.sizeBytes - a.sizeBytes);
118
+ return results.slice(0, cap);
119
+ }
120
+ if (!node_worker_threads_1.isMainThread && node_worker_threads_1.parentPort) {
121
+ node_worker_threads_1.parentPort.on('message', (msg) => {
122
+ try {
123
+ if (msg.kind === 'large') {
124
+ const largeResults = findLargeFilesSync(msg.root, msg.minBytes, msg.maxResults);
125
+ node_worker_threads_1.parentPort.postMessage({ id: msg.id, ok: true, largeResults });
126
+ }
127
+ else {
128
+ const { bytes, files, truncated } = measureFolderTree(msg.root);
129
+ node_worker_threads_1.parentPort.postMessage({ id: msg.id, ok: true, bytes, files, truncated });
130
+ }
131
+ }
132
+ catch (e) {
133
+ node_worker_threads_1.parentPort.postMessage({
134
+ id: msg.id,
135
+ ok: false,
136
+ error: String(e.message),
137
+ });
138
+ }
139
+ });
140
+ }
dist-electron/main.js ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const electron_1 = require("electron");
37
+ const path = __importStar(require("node:path"));
38
+ const fssync = __importStar(require("node:fs"));
39
+ const audit_1 = require("./audit");
40
+ const pythonBackend_1 = require("./pythonBackend");
41
+ const isDev = !!process.env.VITE_DEV_SERVER_URL;
42
+ function notesPath() {
43
+ return path.join(electron_1.app.getPath('userData'), 'auditor-notes.json');
44
+ }
45
+ function loadNotes() {
46
+ try {
47
+ const raw = fssync.readFileSync(notesPath(), 'utf8');
48
+ return JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return {};
52
+ }
53
+ }
54
+ function saveNotes(n) {
55
+ fssync.mkdirSync(path.dirname(notesPath()), { recursive: true });
56
+ fssync.writeFileSync(notesPath(), JSON.stringify(n, null, 2), 'utf8');
57
+ }
58
+ function createWindow() {
59
+ const win = new electron_1.BrowserWindow({
60
+ width: 1280,
61
+ height: 800,
62
+ minWidth: 960,
63
+ minHeight: 640,
64
+ backgroundColor: '#0d0f12',
65
+ titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
66
+ webPreferences: {
67
+ preload: path.join(__dirname, 'preload.js'),
68
+ contextIsolation: true,
69
+ nodeIntegration: false,
70
+ sandbox: false,
71
+ },
72
+ });
73
+ if (isDev) {
74
+ win.loadURL(process.env.VITE_DEV_SERVER_URL);
75
+ }
76
+ else {
77
+ win.loadFile(path.join(__dirname, '../dist/index.html'));
78
+ }
79
+ }
80
+ electron_1.app.whenReady().then(async () => {
81
+ try {
82
+ await (0, pythonBackend_1.startPythonBackend)();
83
+ }
84
+ catch (e) {
85
+ electron_1.dialog.showErrorBox('Python backend required', `${String(e)}\n\nInstall: cd backend && pip install -r requirements.txt`);
86
+ electron_1.app.quit();
87
+ return;
88
+ }
89
+ electron_1.ipcMain.handle('audit:drives', () => (0, audit_1.getDrivesWin)());
90
+ electron_1.ipcMain.handle('audit:listDir', (_e, dirPath, opts) => (0, audit_1.listDirectory)(dirPath, opts ?? {}));
91
+ electron_1.ipcMain.handle('audit:folderSize', (_e, dirPath) => (0, audit_1.computeFolderSize)(dirPath));
92
+ electron_1.ipcMain.handle('audit:largeFiles', (_e, rootPath, minBytes, maxResults) => (0, audit_1.findLargeFiles)(rootPath, minBytes, maxResults));
93
+ electron_1.ipcMain.handle('audit:processes', () => (0, audit_1.getProcessesWin)());
94
+ electron_1.ipcMain.handle('audit:services', () => (0, audit_1.getServicesWin)());
95
+ electron_1.ipcMain.handle('audit:installed', () => (0, audit_1.getInstalledPrograms)());
96
+ electron_1.ipcMain.handle('audit:system', () => (0, audit_1.getSystemSnapshot)());
97
+ electron_1.ipcMain.handle('audit:network', () => (0, audit_1.getNetworkInterfaces)());
98
+ electron_1.ipcMain.handle('audit:env', (_e, keys) => (0, audit_1.getEnvSnapshot)(keys));
99
+ electron_1.ipcMain.handle('audit:startup', () => (0, audit_1.getStartupFolders)());
100
+ electron_1.ipcMain.handle('audit:temp', () => (0, audit_1.getTempAudit)());
101
+ electron_1.ipcMain.handle('audit:tasks', () => (0, audit_1.getScheduledTasksSummary)());
102
+ electron_1.ipcMain.handle('audit:features', () => (0, audit_1.getWindowsFeaturesSnippet)());
103
+ electron_1.ipcMain.handle('audit:openExplorer', (_e, p) => (0, audit_1.openPathInExplorer)(p));
104
+ electron_1.ipcMain.handle('audit:killProcess', (_e, pid) => (0, audit_1.killProcess)(pid));
105
+ electron_1.ipcMain.handle('audit:openExternal', (_e, url) => electron_1.shell.openExternal(url));
106
+ electron_1.ipcMain.handle('clipboard:writeText', (_e, text) => {
107
+ electron_1.clipboard.writeText(text);
108
+ });
109
+ electron_1.ipcMain.handle('notes:getAll', () => loadNotes());
110
+ electron_1.ipcMain.handle('notes:set', (_e, key, value) => {
111
+ const all = loadNotes();
112
+ if (value.trim() === '')
113
+ delete all[key];
114
+ else
115
+ all[key] = value;
116
+ saveNotes(all);
117
+ return all;
118
+ });
119
+ electron_1.ipcMain.handle('notes:delete', (_e, key) => {
120
+ const all = loadNotes();
121
+ delete all[key];
122
+ saveNotes(all);
123
+ return all;
124
+ });
125
+ createWindow();
126
+ electron_1.app.on('activate', () => {
127
+ if (electron_1.BrowserWindow.getAllWindows().length === 0)
128
+ createWindow();
129
+ });
130
+ });
131
+ electron_1.app.on('window-all-closed', () => {
132
+ if (process.platform !== 'darwin')
133
+ electron_1.app.quit();
134
+ });
135
+ electron_1.app.on('before-quit', () => {
136
+ (0, pythonBackend_1.stopPythonBackend)();
137
+ });
dist-electron/measureWorker.js ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.measureFolderAsync = measureFolderAsync;
37
+ exports.findLargeFilesAsync = findLargeFilesAsync;
38
+ exports.terminateMeasureWorkers = terminateMeasureWorkers;
39
+ /**
40
+ * Offloads filesystem walks to worker threads so the Electron main process stays responsive.
41
+ * Pool of 2 workers balances throughput vs disk contention.
42
+ */
43
+ const node_worker_threads_1 = require("node:worker_threads");
44
+ const path = __importStar(require("node:path"));
45
+ const POOL_SIZE = 2;
46
+ let pool = null;
47
+ let seq = 0;
48
+ const pendingMeasure = new Map();
49
+ const pendingLarge = new Map();
50
+ function workerScriptPath() {
51
+ return path.join(__dirname, 'fsWorker.js');
52
+ }
53
+ function rejectAllPending(err) {
54
+ for (const [, p] of pendingMeasure)
55
+ p.reject(err);
56
+ for (const [, p] of pendingLarge)
57
+ p.reject(err);
58
+ pendingMeasure.clear();
59
+ pendingLarge.clear();
60
+ }
61
+ function ensurePool() {
62
+ if (pool)
63
+ return pool;
64
+ pool = [];
65
+ for (let i = 0; i < POOL_SIZE; i++) {
66
+ const w = new node_worker_threads_1.Worker(workerScriptPath());
67
+ w.on('message', (msg) => {
68
+ if (pendingMeasure.has(msg.id)) {
69
+ const p = pendingMeasure.get(msg.id);
70
+ pendingMeasure.delete(msg.id);
71
+ if (!msg.ok) {
72
+ p.reject(new Error(msg.error ?? 'measure failed'));
73
+ return;
74
+ }
75
+ if (msg.bytes !== undefined && msg.files !== undefined && msg.truncated !== undefined) {
76
+ p.resolve({ bytes: msg.bytes, files: msg.files, truncated: msg.truncated });
77
+ }
78
+ else {
79
+ p.reject(new Error('invalid measure response'));
80
+ }
81
+ return;
82
+ }
83
+ if (pendingLarge.has(msg.id)) {
84
+ const p = pendingLarge.get(msg.id);
85
+ pendingLarge.delete(msg.id);
86
+ if (!msg.ok) {
87
+ p.reject(new Error(msg.error ?? 'large file scan failed'));
88
+ return;
89
+ }
90
+ if (msg.largeResults !== undefined) {
91
+ p.resolve(msg.largeResults);
92
+ }
93
+ else {
94
+ p.reject(new Error('invalid large-file response'));
95
+ }
96
+ }
97
+ });
98
+ w.on('error', (err) => {
99
+ rejectAllPending(err);
100
+ });
101
+ pool.push(w);
102
+ }
103
+ return pool;
104
+ }
105
+ function measureFolderAsync(absPath) {
106
+ const workers = ensurePool();
107
+ const id = ++seq;
108
+ return new Promise((resolve, reject) => {
109
+ pendingMeasure.set(id, { resolve, reject });
110
+ const w = workers[id % POOL_SIZE];
111
+ w.postMessage({ id, kind: 'measure', root: absPath });
112
+ });
113
+ }
114
+ function findLargeFilesAsync(root, minBytes, maxResults) {
115
+ const workers = ensurePool();
116
+ const id = ++seq;
117
+ return new Promise((resolve, reject) => {
118
+ pendingLarge.set(id, { resolve, reject });
119
+ const w = workers[id % POOL_SIZE];
120
+ w.postMessage({ id, kind: 'large', root, minBytes, maxResults });
121
+ });
122
+ }
123
+ function terminateMeasureWorkers() {
124
+ if (!pool)
125
+ return;
126
+ for (const w of pool) {
127
+ try {
128
+ w.terminate();
129
+ }
130
+ catch {
131
+ /* ignore */
132
+ }
133
+ }
134
+ pool = null;
135
+ pendingMeasure.clear();
136
+ pendingLarge.clear();
137
+ }
dist-electron/preload.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const electron_1 = require("electron");
4
+ const api = {
5
+ drives: () => electron_1.ipcRenderer.invoke('audit:drives'),
6
+ listDir: (dirPath, opts) => electron_1.ipcRenderer.invoke('audit:listDir', dirPath, opts),
7
+ folderSize: (dirPath) => electron_1.ipcRenderer.invoke('audit:folderSize', dirPath),
8
+ largeFiles: (rootPath, minBytes, maxResults) => electron_1.ipcRenderer.invoke('audit:largeFiles', rootPath, minBytes, maxResults),
9
+ processes: () => electron_1.ipcRenderer.invoke('audit:processes'),
10
+ services: () => electron_1.ipcRenderer.invoke('audit:services'),
11
+ installed: () => electron_1.ipcRenderer.invoke('audit:installed'),
12
+ system: () => electron_1.ipcRenderer.invoke('audit:system'),
13
+ network: () => electron_1.ipcRenderer.invoke('audit:network'),
14
+ env: (keys) => electron_1.ipcRenderer.invoke('audit:env', keys),
15
+ startup: () => electron_1.ipcRenderer.invoke('audit:startup'),
16
+ temp: () => electron_1.ipcRenderer.invoke('audit:temp'),
17
+ tasks: () => electron_1.ipcRenderer.invoke('audit:tasks'),
18
+ features: () => electron_1.ipcRenderer.invoke('audit:features'),
19
+ openExplorer: (p) => electron_1.ipcRenderer.invoke('audit:openExplorer', p),
20
+ killProcess: (pid) => electron_1.ipcRenderer.invoke('audit:killProcess', pid),
21
+ openExternal: (url) => electron_1.ipcRenderer.invoke('audit:openExternal', url),
22
+ clipboardWriteText: (text) => electron_1.ipcRenderer.invoke('clipboard:writeText', text),
23
+ notesGetAll: () => electron_1.ipcRenderer.invoke('notes:getAll'),
24
+ notesSet: (key, value) => electron_1.ipcRenderer.invoke('notes:set', key, value),
25
+ notesDelete: (key) => electron_1.ipcRenderer.invoke('notes:delete', key),
26
+ };
27
+ electron_1.contextBridge.exposeInMainWorld('auditor', api);
dist-electron/pythonBackend.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getPythonBaseUrl = getPythonBaseUrl;
37
+ exports.startPythonBackend = startPythonBackend;
38
+ exports.stopPythonBackend = stopPythonBackend;
39
+ exports.pyGet = pyGet;
40
+ exports.pyPost = pyPost;
41
+ /**
42
+ * Spawns the FastAPI Python backend and exposes HTTP helpers for the main process.
43
+ * Python handles hot paths (filesystem sizes, processes, drives, system, network, env).
44
+ */
45
+ const node_child_process_1 = require("node:child_process");
46
+ const net = __importStar(require("node:net"));
47
+ const path = __importStar(require("node:path"));
48
+ let child = null;
49
+ let baseUrl = '';
50
+ function getPythonBaseUrl() {
51
+ return baseUrl;
52
+ }
53
+ function getFreePort() {
54
+ return new Promise((resolve, reject) => {
55
+ const s = net.createServer();
56
+ s.listen(0, '127.0.0.1', () => {
57
+ const a = s.address();
58
+ const p = typeof a === 'object' && a ? a.port : 0;
59
+ s.close(() => resolve(p));
60
+ });
61
+ s.on('error', reject);
62
+ });
63
+ }
64
+ async function waitForHealth(port, timeoutMs) {
65
+ const url = `http://127.0.0.1:${port}/health`;
66
+ const t0 = Date.now();
67
+ while (Date.now() - t0 < timeoutMs) {
68
+ try {
69
+ const r = await fetch(url);
70
+ if (r.ok)
71
+ return;
72
+ }
73
+ catch {
74
+ /* retry */
75
+ }
76
+ await new Promise((r) => setTimeout(r, 120));
77
+ }
78
+ throw new Error(`Python backend did not respond at ${url}`);
79
+ }
80
+ async function startPythonBackend() {
81
+ const port = await getFreePort();
82
+ const backendDir = path.join(__dirname, '..', 'backend');
83
+ const py = process.env.PYTHON_PATH ?? (process.platform === 'win32' ? 'python' : 'python3');
84
+ child = (0, node_child_process_1.spawn)(py, ['-m', 'uvicorn', 'main:app', '--host', '127.0.0.1', '--port', String(port), '--log-level', 'warning'], {
85
+ cwd: backendDir,
86
+ env: { ...process.env, AUDITOR_PY_PORT: String(port) },
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ windowsHide: true,
89
+ });
90
+ let stderrBuf = '';
91
+ child.stderr?.on('data', (d) => {
92
+ stderrBuf += d.toString();
93
+ if (stderrBuf.length > 8000)
94
+ stderrBuf = stderrBuf.slice(-4000);
95
+ });
96
+ await new Promise((resolve, reject) => {
97
+ const fail = (err) => {
98
+ try {
99
+ child?.kill();
100
+ }
101
+ catch {
102
+ /* ignore */
103
+ }
104
+ reject(err);
105
+ };
106
+ const t = setTimeout(() => {
107
+ fail(new Error(`Python backend startup timed out. Install: cd backend && pip install -r requirements.txt\n${stderrBuf}`));
108
+ }, 25_000);
109
+ child.once('error', (err) => {
110
+ clearTimeout(t);
111
+ fail(err instanceof Error ? err : new Error(String(err)));
112
+ });
113
+ waitForHealth(port, 24_000)
114
+ .then(() => {
115
+ clearTimeout(t);
116
+ resolve();
117
+ })
118
+ .catch((e) => {
119
+ clearTimeout(t);
120
+ fail(e instanceof Error ? e : new Error(String(e)));
121
+ });
122
+ });
123
+ baseUrl = `http://127.0.0.1:${port}`;
124
+ }
125
+ function stopPythonBackend() {
126
+ if (child) {
127
+ try {
128
+ child.kill();
129
+ }
130
+ catch {
131
+ /* ignore */
132
+ }
133
+ child = null;
134
+ }
135
+ baseUrl = '';
136
+ }
137
+ async function pyGet(pathname) {
138
+ const r = await fetch(`${baseUrl}${pathname}`);
139
+ if (!r.ok) {
140
+ const t = await r.text();
141
+ throw new Error(t || r.statusText);
142
+ }
143
+ return r.json();
144
+ }
145
+ async function pyPost(pathname, body) {
146
+ const r = await fetch(`${baseUrl}${pathname}`, {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify(body),
150
+ });
151
+ if (!r.ok) {
152
+ const t = await r.text();
153
+ throw new Error(t || r.statusText);
154
+ }
155
+ return r.json();
156
+ }
electron/audit.ts ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Audit logic: hot paths (filesystem, processes, drives, system, network, env) → Python FastAPI.
3
+ * Windows-specific shell/registry work stays in Node.
4
+ */
5
+ import * as path from 'node:path'
6
+ import * as os from 'node:os'
7
+ import { execFile, execFileSync, spawn } from 'node:child_process'
8
+ import { promisify } from 'node:util'
9
+ import { pyGet, pyPost } from './pythonBackend'
10
+
11
+ const execFileAsync = promisify(execFile)
12
+
13
+ export interface DriveInfo {
14
+ letter: string
15
+ mount: string
16
+ label: string
17
+ totalBytes: number
18
+ freeBytes: number
19
+ usedBytes: number
20
+ }
21
+
22
+ export interface DirEntry {
23
+ name: string
24
+ fullPath: string
25
+ isDirectory: boolean
26
+ sizeBytes: number
27
+ mtimeMs: number
28
+ error?: string
29
+ sizeTruncated?: boolean
30
+ }
31
+
32
+ export interface ProcessRow {
33
+ pid: number
34
+ name: string
35
+ memoryBytes: number
36
+ cpuSeconds?: number
37
+ commandLine?: string
38
+ }
39
+
40
+ export interface ServiceRow {
41
+ name: string
42
+ displayName: string
43
+ state: string
44
+ startType: string
45
+ }
46
+
47
+ export interface InstalledApp {
48
+ name: string
49
+ version: string
50
+ publisher: string
51
+ installLocation: string
52
+ uninstallString: string
53
+ estimatedSizeKb: number
54
+ }
55
+
56
+ export interface NetworkRow {
57
+ name: string
58
+ address: string
59
+ family: string
60
+ internal: boolean
61
+ mac?: string
62
+ }
63
+
64
+ function resolveSafePath(input: string): string {
65
+ const normalized = path.normalize(input)
66
+ return path.resolve(normalized)
67
+ }
68
+
69
+ export async function getDrivesWin(): Promise<DriveInfo[]> {
70
+ return pyGet<DriveInfo[]>('/api/drives')
71
+ }
72
+
73
+ export async function listDirectory(
74
+ dirPath: string,
75
+ options: { maxEntries?: number } = {}
76
+ ): Promise<DirEntry[]> {
77
+ const root = resolveSafePath(dirPath)
78
+ return pyPost<DirEntry[]>('/api/list_dir', {
79
+ path: root,
80
+ max_entries: options.maxEntries ?? 800,
81
+ })
82
+ }
83
+
84
+ export async function computeFolderSize(dirPath: string): Promise<{ bytes: number; files: number; truncated: boolean }> {
85
+ return pyPost<{ bytes: number; files: number; truncated: boolean }>('/api/folder_size', {
86
+ path: resolveSafePath(dirPath),
87
+ })
88
+ }
89
+
90
+ export async function findLargeFiles(
91
+ rootPath: string,
92
+ minBytes: number,
93
+ maxResults: number
94
+ ): Promise<{ path: string; sizeBytes: number }[]> {
95
+ return pyPost<{ path: string; sizeBytes: number }[]>('/api/large_files', {
96
+ path: resolveSafePath(rootPath),
97
+ min_bytes: minBytes,
98
+ max_results: maxResults,
99
+ })
100
+ }
101
+
102
+ export async function getProcessesWin(): Promise<ProcessRow[]> {
103
+ return pyGet<ProcessRow[]>('/api/processes')
104
+ }
105
+
106
+ export async function getServicesWin(): Promise<ServiceRow[]> {
107
+ const script = `
108
+ Get-CimInstance Win32_Service | Select-Object Name,DisplayName,State,StartMode | ConvertTo-Json -Compress
109
+ `
110
+ try {
111
+ const { stdout } = await execFileAsync(
112
+ 'powershell.exe',
113
+ ['-NoProfile', '-NonInteractive', '-Command', script],
114
+ { windowsHide: true, maxBuffer: 20 * 1024 * 1024, timeout: 120_000 }
115
+ )
116
+ const raw = JSON.parse(stdout.trim() || '[]')
117
+ const arr = Array.isArray(raw) ? raw : [raw]
118
+ return arr.map((s: Record<string, unknown>) => ({
119
+ name: String(s.Name ?? ''),
120
+ displayName: String(s.DisplayName ?? ''),
121
+ state: String(s.State ?? ''),
122
+ startType: String(s.StartMode ?? ''),
123
+ }))
124
+ } catch {
125
+ return []
126
+ }
127
+ }
128
+
129
+ export function getInstalledPrograms(): InstalledApp[] {
130
+ const script = `
131
+ $paths = @(
132
+ 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
133
+ 'HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
134
+ 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
135
+ )
136
+ Get-ItemProperty $paths -ErrorAction SilentlyContinue |
137
+ Where-Object { $_.DisplayName } |
138
+ Select-Object DisplayName, DisplayVersion, Publisher, InstallLocation, UninstallString, EstimatedSize |
139
+ ConvertTo-Json -Compress -Depth 4
140
+ `
141
+ try {
142
+ const stdout = execFileSync(
143
+ 'powershell.exe',
144
+ ['-NoProfile', '-NonInteractive', '-Command', script],
145
+ { encoding: 'utf8', windowsHide: true, maxBuffer: 50 * 1024 * 1024 }
146
+ )
147
+ const raw = JSON.parse(stdout.trim() || '[]')
148
+ const arr = Array.isArray(raw) ? raw : [raw]
149
+ const apps: InstalledApp[] = arr.map((r: Record<string, unknown>) => ({
150
+ name: String(r.DisplayName ?? ''),
151
+ version: String(r.DisplayVersion ?? ''),
152
+ publisher: String(r.Publisher ?? ''),
153
+ installLocation: String(r.InstallLocation ?? ''),
154
+ uninstallString: String(r.UninstallString ?? ''),
155
+ estimatedSizeKb: Number(r.EstimatedSize) || 0,
156
+ }))
157
+ const seen = new Set<string>()
158
+ return apps
159
+ .filter((a) => {
160
+ const k = a.name.toLowerCase()
161
+ if (!k || seen.has(k)) return false
162
+ seen.add(k)
163
+ return true
164
+ })
165
+ .sort((a, b) => a.name.localeCompare(b.name))
166
+ } catch {
167
+ return []
168
+ }
169
+ }
170
+
171
+ export async function getSystemSnapshot() {
172
+ return pyGet<{
173
+ hostname: string
174
+ platform: string
175
+ release: string
176
+ arch: string
177
+ uptimeSec: number
178
+ totalMem: number
179
+ freeMem: number
180
+ cpuModel: string
181
+ cpuCount: number
182
+ load1: number
183
+ load5: number
184
+ load15: number
185
+ userInfo: string
186
+ homedir: string
187
+ tmpdir: string
188
+ }>('/api/system')
189
+ }
190
+
191
+ export async function getNetworkInterfaces(): Promise<NetworkRow[]> {
192
+ return pyGet<NetworkRow[]>('/api/network')
193
+ }
194
+
195
+ export async function getEnvSnapshot(keys?: string[]): Promise<Record<string, string>> {
196
+ const all = await pyGet<Record<string, string>>('/api/env')
197
+ if (!keys?.length) return all
198
+ const out: Record<string, string> = {}
199
+ for (const k of keys) {
200
+ const v = all[k]
201
+ if (v !== undefined) out[k] = v
202
+ }
203
+ return out
204
+ }
205
+
206
+ export async function getStartupFolders(): Promise<{ path: string; entries: DirEntry[] }[]> {
207
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming')
208
+ const programData = process.env.PROGRAMDATA ?? 'C:\\ProgramData'
209
+ const folders = [
210
+ path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'),
211
+ path.join(programData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'StartUp'),
212
+ ]
213
+ const result: { path: string; entries: DirEntry[] }[] = []
214
+ for (const f of folders) {
215
+ const entries = await listDirectory(f, { maxEntries: 200 })
216
+ result.push({ path: f, entries })
217
+ }
218
+ return result
219
+ }
220
+
221
+ export async function getTempAudit(): Promise<{ path: string; bytes: number; files: number; truncated: boolean }[]> {
222
+ const dirs = [os.tmpdir(), path.join(os.tmpdir(), '..', 'Temp')].map((p) => path.normalize(p))
223
+ const uniq = [...new Set(dirs)]
224
+ const out: { path: string; bytes: number; files: number; truncated: boolean }[] = []
225
+ for (const d of uniq) {
226
+ try {
227
+ const r = await computeFolderSize(d)
228
+ out.push({ path: d, ...r })
229
+ } catch {
230
+ out.push({ path: d, bytes: 0, files: 0, truncated: false })
231
+ }
232
+ }
233
+ return out
234
+ }
235
+
236
+ export async function getScheduledTasksSummary(): Promise<{ name: string; state: string }[]> {
237
+ const script = `
238
+ Get-ScheduledTask | Select-Object TaskName,State | ConvertTo-Json -Compress
239
+ `
240
+ try {
241
+ const { stdout } = await execFileAsync(
242
+ 'powershell.exe',
243
+ ['-NoProfile', '-NonInteractive', '-Command', script],
244
+ { windowsHide: true, maxBuffer: 20 * 1024 * 1024, timeout: 120_000 }
245
+ )
246
+ const raw = JSON.parse(stdout.trim() || '[]')
247
+ const arr = Array.isArray(raw) ? raw : [raw]
248
+ return arr.map((t: Record<string, unknown>) => ({
249
+ name: String(t.TaskName ?? ''),
250
+ state: String(t.State ?? ''),
251
+ }))
252
+ } catch {
253
+ return []
254
+ }
255
+ }
256
+
257
+ export async function openPathInExplorer(p: string): Promise<void> {
258
+ const resolved = resolveSafePath(p)
259
+ await execFileAsync('explorer.exe', [resolved], { windowsHide: true })
260
+ }
261
+
262
+ export function killProcess(pid: number): Promise<void> {
263
+ return new Promise((resolve, reject) => {
264
+ const proc = spawn('taskkill', ['/PID', String(pid), '/F'], { windowsHide: true })
265
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`taskkill exit ${code}`))))
266
+ proc.on('error', reject)
267
+ })
268
+ }
269
+
270
+ export async function getWindowsFeaturesSnippet(): Promise<string> {
271
+ try {
272
+ const { stdout } = await execFileAsync(
273
+ 'dism.exe',
274
+ ['/Online', '/Get-Features', '/Format:Table'],
275
+ { windowsHide: true, maxBuffer: 5 * 1024 * 1024, timeout: 60_000 }
276
+ )
277
+ return stdout.slice(0, 120_000)
278
+ } catch (e) {
279
+ return String((e as Error).message)
280
+ }
281
+ }
electron/main.ts ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from 'electron'
2
+ import * as path from 'node:path'
3
+ import * as fssync from 'node:fs'
4
+ import {
5
+ getDrivesWin,
6
+ listDirectory,
7
+ computeFolderSize,
8
+ findLargeFiles,
9
+ getProcessesWin,
10
+ getServicesWin,
11
+ getInstalledPrograms,
12
+ getSystemSnapshot,
13
+ getNetworkInterfaces,
14
+ getEnvSnapshot,
15
+ getStartupFolders,
16
+ getTempAudit,
17
+ getScheduledTasksSummary,
18
+ openPathInExplorer,
19
+ killProcess,
20
+ getWindowsFeaturesSnippet,
21
+ } from './audit'
22
+ import { startPythonBackend, stopPythonBackend } from './pythonBackend'
23
+
24
+ const isDev = !!process.env.VITE_DEV_SERVER_URL
25
+
26
+ function notesPath() {
27
+ return path.join(app.getPath('userData'), 'auditor-notes.json')
28
+ }
29
+
30
+ function loadNotes(): Record<string, string> {
31
+ try {
32
+ const raw = fssync.readFileSync(notesPath(), 'utf8')
33
+ return JSON.parse(raw) as Record<string, string>
34
+ } catch {
35
+ return {}
36
+ }
37
+ }
38
+
39
+ function saveNotes(n: Record<string, string>) {
40
+ fssync.mkdirSync(path.dirname(notesPath()), { recursive: true })
41
+ fssync.writeFileSync(notesPath(), JSON.stringify(n, null, 2), 'utf8')
42
+ }
43
+
44
+ function createWindow() {
45
+ const win = new BrowserWindow({
46
+ width: 1280,
47
+ height: 800,
48
+ minWidth: 960,
49
+ minHeight: 640,
50
+ backgroundColor: '#0d0f12',
51
+ titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
52
+ webPreferences: {
53
+ preload: path.join(__dirname, 'preload.js'),
54
+ contextIsolation: true,
55
+ nodeIntegration: false,
56
+ sandbox: false,
57
+ },
58
+ })
59
+
60
+ if (isDev) {
61
+ win.loadURL(process.env.VITE_DEV_SERVER_URL!)
62
+ } else {
63
+ win.loadFile(path.join(__dirname, '../dist/index.html'))
64
+ }
65
+ }
66
+
67
+ app.whenReady().then(async () => {
68
+ try {
69
+ await startPythonBackend()
70
+ } catch (e) {
71
+ dialog.showErrorBox(
72
+ 'Python backend required',
73
+ `${String(e)}\n\nInstall: cd backend && pip install -r requirements.txt`
74
+ )
75
+ app.quit()
76
+ return
77
+ }
78
+
79
+ ipcMain.handle('audit:drives', () => getDrivesWin())
80
+ ipcMain.handle('audit:listDir', (_e, dirPath: string, opts?: { maxEntries?: number }) =>
81
+ listDirectory(dirPath, opts ?? {})
82
+ )
83
+ ipcMain.handle('audit:folderSize', (_e, dirPath: string) => computeFolderSize(dirPath))
84
+ ipcMain.handle('audit:largeFiles', (_e, rootPath: string, minBytes: number, maxResults: number) =>
85
+ findLargeFiles(rootPath, minBytes, maxResults)
86
+ )
87
+ ipcMain.handle('audit:processes', () => getProcessesWin())
88
+ ipcMain.handle('audit:services', () => getServicesWin())
89
+ ipcMain.handle('audit:installed', () => getInstalledPrograms())
90
+ ipcMain.handle('audit:system', () => getSystemSnapshot())
91
+ ipcMain.handle('audit:network', () => getNetworkInterfaces())
92
+ ipcMain.handle('audit:env', (_e, keys?: string[]) => getEnvSnapshot(keys))
93
+ ipcMain.handle('audit:startup', () => getStartupFolders())
94
+ ipcMain.handle('audit:temp', () => getTempAudit())
95
+ ipcMain.handle('audit:tasks', () => getScheduledTasksSummary())
96
+ ipcMain.handle('audit:features', () => getWindowsFeaturesSnippet())
97
+ ipcMain.handle('audit:openExplorer', (_e, p: string) => openPathInExplorer(p))
98
+ ipcMain.handle('audit:killProcess', (_e, pid: number) => killProcess(pid))
99
+ ipcMain.handle('audit:openExternal', (_e, url: string) => shell.openExternal(url))
100
+ ipcMain.handle('clipboard:writeText', (_e, text: string) => {
101
+ clipboard.writeText(text)
102
+ })
103
+
104
+ ipcMain.handle('notes:getAll', () => loadNotes())
105
+ ipcMain.handle('notes:set', (_e, key: string, value: string) => {
106
+ const all = loadNotes()
107
+ if (value.trim() === '') delete all[key]
108
+ else all[key] = value
109
+ saveNotes(all)
110
+ return all
111
+ })
112
+ ipcMain.handle('notes:delete', (_e, key: string) => {
113
+ const all = loadNotes()
114
+ delete all[key]
115
+ saveNotes(all)
116
+ return all
117
+ })
118
+
119
+ createWindow()
120
+ app.on('activate', () => {
121
+ if (BrowserWindow.getAllWindows().length === 0) createWindow()
122
+ })
123
+ })
124
+
125
+ app.on('window-all-closed', () => {
126
+ if (process.platform !== 'darwin') app.quit()
127
+ })
128
+
129
+ app.on('before-quit', () => {
130
+ stopPythonBackend()
131
+ })
electron/preload.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { contextBridge, ipcRenderer } from 'electron'
2
+
3
+ export interface AuditorAPI {
4
+ drives: () => Promise<unknown>
5
+ listDir: (dirPath: string, opts?: { maxEntries?: number }) => Promise<unknown>
6
+ folderSize: (dirPath: string) => Promise<unknown>
7
+ largeFiles: (rootPath: string, minBytes: number, maxResults: number) => Promise<unknown>
8
+ processes: () => Promise<unknown>
9
+ services: () => Promise<unknown>
10
+ installed: () => Promise<unknown>
11
+ system: () => Promise<unknown>
12
+ network: () => Promise<unknown>
13
+ env: (keys?: string[]) => Promise<unknown>
14
+ startup: () => Promise<unknown>
15
+ temp: () => Promise<unknown>
16
+ tasks: () => Promise<unknown>
17
+ features: () => Promise<string>
18
+ openExplorer: (p: string) => Promise<void>
19
+ killProcess: (pid: number) => Promise<void>
20
+ openExternal: (url: string) => Promise<void>
21
+ clipboardWriteText: (text: string) => Promise<void>
22
+ notesGetAll: () => Promise<Record<string, string>>
23
+ notesSet: (key: string, value: string) => Promise<Record<string, string>>
24
+ notesDelete: (key: string) => Promise<Record<string, string>>
25
+ }
26
+
27
+ const api: AuditorAPI = {
28
+ drives: () => ipcRenderer.invoke('audit:drives'),
29
+ listDir: (dirPath, opts) => ipcRenderer.invoke('audit:listDir', dirPath, opts),
30
+ folderSize: (dirPath) => ipcRenderer.invoke('audit:folderSize', dirPath),
31
+ largeFiles: (rootPath, minBytes, maxResults) =>
32
+ ipcRenderer.invoke('audit:largeFiles', rootPath, minBytes, maxResults),
33
+ processes: () => ipcRenderer.invoke('audit:processes'),
34
+ services: () => ipcRenderer.invoke('audit:services'),
35
+ installed: () => ipcRenderer.invoke('audit:installed'),
36
+ system: () => ipcRenderer.invoke('audit:system'),
37
+ network: () => ipcRenderer.invoke('audit:network'),
38
+ env: (keys) => ipcRenderer.invoke('audit:env', keys),
39
+ startup: () => ipcRenderer.invoke('audit:startup'),
40
+ temp: () => ipcRenderer.invoke('audit:temp'),
41
+ tasks: () => ipcRenderer.invoke('audit:tasks'),
42
+ features: () => ipcRenderer.invoke('audit:features'),
43
+ openExplorer: (p) => ipcRenderer.invoke('audit:openExplorer', p),
44
+ killProcess: (pid) => ipcRenderer.invoke('audit:killProcess', pid),
45
+ openExternal: (url) => ipcRenderer.invoke('audit:openExternal', url),
46
+ clipboardWriteText: (text) => ipcRenderer.invoke('clipboard:writeText', text),
47
+ notesGetAll: () => ipcRenderer.invoke('notes:getAll'),
48
+ notesSet: (key, value) => ipcRenderer.invoke('notes:set', key, value),
49
+ notesDelete: (key) => ipcRenderer.invoke('notes:delete', key),
50
+ }
51
+
52
+ contextBridge.exposeInMainWorld('auditor', api)
electron/pythonBackend.ts ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Spawns the FastAPI Python backend and exposes HTTP helpers for the main process.
3
+ * Python handles hot paths (filesystem sizes, processes, drives, system, network, env).
4
+ */
5
+ import { spawn, type ChildProcess } from 'node:child_process'
6
+ import * as net from 'node:net'
7
+ import * as path from 'node:path'
8
+
9
+ let child: ChildProcess | null = null
10
+ let baseUrl = ''
11
+
12
+ export function getPythonBaseUrl(): string {
13
+ return baseUrl
14
+ }
15
+
16
+ function getFreePort(): Promise<number> {
17
+ return new Promise((resolve, reject) => {
18
+ const s = net.createServer()
19
+ s.listen(0, '127.0.0.1', () => {
20
+ const a = s.address()
21
+ const p = typeof a === 'object' && a ? a.port : 0
22
+ s.close(() => resolve(p))
23
+ })
24
+ s.on('error', reject)
25
+ })
26
+ }
27
+
28
+ async function waitForHealth(port: number, timeoutMs: number): Promise<void> {
29
+ const url = `http://127.0.0.1:${port}/health`
30
+ const t0 = Date.now()
31
+ while (Date.now() - t0 < timeoutMs) {
32
+ try {
33
+ const r = await fetch(url)
34
+ if (r.ok) return
35
+ } catch {
36
+ /* retry */
37
+ }
38
+ await new Promise((r) => setTimeout(r, 120))
39
+ }
40
+ throw new Error(`Python backend did not respond at ${url}`)
41
+ }
42
+
43
+ export async function startPythonBackend(): Promise<void> {
44
+ const port = await getFreePort()
45
+ const backendDir = path.join(__dirname, '..', 'backend')
46
+ const py =
47
+ process.env.PYTHON_PATH ?? (process.platform === 'win32' ? 'python' : 'python3')
48
+
49
+ child = spawn(
50
+ py,
51
+ ['-m', 'uvicorn', 'main:app', '--host', '127.0.0.1', '--port', String(port), '--log-level', 'warning'],
52
+ {
53
+ cwd: backendDir,
54
+ env: { ...process.env, AUDITOR_PY_PORT: String(port) },
55
+ stdio: ['ignore', 'pipe', 'pipe'],
56
+ windowsHide: true,
57
+ }
58
+ )
59
+
60
+ let stderrBuf = ''
61
+ child.stderr?.on('data', (d: Buffer) => {
62
+ stderrBuf += d.toString()
63
+ if (stderrBuf.length > 8000) stderrBuf = stderrBuf.slice(-4000)
64
+ })
65
+
66
+ await new Promise<void>((resolve, reject) => {
67
+ const fail = (err: Error) => {
68
+ try {
69
+ child?.kill()
70
+ } catch {
71
+ /* ignore */
72
+ }
73
+ reject(err)
74
+ }
75
+
76
+ const t = setTimeout(() => {
77
+ fail(
78
+ new Error(
79
+ `Python backend startup timed out. Install: cd backend && pip install -r requirements.txt\n${stderrBuf}`
80
+ )
81
+ )
82
+ }, 25_000)
83
+
84
+ child!.once('error', (err) => {
85
+ clearTimeout(t)
86
+ fail(err instanceof Error ? err : new Error(String(err)))
87
+ })
88
+
89
+ waitForHealth(port, 24_000)
90
+ .then(() => {
91
+ clearTimeout(t)
92
+ resolve()
93
+ })
94
+ .catch((e) => {
95
+ clearTimeout(t)
96
+ fail(e instanceof Error ? e : new Error(String(e)))
97
+ })
98
+ })
99
+
100
+ baseUrl = `http://127.0.0.1:${port}`
101
+ }
102
+
103
+ export function stopPythonBackend(): void {
104
+ if (child) {
105
+ try {
106
+ child.kill()
107
+ } catch {
108
+ /* ignore */
109
+ }
110
+ child = null
111
+ }
112
+ baseUrl = ''
113
+ }
114
+
115
+ export async function pyGet<T>(pathname: string): Promise<T> {
116
+ const r = await fetch(`${baseUrl}${pathname}`)
117
+ if (!r.ok) {
118
+ const t = await r.text()
119
+ throw new Error(t || r.statusText)
120
+ }
121
+ return r.json() as Promise<T>
122
+ }
123
+
124
+ export async function pyPost<T>(pathname: string, body: unknown): Promise<T> {
125
+ const r = await fetch(`${baseUrl}${pathname}`, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify(body),
129
+ })
130
+ if (!r.ok) {
131
+ const t = await r.text()
132
+ throw new Error(t || r.statusText)
133
+ }
134
+ return r.json() as Promise<T>
135
+ }
electron/tsconfig.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2022"],
6
+ "outDir": "../dist-electron",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "moduleResolution": "node",
14
+ "declaration": false,
15
+ "noEmitOnError": true
16
+ },
17
+ "include": ["./**/*.ts"]
18
+ }
index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Computer Auditor</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "computer-auditor",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Computer system audit dashboard",
6
+ "main": "dist-electron/main.js",
7
+ "scripts": {
8
+ "dev": "npm run build:electron && concurrently -k \"npm run dev:vite\" \"npm run dev:electron\"",
9
+ "dev:vite": "vite",
10
+ "dev:electron": "wait-on tcp:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .",
11
+ "build:electron": "node node_modules/typescript/lib/tsc.js -p electron/tsconfig.json",
12
+ "build": "node node_modules/typescript/lib/tsc.js -p electron/tsconfig.json && vite build",
13
+ "start": "electron .",
14
+ "preview:app": "cross-env NODE_ENV=production electron ."
15
+ },
16
+ "dependencies": {
17
+ "@types/node": "^22.10.1",
18
+ "@types/react": "^18.3.12",
19
+ "@types/react-dom": "^18.3.1",
20
+ "@vitejs/plugin-react": "^4.3.4",
21
+ "concurrently": "^9.1.0",
22
+ "cross-env": "^7.0.3",
23
+ "electron": "^33.2.0",
24
+ "react": "^18.3.1",
25
+ "react-dom": "^18.3.1",
26
+ "react-icons": "^5.4.0",
27
+ "typescript": "^5.6.3",
28
+ "vite": "^5.4.11",
29
+ "wait-on": "^8.0.1"
30
+ }
31
+ }
src/App.tsx ADDED
@@ -0,0 +1,1073 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
2
+ import {
3
+ MdApps,
4
+ MdDns,
5
+ MdFolderOpen,
6
+ MdMemory,
7
+ MdRefresh,
8
+ MdSchedule,
9
+ MdSettingsApplications,
10
+ MdStorage,
11
+ MdViewModule,
12
+ MdWarning,
13
+ MdOpenInNew,
14
+ MdArrowUpward,
15
+ MdSearch,
16
+ MdNoteAdd,
17
+ MdDeleteForever,
18
+ MdSave,
19
+ MdStopCircle,
20
+ MdComputer,
21
+ MdCode,
22
+ MdContentCopy,
23
+ MdDataUsage,
24
+ } from 'react-icons/md'
25
+ import { IconButton } from './components/IconButton'
26
+ import { formatBytes, formatSizeKb, formatUptime } from './utils/format'
27
+ import type {
28
+ DirEntry,
29
+ DriveInfo,
30
+ InstalledApp,
31
+ NavId,
32
+ NetworkRow,
33
+ ProcessRow,
34
+ ServiceRow,
35
+ SystemSnapshot,
36
+ } from './types'
37
+
38
+ const api = typeof window !== 'undefined' ? window.auditor : undefined
39
+
40
+ function useNotes() {
41
+ const [map, setMap] = useState<Record<string, string>>({})
42
+ const refresh = useCallback(async () => {
43
+ if (!api) return
44
+ setMap(await api.notesGetAll())
45
+ }, [])
46
+ useEffect(() => {
47
+ void refresh()
48
+ }, [refresh])
49
+ const setNote = useCallback(
50
+ async (key: string, value: string) => {
51
+ if (!api) return
52
+ setMap(await api.notesSet(key, value))
53
+ },
54
+ []
55
+ )
56
+ const del = useCallback(
57
+ async (key: string) => {
58
+ if (!api) return
59
+ setMap(await api.notesDelete(key))
60
+ },
61
+ []
62
+ )
63
+ return { map, refresh, setNote, del }
64
+ }
65
+
66
+ const NAV: { id: NavId; label: string; icon: ReactNode }[] = [
67
+ { id: 'overview', label: 'Overview', icon: <MdViewModule /> },
68
+ { id: 'storage', label: 'Storage & volumes', icon: <MdStorage /> },
69
+ { id: 'filesystem', label: 'Folders & files', icon: <MdFolderOpen /> },
70
+ { id: 'processes', label: 'Processes', icon: <MdMemory /> },
71
+ { id: 'services', label: 'Services', icon: <MdSettingsApplications /> },
72
+ { id: 'apps', label: 'Installed software', icon: <MdApps /> },
73
+ { id: 'network', label: 'Network', icon: <MdDns /> },
74
+ { id: 'environment', label: 'Environment', icon: <MdCode /> },
75
+ { id: 'startup', label: 'Startup items', icon: <MdComputer /> },
76
+ { id: 'scheduled', label: 'Scheduled tasks', icon: <MdSchedule /> },
77
+ { id: 'features', label: 'Windows features', icon: <MdWarning /> },
78
+ ]
79
+
80
+ export default function App() {
81
+ const [nav, setNav] = useState<NavId>('overview')
82
+ const [status, setStatus] = useState('')
83
+ const [err, setErr] = useState('')
84
+ const notes = useNotes()
85
+
86
+ const [sys, setSys] = useState<SystemSnapshot | null>(null)
87
+ const [drives, setDrives] = useState<DriveInfo[]>([])
88
+ const [path, setPath] = useState('')
89
+ const [entries, setEntries] = useState<DirEntry[]>([])
90
+ const [proc, setProc] = useState<ProcessRow[]>([])
91
+ const [svc, setSvc] = useState<ServiceRow[]>([])
92
+ const [apps, setApps] = useState<InstalledApp[]>([])
93
+ const [net, setNet] = useState<NetworkRow[]>([])
94
+ const [envText, setEnvText] = useState('')
95
+ const [startupBlocks, setStartupBlocks] = useState<{ path: string; entries: DirEntry[] }[]>([])
96
+ const [tasks, setTasks] = useState<{ name: string; state: string }[]>([])
97
+ const [feat, setFeat] = useState('')
98
+ const [tempAudit, setTempAudit] = useState<{ path: string; bytes: number; files: number; truncated: boolean }[]>(
99
+ []
100
+ )
101
+
102
+ const [selectedKey, setSelectedKey] = useState<string | null>(null)
103
+ const [noteDraft, setNoteDraft] = useState('')
104
+
105
+ const [largeRoot, setLargeRoot] = useState('')
106
+ const [largeMinMb, setLargeMinMb] = useState(100)
107
+ const [largeHits, setLargeHits] = useState<{ path: string; sizeBytes: number }[]>([])
108
+ const [folderTotal, setFolderTotal] = useState<{
109
+ bytes: number
110
+ files: number
111
+ truncated: boolean
112
+ } | null>(null)
113
+
114
+ const [procFilter, setProcFilter] = useState('')
115
+ const [svcFilter, setSvcFilter] = useState('')
116
+ const [appFilter, setAppFilter] = useState('')
117
+
118
+ const clearMsg = () => {
119
+ setErr('')
120
+ setStatus('')
121
+ }
122
+
123
+ const loadSystem = useCallback(async (opts?: { silent?: boolean }) => {
124
+ if (!api) return
125
+ if (!opts?.silent) clearMsg()
126
+ try {
127
+ setSys((await api.system()) as SystemSnapshot)
128
+ if (!opts?.silent) setStatus('System snapshot updated')
129
+ } catch (e) {
130
+ if (!opts?.silent) setErr(String((e as Error).message))
131
+ }
132
+ }, [])
133
+
134
+ const loadDrives = useCallback(async () => {
135
+ if (!api) return
136
+ clearMsg()
137
+ try {
138
+ setDrives((await api.drives()) as DriveInfo[])
139
+ setStatus('Volumes refreshed')
140
+ } catch (e) {
141
+ setErr(String((e as Error).message))
142
+ }
143
+ }, [])
144
+
145
+ const loadDir = useCallback(async () => {
146
+ if (!api || !path.trim()) return
147
+ clearMsg()
148
+ try {
149
+ setStatus('Listing folder (measuring sizes)…')
150
+ setEntries((await api.listDir(path.trim(), { maxEntries: 800 })) as DirEntry[])
151
+ setStatus('Directory listed')
152
+ } catch (e) {
153
+ setErr(String((e as Error).message))
154
+ }
155
+ }, [path])
156
+
157
+ const loadProcesses = useCallback(async () => {
158
+ if (!api) return
159
+ clearMsg()
160
+ setStatus('Loading processes…')
161
+ try {
162
+ const rows = (await api.processes()) as ProcessRow[]
163
+ setProc(rows)
164
+ setStatus(`Processes loaded (${rows.length})`)
165
+ } catch (e) {
166
+ setErr(String((e as Error).message))
167
+ }
168
+ }, [])
169
+
170
+ const loadServices = useCallback(async () => {
171
+ if (!api) return
172
+ clearMsg()
173
+ setStatus('Loading services…')
174
+ try {
175
+ setSvc((await api.services()) as ServiceRow[])
176
+ setStatus('Services loaded')
177
+ } catch (e) {
178
+ setErr(String((e as Error).message))
179
+ }
180
+ }, [])
181
+
182
+ const loadApps = useCallback(async () => {
183
+ if (!api) return
184
+ clearMsg()
185
+ try {
186
+ setApps((await api.installed()) as InstalledApp[])
187
+ setStatus('Installed programs enumerated')
188
+ } catch (e) {
189
+ setErr(String((e as Error).message))
190
+ }
191
+ }, [])
192
+
193
+ const loadNetwork = useCallback(async () => {
194
+ if (!api) return
195
+ clearMsg()
196
+ try {
197
+ setNet((await api.network()) as NetworkRow[])
198
+ setStatus('Network interfaces read')
199
+ } catch (e) {
200
+ setErr(String((e as Error).message))
201
+ }
202
+ }, [])
203
+
204
+ const loadEnv = useCallback(async () => {
205
+ if (!api) return
206
+ clearMsg()
207
+ try {
208
+ const o = (await api.env()) as Record<string, string>
209
+ const lines = Object.keys(o)
210
+ .sort()
211
+ .map((k) => `${k}=${o[k]}`)
212
+ setEnvText(lines.join('\n'))
213
+ setStatus('Environment loaded')
214
+ } catch (e) {
215
+ setErr(String((e as Error).message))
216
+ }
217
+ }, [])
218
+
219
+ const loadStartup = useCallback(async () => {
220
+ if (!api) return
221
+ clearMsg()
222
+ try {
223
+ setStartupBlocks((await api.startup()) as { path: string; entries: DirEntry[] }[])
224
+ setStatus('Startup folders scanned')
225
+ } catch (e) {
226
+ setErr(String((e as Error).message))
227
+ }
228
+ }, [])
229
+
230
+ const loadTasks = useCallback(async () => {
231
+ if (!api) return
232
+ clearMsg()
233
+ setStatus('Loading scheduled tasks…')
234
+ try {
235
+ setTasks((await api.tasks()) as { name: string; state: string }[])
236
+ setStatus('Scheduled tasks loaded')
237
+ } catch (e) {
238
+ setErr(String((e as Error).message))
239
+ }
240
+ }, [])
241
+
242
+ const loadFeatures = useCallback(async () => {
243
+ if (!api) return
244
+ clearMsg()
245
+ setStatus('Querying optional features (may take a minute)…')
246
+ try {
247
+ setFeat(await api.features())
248
+ setStatus('Feature list retrieved')
249
+ } catch (e) {
250
+ setErr(String((e as Error).message))
251
+ }
252
+ }, [])
253
+
254
+ const loadTemp = useCallback(async () => {
255
+ if (!api) return
256
+ clearMsg()
257
+ setStatus('Measuring temp folders…')
258
+ try {
259
+ setTempAudit(
260
+ (await api.temp()) as { path: string; bytes: number; files: number; truncated: boolean }[]
261
+ )
262
+ setStatus('Temp audit complete')
263
+ } catch (e) {
264
+ setErr(String((e as Error).message))
265
+ }
266
+ }, [])
267
+
268
+ useEffect(() => {
269
+ if (!api) return
270
+ void loadSystem()
271
+ const t = setInterval(() => void loadSystem({ silent: true }), 60_000)
272
+ return () => clearInterval(t)
273
+ }, [loadSystem])
274
+
275
+ const [pathSeeded, setPathSeeded] = useState(false)
276
+ useEffect(() => {
277
+ if (!api || pathSeeded) return
278
+ if (sys?.homedir) {
279
+ setPath(sys.homedir)
280
+ setPathSeeded(true)
281
+ }
282
+ }, [api, sys, pathSeeded])
283
+
284
+ useEffect(() => {
285
+ if (selectedKey && notes.map[selectedKey] !== undefined) setNoteDraft(notes.map[selectedKey] ?? '')
286
+ else if (selectedKey) setNoteDraft('')
287
+ }, [selectedKey, notes.map])
288
+
289
+ const selectForNote = (key: string) => {
290
+ setSelectedKey(key)
291
+ setNoteDraft(notes.map[key] ?? '')
292
+ }
293
+
294
+ const saveNote = async () => {
295
+ if (!selectedKey) return
296
+ await notes.setNote(selectedKey, noteDraft)
297
+ setStatus('Note saved')
298
+ }
299
+
300
+ const copyText = async (text: string) => {
301
+ if (!api) return
302
+ try {
303
+ if (navigator.clipboard?.writeText) {
304
+ await navigator.clipboard.writeText(text)
305
+ } else {
306
+ await api.clipboardWriteText(text)
307
+ }
308
+ setStatus('Copied to clipboard')
309
+ } catch {
310
+ try {
311
+ await api.clipboardWriteText(text)
312
+ setStatus('Copied to clipboard')
313
+ } catch (e) {
314
+ setErr(String((e as Error).message))
315
+ }
316
+ }
317
+ }
318
+
319
+ const computeFolderTotal = async () => {
320
+ if (!api || !path.trim()) return
321
+ clearMsg()
322
+ setStatus('Computing folder size (capped walk)…')
323
+ try {
324
+ const r = (await api.folderSize(path.trim())) as { bytes: number; files: number; truncated: boolean }
325
+ setFolderTotal(r)
326
+ setStatus(
327
+ `Folder total ${formatSizeKb(r.bytes, { truncated: r.truncated })} — ${r.files} files counted${r.truncated ? ' (hit file cap)' : ''}`
328
+ )
329
+ } catch (e) {
330
+ setErr(String((e as Error).message))
331
+ }
332
+ }
333
+
334
+ const runLargeScan = async () => {
335
+ if (!api) return
336
+ const root = largeRoot.trim() || path.trim()
337
+ if (!root) return
338
+ clearMsg()
339
+ setStatus('Scanning for large files…')
340
+ try {
341
+ const hits = (await api.largeFiles(root, largeMinMb * 1024 * 1024, 80)) as {
342
+ path: string
343
+ sizeBytes: number
344
+ }[]
345
+ setLargeHits(hits)
346
+ setStatus(`Found ${hits.length} files`)
347
+ } catch (e) {
348
+ setErr(String((e as Error).message))
349
+ }
350
+ }
351
+
352
+ const killPid = async (pid: number) => {
353
+ if (!api) return
354
+ if (!window.confirm(`End process ${pid}? Unsaved data in that process may be lost.`)) return
355
+ try {
356
+ await api.killProcess(pid)
357
+ setStatus(`Sent terminate to PID ${pid}`)
358
+ void loadProcesses()
359
+ } catch (e) {
360
+ setErr(String((e as Error).message))
361
+ }
362
+ }
363
+
364
+ const openExplorer = async (p: string) => {
365
+ if (!api) return
366
+ try {
367
+ await api.openExplorer(p)
368
+ } catch (e) {
369
+ setErr(String((e as Error).message))
370
+ }
371
+ }
372
+
373
+ const filteredProc = useMemo(() => {
374
+ const q = procFilter.trim().toLowerCase()
375
+ if (!q) return proc
376
+ return proc.filter(
377
+ (p) =>
378
+ p.name.toLowerCase().includes(q) ||
379
+ String(p.pid).includes(q) ||
380
+ (p.commandLine ?? '').toLowerCase().includes(q)
381
+ )
382
+ }, [proc, procFilter])
383
+
384
+ const filteredSvc = useMemo(() => {
385
+ const q = svcFilter.trim().toLowerCase()
386
+ if (!q) return svc
387
+ return svc.filter(
388
+ (s) =>
389
+ s.name.toLowerCase().includes(q) ||
390
+ s.displayName.toLowerCase().includes(q) ||
391
+ s.state.toLowerCase().includes(q)
392
+ )
393
+ }, [svc, svcFilter])
394
+
395
+ const filteredApps = useMemo(() => {
396
+ const q = appFilter.trim().toLowerCase()
397
+ if (!q) return apps
398
+ return apps.filter(
399
+ (a) =>
400
+ a.name.toLowerCase().includes(q) ||
401
+ a.publisher.toLowerCase().includes(q) ||
402
+ a.installLocation.toLowerCase().includes(q)
403
+ )
404
+ }, [apps, appFilter])
405
+
406
+ if (!api) {
407
+ return (
408
+ <div className="content-scroll">
409
+ <p className="status-msg error">
410
+ This UI must be run inside the Electron shell so it can reach your system. Use{' '}
411
+ <span className="mono">npm run dev</span>.
412
+ </p>
413
+ </div>
414
+ )
415
+ }
416
+
417
+ const memUsed = sys ? sys.totalMem - sys.freeMem : 0
418
+ const memPct = sys && sys.totalMem ? Math.round((memUsed / sys.totalMem) * 100) : 0
419
+
420
+ return (
421
+ <div className="app-shell">
422
+ <aside className="sidebar">
423
+ <div className="sidebar-brand">
424
+ <h1>Computer Auditor</h1>
425
+ <p>System inspection dashboard</p>
426
+ </div>
427
+ {NAV.map((item) => (
428
+ <button
429
+ key={item.id}
430
+ type="button"
431
+ className={`nav-item ${nav === item.id ? 'active' : ''}`}
432
+ onClick={() => setNav(item.id)}
433
+ >
434
+ {item.icon}
435
+ {item.label}
436
+ </button>
437
+ ))}
438
+ </aside>
439
+
440
+ <div className="main">
441
+ <div className="toolbar">
442
+ <h2 className="panel-title">{NAV.find((n) => n.id === nav)?.label}</h2>
443
+ <span className="toolbar-spacer" />
444
+ <IconButton label="Refresh system snapshot" onClick={() => void loadSystem()}>
445
+ <MdRefresh />
446
+ </IconButton>
447
+ {status && <span className="status-msg">{status}</span>}
448
+ {err && <span className="status-msg error">{err}</span>}
449
+ </div>
450
+
451
+ <div className="content-scroll">
452
+ {nav === 'overview' && (
453
+ <>
454
+ <div className="card-grid">
455
+ <div className="card">
456
+ <div className="card-label">Host</div>
457
+ <div className="card-value">{sys?.hostname ?? '—'}</div>
458
+ <div className="card-sub mono">{sys?.userInfo}</div>
459
+ </div>
460
+ <div className="card">
461
+ <div className="card-label">Platform</div>
462
+ <div className="card-value">{sys?.platform ?? '—'}</div>
463
+ <div className="card-sub mono">
464
+ {sys?.release} / {sys?.arch}
465
+ </div>
466
+ </div>
467
+ <div className="card">
468
+ <div className="card-label">Uptime</div>
469
+ <div className="card-value">{sys ? formatUptime(sys.uptimeSec) : '—'}</div>
470
+ </div>
471
+ <div className="card">
472
+ <div className="card-label">Memory</div>
473
+ <div className="card-value">
474
+ {sys ? `${memPct}% used` : '—'}
475
+ </div>
476
+ <div className="card-sub">
477
+ {sys ? `${formatBytes(memUsed)} / ${formatBytes(sys.totalMem)}` : ''}
478
+ </div>
479
+ {sys && (
480
+ <div className="bar-track">
481
+ <div className="bar-fill" style={{ width: `${memPct}%` }} />
482
+ </div>
483
+ )}
484
+ </div>
485
+ <div className="card">
486
+ <div className="card-label">CPU</div>
487
+ <div className="card-value">{sys?.cpuCount ?? '—'} logical</div>
488
+ <div className="card-sub">{sys?.cpuModel}</div>
489
+ </div>
490
+ <div className="card">
491
+ <div className="card-label">Home & temp</div>
492
+ <div className="card-sub mono">{sys?.homedir}</div>
493
+ <div className="card-sub mono">{sys?.tmpdir}</div>
494
+ </div>
495
+ </div>
496
+
497
+ <div style={{ marginTop: 20 }}>
498
+ <div className="input-row">
499
+ <IconButton label="Load logical volumes" onClick={() => void loadDrives()}>
500
+ <MdStorage />
501
+ </IconButton>
502
+ <IconButton label="Measure temp directories" onClick={() => void loadTemp()}>
503
+ <MdFolderOpen />
504
+ </IconButton>
505
+ </div>
506
+ {drives.length > 0 && (
507
+ <table className="data-table">
508
+ <thead>
509
+ <tr>
510
+ <th>Volume</th>
511
+ <th>Label</th>
512
+ <th>Used</th>
513
+ <th>Free</th>
514
+ <th>Total</th>
515
+ <th />
516
+ </tr>
517
+ </thead>
518
+ <tbody>
519
+ {drives.map((d) => {
520
+ const pct = d.totalBytes ? Math.round((d.usedBytes / d.totalBytes) * 100) : 0
521
+ return (
522
+ <tr key={d.letter}>
523
+ <td className="mono">{d.letter}</td>
524
+ <td>{d.label || '—'}</td>
525
+ <td>
526
+ {formatBytes(d.usedBytes)} ({pct}%)
527
+ </td>
528
+ <td>{formatBytes(d.freeBytes)}</td>
529
+ <td>{formatBytes(d.totalBytes)}</td>
530
+ <td>
531
+ <IconButton
532
+ label="Open in Explorer"
533
+ onClick={() => void openExplorer(d.mount + '\\')}
534
+ >
535
+ <MdOpenInNew />
536
+ </IconButton>
537
+ <IconButton
538
+ label="Attach note to this volume"
539
+ onClick={() => selectForNote(`drive:${d.letter}`)}
540
+ >
541
+ <MdNoteAdd />
542
+ </IconButton>
543
+ </td>
544
+ </tr>
545
+ )
546
+ })}
547
+ </tbody>
548
+ </table>
549
+ )}
550
+ {tempAudit.length > 0 && (
551
+ <table className="data-table" style={{ marginTop: 16 }}>
552
+ <thead>
553
+ <tr>
554
+ <th>Temp path</th>
555
+ <th>Size</th>
556
+ <th>Files (sampled)</th>
557
+ </tr>
558
+ </thead>
559
+ <tbody>
560
+ {tempAudit.map((t) => (
561
+ <tr key={t.path}>
562
+ <td className="mono">{t.path}</td>
563
+ <td>{formatBytes(t.bytes)}</td>
564
+ <td>
565
+ {t.files}
566
+ {t.truncated ? ' (cap reached)' : ''}
567
+ </td>
568
+ </tr>
569
+ ))}
570
+ </tbody>
571
+ </table>
572
+ )}
573
+ </div>
574
+ </>
575
+ )}
576
+
577
+ {nav === 'storage' && (
578
+ <>
579
+ <div className="input-row">
580
+ <IconButton label="Refresh drive list" onClick={() => void loadDrives()}>
581
+ <MdRefresh />
582
+ </IconButton>
583
+ </div>
584
+ <table className="data-table">
585
+ <thead>
586
+ <tr>
587
+ <th>Volume</th>
588
+ <th>Label</th>
589
+ <th>Used</th>
590
+ <th>Free</th>
591
+ <th>Total</th>
592
+ <th />
593
+ </tr>
594
+ </thead>
595
+ <tbody>
596
+ {drives.map((d) => {
597
+ const pct = d.totalBytes ? Math.round((d.usedBytes / d.totalBytes) * 100) : 0
598
+ return (
599
+ <tr key={d.letter}>
600
+ <td className="mono">{d.letter}</td>
601
+ <td>{d.label || '—'}</td>
602
+ <td>
603
+ {formatBytes(d.usedBytes)} ({pct}%)
604
+ <div className="bar-track">
605
+ <div className="bar-fill" style={{ width: `${pct}%` }} />
606
+ </div>
607
+ </td>
608
+ <td>{formatBytes(d.freeBytes)}</td>
609
+ <td>{formatBytes(d.totalBytes)}</td>
610
+ <td>
611
+ <IconButton label="Open volume" onClick={() => void openExplorer(d.mount + '\\')}>
612
+ <MdOpenInNew />
613
+ </IconButton>
614
+ <IconButton label="Note" onClick={() => selectForNote(`drive:${d.letter}`)}>
615
+ <MdNoteAdd />
616
+ </IconButton>
617
+ </td>
618
+ </tr>
619
+ )
620
+ })}
621
+ </tbody>
622
+ </table>
623
+ </>
624
+ )}
625
+
626
+ {nav === 'filesystem' && (
627
+ <>
628
+ <div className="input-row">
629
+ <input
630
+ type="text"
631
+ value={path}
632
+ onChange={(e) => setPath(e.target.value)}
633
+ placeholder="Folder path"
634
+ className="mono"
635
+ style={{ flex: 1, minWidth: 280 }}
636
+ />
637
+ <IconButton label="List directory" onClick={() => void loadDir()}>
638
+ <MdRefresh />
639
+ </IconButton>
640
+ <IconButton label="Parent folder" onClick={() => setPath((p) => p.replace(/[/\\][^/\\]*$/, ''))}>
641
+ <MdArrowUpward />
642
+ </IconButton>
643
+ <IconButton label="Open in Explorer" onClick={() => void openExplorer(path)}>
644
+ <MdOpenInNew />
645
+ </IconButton>
646
+ <IconButton label="Note on this path" onClick={() => selectForNote(`path:${path}`)}>
647
+ <MdNoteAdd />
648
+ </IconButton>
649
+ <IconButton label="Copy current path" onClick={() => void copyText(path)}>
650
+ <MdContentCopy />
651
+ </IconButton>
652
+ <IconButton label="Compute folder size (recursive, capped)" onClick={() => void computeFolderTotal()}>
653
+ <MdDataUsage />
654
+ </IconButton>
655
+ </div>
656
+ {folderTotal && (
657
+ <p className="status-msg" style={{ marginTop: 0 }}>
658
+ Aggregated: {formatSizeKb(folderTotal.bytes, { truncated: folderTotal.truncated })} —{' '}
659
+ {folderTotal.files} files
660
+ {folderTotal.truncated ? ' (enumeration capped for safety)' : ''}
661
+ </p>
662
+ )}
663
+ <div className="input-row">
664
+ <input
665
+ type="text"
666
+ value={largeRoot}
667
+ onChange={(e) => setLargeRoot(e.target.value)}
668
+ placeholder="Large-file scan root (defaults to path above)"
669
+ className="mono"
670
+ style={{ flex: 1, minWidth: 240 }}
671
+ />
672
+ <input
673
+ type="number"
674
+ value={largeMinMb}
675
+ min={1}
676
+ onChange={(e) => setLargeMinMb(Number(e.target.value) || 100)}
677
+ title="Minimum size MB"
678
+ />
679
+ <IconButton label="Find large files" onClick={() => void runLargeScan()}>
680
+ <MdSearch />
681
+ </IconButton>
682
+ </div>
683
+ <table className="data-table">
684
+ <thead>
685
+ <tr>
686
+ <th>Name</th>
687
+ <th>Type</th>
688
+ <th>Size (KB)</th>
689
+ <th />
690
+ </tr>
691
+ </thead>
692
+ <tbody>
693
+ {entries.map((e) => (
694
+ <tr
695
+ key={e.fullPath}
696
+ className={selectedKey === `path:${e.fullPath}` ? 'selected' : ''}
697
+ onDoubleClick={() => e.isDirectory && setPath(e.fullPath)}
698
+ >
699
+ <td className="mono">{e.name}</td>
700
+ <td>{e.isDirectory ? 'Folder' : 'File'}</td>
701
+ <td>
702
+ {e.error
703
+ ? e.error
704
+ : formatSizeKb(e.sizeBytes, { truncated: e.sizeTruncated })}
705
+ </td>
706
+ <td>
707
+ {e.isDirectory && (
708
+ <IconButton label="Enter folder" onClick={() => setPath(e.fullPath)}>
709
+ <MdFolderOpen />
710
+ </IconButton>
711
+ )}
712
+ <IconButton label="Explorer" onClick={() => void openExplorer(e.fullPath)}>
713
+ <MdOpenInNew />
714
+ </IconButton>
715
+ <IconButton label="Note" onClick={() => selectForNote(`path:${e.fullPath}`)}>
716
+ <MdNoteAdd />
717
+ </IconButton>
718
+ </td>
719
+ </tr>
720
+ ))}
721
+ </tbody>
722
+ </table>
723
+ {largeHits.length > 0 && (
724
+ <>
725
+ <h3 style={{ marginTop: 24, fontSize: 13 }}>Large files</h3>
726
+ <table className="data-table">
727
+ <thead>
728
+ <tr>
729
+ <th>Path</th>
730
+ <th>Size (KB)</th>
731
+ <th />
732
+ </tr>
733
+ </thead>
734
+ <tbody>
735
+ {largeHits.map((h) => (
736
+ <tr key={h.path}>
737
+ <td className="mono">{h.path}</td>
738
+ <td>{formatSizeKb(h.sizeBytes)}</td>
739
+ <td>
740
+ <IconButton label="Open location" onClick={() => void openExplorer(h.path)}>
741
+ <MdOpenInNew />
742
+ </IconButton>
743
+ <IconButton label="Note" onClick={() => selectForNote(`path:${h.path}`)}>
744
+ <MdNoteAdd />
745
+ </IconButton>
746
+ </td>
747
+ </tr>
748
+ ))}
749
+ </tbody>
750
+ </table>
751
+ </>
752
+ )}
753
+ </>
754
+ )}
755
+
756
+ {nav === 'processes' && (
757
+ <>
758
+ <div className="input-row">
759
+ <IconButton label="Reload process list" onClick={() => void loadProcesses()}>
760
+ <MdRefresh />
761
+ </IconButton>
762
+ <input
763
+ type="search"
764
+ placeholder="Filter name, PID, command line"
765
+ value={procFilter}
766
+ onChange={(e) => setProcFilter(e.target.value)}
767
+ style={{ minWidth: 240, flex: 1 }}
768
+ />
769
+ </div>
770
+ <table className="data-table">
771
+ <thead>
772
+ <tr>
773
+ <th>PID</th>
774
+ <th>Name</th>
775
+ <th>Memory</th>
776
+ <th>CPU (session est.)</th>
777
+ <th />
778
+ </tr>
779
+ </thead>
780
+ <tbody>
781
+ {filteredProc.map((p) => (
782
+ <tr
783
+ key={p.pid}
784
+ className={selectedKey === `proc:${p.pid}` ? 'selected' : ''}
785
+ title={p.commandLine}
786
+ >
787
+ <td className="mono">{p.pid}</td>
788
+ <td>{p.name}</td>
789
+ <td>{formatBytes(p.memoryBytes)}</td>
790
+ <td>{p.cpuSeconds != null ? `${p.cpuSeconds}s` : '—'}</td>
791
+ <td>
792
+ <IconButton label="Add note for process" onClick={() => selectForNote(`proc:${p.pid}`)}>
793
+ <MdNoteAdd />
794
+ </IconButton>
795
+ <IconButton label="End process" onClick={() => void killPid(p.pid)}>
796
+ <MdStopCircle />
797
+ </IconButton>
798
+ </td>
799
+ </tr>
800
+ ))}
801
+ </tbody>
802
+ </table>
803
+ </>
804
+ )}
805
+
806
+ {nav === 'services' && (
807
+ <>
808
+ <div className="input-row">
809
+ <IconButton label="Reload services" onClick={() => void loadServices()}>
810
+ <MdRefresh />
811
+ </IconButton>
812
+ <input
813
+ type="search"
814
+ placeholder="Filter"
815
+ value={svcFilter}
816
+ onChange={(e) => setSvcFilter(e.target.value)}
817
+ style={{ minWidth: 240, flex: 1 }}
818
+ />
819
+ </div>
820
+ <table className="data-table">
821
+ <thead>
822
+ <tr>
823
+ <th>Name</th>
824
+ <th>Display name</th>
825
+ <th>State</th>
826
+ <th>Start</th>
827
+ <th />
828
+ </tr>
829
+ </thead>
830
+ <tbody>
831
+ {filteredSvc.map((s) => (
832
+ <tr key={s.name} className={selectedKey === `svc:${s.name}` ? 'selected' : ''}>
833
+ <td className="mono">{s.name}</td>
834
+ <td>{s.displayName}</td>
835
+ <td>{s.state}</td>
836
+ <td>{s.startType}</td>
837
+ <td>
838
+ <IconButton label="Note" onClick={() => selectForNote(`svc:${s.name}`)}>
839
+ <MdNoteAdd />
840
+ </IconButton>
841
+ </td>
842
+ </tr>
843
+ ))}
844
+ </tbody>
845
+ </table>
846
+ </>
847
+ )}
848
+
849
+ {nav === 'apps' && (
850
+ <>
851
+ <div className="input-row">
852
+ <IconButton label="Reload installed programs" onClick={() => void loadApps()}>
853
+ <MdRefresh />
854
+ </IconButton>
855
+ <input
856
+ type="search"
857
+ placeholder="Filter"
858
+ value={appFilter}
859
+ onChange={(e) => setAppFilter(e.target.value)}
860
+ style={{ minWidth: 240, flex: 1 }}
861
+ />
862
+ </div>
863
+ <table className="data-table">
864
+ <thead>
865
+ <tr>
866
+ <th>Name</th>
867
+ <th>Version</th>
868
+ <th>Publisher</th>
869
+ <th>Location</th>
870
+ <th>Est. size</th>
871
+ <th />
872
+ </tr>
873
+ </thead>
874
+ <tbody>
875
+ {filteredApps.map((a) => (
876
+ <tr
877
+ key={a.name + a.version}
878
+ className={selectedKey === `app:${a.name}` ? 'selected' : ''}
879
+ >
880
+ <td>{a.name}</td>
881
+ <td className="mono">{a.version}</td>
882
+ <td>{a.publisher}</td>
883
+ <td className="mono">{a.installLocation || '—'}</td>
884
+ <td>{a.estimatedSizeKb ? `${(a.estimatedSizeKb / 1024).toFixed(1)} MB` : '—'}</td>
885
+ <td>
886
+ {a.installLocation ? (
887
+ <IconButton label="Open install folder" onClick={() => void openExplorer(a.installLocation)}>
888
+ <MdOpenInNew />
889
+ </IconButton>
890
+ ) : null}
891
+ <IconButton label="Note" onClick={() => selectForNote(`app:${a.name}`)}>
892
+ <MdNoteAdd />
893
+ </IconButton>
894
+ </td>
895
+ </tr>
896
+ ))}
897
+ </tbody>
898
+ </table>
899
+ </>
900
+ )}
901
+
902
+ {nav === 'network' && (
903
+ <>
904
+ <div className="input-row">
905
+ <IconButton label="Reload interfaces" onClick={() => void loadNetwork()}>
906
+ <MdRefresh />
907
+ </IconButton>
908
+ </div>
909
+ <table className="data-table">
910
+ <thead>
911
+ <tr>
912
+ <th>Interface</th>
913
+ <th>Address</th>
914
+ <th>Family</th>
915
+ <th>MAC</th>
916
+ <th>Internal</th>
917
+ <th />
918
+ </tr>
919
+ </thead>
920
+ <tbody>
921
+ {net.map((n, i) => (
922
+ <tr key={`${n.name}-${n.address}-${i}`}>
923
+ <td>{n.name}</td>
924
+ <td className="mono">{n.address}</td>
925
+ <td>{n.family}</td>
926
+ <td className="mono">{n.mac ?? '—'}</td>
927
+ <td>{n.internal ? 'yes' : 'no'}</td>
928
+ <td>
929
+ <IconButton label="Note" onClick={() => selectForNote(`net:${n.name}:${n.address}`)}>
930
+ <MdNoteAdd />
931
+ </IconButton>
932
+ </td>
933
+ </tr>
934
+ ))}
935
+ </tbody>
936
+ </table>
937
+ </>
938
+ )}
939
+
940
+ {nav === 'environment' && (
941
+ <>
942
+ <div className="input-row">
943
+ <IconButton label="Reload environment block" onClick={() => void loadEnv()}>
944
+ <MdRefresh />
945
+ </IconButton>
946
+ <IconButton label="Note on whole environment view" onClick={() => selectForNote('env:all')}>
947
+ <MdNoteAdd />
948
+ </IconButton>
949
+ </div>
950
+ <textarea className="mono" value={envText} readOnly spellCheck={false} style={{ minHeight: 400 }} />
951
+ </>
952
+ )}
953
+
954
+ {nav === 'startup' && (
955
+ <>
956
+ <div className="input-row">
957
+ <IconButton label="Scan startup folders" onClick={() => void loadStartup()}>
958
+ <MdRefresh />
959
+ </IconButton>
960
+ </div>
961
+ {startupBlocks.map((block) => (
962
+ <div key={block.path} style={{ marginBottom: 20 }}>
963
+ <div className="card-label mono" style={{ marginBottom: 8 }}>
964
+ {block.path}
965
+ </div>
966
+ <table className="data-table">
967
+ <thead>
968
+ <tr>
969
+ <th>Item</th>
970
+ <th>Type</th>
971
+ <th />
972
+ </tr>
973
+ </thead>
974
+ <tbody>
975
+ {block.entries.map((e) => (
976
+ <tr key={e.fullPath}>
977
+ <td className="mono">{e.name}</td>
978
+ <td>{e.isDirectory ? 'Folder' : 'File'}</td>
979
+ <td>
980
+ <IconButton label="Open" onClick={() => void openExplorer(e.fullPath)}>
981
+ <MdOpenInNew />
982
+ </IconButton>
983
+ <IconButton label="Note" onClick={() => selectForNote(`path:${e.fullPath}`)}>
984
+ <MdNoteAdd />
985
+ </IconButton>
986
+ </td>
987
+ </tr>
988
+ ))}
989
+ </tbody>
990
+ </table>
991
+ </div>
992
+ ))}
993
+ </>
994
+ )}
995
+
996
+ {nav === 'scheduled' && (
997
+ <>
998
+ <div className="input-row">
999
+ <IconButton label="Reload tasks" onClick={() => void loadTasks()}>
1000
+ <MdRefresh />
1001
+ </IconButton>
1002
+ </div>
1003
+ <table className="data-table">
1004
+ <thead>
1005
+ <tr>
1006
+ <th>Task</th>
1007
+ <th>State</th>
1008
+ <th />
1009
+ </tr>
1010
+ </thead>
1011
+ <tbody>
1012
+ {tasks.map((t) => (
1013
+ <tr key={t.name} className={selectedKey === `task:${t.name}` ? 'selected' : ''}>
1014
+ <td className="mono">{t.name}</td>
1015
+ <td>{t.state}</td>
1016
+ <td>
1017
+ <IconButton label="Note" onClick={() => selectForNote(`task:${t.name}`)}>
1018
+ <MdNoteAdd />
1019
+ </IconButton>
1020
+ </td>
1021
+ </tr>
1022
+ ))}
1023
+ </tbody>
1024
+ </table>
1025
+ </>
1026
+ )}
1027
+
1028
+ {nav === 'features' && (
1029
+ <>
1030
+ <div className="input-row">
1031
+ <IconButton label="Load DISM feature table" onClick={() => void loadFeatures()}>
1032
+ <MdRefresh />
1033
+ </IconButton>
1034
+ <IconButton label="Note on features output" onClick={() => selectForNote('features:snippet')}>
1035
+ <MdNoteAdd />
1036
+ </IconButton>
1037
+ </div>
1038
+ <pre className="features-pre mono">{feat || 'Run refresh to pull optional features snapshot.'}</pre>
1039
+ </>
1040
+ )}
1041
+ </div>
1042
+
1043
+ <footer className="notes-panel">
1044
+ <header>
1045
+ <strong>Notes</strong>
1046
+ <span className="notes-key">{selectedKey ?? 'Select a row or volume to attach a note key.'}</span>
1047
+ </header>
1048
+ <textarea
1049
+ placeholder="Audit notes for the selected item…"
1050
+ value={noteDraft}
1051
+ onChange={(e) => setNoteDraft(e.target.value)}
1052
+ disabled={!selectedKey}
1053
+ />
1054
+ <div className="input-row" style={{ marginBottom: 0 }}>
1055
+ <IconButton label="Save note" onClick={() => void saveNote()} disabled={!selectedKey}>
1056
+ <MdSave />
1057
+ </IconButton>
1058
+ <IconButton
1059
+ label="Clear note for key"
1060
+ onClick={() => selectedKey && void notes.del(selectedKey)}
1061
+ disabled={!selectedKey}
1062
+ >
1063
+ <MdDeleteForever />
1064
+ </IconButton>
1065
+ <IconButton label="Reload notes from disk" onClick={() => void notes.refresh()}>
1066
+ <MdRefresh />
1067
+ </IconButton>
1068
+ </div>
1069
+ </footer>
1070
+ </div>
1071
+ </div>
1072
+ )
1073
+ }
src/components/IconButton.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ReactNode } from 'react'
2
+
3
+ type Props = {
4
+ label: string
5
+ onClick?: () => void
6
+ disabled?: boolean
7
+ children: ReactNode
8
+ className?: string
9
+ }
10
+
11
+ export function IconButton({ label, onClick, disabled, children, className = '' }: Props) {
12
+ return (
13
+ <span className={`icon-btn-wrap ${className}`.trim()}>
14
+ <button
15
+ type="button"
16
+ className="icon-btn"
17
+ aria-label={label}
18
+ title={label}
19
+ onClick={onClick}
20
+ disabled={disabled}
21
+ >
22
+ {children}
23
+ </button>
24
+ <span className="icon-btn-tooltip" role="tooltip">
25
+ {label}
26
+ </span>
27
+ </span>
28
+ )
29
+ }
src/env.d.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface AuditorBridge {
4
+ drives: () => Promise<unknown>
5
+ listDir: (dirPath: string, opts?: { maxEntries?: number }) => Promise<unknown>
6
+ folderSize: (dirPath: string) => Promise<unknown>
7
+ largeFiles: (rootPath: string, minBytes: number, maxResults: number) => Promise<unknown>
8
+ processes: () => Promise<unknown>
9
+ services: () => Promise<unknown>
10
+ installed: () => Promise<unknown>
11
+ system: () => Promise<unknown>
12
+ network: () => Promise<unknown>
13
+ env: (keys?: string[]) => Promise<unknown>
14
+ startup: () => Promise<unknown>
15
+ temp: () => Promise<unknown>
16
+ tasks: () => Promise<unknown>
17
+ features: () => Promise<string>
18
+ openExplorer: (p: string) => Promise<void>
19
+ killProcess: (pid: number) => Promise<void>
20
+ openExternal: (url: string) => Promise<void>
21
+ clipboardWriteText: (text: string) => Promise<void>
22
+ notesGetAll: () => Promise<Record<string, string>>
23
+ notesSet: (key: string, value: string) => Promise<Record<string, string>>
24
+ notesDelete: (key: string) => Promise<Record<string, string>>
25
+ }
26
+
27
+ declare global {
28
+ interface Window {
29
+ auditor: AuditorBridge
30
+ }
31
+ }
32
+
33
+ export {}
src/index.css ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #0d0f12;
3
+ --bg-panel: #12151a;
4
+ --bg-elevated: #181c22;
5
+ --border: #2a3038;
6
+ --text: #e8eaed;
7
+ --text-muted: #8b939e;
8
+ --accent: #3d8fd1;
9
+ --accent-dim: #2a6a9a;
10
+ --danger: #c94c4c;
11
+ --row-hover: #1e242c;
12
+ --font: 'Segoe UI', system-ui, -apple-system, sans-serif;
13
+ --radius: 6px;
14
+ --sidebar-w: 220px;
15
+ }
16
+
17
+ *,
18
+ *::before,
19
+ *::after {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ html,
24
+ body,
25
+ #root {
26
+ height: 100%;
27
+ margin: 0;
28
+ }
29
+
30
+ body {
31
+ font-family: var(--font);
32
+ font-size: 13px;
33
+ line-height: 1.45;
34
+ color: var(--text);
35
+ background: var(--bg);
36
+ -webkit-font-smoothing: antialiased;
37
+ }
38
+
39
+ .app-shell {
40
+ display: flex;
41
+ height: 100%;
42
+ min-height: 0;
43
+ }
44
+
45
+ .sidebar {
46
+ width: var(--sidebar-w);
47
+ flex-shrink: 0;
48
+ background: var(--bg-panel);
49
+ border-right: 1px solid var(--border);
50
+ display: flex;
51
+ flex-direction: column;
52
+ padding: 12px 0;
53
+ }
54
+
55
+ .sidebar-brand {
56
+ padding: 8px 16px 16px;
57
+ border-bottom: 1px solid var(--border);
58
+ margin-bottom: 8px;
59
+ }
60
+
61
+ .sidebar-brand h1 {
62
+ margin: 0;
63
+ font-size: 14px;
64
+ font-weight: 600;
65
+ letter-spacing: 0.02em;
66
+ color: var(--text);
67
+ }
68
+
69
+ .sidebar-brand p {
70
+ margin: 4px 0 0;
71
+ font-size: 11px;
72
+ color: var(--text-muted);
73
+ }
74
+
75
+ .nav-item {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 10px;
79
+ width: 100%;
80
+ padding: 10px 16px;
81
+ border: none;
82
+ background: transparent;
83
+ color: var(--text-muted);
84
+ font: inherit;
85
+ text-align: left;
86
+ cursor: pointer;
87
+ border-left: 3px solid transparent;
88
+ }
89
+
90
+ .nav-item:hover {
91
+ background: var(--row-hover);
92
+ color: var(--text);
93
+ }
94
+
95
+ .nav-item.active {
96
+ color: var(--text);
97
+ background: var(--bg-elevated);
98
+ border-left-color: var(--accent);
99
+ }
100
+
101
+ .nav-item svg {
102
+ flex-shrink: 0;
103
+ font-size: 16px;
104
+ opacity: 0.85;
105
+ }
106
+
107
+ .main {
108
+ flex: 1;
109
+ min-width: 0;
110
+ display: flex;
111
+ flex-direction: column;
112
+ min-height: 0;
113
+ }
114
+
115
+ .toolbar {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ padding: 10px 16px;
120
+ background: var(--bg-panel);
121
+ border-bottom: 1px solid var(--border);
122
+ flex-shrink: 0;
123
+ }
124
+
125
+ .toolbar-spacer {
126
+ flex: 1;
127
+ }
128
+
129
+ .panel-title {
130
+ margin: 0;
131
+ font-size: 15px;
132
+ font-weight: 600;
133
+ }
134
+
135
+ .content-scroll {
136
+ flex: 1;
137
+ overflow: auto;
138
+ padding: 16px;
139
+ min-height: 0;
140
+ }
141
+
142
+ .card-grid {
143
+ display: grid;
144
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
145
+ gap: 12px;
146
+ }
147
+
148
+ .card {
149
+ background: var(--bg-panel);
150
+ border: 1px solid var(--border);
151
+ border-radius: var(--radius);
152
+ padding: 14px 16px;
153
+ }
154
+
155
+ .card-label {
156
+ font-size: 11px;
157
+ text-transform: uppercase;
158
+ letter-spacing: 0.06em;
159
+ color: var(--text-muted);
160
+ margin-bottom: 6px;
161
+ }
162
+
163
+ .card-value {
164
+ font-size: 18px;
165
+ font-weight: 600;
166
+ }
167
+
168
+ .card-sub {
169
+ margin-top: 4px;
170
+ font-size: 11px;
171
+ color: var(--text-muted);
172
+ }
173
+
174
+ table.data-table {
175
+ width: 100%;
176
+ border-collapse: collapse;
177
+ font-size: 12px;
178
+ }
179
+
180
+ table.data-table th,
181
+ table.data-table td {
182
+ padding: 8px 10px;
183
+ text-align: left;
184
+ border-bottom: 1px solid var(--border);
185
+ }
186
+
187
+ table.data-table th {
188
+ color: var(--text-muted);
189
+ font-weight: 500;
190
+ position: sticky;
191
+ top: 0;
192
+ background: var(--bg-elevated);
193
+ z-index: 1;
194
+ }
195
+
196
+ table.data-table tbody tr:hover {
197
+ background: var(--row-hover);
198
+ }
199
+
200
+ table.data-table tr.selected {
201
+ background: rgba(61, 143, 209, 0.12);
202
+ }
203
+
204
+ table.data-table td:last-child {
205
+ display: flex;
206
+ flex-wrap: wrap;
207
+ gap: 4px;
208
+ align-items: center;
209
+ }
210
+
211
+ .mono {
212
+ font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
213
+ font-size: 11px;
214
+ }
215
+
216
+ .input-row {
217
+ display: flex;
218
+ gap: 8px;
219
+ flex-wrap: wrap;
220
+ align-items: center;
221
+ margin-bottom: 12px;
222
+ }
223
+
224
+ input[type='text'],
225
+ input[type='search'],
226
+ input[type='number'],
227
+ textarea {
228
+ background: var(--bg-elevated);
229
+ border: 1px solid var(--border);
230
+ border-radius: var(--radius);
231
+ color: var(--text);
232
+ padding: 8px 10px;
233
+ font: inherit;
234
+ min-width: 200px;
235
+ }
236
+
237
+ textarea {
238
+ width: 100%;
239
+ min-height: 100px;
240
+ resize: vertical;
241
+ }
242
+
243
+ input:focus,
244
+ textarea:focus {
245
+ outline: none;
246
+ border-color: var(--accent);
247
+ }
248
+
249
+ .icon-btn-wrap {
250
+ position: relative;
251
+ display: inline-flex;
252
+ vertical-align: middle;
253
+ }
254
+
255
+ .icon-btn {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ width: 36px;
260
+ height: 36px;
261
+ padding: 0;
262
+ border: 1px solid var(--border);
263
+ border-radius: var(--radius);
264
+ background: var(--bg-elevated);
265
+ color: var(--text);
266
+ cursor: pointer;
267
+ }
268
+
269
+ .icon-btn:hover:not(:disabled) {
270
+ border-color: var(--accent);
271
+ color: var(--accent);
272
+ }
273
+
274
+ .icon-btn:disabled {
275
+ opacity: 0.4;
276
+ cursor: not-allowed;
277
+ }
278
+
279
+ .icon-btn svg {
280
+ font-size: 18px;
281
+ }
282
+
283
+ .icon-btn-tooltip {
284
+ position: absolute;
285
+ left: 50%;
286
+ bottom: calc(100% + 8px);
287
+ transform: translateX(-50%);
288
+ padding: 4px 8px;
289
+ background: var(--bg-elevated);
290
+ border: 1px solid var(--border);
291
+ border-radius: 4px;
292
+ font-size: 11px;
293
+ white-space: nowrap;
294
+ color: var(--text);
295
+ opacity: 0;
296
+ pointer-events: none;
297
+ transition: opacity 0.12s ease;
298
+ z-index: 50;
299
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
300
+ }
301
+
302
+ .icon-btn-wrap:hover .icon-btn-tooltip {
303
+ opacity: 1;
304
+ }
305
+
306
+ .notes-panel {
307
+ border-top: 1px solid var(--border);
308
+ background: var(--bg-panel);
309
+ padding: 12px 16px;
310
+ flex-shrink: 0;
311
+ max-height: 180px;
312
+ display: flex;
313
+ flex-direction: column;
314
+ gap: 8px;
315
+ }
316
+
317
+ .notes-panel header {
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: space-between;
321
+ gap: 8px;
322
+ }
323
+
324
+ .notes-key {
325
+ font-size: 11px;
326
+ color: var(--text-muted);
327
+ word-break: break-all;
328
+ }
329
+
330
+ .bar-track {
331
+ height: 6px;
332
+ background: var(--border);
333
+ border-radius: 3px;
334
+ overflow: hidden;
335
+ margin-top: 8px;
336
+ }
337
+
338
+ .bar-fill {
339
+ height: 100%;
340
+ background: var(--accent);
341
+ border-radius: 3px;
342
+ }
343
+
344
+ .status-msg {
345
+ font-size: 12px;
346
+ color: var(--text-muted);
347
+ }
348
+
349
+ .status-msg.error {
350
+ color: var(--danger);
351
+ }
352
+
353
+ .features-pre {
354
+ margin: 0;
355
+ padding: 12px;
356
+ background: var(--bg-elevated);
357
+ border: 1px solid var(--border);
358
+ border-radius: var(--radius);
359
+ overflow: auto;
360
+ max-height: 60vh;
361
+ font-size: 11px;
362
+ line-height: 1.4;
363
+ }
364
+
365
+ .checkbox-inline {
366
+ display: inline-flex;
367
+ align-items: center;
368
+ gap: 6px;
369
+ color: var(--text-muted);
370
+ font-size: 12px;
371
+ user-select: none;
372
+ }
373
+
374
+ .checkbox-inline input {
375
+ accent-color: var(--accent);
376
+ }
src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ )
src/types.ts ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface DriveInfo {
2
+ letter: string
3
+ mount: string
4
+ label: string
5
+ totalBytes: number
6
+ freeBytes: number
7
+ usedBytes: number
8
+ }
9
+
10
+ export interface DirEntry {
11
+ name: string
12
+ fullPath: string
13
+ isDirectory: boolean
14
+ sizeBytes: number
15
+ mtimeMs: number
16
+ error?: string
17
+ sizeTruncated?: boolean
18
+ }
19
+
20
+ export interface ProcessRow {
21
+ pid: number
22
+ name: string
23
+ memoryBytes: number
24
+ cpuSeconds?: number
25
+ commandLine?: string
26
+ }
27
+
28
+ export interface ServiceRow {
29
+ name: string
30
+ displayName: string
31
+ state: string
32
+ startType: string
33
+ }
34
+
35
+ export interface InstalledApp {
36
+ name: string
37
+ version: string
38
+ publisher: string
39
+ installLocation: string
40
+ uninstallString: string
41
+ estimatedSizeKb: number
42
+ }
43
+
44
+ export interface NetworkRow {
45
+ name: string
46
+ address: string
47
+ family: string
48
+ internal: boolean
49
+ mac?: string
50
+ }
51
+
52
+ export interface SystemSnapshot {
53
+ hostname: string
54
+ platform: string
55
+ release: string
56
+ arch: string
57
+ uptimeSec: number
58
+ totalMem: number
59
+ freeMem: number
60
+ cpuModel: string
61
+ cpuCount: number
62
+ load1: number
63
+ load5: number
64
+ load15: number
65
+ userInfo: string
66
+ homedir: string
67
+ tmpdir: string
68
+ }
69
+
70
+ export type NavId =
71
+ | 'overview'
72
+ | 'storage'
73
+ | 'filesystem'
74
+ | 'processes'
75
+ | 'services'
76
+ | 'apps'
77
+ | 'network'
78
+ | 'environment'
79
+ | 'startup'
80
+ | 'scheduled'
81
+ | 'features'
src/utils/format.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function formatBytes(n: number): string {
2
+ if (!Number.isFinite(n) || n < 0) return '0 B'
3
+ const u = ['B', 'KB', 'MB', 'GB', 'TB']
4
+ let i = 0
5
+ let v = n
6
+ while (v >= 1024 && i < u.length - 1) {
7
+ v /= 1024
8
+ i++
9
+ }
10
+ return `${v < 10 && i > 0 ? v.toFixed(1) : Math.round(v)} ${u[i]}`
11
+ }
12
+
13
+ /** Exact size as decimal kilobytes (1 KB = 1024 bytes). Use + suffix when walk was capped. */
14
+ export function formatSizeKb(bytes: number, opts?: { truncated?: boolean }): string {
15
+ if (!Number.isFinite(bytes) || bytes < 0) bytes = 0
16
+ const kb = bytes / 1024
17
+ const s = kb.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
18
+ return opts?.truncated ? `${s}+ KB` : `${s} KB`
19
+ }
20
+
21
+ export function formatUptime(sec: number): string {
22
+ const d = Math.floor(sec / 86400)
23
+ const h = Math.floor((sec % 86400) / 3600)
24
+ const m = Math.floor((sec % 3600) / 60)
25
+ const parts: string[] = []
26
+ if (d) parts.push(`${d}d`)
27
+ if (h || d) parts.push(`${h}h`)
28
+ parts.push(`${m}m`)
29
+ return parts.join(' ')
30
+ }
tsconfig.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@/*": ["src/*"]
21
+ }
22
+ },
23
+ "include": ["src"]
24
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "noEmit": true
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
vite.config.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from 'node:path'
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ resolve: {
8
+ alias: {
9
+ '@': path.resolve(__dirname, 'src'),
10
+ },
11
+ },
12
+ base: './',
13
+ build: {
14
+ outDir: 'dist',
15
+ emptyOutDir: true,
16
+ },
17
+ })