Skip to content

Latest commit

 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

███████╗████████╗██████╗  ██████╗ ██████╗ ███████╗
██╔════╝╚══██╔══╝██╔══██╗██╔═══██╗██╔══██╗██╔════╝
███████╗   ██║   ██████╔╝██║   ██║██████╔╝█████╗
╚════██║   ██║   ██╔══██╗██║   ██║██╔══██╗██╔══╝
███████║   ██║   ██║  ██║╚██████╔╝██████╔╝███████╗
╚══════╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝

system vitals, sampled in the dark



bash >= 4">https://img.shields.io/badge/bash-%E2%89%A5%204-4EAA25?logo=gnubash&logoColor=white"> linux | macos zero dependencies GPL-3.0

A strobe light freezes motion by flashing at a fixed interval. strobe does the same to a machine: once a second it flashes /proc and /sys, catches every core, disk, NIC, GPU and process mid-stride, and shows you the frame.

It is one collector written in plain Bash and four ways to look at what it caught: a long-form text report, a bashtop-style terminal UI, a tiny stdlib-Python backend with a JSON + SSE API, and a single-file animated web dashboard. No compiled code, no package manager, nothing to pip install on the box you are worried about. Clone it, run it.

60-second tour

./strobe                  # the report: health verdict, every core, every disk, top processes
./strobe --tui            # live terminal UI: per-core bars, graphs, sortable/filterable processes
./strobe --serve          # web dashboard at http://127.0.0.1:8787 (JSON + SSE under /api/)
./strobe --json | jq .    # one sample as JSON; add --follow for NDJSON, one line per tick

Add --demo to any of those to see it run on synthetic data, on any OS, right now.

What it looks like

The report (./strobe)

Trimmed from ./strobe --demo. The whole thing runs about 265 lines across twelve sections — health summary and the three numbers worth looking at next, CPU with per-core meters, memory, disks with an I/O table, network with top talkers, GPU, thermals and power, peripherals (below), processes by CPU / memory / threads plus per-user totals, services and ports, capacity headroom, and a footer that records the exact command and knobs that produced it. Colour on a terminal, plain text in a file or a pipe, pure ASCII when the locale is not UTF-8, and it fills your terminal when you are reading it and settles at a stable 100 columns when piped or redirected. --width N|auto|fixed (or STROBE_REPORT_WIDTH) overrides, anywhere from 80 to 220 columns — and a wide report spends the room on more per-core columns rather than longer bars.

╔══════════════════════════════════════════════════════════════════════════════════════════════════╗
║  strobe // system report                                             demo-box · overall CAUTION  ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════╝

  Hostname   demo-box                              Report time 2026-08-22 07:19:43 PDT
  OS         Demo Linux 1.0                        Uptime      3d 04:12:10
  Kernel     6.9.0-demo                            Boot time   2026-08-19 03:07:33 PDT
  Arch       x86_64                                Interval    1 s (rates are per second)
  Platform   demo                                  strobe      2.0.0

━━━━ 1 · HEALTH SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  CHECK                  STATUS     DETAIL
  cpu_load               OK         load1 2.70 on 8 cores
  cpu_usage              OK         cpu 34% busy
  memory                 OK         memory 71% used (22.1 GiB of 31.2 GiB)
  swap                   OK         swap 26% used
  disk:/                 OK         / 60% used, inodes 12%
  disk:/home             OK         /home 40% used, inodes 3%
  cpu_temp               OK         cpu 53°C
  gpu_temp               OK         gpu 59°C
  failed_services        CAUTION    1 failed: bluetooth.service
  vpn                    OK         tun0 up at 10.8.0.2

  Verdict: CAUTION — 1 caution on demo-box: failed_services (1 failed: bluetooth.service).

