A gold-on-black, fleet-wide dashboard for pi
coding-agent sessions across the Tailscale network, including Claude Code and
T3 Code.
Its project icon is assets/icon.svg, registered for T3 Code in t3.json and
inlined as the dashboard's browser favicon.
One Go binary, two modes:
┌─ laptop ─┐ ┌─ home ──┐ ┌─ air / grokbot ────┐
│ agent │ │ agent │ │ agent (later) │ watches ~/.pi/agent/sessions
│ :8787 │ │ :8787 │ │ :8787 │ GET /sessions → JSON summaries
└────┬─────┘ └────┬────┘ └─────────┬──────────┘
└──────────────┼──────────────────┘
polled every 5s over Tailscale/MagicDNS
┌──────────▼──────────────────┐
│ hs-pi-dashboard serve :8788 │ on mini (always-on)
└──────────▼──────────────────┘
http://mini:8788 — machine → project → session cards
The live Herdr monitor shows agent states across machines and named Herdr sessions, with machine and state filters. It starts collapsed; select Live Herdr agents to show or hide all machine terminals, filters, and workspace states. Pulse starts expanded unless you previously saved it collapsed. Each section opens independently, and collection continues while Herdr is collapsed. Collection is read-only and does not send input, change focus, start a Herdr server, or read terminal transcripts. Unavailable or stale collectors do not count as live. Agents running only in tmux are not included in this monitor; session history and Pulse remain separate.
Each session title includes its originating application: Pi, T3 Code, Claude Code, or Hermes. This label stays visible on mobile and is separate from the computer running it and the provider/model powering it. Collectors report application identity explicitly. For older agents, the server recognizes T3/Claude ID namespaces and unnamespaced Pi sessions from the reported Pi collector directory. Missing metadata or an unknown source shows Unknown app.
The session table shows each machine, project, provider, model, thinking level, message count, cost, start time, and duration. Active sessions remain visible before they produce assistant text and flash when their state changes; completed transcript sessions without output stay hidden. T3 threads stay visible even without output, with their thread title, repository and state. Rebooted machines keep their last known sessions grayed out.
Above the session grid sits an expandable/collapsible Usage & Balances section built as a runway board. Every quota window (Codex weekly, OpenCode Go 5-hour / weekly / monthly, Antigravity Gemini Models 5-hour + weekly) is one row on a shared 0-125% scale with the plan limit marked at 100%. The solid bar is measured usage; the outlined extension is the forecast at reset, turning ember where it passes the limit, so over-limit forecasts line up down the column. Each row shows percent used, the uncapped forecast, how long until the window runs out at the current pace, and the reset countdown. Providers and their windows sort by urgency, and the section header names the window that runs out first, so the answer stays visible with the section collapsed.
OpenCode Zen and Moonshot AI prepaid balances sit in one line below the board.
Providers with no data collapse into a single "Not reporting" disclosure that
holds the raw collector errors. The agent refreshes usage every 60s
(GET /usage), and the server folds it into /api/fleet. The header shows one
update time; a provider is labeled only when its data is older than three
minutes or when a failed refresh left its last successful reading in place.
Missing values read "no reading" rather than zero. Section and provider
collapse preferences persist, and the 5-second refresh keeps keyboard focus.
Use Choose providers below the section heading to show or hide each quota or balance provider. Changes apply immediately to rows, warnings and the collapsed summary. They are saved in this browser for this dashboard address; other browsers and devices have their own choices. All five existing providers start enabled and Claude starts disabled. To use Claude instead of Antigravity, uncheck Antigravity and check Claude. The selector remains available when the section is collapsed, agents are offline, or every provider is disabled. These are display preferences; agents keep refreshing their cached readings. A provider you switch on that no agent reports yet, such as Claude with older agents, is listed under Not reporting until the agents are updated.
Codex reads ~/.pi/agent/auth.json first, using openai-codex.access and
accountId. Only an absent or unreadable Pi token falls back to
~/.codex/auth.json and its tokens.access_token / account_id. A failed Pi
request does not silently switch to CLI credentials. These paths are relative
to the agent process user's home; this collector does not use CODEX_HOME.
CLI sign-in alone does not replace the preferred Pi session. For human recovery
on Home, start Pi as Windows user User, then enter /login openai-codex
inside Pi and choose the correct ChatGPT account. The dashboard does not launch
login, refresh tokens or change credential files.
Codex cards expose only allowlisted credential-source, usage-response plan and
window-shape metadata. Tokens, emails and account IDs are never published.
JWT account identity may select the request workspace and bind cache reuse
privately; JWT plan claims never prove a current paid quota. The authenticated
usage response's plan_type is the plan evidence. Recognized plan tokens are
plus, pro, prolite, promax, team, business, enterprise and edu.
free is reported for diagnosis but supplies no paid quota. These tokens follow
the Codex account model
and usage-response mapping.
The dashboard supports exact 18,000-second and 604,800-second quota windows for
these plans. No currently supported plan proves a monthly allowance. Missing,
Free, unknown, malformed and legacy plan evidence, and any unsupported window
including 30 days, remain unavailable without a zero or forecast.
All selected Codex reporters are compared before freshness/health selection. Different verified plans or incompatible window families show account mismatch in the section summary and body, with reporting host, safe plan, source and stale/offline context. All Codex quotas and Fleet SUS are withheld during a mismatch. This is evidence of disagreement, not proof of which account is correct. Equal plans/windows cannot prove equal accounts because account identifiers are deliberately not published. Missing metadata or a failed host alone does not establish a mismatch. A healthy verified reporter can replace an unknown or failed one, but conflicting stale/offline evidence remains visible until that reporter updates or is removed from the fleet. Missing 5-hour or weekly windows are legitimate partial reports. Stale, offline and failed same-account cached readings retain measured percentages but lose forecasts, Codex SUS and Fleet SUS. Credential source/account changes inhibit quota cache reuse; when no account identity exists, even a token change invalidates it. Successful unsupported responses replace the previous paid reading immediately. Same-identity request failures retain authenticated plan/window diagnostics even without quota gauges, keeping the original reading time and new refresh error. Unverified metadata is not retained as authenticated evidence.
For synthetic local UI validation, run node scripts/codex-fixture.cjs and open
the printed loopback URL with ?state=mismatch, ?state=matching,
?state=unknown or ?state=failed-free. The last state uses the synthetic
collector/cache contract fixture after an HTTP 401. The fixture serves the
current embedded HTML/assets, labels the page synthetic, and supplies only
invented snapshots. It never reads
credentials or calls a provider.
Claude reports subscription quotas from the read-only
GET https://api.anthropic.com/api/oauth/usage endpoint with the
anthropic-beta: oauth-2025-04-20 header and a Claude Code User-Agent (some
valid sessions are refused otherwise), using an existing subscription session.
Credential precedence on each agent is CLAUDE_CODE_OAUTH_TOKEN, Claude Code's
~/.claude/.credentials.json (claudeAiOauth.accessToken), then Pi's
~/.pi/agent/auth.json (anthropic.access). CLAUDE_CONFIG_DIR replaces the
Claude Code directory and disables fallback to Pi, so a custom profile cannot
silently select another account. Tokens need usage/profile access; API keys and
inference-only setup tokens do not supply subscription usage. Sign in through
Claude Code or Pi as usual. The dashboard never starts login, reads the macOS
Keychain, renews tokens or writes credentials. An expired or inaccessible session
shows a setup/error message; renew it in its owning app. Service processes need
the same home directory or environment override as the signed-in account.
The collector supports five-hour, weekly, model-specific weekly and OAuth-app
windows, Routines weekly usage (including the seven_day_cowork alias), and
model-scoped limits entries (any display group). Structured limits session
and weekly entries are preferred; five_hour and seven_day are fallbacks.
Readings above 100% show as 100% while forecasts use the raw value. Missing or
invalid values remain unavailable, while valid sibling windows still report.
After a rate limit (HTTP 429) each agent waits for Retry-After, or five
minutes when absent, capped at one hour, before asking again. Structured
model-scoped limits replace the legacy Sonnet/Opus weekly windows for the same
model. Forecasts require a valid reset within the known window duration and at least
two minutes elapsed. Extra-usage spend and session token totals are excluded.
The schema and authentication headers were checked against
CodexBar's OAuth fetcher
and its
scoped weekly parser.
Every Claude card has a claude provenance object with allowlisted values and
times only. sourceMachine is the host whose own session produced the reading.
status is that host's last provider result: ok, no_session, auth_rejected
(HTTP 401), access_denied (HTTP 403), rate_limited (HTTP 429), cooldown,
request_failed or invalid_response. lastAttemptAt moves only when a request
was actually sent, so cooldown and no_session leave it alone. lastSuccessAt
and fetchedAt stay at the time of the reading, and nextAttemptAt is the end of
a cooldown.
An agent without a usable session can republish another agent's reading with
-claude-usage-peer name=http://ip:port, for example
-claude-usage-peer home=http://100.101.122.39:8787. The agent then reads the
peer's cached GET /usage once per refresh, and no credential is read or sent.
The session stays on its host and the peer's read sends nothing to Anthropic, so
its cooldown is undisturbed. Other provider cards stay local. The setting replaces
the local Claude session outright: a fallback could silently switch accounts.
The host must be a loopback or Tailscale IP. The client uses no proxy and follows
no redirect. The peer's machine must equal name, and a card the peer itself
republished is refused. Only labels, percentages, resets, window lengths,
forecasts, times, statuses and the collector's own fixed error messages are
copied. Other error text, a missing or future reading time, a malformed window
or a reading older than the retained one fails closed. peerStatus then says
unreachable, http_error, invalid, identity_mismatch, no_card or
out_of_order, with peerLastAttemptAt and peerLastSuccessAt beside it. The
last good reading stays at its original age and the source host's last known
attempt metadata is kept. Errors the peer itself reports are prefixed with its name.
Muse Code reports its subscription allowance from muse usage --json
(Muse Code 1.4.13+), a command that runs no model turn. The agent runs it
with META_API_KEY removed (an exported key would switch to pay-as-you-go),
MUSE_LOGIN=0 and MUSE_NO_AUTO_UPDATE=1, so it never signs in or updates
the CLI, and it reads no credential files. A reading is reused for five
minutes, or until its earliest window resets; failures back off from 5 to 30 minutes. A window that fails
validation, an undocumented window or an old observation adds a warning. The short window and the
weekly window become 5h and Weekly rows with the tier as a note. A missing
CLI, allowance: null, an unsupported schema or malformed output shows under
Not reporting, never as zero. The CLI's free-form reason and hint text
is never shown. Muse Code has no SUS or price. Field mapping and fixtures:
internal/usage/testdata/muse.
The board is plain HTML and CSS embedded in the Go binary, with no chart
library. PRODUCT.md and DESIGN.md record the product context and the
gold-on-black design system for later UI work.
The expandable Pulse monitor counts recently active Pi, Claude Code, T3 and Hermes sessions across the fleet. Pulse and the table use one fleet snapshot, and every counted active session remains in the row payload even beyond the inactive-history cap. Its green trace moves continuously to a live cursor at 75% of the monitor width, resting at the current active-session level while zero stays on the bottom baseline. The dot tracks the trace's rendered value and scale. Each horizontal line is an integer session level from zero through five by default; the grid adds denser integer levels when a count or beat needs more headroom. When an active session row receives a meaningful update, the row flash and cursor fire together with a short beat above that resting level. Cached sessions from offline machines and stale Hermes rows without an end timestamp do not count. The browser pauses the sweep when Pulse is collapsed or the tab is hidden, and reduced-motion mode renders a static trace.
The agent reads Pi and Claude Code session files without talking to either
running process. Claude Code sessions appear in the same table and Pulse count,
with claude-code in the provider column. No Claude hooks or plugin are needed.
Claude transcripts default to ~/.claude/projects, or the projects directory
under CLAUDE_CONFIG_DIR when set. Override the path with agent -claude-dir.
The scanner keeps recent assistant text, model, effort when recorded, message
counts, and tokens including cache reads and writes. Repeated assistant content
blocks count as one API message, not separate responses. Subagent directories,
sidechain records, thinking, and tool payloads are excluded from displayed output.
Activity uses the same recent-write window as Pi, not process liveness. A quiet session stops counting after two minutes by default. Claude transcript records do not provide dollar costs, so their price cells stay blank. No subscription balance is inferred. Future Pi streaming support would use an extension.
T3 metadata reads open SQLite strictly read-only with a 500 ms busy timeout
and one retry inside a 2.5-second projection deadline. The metadata statement
validates the history schema but does not aggregate messages or tool nodes.
Exact history counts refresh at most once every 30 seconds, with a separate
750 ms limit and sequential scans. A failed counter refresh retains its last
values and original t3CountsAt, with t3CountsError; a missing timestamp
means counters are not available yet. History failure never supplies liveness.
No T3 schema or indexes are changed. Each query closes its rows immediately;
there is no read transaction across metadata, history, retry or runtime calls.
Runtime reads get a fresh five-second deadline after SQLite closes. Agents
serve completed /sessions snapshots immediately, coalescing concurrent
requests into one refresh and waiting five seconds after completion before
starting another. Extra clients do not queue database scans, and idle agents
without requests run no refresh timer. Snapshot and source timestamps retain
their original meaning.
SQLite BUSY/LOCKED and read deadlines have distinct errors and may retain
recent active state for at most five minutes, with original lastSuccessAt
and session timestamps. An idle snapshot is never retained as evidence of
available dispatch capacity. Missing/expired snapshots, incompatible schema
and non-timeout runtime failures remain unavailable. lastAttemptAt and
readDurationMs expose the last projection attempt for operational checks.
An opt-in load test requires a database of at least 2 GB. Create a consistent
SQLite online backup named t3-home-load-copy.sqlite, set HS_T3_BENCH_DB to
that copy, and optionally set HS_T3_BENCH_WRITE_COPY=1 to run a bounded WAL
writer against the copy. Run go test ./internal/t3 -run TestProductionSizedReadUnderWriteLoad -v -count=1 -timeout=60s. The test opens
its reader with mode=ro, refuses writes to known live T3 files, compares
before/after plans, and logs only counts, durations and writer commits.
Each agent reads ~/.t3/userdata/state.sqlite in SQLite read-only mode and verifies
live states through T3's local /api/orchestration/shell endpoint. It collects
all non-deleted threads across every project in that environment, including closed
tabs and archived history. T3 rows remain in the payload beyond the transcript
history cap; the table paginates them. No T3 plugin, agent tool call or open tab is
required. The collector reads metadata, turn timing and provider counters without
reading prompt text or displaying tool payloads.
The collector supports both V1 state.sqlite and Orchestrator V2
statev2.sqlite. When the default environment upgrades, it switches to V2 and
keeps imported thread identities stable. V2 reads its current thread, run,
provider-turn and tool-node projections; the frozen legacy tables are never used
as a live fallback. All non-deleted threads are included, including delegated
child threads, archived history and threads with no assistant output. Independent
environments retain separate identities.
V2 live snapshots use the same local authenticated shell endpoint with
x-t3-orchestration-protocol: 2. Working, connecting, waiting and background
monitoring states come from that runtime, so quiet work stays active without
recent transcript writes. Persisted running metadata alone never counts as live.
Unknown schema or runtime states produce an unavailable source and recover on a
later poll. V2 shell responses allow up to 64 MiB of decompressed JSON, including
active and archived history; V1 keeps its 8 MiB limit. Larger responses are
unavailable rather than partially counted. A V2 run waiting for checkpoint
capture appears as working; only a pending approval or input request appears as
waiting for the user. T3 can leave a request pending after its run has ended,
and nobody can answer it. When the shell reports activityRunStatus: null, no
run is live, so the request does not make the thread waiting or active. The
thread's state then comes from its run status and background tasks, and its
Input or Approval label is unchanged. Servers that omit the field still count
the request. Explicitly configure state.sqlite to monitor a separate
V1 instance;
an upgraded runtime cannot validate its frozen V1 copy as live.
V2 statistics use distinct tool nodes and the latest provider-turn counter
snapshot, with current provider-thread context usage. Unreported totals remain
n/a; zero remains zero. Only metadata and selected counter fields are read
from V2 JSON projections, excluding prompt text and tool inputs/outputs.
T3 V2 rows show the sidebar status separately from activity: Working, Goal,
Waiting, Approval, Input, Limited, Failed, Woke, or Done.
A ready, acknowledged thread has no badge. Working stays green. Approval and
input, including requests from live app-owned subagents, take precedence over
runtime work. As in upstream threadShell.ts, a live app-owned child's request
propagates through its ancestry even when an intermediate parent has settled;
ending the requesting child clears that propagated request. Usage-limit failures
show Limited until recovery. Done and Woke
use the server's visit and snooze metadata. Missing visited metadata never
invents an unread completion, and unavailable/offline sources override cached
labels. V1 and older dashboard agents retain their existing normalized labels.
The mapping follows T3 Code's current sidebar at
1451e7cb2303
and its shell presentation rules, rather than the older status-pill helper.
The fleet's local Waiting patch also treats background command tasks as waiting;
unpatched upstream only lets subagent, monitor and background-task kinds hold
completion. A checkpoint-only run remains Working. The agent exposes this
source-backed label in optional t3Status; an empty string means no badge.
The separate status and active fields continue to describe verified activity.
Label changes alone do not alter Pulse, so a Waiting badge on an idle runtime
need not count as live work.
Legacy activity states include connecting, working, monitoring, waiting, idle,
stopped and error. Pulse counts verified connecting, working, monitoring and
waiting threads. Quiet tools and background monitoring stay active regardless of
-active-window; that flag still controls native transcript activity. Open,
snoozed or unsettled threads are not active merely because they exist. The table
and Pulse use the same agent snapshot on each five-second dashboard refresh.
Duration uses the latest turn, not the lifetime of the thread.
The agent discovers each environment's loopback origin from the adjacent
server-runtime.json. It verifies the recorded process creation time and that
the process owns the listening port before sending any credential. Under the
same OS account, it runs the installed T3 CLI's
auth pairing create --base-dir <environment> --ttl 1m --json, then exchanges
that one-time credential at /oauth/token, requesting only orchestration:read.
When the CLI advertises --scope, the pairing grant itself also requests
orchestration:read. Older V1-compatible CLIs, including 0.0.45, have no scope
flag: their grant carries standard client scopes, expires after one minute,
is single-use, and is immediately narrowed at the verified local server.
The resulting bearer always has exactly orchestration:read.
It refuses broader bearer tokens, redirects and non-loopback origins. This supported
auth flow creates T3-managed access records; it does not modify thread projections
or restart T3. The session token stays in agent memory and is reused until expiry
or runtime restart. Renewal uses a new local pairing grant; an HTTP 401/403 clears
the token and retries on the next poll. No token belongs in service arguments,
environment variables, files, logs or dashboard payloads.
V2 requires a CLI compatible with the current server: an old V1 CLI can create
a pairing grant in the legacy database that the V2 server cannot exchange.
Install a matching T3 CLI and expose it on the agent's
PATH, or pass -t3-bin /absolute/path/to/t3. On Windows, use the native t3.exe,
not an npm .cmd or PowerShell wrapper. The agent also discovers the native
binary beside the standard npm launcher. That installation stores it
under %APPDATA%\npm\node_modules\t3\node_modules\@t3code\t3-win32-x64\t3.exe.
The environment base directory comes from the database path: for
~/.t3-server/userdata/state.sqlite, pairing uses ~/.t3-server. Separate
configured environments get separate credentials and stable thread identities.
When server-runtime.json names a serverVersion and
<base>/runtime/versions/<serverVersion>/t3 (t3.exe on Windows) exists, the
agent pairs with that native CLI, which matches the running server and needs no
Node launcher. Otherwise it runs the configured CLI with T3CODE_HOME=<base>,
so an npm launcher finds runtimes in the monitored environment instead of
downloading into ~/.t3. Before this, a launcher whose version was missing
from ~/.t3 downloaded on every poll, and the deadline killed each download,
leaving partial .staging-* folders (#42).
The supplied Air LaunchAgent sets an absolute -t3-bin path because launchd does
not inherit an interactive shell's PATH. Adjust that path for other installations.
Database and live requests share a 2.5-second polling deadline, within the fleet request limit of 3 seconds. Native CLI pairing commands alone may continue for up to 30 seconds across polls, with at most one command in flight per source. A later poll consumes the result instead of restarting a slow Windows cold start. Pending pairing reports unavailable; credentials and results remain in memory. Missing discovery, a stopped runtime, failed authentication, timeouts or incompatible responses leave T3 metadata visible as unavailable and clear its active count. Fresh or stale persisted running metadata cannot keep a dead thread active. Collection retries on each poll and recovers without duplicate rows. A compact T3 collection disclosure keeps paths and failure details hidden until expanded. Actual unavailable/offline sources retain a highlighted status in that summary.
T3 rows start collapsed. Click their title or use Tab followed by Enter/Space to expand statistics and any stored assistant output, even when no output exists. Expansion and diagnostic disclosure survive polling; row expansion also survives pagination. Collapse the row to return to the compact list.
The expanded statistics line shows distinct tool calls and the latest reported input,
output, total, cache, reasoning and context counters. Repeated cumulative snapshots
replace previous counters rather than adding to them. n/a means the provider did
not report that metric; zero remains zero. Context usage is separate from total
processed tokens. Claude and Codex report different subsets, and these values are
provider telemetry, not billing totals. T3 costs are unavailable. A native Claude
transcript associated with a T3 provider session/thread is excluded so the session
and Pulse count once. V2 retains every linked Claude native identity across
provider switches, including switches to Codex. The newest linked native output
remains expandable on the app thread.
If T3 metadata is inactive or unavailable while the native transcript has fresh
writes, the row explicitly says "Claude transcript active" and Pulse counts that
independent activity once. Its T3 state remains visible, statistics are marked
cached, and unavailable turn timing is not inferred from transcript lifetime.
Configure each environment with a repeated flag, or disable collection explicitly:
./hs-pi-dashboard agent -addr <tailscale-ip>:8787 -t3-db /path/to/userdata/statev2.sqlite
./hs-pi-dashboard agent -addr <tailscale-ip>:8787 -t3-db /first/state.sqlite -t3-db /second/state.sqlite
./hs-pi-dashboard agent -addr <tailscale-ip>:8787 -t3-db ~/.t3-server/userdata/state.sqlite -t3-bin /path/to/t3
./hs-pi-dashboard agent -addr <tailscale-ip>:8787 -t3-db offMissing, locked or incompatible databases produce a visible T3 collection warning without breaking Pi collection. Cached metadata remains visible but never counts as live. Collection retries on each poll and recovers when the database is readable again. It never creates a missing database. Thread IDs include the environment path so independent environments cannot collide. T3's SQLite projections are an internal interface; schema changes may require a dashboard update.
For fleet rollout, build all OS targets from the same reviewed revision with
python deploy/build_fleet.py, then replace and restart every dashboard agent
and Mini's dashboard server with that build. Transfer macOS executables in a tar
archive, extract into a staging directory with a command-line tool, and verify
the executable SHA-256 before launch. Direct Taildrop executable downloads can
acquire quarantine metadata and stall before the program starts.
Configure database paths on the machine
that owns each T3 environment, under the user account that can read its userdata.
Check each agent's /sessions source health and the dashboard's /api/fleet payload.
The dashboard can be opened in T3's browser panel as an ordinary page. Merge and
fleet installation are separate from implementing this collector.
The agent also pushes its T3 threads to project-manager's pmd, so the Chief's
dispatch checks can ask pmd's GET /v1/assignments instead of scanning each
machine over ssh (#74).
It posts a full snapshot to POST <pmd>/v1/ingest/t3/<machine> every 30 seconds,
and sooner when the T3 database or its WAL changes, but never more often than once
every 5 seconds. The body follows pmd's
ingest contract:
every unarchived, undeleted thread (subagents included) with its project, branch,
lineage, model, latest run, active/queued/settled state and known PRs, plus
pending requests whose run hasn't finished. It is a port of project-manager's
reference sender scripts/t3/snapshot.py. A fixture test compares the two
field for field, and a pinned copy of the script lives in
internal/t3/testdata/ingest. The database is read in one read-only transaction.
No prompts, messages or tool payloads are sent. Question text isn't in the
database, so questions is always [].
The pinned copy is scripts/t3/snapshot.py at project-manager 30bf2446
(#116).
settled follows the rule of
project-manager#115,
which Chief's scripts/t3-activity.py applies too. It is true only when
settledOverride is settled and settledAt is a zoned RFC 3339 date-time
(Z or ±hh:mm). With a latest run, that run must have finished (completed,
failed, interrupted or cancelled), and settledAt must be at or after its
completed_at, or its requested_at when completed_at is null. That time must
be a zoned RFC 3339 date-time too, so an empty completed_at gives false,
though latestRun.at then shows requested_at. Both are compared as instants,
with offsets applied and every fractional digit kept. A missing, null, naive or
malformed timestamp gives false. With no run, a valid settledAt is enough.
| Flag | Default | Meaning |
|---|---|---|
-pmd-url |
http://100.97.164.73:8790 |
pmd base URL; off disables the push |
-pmd-token-file |
~/.config/hs-pi-dashboard/pmd-token |
bearer token, one line; missing turns the push off |
-pmd-t3 env=path |
the only T3 database, as "" |
push this database as environment env; repeat for several installs |
With no -pmd-t3, the agent pushes its only T3 database (the default
~/.t3/userdata/statev2.sqlite, or the one -t3-db) as environment "". With
several -t3-db and no -pmd-t3, it pushes nothing and logs why. Air runs two
installs:
./hs-pi-dashboard agent ... \
-pmd-t3 server=$HOME/.t3-server/userdata/statev2.sqlite \
-pmd-t3 desktop=$HOME/.t3/userdata/statev2.sqliteEach machine has its own token, named hs-pi@<machine> with scope ingest:t3.
It may post only to its own machine's path. pmd token add prints only the
token on stdout, so on mini it can go straight into a private file:
mkdir -p ~/.config/hs-pi-dashboard
(umask 077; pmd token add hs-pi@mini --scopes ingest:t3 > ~/.config/hs-pi-dashboard/pmd-token)For another machine, write that machine's token the same way and pipe it over ssh into the same path under the agent's user, without echoing it.
On Windows the file is C:\Users\<user>\.config\hs-pi-dashboard\pmd-token.
Restrict it to the agent's user with icacls (macOS and Linux need mode 600, or the
push stays off). The token is read before every push, so adding or rotating it needs no
restart, and it is never logged.
The push runs beside the dashboard agent and never blocks it. Logs show one
line per state change: push on or off, first success, each distinct failure.
Network errors and 5xx are retried on the next tick, and 409 stale_snapshot
is dropped (the next snapshot heals it). A 400 or 413 waits until the
snapshot changes. Any other 4xx (403 wrong_machine, 404 unknown_machine,
a revoked token) waits until the token file changes. Either kind of hold
retries once after 15 minutes, so a fix on pmd's side heals.
HS_PI_INGEST_LIVE_DB=<statev2.sqlite> go test ./internal/t3 -run LiveDatabase
compares both senders on a real database.
Weekly rows show Sub Utilization Score (SUS), from 0 to 100, next to their usage bars. Fleet SUS appears in the usage summary. A score of 100 means landing exactly at the weekly allowance at reset. Below 100% projected use, the score equals the projection. Above 100%, the score subtracts the share of the window spent locked out: 100 − 100 × max(time to reset − time to limit, 0) / window length. It uses the same projection and run-out instant as the bars, preserving the full projected lockout interval when cached data ages beyond that instant. Scores are rounded to the nearest whole number, with green at 90+, yellow at 75–89, and red below 75.
Claude Weekly, Codex Weekly, and Antigravity's Gemini Models weekly window qualify. Argon and third-party pools are excluded from Antigravity metrics and Fleet SUS. Five-hour windows do not get SUS. Windows younger than 12 hours at the reading's fetch time, missing forecasts and failed readings show n/a. Cached early readings cannot age into a score without a new sample. Expired windows also show n/a. Provider filters apply to Fleet SUS.
Each provider's scored weekly buckets are averaged first, then that provider is weighted once, preserving fractional provider means until the final Fleet SUS is rounded. Current monthly weights are Claude $200, Codex $200 and Antigravity $19.99. If any scored provider has an unknown price, Fleet SUS uses equal provider weights. Providers with no score are excluded.
Set HS_PI_DASHBOARD_CLAUDE_PRICE_USD, HS_PI_DASHBOARD_CODEX_PRICE_USD
or HS_PI_DASHBOARD_ANTIGRAVITY_PRICE_USD to override a current price.
Claude and Codex are planned to drop to $100 at their next billing periods.
Record the actual dates with HS_PI_DASHBOARD_CLAUDE_NEXT_PRICE_AT and
HS_PI_DASHBOARD_CODEX_NEXT_PRICE_AT, using RFC3339 timestamps including
the time-zone offset. Until each configured date, its current price applies;
afterward its next price defaults to $100. The corresponding
HS_PI_DASHBOARD_<PROVIDER>_NEXT_PRICE_USD overrides that value. No billing
date is guessed. Invalid price/date configuration makes the price unknown.
Antigravity weekly groups come from the structured
agy -p /usage --output-format json command,
requiring num_turns: 0 and command.name: usage. Only the canonical
gemini-5h and gemini-weekly bucket IDs supply Antigravity metrics. Group
names and display labels never select a pool. The parser accepts the CLI print format
id/remaining_fraction/reset_time and the quota API camelCase fields.
Missing, disabled, duplicate, expired or malformed Gemini windows are unavailable
rather than zero or unrelated fallback. A valid window remains usable when the
other is missing. Gauges carry bucketId through /api/fleet; the renderer
revalidates Gemini identity and timing before card selection, forecasts, warnings
and SUS. Label-only cached reports from older agents show unavailable until
updated. Fixture provenance and the live-capture limitation are documented in
the synthetic fixture notes.
The collector requires an existing CLI session
and never starts sign-in when its session file is absent. Set
HS_PI_DASHBOARD_AGY_BIN if the service cannot find the CLI. A 15-second
CLI deadline and 4 MiB output bound apply. The CLI and legacy HTTP fallback
share one 20-second refresh deadline. When CLI usage is unavailable, the HTTP
endpoint may supply only canonical Gemini windows, with an explicit source
warning and no SUS. An endpoint without valid Gemini reports unavailable.
These are live readings, labeled separately from cached refresh failures. Grok coverage
warnings also describe the current scan rather than cached quota data.
Grok / X has no available quota data, so it shows observed turns over the last
seven days and SUS n/a. Each agent reads only local turn IDs/timestamps from
~/.t3/userdata/statev2.sqlite and ~/.grok/sessions/*/*/usage.json.
Set HS_PI_DASHBOARD_GROK_T3_DB for a different T3 database. Both SQLite
and JSON sources are read only; no prompts or credentials enter the activity
card. Hashed native turn identities deduplicate overlap between T3, the Grok
CLI and reporting fleet agents. Payload-only T3 drivers and native provider-thread
identities are supported. Merged activity retains the oldest reporting timestamp
and labels offline contributors. Coverage is limited to files currently present
on reporting agents, with unavailable sources stated explicitly. Missing
sources show activity unavailable rather than an invented zero or percentage.
History collection is bounded to three seconds, 256 files, 4,096 directory
entries and 16 MiB of aggregate JSON per refresh, with at most 10,000 retained
turns. A bound reached is reported as partial coverage. Supported T3 V2
layouts use native turn columns or provider-thread metadata with explicit
turn timestamps; missing timestamps are reported as unavailable. This is
observed activity, not a subscription allowance.
Set GITHUB_BILLING_ORG=hemsoft-dev in the agent service environment to add a
GitHub card to Usage & Balances. It appears automatically in Choose providers,
with the same browser-local selection behavior. Configure it on one agent for
each organization; reports from multiple agents for the same org deduplicate,
while different organizations keep separate cards.
The collector runs the installed GitHub CLI, using the existing GH_TOKEN,
GITHUB_TOKEN or stored gh account configuration. It never starts a login,
reads a token into dashboard data, or changes an account or billing setting.
Use an org owner account with admin:org for a classic token, or the equivalent
organization Administration read permission. Services use the same home and gh
configuration as that account. HS_PI_DASHBOARD_GH_BIN also applies to billing.
Missing access, rate limits, unavailable gh/API and invalid responses appear on
this card without affecting other providers. Each refresh has a 20-second
budget and each CLI response a 4 MiB limit.
Read-only GitHub usage APIs
provide monthly Copilot AI credits and net/gross spend by product.
The budgets API supplies all
budget pages, up to a safe ten-page limit; an incomplete report is an error.
Both the live singular budget_product_sku and documented plural
budget_product_skus are accepted. A product counts as covered only by an
organization-wide or applicable enterprise ProductPricing budget that stops usage. Repository, SKU or
user budgets remain visible without claiming they cover a whole product.
The card shows the UTC billing month, credits, net/gross dollars, each budget's
amount and stop state, and month-end credits/net-spend projections at the
month-to-date pace. An empty month is zero. Summary totals include every reported SKU, including
subscription SKUs such as copilot_for_business when GitHub returns them.
AI-credit amounts remain a separate report. Projections apply the month-to-date
pace to all reported summary spend, including subscription charges; they are estimates,
not invoice amounts or spending caps. The expected copilot_for_business seat
SKU is included in totals and labeled as seats, but does not trigger a usage
alert merely because it has no stop budget. Other positive product usage without
an applicable stop budget requires attention. Other Copilot SKUs and AI-credit
net spend require attention even when a policy or budget should prevent overage;
unknown SKUs are never treated as seats.
GitHub does not expose the Copilot paid-usage policy through these APIs. Set
GITHUB_COPILOT_PAID_USAGE=disabled on the configured billing agent to record
the owner-confirmed policy (hemsoft-dev: "AI credit paid usage: Disabled"). The
card labels it as agent configuration, not API-verified protection, and shows
"Paid AI usage disabled" for Copilot instead of a missing-budget warning.
enabled is also accepted; leaving it unset keeps the policy unknown. This
setting documents the policy and never changes it or suppresses actual usage
charges. Invalid values fail inline. A missing budget alone proves no policy.
API failures keep the last successful reading from the same UTC billing month
with its original timestamp and an explicit cached error. They never reuse a
previous month as the current month, show a fabricated zero, or hide failed
quota providers in the collapsed summary. Billing defaults to a separate 15-minute cadence. Set
GITHUB_BILLING_INTERVAL to a Go duration such as 30m; values below
15 minutes are clamped, and invalid values use the default. Between collections,
cards retain their original timestamp. Access errors also wait this interval.
Each configured org makes 12 requests/hour with one budgets page at the default
interval, or at most 48 with ten pages. Other providers keep their cadence. Rate-limit responses honor Retry-After and primary reset times,
with a cooldown of at least the billing interval; secondary 403 responses are distinct from
missing scope. Net/gross product totals and net projections preserve the usage
summary exactly. If the AI-credit report is ahead, the card highlights its
separate Copilot dollar amounts without adding overlapping reports together.
Agents expose cached GET /projects snapshots from gh x status --json
(schemaVersion 1, gh-x v0.22.0 or newer). Install gh-x and authenticate gh
under the service account. Service processes resolve gh from PATH and common
native install locations, including /opt/homebrew/bin/gh under launchd. Set
HS_PI_DASHBOARD_GH_BIN=/absolute/path/to/gh to select another installation.
Each agent runs only its own configured checkouts,
serially, with a 45-second deadline per checkout and an 8 MiB output limit.
The default refresh is 30 minutes, with a minimum of 15 minutes. A new sweep
waits the full interval after the previous sweep completes. HTTP requests
never launch scans. A GitHub rate-limit failure stops the remaining sweep:
skipped checkouts retain their previous documents with an explicit rate-limit
error. The next sweep waits at least the normal interval and any later reset
deadline exposed by gh-x stderr or unavailable JSON section reasons through
X-RateLimit-Reset or Retry-After seconds. A successful gh-x exit with a
rate-limited section also stops the sweep.
Without a reset deadline, the normal interval applies. Diagnostic stderr is
scanned while draining, with only a 32 KiB prefix and a small bounded overlap
retained, and is never displayed verbatim; other failures retain the
installation/authentication or invalid-document messages.
Projects is off by default. Repeat -project-path /absolute/checkout for a
short explicit list of active repositories; -project-path off disables it.
Unset, empty and whitespace-only lists never discover repositories or fall
back to the working directory. Linked worktrees are represented by gh-x health,
rather than scanned independently. Use -projects-poll 30m to select a cadence;
shorter than 15m is clamped to 15m. Budget the list across all agents sharing an
account: 12 repositories at 30m means at most about 24 status runs per hour,
and each run can make multiple GitHub requests. Use the native
ProjectDashboard repository list rather than every checkout under ~/github.
No shell or GitHub status
policy is reproduced by the dashboard. It preserves gh-x clean booleans and
review/check verdict strings, including unknown values.
The hub merges the newest document per hosted repository into /api/fleet.projects
while retaining health and collection failures for every machine and checkout.
The expandable Projects section shows PRs, issues, merged PRs and runs once
per repository, with GitHub links and each section's own freshness/error metadata.
Counts are row counts from gh-x, so its default five merged PRs/runs remain five.
An unavailable section is never presented as an empty success. Failed refreshes
retain their last document and original timestamp; offline machines remain explicit.
The hub also keeps each checkout's last document through an agent restart: when
an agent reports a checkout without a document (its first sweep is pending or
failed) or with an older one, the hub shows the document it already holds beside
the agent's message. A checkout the hub has never seen a document for shows the
agent's result as is.
Missing/old agents report a Projects warning without affecting session or usage collection.
Install the patch-level Go version declared in go.mod, or a newer
stable release. CI reads that same minimum and uses the pinned version on each
platform. Fleet builds verify the local toolchain with GOTOOLCHAIN=local and
refuse older, development or unrecognized versions before compiling or replacing
an artifact. Upgrade Go explicitly; the builder does not auto-download a toolchain.
go build ./...
go test ./...scripts/accessibility-check.cjs runs axe-core against the synthetic fixture
in mismatch and failed-free states at 1280×900 and 390×844 after expanding
collapsed sections. It checks real Tab focus and Enter activation for the
Pulse toggle and footer link. Install the pinned tools outside the repository:
a11y_tools="$(mktemp -d)"
npm install --prefix "$a11y_tools" playwright@1.64.1 axe-core@4.13.0
"$a11y_tools/node_modules/.bin/playwright" install --only-shell chromium
NODE_PATH="$a11y_tools/node_modules" node scripts/accessibility-check.cjs \
--json accessibility-report/report.json --screenshot-dir accessibility-reportThe command exits 1 for axe violations or keyboard failures and 2 if the scan
cannot complete. Incomplete axe results remain listed for manual review.
The Accessibility workflow runs on
pull requests and pushes to main, and uploads the report and screenshots.
The three decorative chevrons are hidden from the accessibility tree. The
short Pulse count is meaningful text: its green on the opaque dark readout
has 15.32:1 contrast; the label has 13.61:1. The opaque background prevents the
animated trace from reducing that contrast. Recheck these dispositions if
those elements, colors or layers change.
CI builds, vets, staticchecks and tests natively on Windows, Linux and macOS
arm64. Linux runs the full Go suite with the race detector, including its
process-verification tests. Every platform also runs pinned govulncheck v1.9.0
on its native source configuration; reachable vulnerabilities fail CI. Package
execution is serialized with -p 1 to keep bounded collector tests from competing
with other test binaries. Go formatting, Node renderer tests, Python deployment
tests and deadcode -test ./... run once on Linux. Including test roots
keeps shared test-support packages in the reachability analysis. The stable
verify check aggregates all three platform results. The main ruleset
requires verify from GitHub Actions on an up-to-date branch before merging.
Main changes must go through a pull request; no formal approval count is
required. Force pushes and deletion are blocked, and there are no bypass actors,
including administrators. The existing clean-current-head Codex review policy
still applies to automation.
The versioned ruleset payload records the policy. Repository administrators manage the live ruleset through GitHub's rules API or settings. If CI job names change, update the required check in both the payload and live ruleset in the same change.
Cross-compile for the fleet with explicit OS and architecture targets. The builder checks each binary's format and architecture before replacing an existing artifact. It also keeps Windows build processes hidden.
python deploy/build_fleet.py
# Build one target:
python deploy/build_fleet.py --target linux-amd64
# Test the format checks, including a Windows binary mislabeled as Linux:
python -m unittest discover -s deploy -p test_build_fleet.pyThe Linux and Windows legs also write a statement coverage profile and run
scripts/crap_gate.py. It joins go tool cover -func
with pinned gocyclo v0.6.0 complexity for every non-test function built on that
platform and computes CRAP = complexity² × (1 − coverage)³ + complexity. Go
reports statement coverage only, so the score uses statement coverage, not
branch coverage. The job summary shows per-package coverage and the 20 worst
functions; the coverage-crap-Linux and coverage-crap-Windows artifacts hold
the profile, the summary and every function's complexity, coverage and CRAP.
scripts/crap-gate.json declares the thresholds, and the step fails when:
- total statement coverage falls below the platform's floor, set half a point under the value measured when the gate was added (Linux 83.0% from 83.5%, Windows 82.5% from 83.0%);
- a function scores above CRAP 30 without a checked-in exception that gives a reason and a ceiling for that platform, which covers any new or changed function that crosses 30;
- a function with an exception scores above its ceiling, or the platform's
worst score rises above its declared baseline (56.0 on both platforms, from
mainand(*Fetcher).refreshAntigravity); - an exception names a function that no longer exists.
The exceptions record the functions above 30 at the baseline with their
measured scores: the same 12 on Linux and Windows. Windows tests for
verifyRuntimeProcess and runtimePort (#97) removed their Windows-only
exceptions and lowered the Windows baseline from 210.0. Fixture tests for
credsFromMap (#99) removed its exception and lowered both baselines from 72.0.
The fleet poll rework and its tests (#118) brought (*Server).Run under 30 and
removed its exception. The remaining functions need no rewrite, but any change
that lowers their coverage or raises their complexity fails. When a function
improves, the step prints a notice; lower or remove its ceiling in the same
change. Run the gate locally on Linux with pinned gocyclo installed:
go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0
go test -race -p 1 -covermode=atomic -coverprofile=cover.out ./...
python3 scripts/crap_gate.py --platform linux --profile cover.out
# Print measured values to refresh crap-gate.json after a deliberate change:
python3 scripts/crap_gate.py --platform linux --profile cover.out --suggest
# Test the gate, including samples it must reject:
python3 -m unittest discover -s scripts -p "test_*.py"go test ./... includes steady-state checks that run hundreds of agent
collection cycles and fleet polls against fake providers and CLIs. After
warm-up and GC, they check goroutines, OS threads, live heap, open handles
and child processes against documented bounds.
docs/resource-growth.md explains the bounds and
negative controls. resource-growth.yml
runs them verbosely on pull requests, weekly on Mondays at 09:17 UTC with
3,000 cycles, and on demand.
The Mutation workflow runs pinned gremlins v0.6.0 against the declared Go scope, weekly and on relevant pull requests. It fails below 83.0% efficacy, above the timeout limit, or if a required boundary mutant survives. The target is 90%.
Mutants run only inside a bubblewrap sandbox, with private HOME and caches, no host network and no real profiles or credentials. Synthetic host canaries and 21 in-sandbox checks prove confinement before and after the run; failure stops the job. Only committed HEAD is staged, so commit intended changes first. See docs/mutation-testing.md for setup, scope, measured results and report interpretation. On Linux with the public tools and modules already installed:
mutation_work="$(mktemp -d)"
python3 scripts/mutation_sandbox.py --go-root "$(go env GOROOT)" \
--gremlins "$(go env GOPATH)/bin/gremlins" \
--module-cache "$(go env GOMODCACHE)" --work "$mutation_work/run" \
--report-dir mutation-report# Windows (home, laptop) — bind the Tailscale IP so the agent stays off the LAN
.\hs-pi-dashboard.exe agent -addr 100.101.122.39:8787
# macOS/Linux
./hs-pi-dashboard agent -addr <tailscale-ip>:8787Endpoints: GET /sessions (snapshot JSON), GET /usage (provider plan and
balance cards), GET /herdr (cached live agent states), GET /health.
Herdr is discovered from installed CLI locations, or can be specified with
-herdr-bin. Machines without Herdr report unavailable telemetry rather than
an empty healthy monitor.
./hs-pi-dashboard serve -addr 100.97.164.73:8788Endpoints: / (dashboard), /api/fleet (aggregated JSON), /healthz.
Agents are configured with -fleet name|url pairs; the default covers all
five computers from the fleet map. An optional third field,
name|url|https-terminal-url, adds a link to that machine's browser terminal.
Terminal links require HTTPS and cannot embed credentials. Machines that don't
answer are shown offline with their last known sessions grayed out.
A restarted agent answers /sessions at once with a placeholder that has no
sessionsDir and no sessions array until its first collection completes.
A completed collection always reports sessionsDir, even with no sessions.
For a placeholder the server keeps the machine online with the error
session collection pending after agent start, shows the placeholder's source
health, and keeps the last known sessions as inactive history, so neither
the active count nor Pulse counts them. The first completed collection
replaces them.
The server answers / and /healthz as soon as it starts and polls in the
background, every 5 seconds by default. Each poll sends every machine's
/sessions, /herdr and /projects requests at the same time, each with a
3-second deadline, and the next poll starts only after the previous one ends.
A machine that accepts connections but never answers therefore costs about
3 seconds per poll, and the other machines keep refreshing every 5 seconds.
Run as the regular user (no admin needed for the task itself):
schtasks /create /tn "hs-pi-dashboard-agent" /sc onlogon /rl limited /f `
/tr '"C:\path\to\hs-pi-dashboard.exe" agent -addr 100.101.122.39:8787'
schtasks /run /tn "hs-pi-dashboard-agent"If other machines can't reach port 8787, allow it inbound once (admin):
netsh advfirewall firewall add rule name="hs-pi-dashboard agent" dir=in action=allow protocol=TCP localport=8787Ship the arm64 binary via Taildrop from any tailnet machine, then set it up on the Mac (the file arrives in ~/Downloads):
tailscale file cp dist/hs-pi-dashboard-darwin-arm64 franzs-macbook-air:mkdir -p ~/bin ~/logs
mv ~/Downloads/hs-pi-dashboard-darwin-arm64 ~/bin/hs-pi-dashboard
chmod +x ~/bin/hs-pi-dashboard
mkdir -p ~/Library/LaunchAgents
cp deploy/com.hemsoft.hs-pi-dashboard.agent.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.hemsoft.hs-pi-dashboard.agent.plistRunAtLoad + KeepAlive start the agent at login and restart it after crashes or a failed Tailscale bind, so reboots no longer leave the machine dark.
Mini runs both processes from one binary, ~/bin/hs-pi-dashboard, as systemd
user units. The versioned units in deploy/ are the ones Mini runs:
| Unit | Command | Restart policy |
|---|---|---|
hs-pi-dashboard.service |
serve on 100.97.164.73:8788 |
Restart=on-failure, RestartSec=5 |
hs-pi-dashboard-agent.service |
agent on 100.97.164.73:8787 |
Restart=always, RestartSec=5 |
Mini's flags and settings live in drop-ins beside each unit:
hs-pi-dashboard.service.d/herdr-fleet.conf (the -fleet list with terminal
links), and hs-pi-dashboard-agent.service.d/ with projects.conf (Projects
checkouts, -projects-poll, -claude-usage-peer), github-billing.conf
(GITHUB_BILLING_ORG) and github-copilot-policy.conf
(GITHUB_COPILOT_PAID_USAGE). A drop-in that changes the command clears it
with an empty ExecStart= line first, then gives the full command. The units
use %h for the account's home folder; systemctl --user cat on Mini shows
the same lines with /home/franz written out.
No unit or drop-in holds a secret. The agent gets GitHub access from gh's
stored login under the service account, and its pmd token from
~/.config/hs-pi-dashboard/pmd-token (mode 600, see
pmd T3 snapshot push). If a host needs a secret in
the service environment, such as GH_TOKEN, put it in a local drop-in or
EnvironmentFile= outside the repository, readable only by the account
(chmod 600), and never commit it.
Install on a Linux account, starting from a repository checkout. Ship the
Linux binary via Taildrop, or build it with python deploy/build_fleet.py --target linux-amd64:
mkdir -p ~/bin ~/.config/systemd/user
mv ~/Downloads/from-home--hs-pi-dashboard-linux-amd64 ~/bin/hs-pi-dashboard
chmod +x ~/bin/hs-pi-dashboard
cp deploy/hs-pi-dashboard.service deploy/hs-pi-dashboard-agent.service ~/.config/systemd/user/
cp -r deploy/hs-pi-dashboard.service.d deploy/hs-pi-dashboard-agent.service.d ~/.config/systemd/user/
# keep both services running without an open login session, and after reboots:
loginctl enable-linger "$USER"
systemctl --user daemon-reload
systemctl --user enable --now hs-pi-dashboard.service hs-pi-dashboard-agent.service
systemctl --user show -p Restart hs-pi-dashboard.service hs-pi-dashboard-agent.serviceThe units bind Mini's Tailscale address. On another host, change -addr (and
-machine for the agent) in a drop-in such as local.conf, which clears and
replaces ExecStart= like projects.conf does, and drop the drop-ins that
don't apply. A bind fails until Tailscale is up; both restart policies retry
every 5 seconds until it succeeds. Mini's crontab has no dashboard entry;
remove any old @reboot line from an earlier install.
After editing a unit or drop-in, reload and restart:
systemctl --user daemon-reload
systemctl --user restart hs-pi-dashboard.service hs-pi-dashboard-agent.service
systemctl --user status hs-pi-dashboard.service hs-pi-dashboard-agent.serviceLogs go to the user journal:
journalctl --user -u hs-pi-dashboard.service -u hs-pi-dashboard-agent.service -f
journalctl --user -u hs-pi-dashboard-agent.service --since "1 hour ago"Check that the units in deploy/ still match what runs:
systemd-analyze --user verify deploy/hs-pi-dashboard.service deploy/hs-pi-dashboard-agent.service should print nothing about these units
(it also checks that ~/bin/hs-pi-dashboard exists and loads the .d folders
beside them; over SSH, set XDG_RUNTIME_DIR=/run/user/$(id -u) first),
and systemctl --user cat hs-pi-dashboard.service hs-pi-dashboard-agent.service
should show the files in deploy/ apart from comments and %h. CI runs
deploy/test_systemd_units.py, which checks the restart policies, that each
flag exists in main.go, and that no unit names a secret.
Both units run the same binary, so an update or rollback restarts both. Keep
the binary being replaced under a name that records the commit, as Mini does
(hs-pi-dashboard.before-<commit>), then replace and restart:
cp -p ~/bin/hs-pi-dashboard ~/bin/hs-pi-dashboard.before-<old-commit>
# <new-binary> is the shipped or built hs-pi-dashboard-linux-amd64
install -m 755 <new-binary> ~/bin/hs-pi-dashboard.new
mv ~/bin/hs-pi-dashboard.new ~/bin/hs-pi-dashboard
systemctl --user restart hs-pi-dashboard.service hs-pi-dashboard-agent.serviceTo roll back, put the previous binary back the same way and restart:
ls -lt ~/bin/hs-pi-dashboard.before-*
install -m 755 ~/bin/hs-pi-dashboard.before-<old-commit> ~/bin/hs-pi-dashboard.new
mv ~/bin/hs-pi-dashboard.new ~/bin/hs-pi-dashboard
systemctl --user restart hs-pi-dashboard.service hs-pi-dashboard-agent.service
journalctl --user -u hs-pi-dashboard.service -u hs-pi-dashboard-agent.service -n 50Then check http://100.97.164.73:8788/healthz and the agent's
http://100.97.164.73:8787/health. If a unit or drop-in change caused the
problem, restore the previous file from git (git show <old-commit>:deploy/<file>),
copy it into ~/.config/systemd/user/, and run daemon-reload and restart.
No authentication by design: agents and the server bind to Tailscale interface addresses, so only tailnet devices (including iphone/ipad) can reach them. Add a shared token in v2 before any broader exposure.
The public repository enables GitHub secret scanning and push protection for supported secret patterns, plus Dependabot alerts and security updates. Review alerts on the repository security page. Revoke any exposed credential before resolving its alert; test fixtures require a documented false-positive or test-only reason.
Dependabot configuration checks Go modules and GitHub
Actions weekly on Mondays at 04:00 US Eastern. Action updates retain full commit
SHA pins and same-line # vX.Y.Z comments, which
the action pin guard verifies against upstream
tags. Update PRs must pass the existing CI and review gates before merge.
CodeQL analyzes the Go, JavaScript/TypeScript,
Python and GitHub Actions code on every pull request and every push to main
with GitHub's default query suite. Its alerts appear on the pull request and
under Code scanning on the repository security page. The CodeQL jobs are
advisory: the main ruleset requires only
verify, so an alert does not block a merge by itself. Check a pull request's
code-scanning results before merging, and fix each alert or dismiss it with a
written reason. The Go analysis builds on Linux, so its database may omit files
limited to other systems by name (*_windows.go, *_darwin.go).
- v2: pi extension reporting live turn/tool events from running sessions
- v1: usage runway board (Codex, OpenCode Go, Antigravity, Zen, Moonshot)
- v2: xAI usage once a source exists (card removed for now — Grok exposes no usage API)
- v2: optional bearer token between server and agents
- v2: systemd user units for mini (with lingering) instead of cron
- v3: click-through session transcript, cost rollups per project