Upload 28 files
Browse files- .gitignore +5 -0
- FEATURES.md +156 -0
- backend/main.py +346 -0
- backend/requirements.txt +4 -0
- dist-electron/audit.js +232 -0
- dist-electron/fsWorker.js +140 -0
- dist-electron/main.js +137 -0
- dist-electron/measureWorker.js +137 -0
- dist-electron/preload.js +27 -0
- dist-electron/pythonBackend.js +156 -0
- electron/audit.ts +281 -0
- electron/main.ts +131 -0
- electron/preload.ts +52 -0
- electron/pythonBackend.ts +135 -0
- electron/tsconfig.json +18 -0
- index.html +12 -0
- package-lock.json +0 -0
- package.json +31 -0
- src/App.tsx +1073 -0
- src/components/IconButton.tsx +29 -0
- src/env.d.ts +33 -0
- src/index.css +376 -0
- src/main.tsx +10 -0
- src/types.ts +81 -0
- src/utils/format.ts +30 -0
- tsconfig.json +24 -0
- tsconfig.node.json +11 -0
- vite.config.ts +17 -0
.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 |
+
})
|