━━━━ 2 · CPU ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Model      Demo Ryzen 7 8800X 8-Core Processor
  Cores      8 logical      Clock  3106 MHz avg     Temp  53°C  OK
  Load       2.70  2.36  2.30  (1/5/15 min)   per-core ratio 0.33 → OK
  Busy        34%  user 24%  sys 9%  iowait 1%  steal 0%  idle 66%  OK
  Aggregate  [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░········································]  34%

  per-core meters (8 cores) ──────────────────────────────────────────────────────────────────────
  core   usage                             busy  clock
  cpu0   [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]    0%   2200 MHz
  cpu1   [███████░░░░░░░░░░░░░░░░░░░░░░░]   23%   2798 MHz
  cpu2   [█████████████░░░░░░░░░░░░░░░░░]   44%   3344 MHz
  cpu3   [██████░░░░░░░░░░░░░░░░░░░░░░░░]   19%   2694 MHz
  cpu4   [█████████████░░░░░░░░░░░░░░░░░]   42%   3292 MHz
  cpu5   [███░░░░░░░░░░░░░░░░░░░░░░░░░░░]   11%   2486 MHz
  cpu6   [█████████████████████░░░░░░░░░]   71%   4046 MHz
  cpu7   [█████████████████████░░░░░░░░░]   69%   3994 MHz

━━━━ 3 · MEMORY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  … MEMORY, DISKS, NETWORK, GPU, THERMALS & POWER, PROCESSES, SERVICES & PORTS, FOOTER

The TUI (./strobe --tui)

A real frame, rendered headlessly at 100×30 from ./strobe --tui --demo (colour stripped; on a terminal the bars and borders carry the theme gradient). Panels collapse to fit, the process list takes whatever is left, and every frame is diffed per line so it sips CPU. Spec in docs/TUI.md.

╭─ strobe ⠏ ─ demo-box ─ 6.9.0-demo ─ up 3d 04:12 ─ load 3.74 3.89 4.13 ─ 13 28 30 ────────────────╮
│ 1 CPU ███████████▎░░░░░░░░░░░░  47%  usr 32 sys 13 io 2 st 0  3.4 GHz  57°C                      │
│c0 █████████████████▎░░░░░░  72%  c1 ███▎░░░░░░░░░░░░░░░░░░░░  14%                                │
│c2 █████████▌░░░░░░░░░░░░░░  40%  c3 ██████████████▋░░░░░░░░░  61%                                │
│c4 ▍░░░░░░░░░░░░░░░░░░░░░░░   2%  c5 ████████████████████▍░░░  85%                                │
│c6 █████████████████▊░░░░░░  74%  c7 ███████▉░░░░░░░░░░░░░░░░  33%                                │
│┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ now 47%  peak 64%│
│┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈▁▂▂▂▃▄▄▄▄▄▄▄▄▄▃▂▁▁┈┈┈┈┈┈┈│
│┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈▅▆▆█████████████████████▇▇▇▇▆▇│
│                                                                    ██████████████████████████████│
├─ 2 MEM ────────────────────────────────────┬─ 3 DISK ────────────────────────────────────────────┤
│used ███████▋░░░░░  59%  18.4G/31.2G        │/         ██████▋░░░░  61%  290G/476G io 29.1M       │
│avail 12.8G  cache 6.4G  buf 656M  shr 302M │/home     ████▌░░░░░░  41%  782G/1.8T io 13.1M       │
│swap ███░░░░░░░░░░  24%  1.9G/8.0G          │                         ▅▃▆▆█▇█▆▇▅▅▆▄▄ ⇅ all 78.1M/s│
│              ▄▄▄▄▄▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅ peak 62%│                       ▇███████████████████▇▇████████│
├─ 4 NET ────────────────────────────────────┴────┬─ 5 GPU · 6 TEMPS · BAT ────────────────────────┤
│▼ 13.2M/s  ▲ 1.1M/s   VPN tun0 10.8.0.2          │gpu0 ████████▎  92%  7.0G/23.9G  77°C           │
│eth0 10.0.0.42 · wlan0 192.168.1.17              │temps k10temp 57°  nvme 41°  iwlwifi_1 44°      │
│▼                  ▆▅▆▆▆▆▆▇███▇▇▆▇▆▆▅▅▅ ▼ 13.8M/s│bat   █▎░░░░░░░  85%  ▼ discharging             │
│▲ █████████████████▁▂ ▂▂▃▃▃▄▂▃▂▄▄▄▅▄▃▂▂▁ ▲ 2.0M/s│                  █▆▅▅▄▄▃ gpu0 now 92%  peak 98%│
├─▶ 7 PROC sort:cpu  20/212 ──────────────────────┴────────────────────────────────────────────────┤
│   PID USER     CPU%▼      MEM%         RSS S  THR COMMAND                                        │
│▌ 5120 alice     69.4 ██▊░  2.0 ▌░░░   640M R   16 ffmpeg -i input.mkv -c:v libx265 -preset slow o│
│  1201 postgres   8.4 ▎░░░  2.6 ▋░░░   832M R    7 postgres 16/main: checkpointer                 │
│∙ 1702 root       6.0 ▏░░░  1.1 ▍░░░   352M R    8 Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/│
│∙ 3120 alice      5.5 ▏░░░  5.8 ▉░░░   1.8G R   52 firefox -contentproc -childID 3                │
│∙ 5200 alice      5.0 ▏░░░  0.4 ▎░░░   128M R    2 make -j8                                       │
│∙ 2210 alice      4.7 ▏░░░  3.1 ▋░░░   992M S   11 node /srv/app/dist/server.js                   │
│  1044 root       3.5 ░░░░  0.8 ▎░░░   256M S   24 dockerd -H fd://                               │
╰─ [q]uit [h]elp [tab] [↑↓] [s/S]ort [/]filter [k]ill [e] [1-9] [+/-] [g] [c] [t] [m] [H] [p] [w] ─╯

