Picard — your web top interface

PICARD

Your web top interface
A Perl CGI system monitor with an LCARS soul
Marian Marinov — Summer 2026 Perl Community Conference
// STARDATE 126184.3   ·   MAKE IT SO

What is Picard?

  • A top(1)-like system monitor that runs in your browser
  • Backend: Perl CGI, ~500 lines, one file
  • Frontend: static HTML/CSS/jQuery, no build step
  • Reads everything from /proc — zero external tools*
  • LCARS-themed after Star Trek: Picard consoles
  • Bonus: aggregated view of LXD / Incus containers

* only who is invoked, for a users count.

tl;dr — if you can drop a CGI script onto a web server, you have a running system dashboard in about 60 seconds.
Perl 5.20+ JSON::XS /proc CGI Taint mode jQuery

The default view

Picard Star Trek Dark theme
LCARS · Star Trek Picard-era interface · Dark

Why Perl CGI in 2026?

What it buys you

  • One process per request — no daemon to babysit
  • No build system, no npm, no cargo, no venv
  • Perl is everywhere — a working monitor on any Linux box, tonight
  • Taint mode (-T) — free input hygiene
  • Boring is a feature — runs the same in five years

What CGI is not

  • Not the fastest per-request — who cares, we’re polling once every few seconds
  • Not stateful — state lives on disk (see: cache slide)
  • Not a framework — it’s a script that prints JSON
The whole backend is 488 lines of Perl. No dependencies you don’t already have on a working system.

What you get on one screen

Uptime

hostname, uptime, logged-in users

Load average

1 / 5 / 15 min, colour-coded thresholds

Tasks

total · running · sleeping · stopped · zombie

CPU

us / sy / ni / id / wa / hi / si / st bar chart

Memory

total / free / used / buff·cache / avail

Swap

optional, via ?show_swap=1

Processes

PID, USER, PR, NI, VIRT, RES, SHR, S, %CPU, %MEM, TIME+, CMD — sortable

Containers

LXD / Incus aggregation via cgroup parsing

Auto-refresh

1 s … 60 s, remembered per browser

Everything above is read from /proc on every tick.

Light theme, same LCARS bones

Picard Star Trek Light theme
Persisted via localStorage — Light · Dark · Picard · Classic

Aggregated container view

Picard container aggregation view
cgroup-derived: /lxc.payload.<name>/ (v2)  ·  /lxc/<name>/ (v1)  ·  also detects qemu-system … -name X

Architecture at a glance

Browser index.html jQuery poll loop LCARS UI Web server Apache · Nginx + fcgiwrap · server.pl any CGI-capable host picard.cgi Perl 5, -T taint mode JSON::XS output ~488 lines /proc kernel view who user count /tmp/picard-cache-N.json flock-based coordination TTL = 1 s /cgi-bin/picard.cgi JSON fork + exec stdout (JSON) read/write cache One request ⇒ one CGI invocation ⇒ one /proc walk Concurrent requests share the walk via the cache file (next slide)

What happens on a tick

1. Poll JS setInterval fires 2. HTTP GET /cgi-bin/picard.cgi 3. CGI exec web server forks perl -T 4. Coordinate O_CREAT | O_EXCL + flock 5. Walk /proc iterate every PID 6. Sample CPU two reads, 500 ms apart 7. Aggregate containers, sort, format 8. Encode JSON::XS to stdout 9. HTTP body web server relays JSON 10. Render jQuery updates DOM 11. END{ } creator unlinks cache Ten steps, one CGI invocation

Where the numbers come from

