Turn any old phone into a live system monitor for your PC. One small binary serves a beautiful dashboard — CPU, GPU, RAM, disk, network, temperatures, and power — as an installable web app you add to your Home Screen. The host does all the work; the device just paints the screen.
The dashboard below is running on an iPhone 8 Plus (2017) as a Home-Screen web app — no cable, no companion app to install, no GPU. The phone is just on Wi-Fi painting HTML; every sensor is read on the host PC and pushed over the network. That's why a years-old handset with nothing plugged in makes a perfect, near-zero-power desk monitor.
| Idle | Under CPU load |
|---|---|
![]() |
![]() |
| Gauges tick live at a glance | Same phone, host under load — the CPU clock ring fills and the temperature crosses its threshold and turns amber, in real time |
- 📱 Installable PWA — add it to any phone, tablet, or browser Home Screen. Runs great on old hardware.
- 🪶 Light on the device — the host collects everything and streams it; the phone only renders, so there's no GPU use and almost no battery/CPU cost on the device. Old, wireless, and always-on is exactly the point.
- 🧊 Single binary, zero dependencies — stdlib-only Go with all assets embedded. One file per OS, nothing to install alongside it.
- 🖥️ Full-system telemetry — CPU, GPU, RAM, disk, network, temperatures, and power, refreshing live over Server-Sent Events.
- 🎛️ At-a-glance gauges — concentric rings (CPU utilization outer, core-clock inner), a small live trend per card, and amber/red warning thresholds.
- 🔘 Quick controls — mute mic, play/pause media, mute speaker, and lock the screen straight from the dashboard footer.
- ♻️ Graceful degradation — a sensor that can't be read shows
unavailablewith a reason; it never breaks the rest of the dashboard. - 🐧 🪟 Cross-platform — Linux (
/proc+ sysfs + RAPL) and Windows (PowerShell + an embedded LibreHardwareMonitor bridge for CPU power and board temps).
Download the latest SysmonAgent-Setup-<version>.exe from the
Releases page and
double-click it. The wizard installs a background service, opens the firewall, and adds a
dashboard shortcut. The build is unsigned, so Windows SmartScreen shows a one-time
"Windows protected your PC" → More info → Run anyway.
Needs Go 1.22+ (only to build — a prebuilt binary has no Go requirement).
git clone https://github.com/vantuan5644/sysmon-agent system-monitor
cd system-monitor
./build.sh # -> ./sysmon-agent
./sysmon-agent # serves on 0.0.0.0:9099Open http://HOST_IP:9099/ from any device on the same network.
The dashboard is an installable PWA. Browsers only allow "install" over HTTPS, and the easiest way to get that is Tailscale Serve (no certificates to manage):
# agent already listening on 127.0.0.1:9099
sudo tailscale serve --bg --https=9443 http://127.0.0.1:9099Open https://TAILSCALE_HOST:9443/ on the device → Add to Home Screen (Safari) or
Install app (Chrome) → set the device's auto-lock to Never for an always-on monitor.
Any other HTTPS reverse proxy (Caddy, nginx, Cloudflare Tunnel) works too.
Run options & endpoints
Defaults bind all interfaces on port 9099 (good for a trusted LAN or a Tailscale network).
./sysmon-agent # 0.0.0.0:9099
./sysmon-agent -bind 127.0.0.1 -port 9099
./sysmon-agent -settings ./settings.json
./sysmon-agent -tls -cert ./cert.pem -key ./key.pem # optional direct TLS (proxy is preferred)
SYSMON_BIND=127.0.0.1 SYSMON_PORT=9099 ./sysmon-agent| Endpoint | Purpose |
|---|---|
/ |
the dashboard |
/healthz |
cheap process liveness |
/readyz |
proves metrics are actually collectable |
/api/status |
agent metadata + active display settings |
/api/metrics |
the live metrics payload |
/api/stream |
Server-Sent Events live metrics push |
When bound to a wildcard like 0.0.0.0, startup logs print likely dashboard URLs
(Tailscale addresses first), skipping virtual/container interfaces. Open the port through
the firewall only on trusted LAN or Tailscale networks.
Configuration (flags / env)
Display settings persist with -settings PATH (or SYSMON_SETTINGS). Refresh interval and
warning thresholds are host-side config (flags/env, not touch controls); a 0/unset
value keeps the saved default.
| Flag / env | Range / default | Meaning |
|---|---|---|
-bind / SYSMON_BIND |
0.0.0.0 |
HTTP bind address |
-port / SYSMON_PORT |
9099 |
HTTP listen port |
-fast-ms / SYSMON_FAST_MS |
min 100, default 200 | fast-lane (CPU/RAM) interval |
-slow-ms / SYSMON_SLOW_MS |
min 500, default 1500 | slow-lane (power/temps/disk/net/GPU) interval |
-refresh-ms / SYSMON_REFRESH_MS |
{250,500,1000,2000} | dashboard refresh interval |
-cpu-warn / -mem-warn / -disk-warn / -gpu-warn |
50–90 | utilization warn thresholds (%) |
-temp-warn / SYSMON_TEMP_WARN |
50–90 (°C) | temperature warn threshold |
-settings / SYSMON_SETTINGS |
path | optional JSON file for persisted settings |
-tls / SYSMON_TLS |
bool | enable direct TLS (-cert/-key) |
-self-check / -wait-health / -wait-ready |
bool | in-process checks / startup gates |
Invalid persisted settings are backed up with a .bad-... suffix and the agent starts with
defaults so the monitor still comes up.
Deploy as a service (Linux systemd / Windows service)
Linux. Two units ship under deploy/:
deploy/sysmon-agent.user.service(recommended for desktops) runs under your per-user systemd manager so the footer controls (mic/media/ speaker/lock) can reach PipeWire/PulseAudio,playerctl, andloginctl lock-session.deploy/sysmon-agent.serviceruns as a system service for headless hosts.
./build.sh
sudo install -m0755 sysmon-agent /usr/local/bin/sysmon-agent
# user unit:
install -m0644 deploy/sysmon-agent.user.service ~/.config/systemd/user/sysmon-agent.service
systemctl --user enable --now sysmon-agent.service
sudo loginctl enable-linger "$USER"run-linux.sh rebuilds, reinstalls to /usr/local/bin, and restarts the
right unit. Don't add ProtectSystem/PrivateDevices/ProtectKernelModules — they break
the /proc, sysfs, hwmon, and RAPL reads.
Windows. install-windows.ps1 registers a native SysmonAgent service (pure stdlib SCM
integration — the same binary is both console app and service). From an elevated PowerShell:
.\install-windows.ps1 -Action Install
.\install-windows.ps1 -Action Status # probes /readyz, reports settings
.\install-windows.ps1 -Action Update # download + verify + swap + rollback
.\install-windows.ps1 -Action UninstallThe service runs as LocalSystem, which is why it can load the LibreHardwareMonitor kernel driver every boot. Don't run it interactively under an unprivileged account for production, or CPU power and board temps degrade.
Updating. Three paths, in order of convenience:
- In-dashboard. The agent checks for a newer release once at startup and every 24 h,
and the dashboard shows a
vX.Y.Z available - Updatebanner. One tap downloads the new binary, SHA-256 verifies it against the release'sSHA256SUMS.txt, then hands it to a detached helper that stops the service, swaps the binary, restarts, polls/readyz, and rolls back to the previous binary if the new one fails readiness. The dashboard reloads itself once the new build is up. No SmartScreen prompt — that only fires on an interactive double-click, not when the service replaces its own binary. -Action Update— the same download/verify/swap/rollback driven entirely from the script, with no in-app network calls. Good for a weekly elevated Scheduled Task:.\install-windows.ps1 -Action Update -DryRun # report only .\install-windows.ps1 -Action Update -Force # reapply same version .\install-windows.ps1 -Action Update -UpdateVersion v1.2.3 # pin or downgrade
- Re-run the installer.
SysmonAgent-Setup-<ver>.exestops the running service and waits for it to release the binary before overwriting, so upgrading in place works without uninstalling first.
Self-update is confined to the LocalSystem Windows service — console runs and Linux
hosts refuse it (HTTP 501) and should use the installer or the systemd/package path. Every
release must publish SHA256SUMS.txt: it is what authenticates the download before a
SYSTEM-privileged process executes it, and both engines refuse to update without it.
Turning the check off. It is on by default. Toggle update_check_enabled via
POST /api/settings, or hard-disable it host-side with -no-update-check /
SYSMON_UPDATE_CHECK=0 (flag/env wins over the setting). Disabled means no outbound calls
at all; the only endpoint ever contacted is api.github.com.
Metrics & API
GET /api/metrics returns hostname/OS/arch/timestamp plus:
- CPU usage %, package power (W) when exposed, current + max/boost clock (MHz), die temp.
- GPU usage/VRAM/temp/power (NVIDIA via
nvidia-smi; AMD/Intel via DRM sysfs on Linux). - RAM used/total/%, disk per mounted local filesystem, network RX/TX per interface.
- Temperatures from Linux hwmon/thermal or Windows ACPI/LibreHardwareMonitor, and PSU total output power when a USB-linked smart PSU is present (Windows LHM bridge).
Unavailable sensors come back as available: false with an error string instead of failing
the request; collection_errors rolls them up into compact name: reason summaries.
A resident sampler keeps one warm snapshot refreshed by a fast lane (CPU/RAM, ~5 Hz) and
a slow lane (power/temps/disk/net/GPU, ~0.7 Hz), so /api/metrics reads memory instead of
spawning a collection per request. Concurrent requests share one in-flight collection, and
/api/stream pushes fresh snapshots over SSE with a keepalive every 15 s.
Platform notes
Linux — CPU/memory/disk/network from /proc; CPU package power from Intel/AMD RAPL
(/sys/class/powercap), unavailable on hosts without it; temperatures from /sys/class/hwmon
and /sys/class/thermal. NVIDIA needs nvidia-smi in PATH; AMD via the amdgpu DRM
sysfs; Intel iGPU is best-effort. Container/bridge and remote-mount interfaces are skipped.
Windows — CPU/memory/disk/network via PowerShell/CIM. CPU package power and CPU/board/RAM
temperatures aren't exposed by any native Windows API, so the agent ships an embedded
LibreHardwareMonitor bridge (loads LibreHardwareMonitorLib.dll directly — no GUI, no
WMI). Install it machine-wide plus PowerShell 7+ once, elevated:
choco install librehardwaremonitor -y # machine-wide (recommended)
winget install --scope machine Microsoft.PowerShell # pwsh — required for the bridgeA per-user install lands in a profile the LocalSystem service can't read, so those sensors silently go unavailable. The same bridge also reports PSU output power for USB-linked smart PSUs (Corsair HXi/RMi, NZXT, Seasonic, …).
Build the Windows installer
The dist/ flow turns the source into a double-clickable installer, cross-compilable from
Linux/macOS/Windows (no wine):
./dist/build-installer.sh 1.0.0 # -> dist/out/SysmonAgent-Setup-1.0.0.exe (~3 MB)
./dist/build-windows.sh 1.0.0 # just the standalone exe, no installerIt cross-compiles a stripped, version-stamped exe with an embedded icon, then wraps it in an
NSIS Modern-UI wizard that runs the existing install-windows.ps1 (service + firewall +
recovery + /readyz gate) and registers a clean uninstaller. Needs Go 1.22+ and
NSIS (makensis; apt install nsis / brew install nsis / AUR nsis); ImageMagick is
optional for icon regeneration. For public distribution, sign the exe + installer to avoid
the SmartScreen warning.
Verify & test
go test ./... # unit tests
go run . -self-check # in-process HTTP checks, no socket
./verify-no-listen.sh # gofmt + tests + vet + self-check + cross-compile + JS verifiers
./verify.sh # real-host smoke: build, start on :19099, check API/PWA/settings, stop
./verify-deployed.sh # checks an already-running service + published device URLverify-deployed.sh waits for a fresh real Home-Screen status-strip tap, proving the
installed PWA path works. Windows equivalents: verify-windows.ps1,
verify-deployed-windows.ps1.
Full host-side dependency matrix (Go, per-platform runtime tools, optional PowerShell / LibreHardwareMonitor / Node) is in REQUIREMENTS.md.
Creative Commons Attribution-NonCommercial 4.0 (CC-BY-NC 4.0) — free to use, share, and adapt for non-commercial purposes with credit. Commercial use requires a separate license from the maintainer.


