Repo: StaticHumStudio/HumWatch
License: MIT
Tagline: "What hums beneath the shell."
HumWatch is a self-hosted, local-first hardware monitoring system for Windows PCs accessible over your local or overlay network. Each monitored PC runs a lightweight Python agent that collects full hardware telemetry — thermal, performance, battery, and system metrics — stores 7 days of history in a local SQLite database, and serves both a REST API and a web dashboard on a single port.
There is no central server. Each PC is its own self-contained monitoring station. The dashboard includes a multi-machine view with automatic peer discovery across LAN, Tailscale, ZeroTier, WireGuard, and other overlay networks.
- Local-first: All data stays on the machine that generated it. No cloud, no external dependencies.
- Zero-config discovery: If a machine has a battery, battery metrics appear automatically. If it has a discrete GPU, GPU metrics appear. No configuration files to edit.
- Network-agnostic: Works over LAN, Tailscale, ZeroTier, WireGuard, or any overlay network. Binds to
0.0.0.0by default so it's reachable from any interface. - Themeable: Ships with Static Hum Studio's signature aesthetic but uses a clean CSS custom property system so anyone can re-theme it in minutes.
┌─────────────────────────────────────────────┐
│ Each Windows PC │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ Collector │───▶│ SQLite (WAL) │ │
│ │ (10s loop) │ │ 7-day rolling │ │
│ └─────────────┘ └──────────────────┘ │
│ │ ▲ │
│ ▼ │ │
│ ┌─────────────────────────────────────┐ │
│ │ FastAPI Server (:9100) │ │
│ │ ├── /api/v1/* — REST endpoints │ │
│ │ ├── /api/v1/sse — real-time stream │ │
│ │ └── /* — static dashboard SPA │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
▲
│ HTTPS over LAN / overlay network
▼
┌─────────────────────────────────────────────┐
│ Browser (any device on network) │
│ ├── Single-machine view │
│ └── Multi-machine dashboard │
└─────────────────────────────────────────────┘
Default: 9100. Configurable via HUMWATCH_PORT environment variable or config.json.
| Component | Technology | Purpose |
|---|---|---|
| Runtime | Python 3.11+ | Agent runtime |
| Web Framework | FastAPI + Uvicorn | API server, SSE, static file serving |
| System Metrics | psutil | CPU, memory, disk, network, battery, per-process stats |
| Hardware Sensors | LibreHardwareMonitorLib via pythonnet (clr) | CPU/GPU temps, fan speeds, voltages, clock speeds |
| Database | SQLite (WAL mode) | 7-day rolling time-series storage |
| Scheduling | asyncio background task | 10-second collection loop |
| Windows Service | nssm (Non-Sucking Service Manager) | Run agent as auto-start Windows service |
| Component | Technology | Purpose |
|---|---|---|
| Framework | Vanilla JS (single-page app) | No build step, served as static files |
| Charts | Chart.js 4.x (via CDN) | Historical time-series graphs |
| Real-time | Server-Sent Events (EventSource API) | Live gauge updates without polling |
| Theming | CSS custom properties | Full re-theming via single file override |
| Layout | CSS Grid + Flexbox | Responsive, adapts to mobile/tablet/desktop |
| Icons | Lucide Icons (via CDN) | Lightweight, consistent iconography |
The collector gathers metrics in these categories. Each category auto-detects availability — if the hardware isn't present, the category is silently skipped.
cpu_temp_package— Package/overall CPU temperature (°C)cpu_temp_core_N— Per-core temperature (°C), one per logical corecpu_load_total— Total CPU utilization (%)cpu_load_core_N— Per-core utilization (%)cpu_clock_core_N— Per-core clock speed (MHz)cpu_power_package— Package power draw (W), if availablecpu_voltage— Core voltage (V), if available
gpu_temp— GPU core temperature (°C)gpu_load— GPU utilization (%)gpu_clock_core— Core clock speed (MHz)gpu_clock_memory— Memory clock speed (MHz)gpu_vram_used— VRAM used (MB)gpu_vram_total— VRAM total (MB)gpu_power— GPU power draw (W), if availablegpu_fan_speed— Fan speed (RPM or %), if available
mem_used— Used RAM (MB)mem_total— Total RAM (MB)mem_percent— RAM utilization (%)mem_swap_used— Used swap/page file (MB)mem_swap_total— Total swap/page file (MB)
disk_read_rate— Read throughput (MB/s), per physical diskdisk_write_rate— Write throughput (MB/s), per physical diskdisk_usage_DRIVE— Usage % per mounted volume (e.g.,disk_usage_C)disk_temp_N— Drive temperature (°C), if available via SMART/LHM
net_sent_rate— Upload throughput (MB/s)net_recv_rate— Download throughput (MB/s)net_bytes_sent— Total bytes sent since bootnet_bytes_recv— Total bytes received since boot
fan_N_speed— Individual fan RPM values from LibreHardwareMonitorfan_N_name— Human-readable fan label
battery_percent— Charge level (%)battery_plugged— AC adapter connected (boolean → stored as 1/0)battery_time_remaining— Estimated seconds remaining (null if charging)battery_voltage— Current voltage (V), if available via LHMbattery_charge_rate— Charge/discharge rate (W), if availablebattery_designed_capacity— Designed full charge capacity (mAh), if availablebattery_current_capacity— Current full charge capacity (mAh), if availablebattery_wear_level— Derived:current_capacity / designed_capacity * 100(%)battery_temp— Battery temperature (°C), if availablebattery_cycle_count— Charge cycles, if reported by hardware
uptime_seconds— System uptimeboot_time— Last boot timestamp (ISO 8601)
-- Machine identity (one row, updated on each startup)
CREATE TABLE machine_info (
id INTEGER PRIMARY KEY DEFAULT 1,
hostname TEXT NOT NULL,
os_version TEXT,
cpu_name TEXT,
gpu_name TEXT,
total_ram_mb INTEGER,
network_ip TEXT,
agent_version TEXT,
last_boot TEXT,
updated_at TEXT NOT NULL
);
-- Time-series metric storage
CREATE TABLE metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL, -- ISO 8601, UTC
category TEXT NOT NULL, -- 'cpu', 'gpu', 'memory', 'disk', 'network', 'fan', 'battery', 'system'
metric_name TEXT NOT NULL, -- e.g., 'cpu_temp_core_0'
value REAL NOT NULL,
unit TEXT -- '°C', '%', 'MB', 'MHz', 'RPM', 'W', 'V', 'MB/s', 's', 'mAh', 'bool'
);
-- Indices for query performance
CREATE INDEX idx_metrics_timestamp ON metrics(timestamp);
CREATE INDEX idx_metrics_category_timestamp ON metrics(category, timestamp);
CREATE INDEX idx_metrics_name_timestamp ON metrics(metric_name, timestamp);
-- Snapshot of top processes at each collection interval
CREATE TABLE process_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL, -- ISO 8601, UTC
pid INTEGER NOT NULL,
name TEXT NOT NULL,
cpu_percent REAL,
memory_mb REAL
);
CREATE INDEX idx_process_timestamp ON process_snapshots(timestamp);- Rolling window: 7 days.
- Cleanup job: Runs once per hour via asyncio task. Deletes all rows from
metricsandprocess_snapshotswheretimestampis older than 7 days. - Approximate storage: At 10-second intervals with ~40 metrics per tick, expect ~2.4M rows/week. SQLite in WAL mode handles this comfortably. Estimated database size: 200–400 MB depending on hardware sensor count.
- Process snapshots: Store top 10 processes by CPU usage at each interval.
All endpoints are served from the same FastAPI instance on port 9100. The
versioned /api/v1 prefix is the client contract. /api is retained as a
compatibility alias for the current version.
GET /api/v1/info
Returns machine_info row: hostname, OS, CPU name, GPU name, RAM, network IP, agent version, last boot, uptime.
GET /api/v1/current
Returns the most recent reading for every metric, grouped by category. This is what the live dashboard uses on initial load before SSE takes over.
Response shape:
{
"timestamp": "2026-03-01T15:30:00Z",
"categories": {
"cpu": {
"cpu_temp_package": {"value": 62.0, "unit": "°C"},
"cpu_load_total": {"value": 34.2, "unit": "%"},
...
},
"gpu": { ... },
"memory": { ... },
"disk": { ... },
"network": { ... },
"fan": { ... },
"battery": { ... },
"system": { ... }
}
}GET /api/v1/history?metric={metric_name}&from={iso_timestamp}&to={iso_timestamp}&resolution={seconds}
Parameters:
metric(required): Metric name, e.g.,cpu_temp_packagefrom(optional): Start of range. Default: 1 hour ago.to(optional): End of range. Default: now.resolution(optional): Bucket size in seconds for downsampling. Default: auto-calculated based on range (10s for <1h, 60s for <6h, 300s for <24h, 900s for >24h). Data is averaged within each bucket.
Response: Array of {timestamp, value} objects.
GET /api/v1/history/multi?metrics={comma_separated}&from={iso}&to={iso}&resolution={seconds}
Same as above but accepts multiple metric names. Returns object keyed by metric name.
GET /api/v1/processes
Returns the most recent process snapshot (top 10 by CPU).
GET /api/v1/processes/history?from={iso}&to={iso}
Returns process snapshots within the time range.
GET /api/v1/sse
Server-Sent Events stream. Emits a data event every 10 seconds with the same shape as /api/v1/current. The client uses EventSource to subscribe.
Event format:
event: metrics
data: {"timestamp": "...", "categories": { ... }}
event: processes
data: {"timestamp": "...", "processes": [ ... ]}
GET /api/v1/health
Returns {"status": "ok", "version": "x.y.z", "api_version": 1, "capabilities": [...], "auth_required": true, "tls_required": true}. Health is unauthenticated so peer discovery can check reachability before requesting protected machine information.
GET /api/v1/config
Returns current configuration (collection interval, retention days, port, enabled categories).
Remote and non loopback listeners use HTTPS with a configured certificate and
private key. The certificate SAN must cover the hostname or IP used by the
client. Clients trust the issuing CA through the operating system or browser
trust store. HumWatch peer clients use trusted_ca_file or
HUMWATCH_TRUSTED_CA_FILE and must fail closed on certificate errors.
The token is stored outside the repository at auth_token_file, or supplied
through HUMWATCH_AUTH_TOKEN_FILE. It must contain at least 32 bytes of token
material. The shared token quality predicate also requires at least 8 distinct
characters, a Shannon entropy estimate of at least 2.5 bits per character, and
no character to account for more than 75 percent of the token. It accepts
cryptographically generated URL safe tokens and normal 64 character hex
tokens. HUMWATCH_AUTH_TOKEN is an environment based development option.
Operators should create the file with a cryptographically random value,
restrict it to the service account, and rotate it by stopping HumWatch,
replacing the file, updating all clients, and restarting the service. The old
token is invalid after reload. Token values never appear in URLs, cookies,
request bodies, logs, or reports.
Protected endpoints require exactly Authorization: Bearer <token> and return
401 Unauthorized with WWW-Authenticate: Bearer when credentials are
missing, malformed, or incorrect. Health is the sole unauthenticated API
capability. The explicit local development exception is
host = 127.0.0.1 (or another loopback address) with
allow_insecure_localhost = true. It permits plaintext only on loopback and
still requires the bearer token. It is never valid for LAN, Tailscale, or
other remote listeners.
The SlipShell client seam should inject the base URL, bearer token, and CA
bundle. It first calls /api/v1/health without credentials, requires
api_version: 1 and expected capabilities, then calls protected endpoints
over HTTPS with the bearer header. A 401 is an authentication or token
rotation event. A certificate failure is a trust configuration error. The
client never disables certificate verification or puts credentials in a URL.
Clients pin /api/v1 and must fail closed when api_version is unsupported.
Backward incompatible changes require a new version. Existing token and
certificate paths are preserved across HumWatch upgrades. The /api alias is
for transition compatibility only and is not the seam for new clients.
The dashboard is a vanilla JS single-page application served as static files by FastAPI. No build step. All dependencies loaded via CDN with integrity hashes.
The default landing page. Shows:
- Machine identity card: Hostname, OS, CPU, GPU, RAM, uptime, network IP, agent version.
- Live gauges: Circular/arc gauges for CPU temp, GPU temp, CPU load, GPU load, RAM usage. These update in real-time via SSE.
- Battery widget: (Only if battery detected.) Shows charge %, plugged status, estimated time remaining, wear level as a health bar. Subtle animation when charging.
- Alert indicators: Visual warning if any temp exceeds configurable thresholds (default: CPU > 85°C = warning amber, > 95°C = critical red; GPU > 80°C = warning, > 90°C = critical).
- Quick sparklines: Tiny inline charts showing the last 5 minutes for key metrics.
- Per-core temperature chart (line chart, all cores overlaid, color-coded)
- Per-core load chart
- Per-core clock speed chart
- Package power draw (if available)
- Voltage reading
- Time range selector: 5m, 15m, 1h, 6h, 24h, 3d, 7d
- Temperature history chart
- Load history chart
- VRAM usage (used/total) with history
- Core and memory clock charts
- Power draw chart (if available)
- Fan speed chart (if available)
- Time range selector
- RAM usage over time (area chart, used vs. total)
- Swap usage over time
- Current top processes by memory (table, live-updating)
- Read/write throughput charts per physical disk
- Volume usage bars (C:, D:, etc.)
- Drive temperatures (if available)
- Upload/download throughput chart
- Cumulative transfer since boot
- Charge % over time (area chart)
- Plugged/unplugged status timeline (binary bar along the bottom of the chart)
- Charge/discharge rate over time
- Voltage over time
- Battery health: designed vs current capacity, wear level
- Temperature over time (if available)
- Cycle count display
- Table of top processes at current moment (live via SSE)
- Ability to click a timestamp on any chart to see what processes were running at that time
- Peers are auto-discovered on the network (Tailscale, ZeroTier, LAN subnet scan). Users can also manually enter IPs or hostnames. Manual entries are saved in
localStorage. - For each machine: shows a compact summary card (hostname, CPU temp, GPU temp, CPU load, RAM %, battery % if applicable, online/offline status).
- Clicking a card opens that machine's full dashboard in a new tab (navigates to its network IP).
- Machines that don't respond to
/api/v1/healthwithin 3 seconds show as "offline" with a muted card. - Auto-refreshes status every 30 seconds.
Sidebar navigation on desktop, bottom tab bar on mobile. Pages:
- Overview (home icon)
- CPU (chip icon)
- GPU (monitor icon)
- Memory (database icon)
- Disk (hard-drive icon)
- Network (wifi icon)
- Battery (battery icon) — only shown if battery detected
- Processes (list icon)
- Machines (server icon) — multi-machine view
- Settings (gear icon) — theme selector, alert thresholds, machine list management
- Desktop (>1024px): Sidebar nav, multi-column gauge layout.
- Tablet (768–1024px): Collapsible sidebar, 2-column layout.
- Mobile (<768px): Bottom tab bar (top 5 most important pages, with "more" overflow), single-column, stacked cards.
All visual styling flows through CSS custom properties defined in a single :root block in theme.css. Anyone can override the entire look by replacing or overriding this file.
:root {
/* === CORE PALETTE === */
--hw-bg-primary: #0a0a0f; /* Main background */
--hw-bg-secondary: #12121a; /* Card/panel background */
--hw-bg-tertiary: #1a1a26; /* Nested element background */
--hw-bg-hover: #22222e; /* Hover state background */
--hw-text-primary: #e8e6e1; /* Primary text */
--hw-text-secondary: #9a9890; /* Secondary/muted text */
--hw-text-tertiary: #6a6860; /* Tertiary/disabled text */
--hw-accent-primary: #c9a84c; /* Gold/amber — primary accent */
--hw-accent-primary-dim: #7a6a30; /* Gold dimmed for backgrounds */
--hw-accent-secondary: #4ecdc4; /* Bioluminescent teal — secondary accent */
--hw-accent-secondary-dim: #2a6e68; /* Teal dimmed */
--hw-accent-glow: #5bff8f; /* Bioluminescent green — highlights, active states */
/* === STATUS COLORS === */
--hw-status-ok: #4ecdc4; /* Normal/good */
--hw-status-warn: #f0a830; /* Warning (amber) */
--hw-status-critical: #e74c3c; /* Critical/danger (red) */
--hw-status-offline: #555555; /* Offline/unavailable */
/* === GAUGE COLORS === */
--hw-gauge-cold: #4ecdc4; /* Low temp — teal */
--hw-gauge-warm: #c9a84c; /* Medium temp — gold */
--hw-gauge-hot: #e74c3c; /* High temp — red */
/* === CHART PALETTE === */
--hw-chart-1: #c9a84c;
--hw-chart-2: #4ecdc4;
--hw-chart-3: #5bff8f;
--hw-chart-4: #e74c3c;
--hw-chart-5: #9b59b6;
--hw-chart-6: #3498db;
--hw-chart-7: #e67e22;
--hw-chart-8: #1abc9c;
/* === TYPOGRAPHY === */
--hw-font-display: 'JetBrains Mono', 'Fira Code', monospace; /* Headers, gauges, data */
--hw-font-body: 'Inter', 'Segoe UI', system-ui, sans-serif; /* Body text, labels */
--hw-font-size-xs: 0.7rem;
--hw-font-size-sm: 0.8rem;
--hw-font-size-md: 0.95rem;
--hw-font-size-lg: 1.2rem;
--hw-font-size-xl: 1.6rem;
--hw-font-size-xxl: 2.4rem;
/* === SPACING === */
--hw-space-xs: 4px;
--hw-space-sm: 8px;
--hw-space-md: 16px;
--hw-space-lg: 24px;
--hw-space-xl: 32px;
--hw-space-xxl: 48px;
/* === BORDERS & RADIUS === */
--hw-border-color: #2a2a36;
--hw-border-radius-sm: 4px;
--hw-border-radius-md: 8px;
--hw-border-radius-lg: 12px;
/* === EFFECTS === */
--hw-shadow-card: 0 2px 12px rgba(0, 0, 0, 0.4);
--hw-shadow-glow: 0 0 20px rgba(201, 168, 76, 0.15);
--hw-transition-fast: 150ms ease;
--hw-transition-normal: 300ms ease;
/* === BRANDING === */
--hw-brand-name: "Static Hum Studio";
--hw-product-name: "HumWatch";
}The default theme follows the established Static Hum Studio aesthetic:
- Void black backgrounds with very subtle noise texture overlay (CSS
background-imagewith inline SVG noise) - Gold/amber primary accents (
#c9a84c) for headings, active nav items, gauge highlights - Bioluminescent teal (
#4ecdc4) for secondary data, status-ok indicators - Bioluminescent green (
#5bff8f) for highlights, active pulses, connection indicators - JetBrains Mono for all data/numeric displays — gives it that "terminal readout" feel
- Subtle glow effects on active elements — not overdone, just enough to feel like something ancient and luminous is tracking your hardware
- Card-based layout with very subtle borders, slight inner shadow, feels like stone tablets with glowing inscriptions
- A small Static Hum Studio wordmark in the sidebar footer, styled as
font-variant: small-capswith a subtle gold glow
Include a THEMING.md in the repo:
- Copy
static/css/theme.css→static/css/theme-custom.css - Edit the custom properties
- Set
"theme_override": "theme-custom.css"inconfig.json - Restart HumWatch
Provide two example alternate themes in static/css/themes/:
theme-light.css— Clean light theme (white bg, dark text, blue accents)theme-terminal.css— Classic green-on-black terminal aesthetic
Located next to the agent executable. All fields optional with sensible defaults.
{
"port": 9100,
"collection_interval_seconds": 10,
"retention_days": 7,
"data_root": "/var/lib/humwatch",
"db_path": "/var/lib/humwatch/humwatch.db",
"auth_token_file": "/etc/humwatch/token",
"tls_certfile": "/etc/humwatch/humwatch.crt",
"tls_keyfile": "/etc/humwatch/humwatch.key",
"trusted_ca_file": "/etc/humwatch/humwatch-ca.crt",
"allow_insecure_localhost": false,
"theme_override": null,
"alert_thresholds": {
"cpu_temp_warn": 85,
"cpu_temp_critical": 95,
"gpu_temp_warn": 80,
"gpu_temp_critical": 90,
"ram_percent_warn": 85,
"ram_percent_critical": 95,
"disk_percent_warn": 85,
"disk_percent_critical": 95,
"battery_low_warn": 20,
"battery_low_critical": 10
},
"process_snapshot_count": 10,
"enable_categories": ["cpu", "gpu", "memory", "disk", "network", "fan", "battery", "system"]
}HUMWATCH_PORT→ overridesconfig.jsonportHUMWATCH_DB→ overridesconfig.jsondb_pathHUMWATCH_INTERVAL→ overrides collection intervalHUMWATCH_AUTH_TOKEN_FILE→ overrides the protected bearer token fileHUMWATCH_AUTH_TOKEN→ supplies a development token when no token file is configuredHUMWATCH_TLS_CERTFILEandHUMWATCH_TLS_KEYFILE→ override the HTTPS certificate and private keyHUMWATCH_TRUSTED_CA_FILE→ supplies the CA bundle for peer certificate trustHUMWATCH_ALLOW_INSECURE_LOCALHOST→ enables the explicit loopback plaintext development exception
HumWatch/
├── README.md
├── THEMING.md
├── LICENSE # MIT
├── requirements.txt
├── config.json # Default configuration
├── install-service.ps1 # PowerShell script to install via nssm
├── uninstall-service.ps1 # PowerShell script to remove service
│
├── agent/
│ ├── __init__.py
│ ├── main.py # Entry point: FastAPI app, startup, shutdown
│ ├── config.py # Configuration loading (config.json + env vars)
│ ├── database.py # SQLite connection, schema init, WAL mode setup
│ ├── collector.py # Main collection loop (asyncio background task)
│ ├── sensors/
│ │ ├── __init__.py
│ │ ├── base.py # Abstract sensor interface
│ │ ├── cpu.py # CPU metrics (psutil + LHM)
│ │ ├── gpu.py # GPU metrics (LHM)
│ │ ├── memory.py # RAM/swap metrics (psutil)
│ │ ├── disk.py # Disk I/O, usage, temps (psutil + LHM)
│ │ ├── network.py # Network throughput (psutil)
│ │ ├── fan.py # Fan speeds (LHM)
│ │ ├── battery.py # Battery metrics (psutil + LHM)
│ │ └── system.py # Uptime, boot time (psutil)
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── info.py # /api/v1/info
│ │ ├── current.py # /api/v1/current
│ │ ├── history.py # /api/v1/history, /api/v1/history/multi
│ │ ├── processes.py # /api/v1/processes
│ │ ├── sse.py # /api/v1/sse (Server-Sent Events)
│ │ ├── health.py # /api/v1/health
│ │ └── config_route.py # /api/v1/config
│ └── services/
│ ├── __init__.py
│ ├── retention.py # Hourly cleanup job
│ ├── machine_info.py # Populate/update machine_info table
│ └── downsampler.py # Query-time downsampling logic for history API
│
├── static/
│ ├── index.html # SPA shell
│ ├── css/
│ │ ├── theme.css # Default Static Hum theme (all custom properties)
│ │ ├── layout.css # Grid, responsive rules
│ │ ├── components.css # Cards, gauges, tables, nav
│ │ └── themes/
│ │ ├── theme-light.css # Light alternate theme
│ │ └── theme-terminal.css # Green terminal alternate theme
│ ├── js/
│ │ ├── app.js # SPA router, page initialization
│ │ ├── api.js # API client (fetch wrapper)
│ │ ├── sse.js # SSE connection manager with auto-reconnect
│ │ ├── charts.js # Chart.js configuration factory
│ │ ├── gauges.js # Circular gauge rendering (canvas-based)
│ │ ├── pages/
│ │ │ ├── overview.js # Home/overview page logic
│ │ │ ├── cpu.js
│ │ │ ├── gpu.js
│ │ │ ├── memory.js
│ │ │ ├── disk.js
│ │ │ ├── network.js
│ │ │ ├── battery.js
│ │ │ ├── processes.js
│ │ │ ├── machines.js # Multi-machine view
│ │ │ └── settings.js # Settings page
│ │ └── utils/
│ │ ├── format.js # Number formatting, unit display
│ │ ├── time.js # Time range helpers, relative time
│ │ └── theme.js # Theme switching logic
│ └── assets/
│ ├── favicon.svg # HumWatch icon
│ └── logo.svg # Static Hum Studio wordmark (small, for footer)
│
├── lib/
│ └── LibreHardwareMonitorLib.dll # Bundled LHM library
│ └── HidSharp.dll # LHM dependency
│
└── scripts/
├── install-service.ps1
├── uninstall-service.ps1
└── download-lhm.ps1 # Helper to download LibreHardwareMonitorLib
- Windows 10/11
- Python 3.11+
- Admin privileges (required for hardware sensor access)
- Network connectivity (LAN, Tailscale, ZeroTier, WireGuard, etc.)
# Clone the repo
git clone https://github.com/StaticHumStudio/HumWatch.git
cd HumWatch
# Install Python dependencies
pip install -r requirements.txt
# Download LibreHardwareMonitorLib (if not bundled)
python scripts/download-lhm.py
# Run (must be elevated / admin)
python -m agent.mainAccess at https://localhost:9100 or https://<network-ip>:9100 after
trusting the configured certificate. Plaintext is allowed only when
host is loopback and allow_insecure_localhost is explicitly true.
# Run as Administrator
.\scripts\install-service.ps1
# This uses nssm to:
# 1. Create a service named "HumWatch"
# 2. Set it to auto-start
# 3. Run with SYSTEM account (has admin access for sensors)
# 4. Configure stdout/stderr logging to ./logs/fastapi>=0.110.0
uvicorn[standard]>=0.27.0
psutil>=5.9.0
pythonnet>=3.0.3
aiosqlite>=0.19.0
pydantic>=2.0.0
# Pseudocode for LHM initialization
import clr
clr.AddReference('./lib/LibreHardwareMonitorLib')
from LibreHardwareMonitor.Hardware import Computer, HardwareType, SensorType
computer = Computer()
computer.IsCpuEnabled = True
computer.IsGpuEnabled = True
computer.IsMemoryEnabled = True
computer.IsStorageEnabled = True
computer.IsBatteryEnabled = True
computer.IsControllerEnabled = True
computer.Open()
# Each collection tick:
for hardware in computer.Hardware:
hardware.Update()
for sensor in hardware.Sensors:
# sensor.SensorType: Temperature, Load, Clock, Voltage, Power, Fan, etc.
# sensor.Value: float or None
# sensor.Name: human-readable label
passImportant: LHM's Computer object must be created and used in the same thread. Use a dedicated thread for sensor collection, push results to the async event loop via asyncio.run_coroutine_threadsafe or a thread-safe queue.
psutil gives cumulative byte counters. To get rates:
# Store previous reading
# rate = (current_bytes - previous_bytes) / interval_seconds
# Convert to MB/sCompute this in the collector, store the computed rate (not raw counters).
The frontend SSE client should:
- Use
EventSourcewith automatic reconnection - Show a "Reconnecting..." indicator if the connection drops
- On reconnect, fetch
/api/v1/currentonce with the bearer header to fill any gap, then resume SSE
For historical queries spanning large time ranges, the API automatically downsamples:
| Range | Resolution | Method |
|---|---|---|
| < 1 hour | 10s (raw) | No downsampling |
| 1–6 hours | 60s | AVG per bucket |
| 6–24 hours | 300s (5min) | AVG per bucket |
| 1–7 days | 900s (15min) | AVG per bucket |
The resolution query param can override this for custom requests.
Since each machine serves its own API, the multi-machine view on Machine A fetching data from Machine B is a cross-origin request. HumWatch does not infer trust from network location. It uses an explicit configured-origin allowlist, and it rejects a wildcard outright:
app.add_middleware(
CORSMiddleware,
allow_origins=validate_cors_origins(config.cors_origins), # explicit dashboard origins only
allow_methods=["GET"],
allow_headers=["Authorization"],
)cors_origins (env: HUMWATCH_CORS_ORIGINS, comma separated) defaults to an empty list, so cross-origin requests are blocked until an operator lists the peer dashboard origins that should be allowed to fetch this machine's API. validate_cors_origins rejects a * entry and raises SecurityConfigurationError at startup rather than starting with an open policy. Authorization is the only permitted cross-origin request header, and GET is the only permitted method, since the multi-machine view only reads data. Being reachable only on a local or overlay network is not treated as a substitute for this allowlist.
The README should include:
- Hero section with logo and tagline: "What hums beneath the shell."
- Screenshot/GIF of the dashboard in action
- Feature list — what it monitors
- Quick start — 4 commands to get running
- Installation as service instructions
- Multi-machine setup guide
- Theming — link to THEMING.md
- Configuration reference
- API documentation — brief overview with link to full spec
- Tech stack summary
- Contributing section
- License (MIT)
- Footer: "A Static Hum Studio Production"
Every page of the dashboard includes a subtle footer in the sidebar (desktop) or at the bottom of the settings page (mobile):
─── A Static Hum Studio Production ───
Styled in small-caps, --hw-text-tertiary color, with a very subtle gold glow on hover that links to https://statichum.studio (or whatever the current URL is).
These are explicitly out of scope for the initial build but noted for potential future versions:
- Alert notifications (push, email, webhook)
- Linux agent support
- Historical data export (CSV, JSON)
- Custom dashboard layouts / widget arrangement
- Docker container deployment option
- Prometheus/Grafana metric export endpoint
- Per-process historical tracking (beyond top 10 snapshots)
- Dark/light mode auto-detection via
prefers-color-scheme
End of specification. This document should be sufficient for Claude Code to build the complete HumWatch application.