Status Of Me
Terminal-style personal status dashboard. Three subsystems sharing one data contract.
project/
├── frontend/ Vue 3 + Vite. Monospace terminal UI (Rosé Pine Dawn).
├── server/ Node + ws + node:sqlite. Ingests state, fans out over WebSocket.
├── pusher/ Node daemon. Collects local metrics, pushes to server, SNI tray + config UI.
└── sample-state.json StatePayload example for tests/manual POST (NOT a runtime seed).
pusher ──POST /state (30s)──► server ──WS broadcast──► frontend
──POST /admin/config─► ──GET /api/state─►
◄──── WS /ws relay ─────► (visitor message ⇄ owner reply)
- pusher runs as a local daemon on your workstation. Embedded HTTP config UI at
http://localhost:8765, optional DBus SNI tray icon. Collects cpu/gpu/mem/net/disk + screen-time + keyboard heatmaps and pushes aStatePayloadevery 30s. Also opens a/wsclient to the server so visitor messages arrive as desktop notifications (optionally moderated by an LLM); owner replies are sent back over the same socket. - server (VPS) accepts
POST /state, persists history to SQLite, broadcasts{type:"state",payload}to all WebSocket clients on/ws. Seed viaGET /api/state. Also relays ephemeral{type:"message"}/{type:"reply"}envelopes between clients (never persisted). - frontend renders the full terminal dashboard: half-block status banner, 7×24
heatmaps, btop-style braille resource sparklines, screen-time bars, custom text area,
and a floating pet overlay (shown when reply is enabled) for the visitor message/reply
chat. The browser tab title is
StatusOf<your_name>(setyour_namein the pusher config UI).
Data contract is frozen in server/src/schemas/state-payload-v5.schema.json.
Requirements: Node ≥ 22 (server and pusher use the built-in node:sqlite, which is
stable from Node 22). Linux for the pusher (reads /proc, /sys/class/drm, and uses DBus SNI).
# 1. server (VPS or local :8787)
cd server && npm install && node src/index.js
# 2. pusher daemon (local :8765 config UI + tray)
cd pusher && npm install && node src/index.js
# 3. frontend
cd frontend && npm install
npm run dev # dev server with HMR
npm run build # static build → dist/ (serve with any static host)Ports and keys:
- server listens on
server/config.json→port(default8787). Setapi_key(defaultchange-me-server-key— change it)./stateand/admin/configare authenticated by this key. - pusher config UI is fixed at
http://127.0.0.1:8765. Point the pusher at your server in the UI's Server tab (server_url,api_key) or inpusher/server.json. - frontend is a static SPA; it talks to the server over
GET /api/state+/ws. Configure the server origin in the frontend build/proxy (Vitedevproxies/apiand/wsperfrontend/vite.config.*).
- Install Node ≥ 22 on the VPS.
- Copy the
server/directory to the host (e.g./opt/statusofme/server). cd /opt/statusofme/server && npm install --omit=dev.- Edit
config.json: set a strongapi_keyand theportyou want to expose (default8787). SQLite history is written toserver/data/state.db(created automatically; ensure the process user can write thedata/directory). - Run behind a TLS reverse proxy (nginx/Caddy). The server speaks plain HTTP + WS;
terminate TLS at the proxy and forward
/state,/api/, and the/wsupgrade. Example nginx location for the WebSocket:location /ws { proxy_pass http://127.0.0.1:8787; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
- Persist the process with systemd (see below).
cd frontend && npm install && npm run build produces dist/. Serve dist/ from any
static host (nginx, Caddy, a CDN, or the same reverse proxy as the server). Ensure the
host forwards /api and /ws to the server origin, mirroring the Vite proxy above —
otherwise the SPA cannot reach GET /api/state or the /ws socket.
The pusher runs on the machine whose status you are reporting (Linux). It needs no public
ports — it only makes outbound POST requests to the server and serves its config UI on
127.0.0.1:8765. Configure server_url + api_key in the Server tab of the config
UI, set your your_name in the Pusher tab, then persist it as a user service (below).
Create /etc/systemd/system/statusofme-server.service:
[Unit]
Description=StatusOfMe server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# Use the absolute path to node (run `which node`); a bare "node" may not be on
# systemd's PATH. nvm/fnm users MUST point at the resolved binary.
ExecStart=/usr/bin/node /opt/statusofme/server/src/index.js
WorkingDirectory=/opt/statusofme/server
Restart=on-failure
RestartSec=5
User=statusofme
Group=statusofme
# Hardening (optional): the only writable path the server needs is data/.
ReadWritePaths=/opt/statusofme/server/data
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now statusofme-server
sudo systemctl status statusofme-server
journalctl -u statusofme-server -f # follow logsThe pusher's tray icon talks to your session's DBus (org.kde.StatusNotifierItem), so it
MUST run inside your graphical login session, NOT as a system service. Use a systemd user
unit, not a system unit.
Create ~/.config/systemd/user/statusofme-pusher.service:
[Unit]
Description=StatusOfMe pusher daemon
# Wait for the graphical session so the DBus session bus exists for the SNI tray.
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/node %h/statusofme/pusher/src/index.js
WorkingDirectory=%h/statusofme/pusher
Restart=on-failure
RestartSec=5
[Install]
WantedBy=graphical-session.targetEnable and start (note --user, no sudo):
systemctl --user daemon-reload
systemctl --user enable --now statusofme-pusher
systemctl --user status statusofme-pusher
journalctl --user -u statusofme-pusher -fIf the pusher should keep running after you log out (e.g. a headless metrics box), enable
lingering for your user: sudo loginctl enable-linger $USER. Without a graphical session
the daemon still runs and pushes metrics — only the tray icon is skipped (it logs a warning
and continues, since the tray is fully optional).
The pusher registers a StatusNotifierItem (SNI) tray icon automatically on startup —
there is nothing to register manually. The icon is the StatusOfMe logo (logo.svg, embedded
as raster pixmaps in src/logo-pixmap.js), the same artwork used as the page favicon.
Left-clicking it opens the config UI (http://localhost:8765); the menu also offers
open-setting and quit. Connection status (connected / stale / disconnected) is surfaced in
the icon's tooltip, not by changing the icon.
Requirements for the icon to appear:
dbus-nextmust be installed — it is a normal dependency, sonpm installinpusher/covers it. If it is missing, the daemon logs[tray]warnings and runs without an icon.- A running session DBus bus — present in any normal desktop login. When the pusher runs
as a
--usersystemd service (above), it inherits the session bus automatically. - An SNI host in your panel — i.e. a "Status Notifier" / "AppIndicator" tray. KDE Plasma
and most GNOME setups (with the AppIndicator extension) provide one out of the box. On
minimal compositors (niri, sway, hyprland) run a standalone SNI host such as
waybar(with thetraymodule) orsnixembed; without a host the SNI is published on DBus but nothing draws it.
Verify the item is published even when no panel shows it:
# lists registered StatusNotifierItem services on the session bus
busctl --user list | grep -i StatusNotifier
# or watch the pusher log line on startup:
# [tray] registered SNI as org.kde.StatusNotifierItem-<pid>-1Theme: single Rosé Pine Dawn (light). Switch by replacing
frontend/src/theme/rose-pine-dawn.css or the data-theme attribute on index.html.
Released under the MIT License © 白鞋油 (@professor-lee).