Step 3: Confirm metrics in pushgateway after manual run
# IMPORTANT: pushgateway port 9091 is NOT exposed externally (firewalled).# Always check from INSIDE vps-i1 via SSH — do NOT curl from bms-4 or your workstation.ssh root@217.154.82.162curl -s http://localhost:9091/metrics | grep p4_claude_sub_usage_pct
Expected — both accounts present after a successful push:
If Prometheus returns empty result but pushgateway has data → Prometheus hasn’t scraped yet (wait 15s) or pushgateway scrape is broken.
1a. Diagnosis — one account “N/A” (no data at all), the other shows zeros
These are two DIFFERENT root causes that look similar on the dashboard but require different fixes.
Observed together 2026-07-08.
“N/A” (account missing entirely from pushgateway, script logs show valid usage%)
The script computes real data locally but the SSH push (_push_prometheus() in
check-sub-usage.py, uses ssh root@217.154.82.162 ... with no -i flag) never reaches
vps-i1. Root cause: the worker user’s private key file has a non-default name (e.g.
vps_root_key instead of id_ed25519) with no ~/.ssh/config entry pointing to it — the
SSH client silently only tries default-named identity files and gives up with
Permission denied (publickey). This can persist even after the matching public key IS present
in authorized_keys on vps-i1, because the private key is never offered in the first place.
Also verify the public key derived from that private key (ssh-keygen -y -f <keyfile>) is
actually present in root@vps-i1:/root/.ssh/authorized_keys — if it was rotated at some point
without updating vps-i1, append it there too (back up the file first).
Zero values + blocked=1 (script logs show API HTTP 401)
Check the token-expiry alert first (#3420). Since 2026-07-09 this exact failure mode is alerted
proactively: ClaudeRunnerTokenExpired (critical) / ClaudeRunnerTokenExpiringSoon (warning) fire off
claude_runner_token_expires_seconds{account,machine} (pushed by check-sub-usage.py). If that alert
is already firing for this account, skip straight to re-auth — no manual diagnosis needed. Query in
Grafana/Prometheus: claude_runner_token_expires_seconds{account="<radieu|ecotrans>"} — a value <= 0
confirms the token is expired.
The account’s OAuth token has expired — this is the scenario covered by
claude-oauth-reauth.md / claude-runner-oauth-rotation.md.
Confirm with the expiry check script in that playbook. Fix requires the OAuth browser
authorize step — either full Playwright MCP automation (needs Playwright MCP connected in the
active Claude Code session — it is NOT available in every session/environment, check via
ToolSearch before assuming Option A is possible) or the semi-manual flow: start claude auth login in a background process as the target Linux user, capture the printed OAuth URL, have a
human open it and authorize, then inject the returned CODE#STATE back into the waiting
process. See claude-runner-oauth-rotation.md for the auth_v4.py script pattern (works
per-account by giving each account its own CODE_FILE/log path, e.g.
/tmp/auth_code_v3_<account>).
1b. Diagnosis — panel layout overlaps (panels render on top of each other)
If server stats panels (BMS-4, VPS-I1 at y=15) or GH Actions Runners stat (y=23) are hidden under other panels:
# Check panel gridPos in the deployed Grafana dashboardssh root@217.154.82.162python3 /tmp/list-panels.py | sort -t= -k2 -n
Expected clean layout (no y overlaps between panel groups):
y=0: queue stats
y=4: subscription panels (8 panels w=3 each)
y=7: time series (Queue Depth, GH Actions, BMS RAM) h=8
y=15: BMS-4 + VPS-I1 server stats h=4
y=19: BMS-3 + DEV-LAPTOP server stats h=4
y=23: GH Actions Runners stat h=3
y=26: Claude Workers RSS (full width w=24, h=8) ← was wrongly at y=15 w=16
y=34: Active Workers | RAM per Server
y=40: Running | Dispatched/Queued
y=50: Failed | Done
y=60: Active Sessions (full width)
If P102 (Claude Workers RSS) appears at y=15 with w=16 → it overlaps server stat panels.
Fix: edit JSON, set P102 to y=26, w=24, then deploy via gf_push.py (see Section 2).
2. Updating the dashboard JSON
CRITICAL: This dashboard uses allowUiUpdates: true in provisioning config.
This means Grafana’s internal DB version takes precedence over the filesystem file. A simple git pull + reload does NOT update the dashboard. You MUST push via the Grafana API.
Dashboard JSON location
Git: monitoring/grafana/provisioning/dashboards/worker-queue.json (on dev branch, PR #2287 + #2311)
Server path: /opt/p24-infra/monitoring/grafana/provisioning/dashboards/worker-queue.json
Workflow for JSON changes
Edit JSON locally (use worktree for feature branch, or on current branch)
Validate JSON:
$content = [System.IO.File]::ReadAllText("path/to/worker-queue.json")$content | ConvertFrom-Json # throws if invalid