In a real terminal it carries the theme gradient — six themes, cycled with c:

strobe TUI in a terminal: gradient per-core bars, history graphs, per-process CPU sparklines

What is plugged in

Every view also answers "what is attached to this machine, and where" — the monitor and the port it is on, keyboards and mice, webcams, microphones, headsets, USB devices and their bus-port path, Bluetooth peripherals. Real output from a Mac mini:

  displays (1) ───────────────────────────────────────────────────────────────
  DISPLAY                          PORT           RESOLUTION       PRIMARY
  LG HDR WFHD                      DisplayPort    2560x1080@60Hz   primary

  attached devices (16) ──────────────────────────────────────────────────────
  KIND       NAME                         VENDOR        PORT           STATE
  input      Magic Keyboard with Touch I… Apple Inc.    USB 0x14400000 CONNECTED
  input      Magic Trackpad               -             Bluetooth      CONNECTED
  camera     Logitech BRIO                Logitech Inc. USB 0x14800000 CONNECTED
  audio-in   Logitech BRIO                -             USB            IN USE
  audio-out  PRO X 2                      Apple Inc.    Bluetooth      CONNECTED

  capture devices — microphone and camera ─────────────────────────────────────
  Microphone  Logitech BRIO — IN USE
  Camera      Logitech BRIO — state unknown (macOS exposes no reliable signal)

A device reads IN USE only when the platform can actually prove it — a running PCM capture stream on Linux, a live IOAudioEngine on macOS. When it cannot be determined the answer is unknown, never a guess: macOS exposes no dependable camera-in-use signal, so strobe says so rather than inventing a green light. The TUI puts the same data behind panel 0 and keeps a ● mic ○ cam indicator in its header; the dashboard gives it a panel with a tally light.

The dashboard (./strobe --serve)

strobe web dashboard: animated gauges, per-core heat strip, history graphs and process table

One HTML file, no build step, no CDN. Gauges, a per-core heat strip, scrolling history graphs and the process table, all driven by the same JSON the CLI prints, streamed over Server-Sent Events.

Install

git clone https://github.com/JeffPHenry/strobe.git
cd strobe
./strobe

That is the whole install. To run it from anywhere, link it onto your PATH:

ln -s "$PWD/strobe" /usr/local/bin/strobe     # already on PATH on most systems
# or, if that is not writable:
mkdir -p ~/.local/bin && ln -s "$PWD/strobe" ~/.local/bin/strobe