/proc the kernel’s live view no external tools required /proc/uptime seconds since boot /proc/loadavg 1 / 5 / 15 min averages /proc/stat CPU tick counters (sampled 2×) /proc/meminfo MemTotal, MemFree, Buffers… /proc/<pid>/* stat · status · statm cmdline · cgroup Uptime block days, HH:MM string Load block colour-coded threshold CPU bar us / sy / ni / wa / hi / si / st Memory bar total · used · buff/cache · avail Process table + container name via cgroup Container aggregate grouped totals by cgroup name Left: source · Middle: /proc · Right: output block

The tricky bit: many clients, one /proc walk

CGI A (creator) CGI B (reader) CGI C (reader) /tmp/picard-cache-0.json O_CREAT | O_EXCL flock(LOCK_EX) while writing flock(LOCK_SH) to read creator writes → unlink in END{ } /proc walked once write read read walk once One walk, N readers
  • Every browser tab is another CGI invocation.
  • Without coordination, every request walks /proc independently.
  • The first request creates the file with O_CREAT|O_EXCL and takes LOCK_EX.
  • Later requests take LOCK_SH and block until the writer close()s.
  • They then read the same JSON payload.
  • TTL: 1 s — coalesces bursts, feels live.

Cache file lifecycle

CGI A CGI B cache file /proc t=0 sysopen O_CREAT | O_EXCL → i_am_creator = 1 t=1 walk /proc, gather CPU, mem, processes, containers t=2 write JSON payload · close() releases LOCK_EX t=1.5 stat() → file exists · fresh (age < TTL) t=1.6 open() + flock(LOCK_SH)  — blocks until A closes t=2.1 inode check → read full payload → return t=∞ END{ } in creator process: unlink Two clients race — /proc is walked exactly once

Boring, careful Perl

Defence in depth

  • #!/usr/bin/perl -T — taint mode everywhere
  • Query string parsed only through a strict regex whitelist
  • $show_swap normalised to 0/1 before it goes anywhere near a filename
  • Cache file created with 0600, in /tmp, TTL-checked to detect crash leftovers

What Picard doesn’t do

  • No shell-out to ps, top, or free
  • No system() anywhere in the hot path
  • No writes to /proc
  • No auth built in — that’s the web server’s job (basic auth recipes ship in INSTALL.md)
Process listings leak PIDs, usernames and command lines — put Picard on a trusted network or behind auth. Just like top itself.

Three ways to serve it

Browser GET /cgi-bin/picard.cgi server.pl tiny IO::Socket::INET server forks CGI directly development · trusted LANs Apache · mod_cgid ScriptAlias /cgi-bin/ AddHandler cgi-script .cgi classic prod setup Nginx + fcgiwrap fastcgi_pass unix:.../fcgiwrap.socket recommended for prod picard.cgi same script, all three deployments The frontend and CGI never change — only the transport does

Bonus: lxdtop — Perl Curses TUI

  • Same JSON API, different frontend.
  • Curses + JSON::PP — both cheap deps.
  • Runs the CGI directly, or hits it over HTTP.
  • Sort: %CPU %MEM name procs. UTF-8 bars or --ascii.
The JSON API is not just for the web UI — anything that speaks HTTP or can fork a CGI is a first-class client.
$ lxdtop -d 2

CONTAINER      PROCS   %CPU   %MEM       RES
confoo            16  260.0   14.7      6.9g
local machine     57   12.0    1.7      1.2g
ebpf (vm)          1    4.0    2.8      1.3g
bgp               10    2.0    0.0     73.5m
grafana           12    2.0    0.7    401.6m
uptrace            9    0.0    0.4    264.0m
devops            21    0.0    0.7    498.8m
pihole            11    0.0    0.3    226.6m
vpn               10    0.0    0.0     72.1m
─────────────────────────────────────────────
 q quit  p pause  space refresh  d/s delay
 M %MEM  P %CPU  N name  T procs  R reverse

The API

GET /cgi-bin/picard.cgi?show_swap=1  → JSON

{
  "hostname":  "stargazer",
  "timestamp": "Fri Jul 03 12:00:00 2026",
  "uptime":    { "seconds": 1710191, "text": "19 days, 19:03" },
  "users":     2,
  "loadavg":   { "one": 0.49, "five": 0.54, "fifteen": 0.56 },
  "tasks":     { "total": 533, "running": 1, "sleeping": 532, "stopped": 0, "zombie": 0 },
  "cpu":       { "us": 3.3, "sy": 1.1, "ni": 0.0, "id": 94.8, "wa": 0.0, "hi": 0.0, "si": 0.7, "st": 0.0 },
  "memory":    {
    "ram":  { "total": 31317.5, "free": 1419.3, "used": 25650.8, "buff_cache": 4247.4, "available": 4120.7 },
    "swap": { "total":  8192.0, "free": 8127.5, "used":    64.5 }
  },
  "processes":  [
    { "pid": 922184, "user": "hackman", "pr": 20, "ni": 0,
      "virt": 21370265600, "res": 910475264, "shr": 150278144,
      "state": "S", "cpu": 9.5, "mem": 2.8, "time": "217:04.36",
      "command": "librewolf", "container": "local machine" }
  ],
  "containers": [
    { "name": "confoo", "cpu": 260.0, "mem": 14.7, "procs": 16, "res": 7409336320 }
  ]
}

For the LCARS-averse: Classic theme

Picard Classic Dark theme
Same data, minimal monospace chrome — one click to toggle.

By the numbers

488

lines of Perl in picard.cgi

196

lines of Perl in server.pl

841

lines of Perl in lxdtop

1

runtime dependency: JSON::XS

0

build steps · 0 bundlers · 0 lockfiles

4

theme combinations, one CSS file

1 s

cache TTL — coalesces bursts, feels live

3

deployment modes supported out of the box

concurrent tabs, still one /proc walk per second

Engage.

github.com/hackman/picard
$ git clone https://github.com/hackman/picard
$ cd picard
$ perl server.pl 8080
$ xdg-open http://localhost:8080
Thank you.
Questions?  ·  Long live top(1).
// END TRANSMISSION
Note: You are viewing an auto-generated directory index.