Then strobe, strobe --tui, strobe --serve work from any directory. strobe resolves its own symlink, so it finds lib/, server/ and web/ wherever you link it from — which also means git pull updates the command, with nothing to reinstall. Remove it with rm /usr/local/bin/strobe.

macOS. Apple ships bash 3.2; strobe needs 4+. brew install bash and you are done: strobe notices the old shell and re-executes itself under /opt/homebrew/bin/bash or /usr/local/bin/bash automatically. python3 (the Xcode command-line tools' copy is fine) gives you per-core CPU numbers and the web backend; without it you still get aggregate CPU and everything else.

Debian / Ubuntu. Nothing to install for the core sections. Optional packages light up optional sections:

Package Section it enables
iproute2 (ss) listening ports, with owning process when run as root
systemd (systemctl) failed units
docker.io / docker-ce running containers
nvidia-driver-* (nvidia-smi) GPU utilisation, VRAM, temperature, power
bluez (bluetoothctl) connected Bluetooth peripherals
alsa-utils (amixer) or pulseaudio-utils (pactl) microphone mute state
python3 --serve backend
mailutils / bsd-mailx (mail) --email delivery
jq not needed by strobe, but you will want it for --json

Every optional section degrades to a one-line "skipping" note when its tool is missing. Nothing dies.

Usage

Flags

Usage: strobe [MODE] [OPTIONS]

Modes (default: text report):
  --json             Print one JSON sample and exit.
  --follow           With --json: print one JSON object per line, forever.
  --tui              Interactive terminal UI.
  --serve [PORT]     Start the web dashboard backend (default port 8787).

Options:
  --output FILE      Report mode: also write the report to FILE.
  --email ADDR       Report mode: also mail the report to ADDR (needs 'mail').
  --interval N       Sampling interval in seconds for follow/tui/serve (default 1).
  --top N            Number of processes to collect (default 20).
  --demo             Use synthetic, animated data (works anywhere).
  --no-color         Disable ANSI colour.
  --version          Print the version and exit.
  --help, -h         Show this message and exit.

--follow on its own implies --json. --interval accepts fractions (--interval 0.5). The report always samples over one interval so rates (CPU %, disk and network B/s) are real, not zero.

Environment knobs

All of these are read by lib/core.bash and can be set for any mode.

Variable Default What it does
STROBE_PROC /proc root of procfs; point it at a fixture tree to replay a machine
STROBE_SYS /sys root of sysfs
STROBE_ETC /etc where os-release and passwd are read from
STROBE_DEMO unset 1 forces synthetic data (same as --demo)
STROBE_INTERVAL 1 sampling interval in seconds (--interval)
STROBE_TOP_N 20 processes to collect (--top)
STROBE_VPN_IFACE tun0 interface whose presence and address means "VPN up"
STROBE_SLOW_EVERY 5 ticks between refreshes of the expensive collectors (ss, systemctl, docker, nvidia-smi, logins)
STROBE_PORT 8787 backend port (--serve PORT)
STROBE_BIND 127.0.0.1 backend bind address; see the security note before changing it
NO_COLOR unset any value disables colour, per no-color.org
STROBE_FORCE_COLOR unset 1 keeps colour on when stdout is not a tty
STROBE_ASCII unset 1 forces the pure-ASCII glyph set in the report (automatic outside UTF-8 locales)
STROBE_REPORT_WIDTH auto report width: a number (80–220), auto, or fixed. Unset fills the terminal when you are reading the report and pins 100 when it is piped or redirected, so files keep diffing. Same knob as --width
STROBE_ALLOW_SUDO unset macOS only: 1 allows one passwordless sudo -n powermetrics per slow tick to read the CPU die temperature
STROBE_TUI_FRAMES, STROBE_TUI_COLS, STROBE_TUI_LINES, STROBE_TUI_KEYS unset headless TUI: render N frames to stdout at a fixed size, feeding keys; used by the tests

Health thresholds

Defined once, at the top of lib/core.bash, and printed in the report footer so a reader always knows which rules produced the verdict.

Metric Caution Unhealthy Applies to
cpu load >= 1 (CPU_LOAD_CAUTION) >= 2 (CPU_LOAD_UNHEALTHY) load1 / logical cores
cpu busy >= 80% (CPU_PCT_CAUTION) >= 95% (CPU_PCT_UNHEALTHY) user + sys + iowait + steal
memory >= 85% (MEM_PCT_CAUTION) >= 95% (MEM_PCT_UNHEALTHY) used = total - available
swap >= 50% (SWAP_PCT_CAUTION) - swap used
disk >= 90% (DISK_USAGE_CAUTION) >= 95% (DISK_USAGE_UNHEALTHY) space and inodes, per mount
temperature >= 75°C (TEMP_CAUTION) >= 90°C (TEMP_UNHEALTHY) CPU package and GPU
services any failed unit - systemctl --failed
vpn tun0 down - VPN interface has no address

Overall health is the worst check. The report's last line is a one-line SUMMARY host=… load1=… cpu=… mem=… disk=… status=… that is easy to grep out of a log directory.

TUI keys

Key Action
q, Esc, Ctrl-C quit (restores the terminal)
h, ? help overlay — reflows to the terminal, goes two-column when short, scrolls when even that overflows
Tab / Shift-Tab cycle focus between panels; focus changes what the graphs plot (the selected interface, the selected mount, per-core instead of total)
j move selection in the focused list (processes, disks, interfaces)
PgUp PgDn Home End page / jump in the process list
s / S cycle process sort forward / backward: cpu → mem → pid → name
/ filter (substring on command/args/user/pid); Esc clears
e details of the selected process in a footer box: full args, user, state, threads, cpu, mem, rss
k / K kill the selected process, with confirmation — TERM, or KILL with K
T grouping view: consecutive rows with the same command collapse to one ×N row with summed cpu/mem/rss; expand and collapse
i swap the footer key hints for a one-line explanation of the focused panel's numbers
1..9 toggle panels (1 CPU, 2 MEM, 3 DISK, 4 NET, 5 GPU, 6 TEMPS, 7 PROC, 8 SERVICES/PORTS, 9 HEALTH)
+ / - interval up/down (0.5 s steps, 0.5 – 10)
g cycle graph glyphs: block ▁▂▃▄▅▆▇█ → braille ⣀⣤⣶⣿ → dots
c cycle theme: aurora → synthwave → ember → glacier → matrix → mono
t cycle per-core view: bars grid → heat strip → one sparkline per core
m mini mode for tiny tmux panes — as few as 7 rows, and it fills whatever it is given
H horizontal layout: panels left, full-height process list right (needs ≥ 140 cols)
r force the slow collectors to refresh now
p pause / resume sampling
w write a full text report to ./strobe-report-<timestamp>.txt

Minimum terminal size is 80×24; truecolor when COLORTERM advertises it, 256-colour otherwise, and a mono theme for everything else.

HTTP API (--serve)

./strobe --serve [PORT] runs server/strobe_server.py, which spawns strobe --json --follow, keeps the latest sample plus a ring buffer of history (900 samples by default), opens your browser, and serves:

Route Response
GET / the dashboard (web/index.html)
GET /api/stats latest sample (JSON, same schema as --json)
GET /api/history?n=300 {"samples":[...]}, oldest → newest
GET /api/stream text/event-stream; one data: <sample> event per tick, with replay of recent history on connect
GET /api/report the plain-text report (text/plain)
GET /api/meta {"version","hostname","interval","started","demo","mode","platform","history","samples","clients"}

/api/* answers with Access-Control-Allow-Origin: * so other local tools can read it. ./strobe --serve accepts --bind ADDR and --no-open (or STROBE_BIND / STROBE_NO_OPEN=1); the backend can also be run directly for its extra switches: python3 server/strobe_server.py --help (--history N, --mock for a bash-free synthetic feed, --quiet, -v, --version).

curl -s localhost:8787/api/stats | jq .health
curl -sN localhost:8787/api/stream | head -c 2000

JSON for scripting

--json prints one object; --json --follow prints one per line (NDJSON), forever, until you stop reading. Keys are stable and documented in docs/ARCHITECTURE.md; unknown values are null.

# per-core busy %, as a list
./strobe --json | jq '.cpu.per_core'

# the top process
./strobe --json | jq -r '.processes.list[0] | "\(.pid)\t\(.user)\t\(.cpu)%\t\(.cmd)"'

# one-word verdict, handy in scripts
./strobe --json | jq -r '.health.overall'

# disks over 80%
./strobe --json | jq -r '.disks[] | select(.pct > 80) | "\(.mount) \(.pct)%"'

# a live one-line-per-second feed of the numbers you care about
./strobe --json --follow | jq -c --unbuffered '{t: .ts, cpu: .cpu.total, mem: .memory.pct, rx: .net.rx_bps}'

# 60 seconds of samples to a file, then the average CPU
./strobe --json --follow | head -n 60 > samples.ndjson
jq -s 'map(.cpu.total) | add / length' samples.ndjson

Nightly report with cron or a systemd timer

cron, one line, one file per night:

0 2 * * *  /home/you/.local/bin/strobe --no-color --output /var/log/strobe/$(date +\%F).txt >/dev/null 2>&1

systemd, if you prefer journald to know about it (systemctl --user works the same way for an unprivileged user):

# /etc/systemd/system/strobe-report.service
[Unit]
Description=strobe nightly system report

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'exec /usr/local/bin/strobe --no-color --output /var/log/strobe/$(date +%%F).txt'
# /etc/systemd/system/strobe-report.timer
[Unit]
Description=Run strobe-report nightly

[Timer]
OnCalendar=02:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now strobe-report.timer

Add --email ADDR to either ExecStart or the cron line to have the same text mailed through the local mail command.

Over SSH

strobe only needs bash on the far end, so a checkout in ~/.local/bin on each box is the whole deployment.

ssh box 'strobe --json' | jq .health            # structured verdict from a remote host
ssh box 'strobe --no-color' > box-report.txt    # the full report, saved locally
for h in web1 web2 db1; do ssh "$h" 'strobe --json' | jq -r '"\(.system.hostname)\t\(.health.overall)"'; done

Architecture

strobe  (entry point / dispatcher)
│
├── lib/core.bash          has(), thresholds, env knobs, formatting, JSON primitives, colour, platform
├── lib/collect.bash       THE collector: fills bash globals from /proc + /sys (linux) or a synthetic model (demo)
│   ├── lib/collect_darwin.bash   macOS overrides: sysctl, vm_stat, netstat, iostat, ps
│   └── lib/darwin_cpu.py         per-core CPU ticks on macOS (host_processor_info via ctypes)
├── lib/report.bash        long-form text report  (default, --output, --email)
├── lib/tui.bash           terminal UI            (--tui)
├── server/strobe_server.py HTTP + SSE backend    (--serve)  ← spawns `strobe --json --follow`
└── web/index.html         animated dashboard, served by the backend

One collector, one data contract, four renderers. The collector is sourced and called in-process by the report and the TUI; the web side talks to it through --json --follow. Rates come from deltas between two ticks, which is why every mode takes one interval before it has something to show.

Hacking

strobe                 dispatcher: bash-version gate, symlink resolution, arg parsing, mode dispatch
lib/core.bash          thresholds and STROBE_* defaults live at the top of this file
lib/collect.bash       _lin_* (procfs/sysfs), _demo_* (synthetic), collect_* dispatchers, collect_json
lib/collect_darwin.bash _dar_* collectors; re-declares collect_* to add a darwin branch
lib/report.bash        _rep_* one function per section, report_main
lib/tui.bash           _t_panel_* one function per panel, _t_render, _t_handle_key
server/strobe_server.py stdlib only: ThreadingHTTPServer, collector thread, SSE fan-out
web/index.html         the dashboard, CSS + JS inline, no build
tests/run.bash         lint + fixture collector + demo + CLI + server smoke  (make test)
tests/test_report.bash tests/test_tui.bash tests/test_darwin.bash   standalone suites
tests/fixtures/        proc/, proc2/ (two ticks of a fake machine), sys/, etc/
docs/ARCHITECTURE.md   the data contract; docs/TUI.md the TUI spec
make test        # tests/run.bash
make lint        # bash -n + shellcheck on everything
make demo        # ./strobe --demo
make tui         # ./strobe --tui
make serve       # ./strobe --serve 8787  (PORT=9000 make serve)
bash tests/test_report.bash; bash tests/test_tui.bash; bash tests/test_darwin.bash

On macOS run all of that with BASH=/usr/local/bin/bash make test (or /opt/homebrew/bin/bash).

Fixture tests. The collector never touches the live system in tests: STROBE_PROC=tests/fixtures/proc STROBE_SYS=tests/fixtures/sys STROBE_ETC=tests/fixtures/etc makes it read a frozen machine, and swapping STROBE_PROC to proc2 between collect_init and collect_tick gives deterministic deltas (core 2 at 100%, core 3 at 0%, a known number of bytes through eth0). To reproduce a bug from a real box, copy the relevant /proc and /sys files into a new fixture tree and assert on the globals.

Adding a collector. Add _lin_foo (and _demo_foo, and _dar_foo if macOS can supply it), a collect_foo dispatcher, and a line in _collect_run. Declare the new globals in _collect_reset, emit them in collect_json, write the contract into docs/ARCHITECTURE.md, and add fixture files plus assertions to tests/run.bash. Then render: a _rep_foo section in lib/report.bash, a _t_panel_foo in lib/tui.bash, a panel in web/index.html. Rules: gate optional tools with has, never print from lib/collect*.bash, never exit, and keep expensive work behind _SLOW.

Adding a TUI theme. A theme is a name in T_THEME_NAMES and five RGB stops in T_THEME_STOPS in lib/tui.bash; borders, gradients and accents all derive from the stops. Utilisation colours come from grad_pct_color in lib/core.bash and are the same in every theme.

Data contract. docs/ARCHITECTURE.md is binding: globals, JSON keys and HTTP routes. If you change the code, change the doc in the same commit.

FAQ

It says "needs bash >= 4" on my Mac. macOS ships bash 3.2 from 2007. brew install bash; strobe re-executes itself under the Homebrew bash automatically, so ./strobe keeps working from the system shell. You can also call it explicitly: /usr/local/bin/bash strobe.

Why is the web backend Python when everything else is Bash? Because a correct HTTP/1.1 server with keep-alive, SSE fan-out and a ring buffer is a bad idea in Bash and a 500-line stdlib script in Python. It uses only the standard library, so any python3 works, and the collector it runs is still the same bash script. If you do not want Python, --json --follow gives you the same stream to pipe wherever you like.

Headless server, dashboard on my laptop? Leave the backend bound to localhost on the server and forward the port:

ssh -L 8787:127.0.0.1:8787 box 'strobe --serve'
# then open http://127.0.0.1:8787 locally

--serve tries to open a browser; on a headless box that is a no-op.

Is it safe to expose the dashboard? The backend binds to 127.0.0.1 by default and the API is read-only, but it exposes hostnames, IPs, listening ports, logged-in users and process command lines. STROBE_BIND=0.0.0.0 ./strobe --serve (or ./strobe --serve --bind 0.0.0.0) makes all of that readable by anyone who can reach the port, with no authentication. The backend is a stdlib ThreadingHTTPServer and is not hardened for the public internet: keep it on a trusted network, behind SSH, or behind a reverse proxy that does auth and TLS.

Does it need root? No. Everything in /proc and /sys that strobe reads is world-readable. Running as root only adds process names next to listening ports (ss -p) and lets the TUI's k kill other users' processes.

What does demo mode actually do? --demo swaps the collectors for an animated synthetic model (eight cores, two disks, three interfaces, a GPU, a flaky bluetooth.service) so every mode can be tried, screenshotted and tested on any OS without touching the host.

License

GPL-3.0. See LICENSE.


made by Jeff Henry

About

system vitals, sampled in the dark — a Bash system monitor with a long-form report, a bashtop-style TUI, and an animated localhost dashboard. Linux and macOS, zero dependencies.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages