Initial wiki: six-page documentation set
Pages: - Home: overview, cluster-at-a-glance table, page index, with Monitor Dash render embedded. - Architecture: two-architecture tradeoff (A: OTEL-native, B: ccusage-sourced), shared daemon contract (/usage JSON shape), calibration env vars. - DataSources: architecture A implementation (docker-compose, otel-collector-config, prometheus.yml, Claude Code env, claude_code_* metric schema, PromQL per gauge, daemon sketch); architecture B implementation (ccusage CLI vs MCP server, watchdog JSONL tail, daemon sketch); recommendation. - Hardware: four-gauge layout (5h fuel, tach, thinking ratio, cache hit), X27.168 steppers with SwitecX25 library, ESP32 DevKit wiring with concrete GPIO assignments, firmware structure, enclosure V1 notes, BOM (~$80-90). - Roadmap: eight phases (A daemon stdout through H character metrics), deferred list, out-of-scope list. - Ideas: four exotic enclosure variants with render (steampunk chronometer, retro VFD array, minimalist birch e-ink, bio-digital cybernetic cluster), metrics brainstorm organised in seven categories, hardware / software wild ideas, unlikely-but-noted. Images committed: monitor-dash.png referenced from Home and Hardware; exotic-dashes.png referenced from Ideas.
commit
461679dd64
8 changed files with 964 additions and 0 deletions
97
Architecture.md
Normal file
97
Architecture.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
Three components in series: telemetry source, local daemon, firmware.
|
||||||
|
The source is the variable; the daemon and firmware are identical
|
||||||
|
across both architectures.
|
||||||
|
|
||||||
|
```
|
||||||
|
[ Claude Code ]
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ telemetry source ] <--- A: OTEL + Prometheus
|
||||||
|
<--- B: ccusage + JSONL tail
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ claude-gauge daemon ] Python, FastAPI, GET /usage
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ ESP32 firmware ] polls /usage ~1 Hz
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ four needles + annunciator ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Two architectures
|
||||||
|
|
||||||
|
Pick one. Both feed the same daemon interface, so the firmware
|
||||||
|
never knows which backend is wired up.
|
||||||
|
|
||||||
|
| | A. OTEL-native | B. ccusage-sourced |
|
||||||
|
|---|---|---|
|
||||||
|
| Data source | Claude Code OTLP -> collector -> Prometheus | `ccusage` CLI + watchdog JSONL tail |
|
||||||
|
| External deps | Docker Compose stack (3 services) | Node + `bunx ccusage` |
|
||||||
|
| Deep-stats dashboard | Grafana dashboard 25052 for free | `ccusage` TUI, or build something |
|
||||||
|
| Short-window tach | Limited by scrape interval (15s) | Sub-second via JSONL tail |
|
||||||
|
| Operational weight | Moderate | Tiny |
|
||||||
|
| Homelab-enterprise fit | Strong | Weak |
|
||||||
|
| Survivability through Claude Code updates | High (OTEL schema is documented) | Medium (JSONL is implementation detail) |
|
||||||
|
|
||||||
|
See [DataSources](DataSources) for the implementation-level specifics
|
||||||
|
of each.
|
||||||
|
|
||||||
|
## The daemon
|
||||||
|
|
||||||
|
Python service that exposes a single stable HTTP contract. The
|
||||||
|
firmware polls it ~1 Hz.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /usage
|
||||||
|
-> {
|
||||||
|
"rate_1m": <number>, tokens/min, short window
|
||||||
|
"window_5h_tokens": <number>,
|
||||||
|
"window_5h_pct": <0..1>,
|
||||||
|
"thinking_ratio": <0..1>, thinking tokens / output tokens
|
||||||
|
"cache_hit_rate": <0..1>,
|
||||||
|
"last_model": "opus"|"sonnet"|"haiku",
|
||||||
|
"hot": <bool>, tach above redline
|
||||||
|
"warn": <bool>, fuel or cache anomaly
|
||||||
|
"stall": <bool>, no telemetry for N minutes
|
||||||
|
"idle": <bool>, daemon up, no activity
|
||||||
|
"updated_at": <iso8601>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Two implementations share this shape:
|
||||||
|
|
||||||
|
* `daemon_prom.py` — PromQL queries against Prometheus (A)
|
||||||
|
* `daemon_ccusage.py` — ccusage subprocess + watchdog tail (B)
|
||||||
|
|
||||||
|
The firmware is unaware. Swapping backends is a daemon-only change.
|
||||||
|
|
||||||
|
## Why this shape
|
||||||
|
|
||||||
|
The firmware is the slow part of the system. ESP32s are not good at
|
||||||
|
JSON parsing at high frequency, they do not roundtrip Prometheus,
|
||||||
|
and their HTTP stacks are brittle. Put all the reasoning on the
|
||||||
|
daemon side. The firmware receives a flat, pre-computed structure
|
||||||
|
and only does mapping (0-1000 scale -> stepper target).
|
||||||
|
|
||||||
|
This also means the firmware contract is stable across architectures
|
||||||
|
AND across future data-source changes. When Anthropic ships a native
|
||||||
|
usage endpoint, the daemon swaps its input and the hardware does not
|
||||||
|
know.
|
||||||
|
|
||||||
|
## Ceilings and calibration
|
||||||
|
|
||||||
|
The daemon does not know the plan caps. User configures local ceilings
|
||||||
|
via environment:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLAUDE_GAUGE_5H_CEILING tokens that count as 100% on 5h fuel
|
||||||
|
CLAUDE_GAUGE_TACH_REDLINE tokens/min that count as redline
|
||||||
|
CLAUDE_GAUGE_STALL_MINUTES minutes of silence before STALL lights
|
||||||
|
```
|
||||||
|
|
||||||
|
Both architectures consume the same variables. Calibrate by running
|
||||||
|
for a week and comparing to `ccusage blocks` or Claude's `/usage`
|
||||||
|
output.
|
||||||
333
DataSources.md
Normal file
333
DataSources.md
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
# Data Sources
|
||||||
|
|
||||||
|
Two architectures, implemented side by side. Pick one; both produce
|
||||||
|
the same `/usage` payload for the firmware.
|
||||||
|
|
||||||
|
## A. OTEL-native
|
||||||
|
|
||||||
|
Uses Claude Code's built-in OpenTelemetry support. Mirrors the
|
||||||
|
reference stack published in `anthropics/claude-code-monitoring-guide`.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
Three containers: OTEL collector, Prometheus, Grafana.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
otel-collector:
|
||||||
|
image: otel/opentelemetry-collector-contrib:latest
|
||||||
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
|
volumes:
|
||||||
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
|
ports:
|
||||||
|
- "4317:4317" # OTLP gRPC
|
||||||
|
- "4318:4318" # OTLP HTTP
|
||||||
|
- "8889:8889" # Prometheus scrape
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
ports: ["9090:9090"]
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--storage.tsdb.retention.time=8d'
|
||||||
|
- '--web.enable-lifecycle'
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
### OTEL collector config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc: { endpoint: 0.0.0.0:4317 }
|
||||||
|
http: { endpoint: 0.0.0.0:4318 }
|
||||||
|
|
||||||
|
processors:
|
||||||
|
batch: { timeout: 1s, send_batch_size: 1024 }
|
||||||
|
memory_limiter: { check_interval: 1s, limit_mib: 512 }
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
prometheus:
|
||||||
|
endpoint: "0.0.0.0:8889"
|
||||||
|
send_timestamps: true
|
||||||
|
metric_expiration: 192h # 8 days; needed for 7d queries
|
||||||
|
enable_open_metrics: true
|
||||||
|
|
||||||
|
service:
|
||||||
|
pipelines:
|
||||||
|
metrics:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [memory_limiter, batch]
|
||||||
|
exporters: [prometheus]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prometheus scrape config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'otel-collector'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['otel-collector:8889']
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code environment
|
||||||
|
|
||||||
|
Set in the shell Claude Code launches in. User profile or
|
||||||
|
`~/.claude/settings.json` managed settings both work.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLAUDE_CODE_ENABLE_TELEMETRY=1
|
||||||
|
export OTEL_METRICS_EXPORTER=otlp
|
||||||
|
export OTEL_LOGS_EXPORTER=otlp
|
||||||
|
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||||
|
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||||
|
export OTEL_METRIC_EXPORT_INTERVAL=10000 # 10s for responsiveness
|
||||||
|
export OTEL_METRICS_INCLUDE_SESSION_ID=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code metrics (surfaced in Prometheus)
|
||||||
|
|
||||||
|
After OTEL-to-Prom conversion:
|
||||||
|
|
||||||
|
| Prometheus metric | Labels |
|
||||||
|
|---|---|
|
||||||
|
| `claude_code_token_usage_tokens_total` | `type` (input, output, cacheRead, cacheCreation), `model` |
|
||||||
|
| `claude_code_cost_usage_USD_total` | `model` |
|
||||||
|
| `claude_code_session_count_total` | |
|
||||||
|
| `claude_code_active_time_total_seconds_total` | `type` (user, cli) |
|
||||||
|
| `claude_code_lines_of_code_count_total` | `type` (added, removed) |
|
||||||
|
| `claude_code_commit_count_total` | |
|
||||||
|
| `claude_code_pull_request_count_total` | |
|
||||||
|
| `claude_code_code_edit_tool_decision_count_total` | `tool_name`, `decision`, `language` |
|
||||||
|
|
||||||
|
Full event schema is in the [Claude Code monitoring docs](https://code.claude.com/docs/en/monitoring-usage).
|
||||||
|
|
||||||
|
### PromQL for each gauge
|
||||||
|
|
||||||
|
```promql
|
||||||
|
# Tokens/min (tach)
|
||||||
|
sum(rate(claude_code_token_usage_tokens_total[1m])) * 60
|
||||||
|
|
||||||
|
# 5h fuel
|
||||||
|
sum(increase(claude_code_token_usage_tokens_total[5h]))
|
||||||
|
|
||||||
|
# Thinking/output ratio (temp gauge)
|
||||||
|
# Note: OTEL does not emit thinking tokens as a dedicated metric;
|
||||||
|
# derive via event stream (claude_code.api_request + prompt.id
|
||||||
|
# correlation) or fall back to a constant 0 until instrumented.
|
||||||
|
|
||||||
|
# Cache hit rate (boost gauge)
|
||||||
|
sum(rate(claude_code_token_usage_tokens_total{type="cacheRead"}[5m]))
|
||||||
|
/ sum(rate(claude_code_token_usage_tokens_total{type=~"input|cacheRead|cacheCreation"}[5m]))
|
||||||
|
|
||||||
|
# Last model (approximation)
|
||||||
|
topk(1, claude_code_token_usage_tokens_total{type="output"})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daemon (A)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/claude_gauge/
|
||||||
|
daemon_prom.py FastAPI, PromQL queries, /usage
|
||||||
|
config.py ceilings, URLs, thresholds
|
||||||
|
windows.py query builders and result parsing
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def prom(q: str) -> float:
|
||||||
|
r = await client.get(f"{PROM}/api/v1/query", params={"query": q})
|
||||||
|
data = r.json()["data"]["result"]
|
||||||
|
return float(data[0]["value"][1]) if data else 0.0
|
||||||
|
|
||||||
|
@app.get("/usage")
|
||||||
|
async def usage():
|
||||||
|
rate_1m = await prom(Q_TACH)
|
||||||
|
win_5h = await prom(Q_5H)
|
||||||
|
cache = await prom(Q_CACHE)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deep-stats dashboard
|
||||||
|
|
||||||
|
Import **Grafana Labs dashboard 25052** ("Claude Code") against the
|
||||||
|
Prometheus data source. That is the deep-stats surface. No custom
|
||||||
|
web UI needed.
|
||||||
|
|
||||||
|
### Pros and cons
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
* Uses the platform feature Anthropic ships
|
||||||
|
* Grafana dashboard is free
|
||||||
|
* Metric schema is documented and stable
|
||||||
|
* Generalises cleanly if other hosts also run Claude Code
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
* Scrape interval caps tach responsiveness at ~15s
|
||||||
|
* Three containers to run
|
||||||
|
* Requires env-var changes on every Claude Code launch surface
|
||||||
|
|
||||||
|
If the 15s cap is annoying, bolt a JSONL tail from B on top for the
|
||||||
|
tach path only. Hybrid. See [Hardware](Hardware) for the firmware
|
||||||
|
contract that stays identical either way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. ccusage-sourced
|
||||||
|
|
||||||
|
One process. No collector, no Prometheus, no Grafana. The daemon
|
||||||
|
subprocess-calls `ccusage` for the fuel gauges and tails JSONL
|
||||||
|
directly for the tach.
|
||||||
|
|
||||||
|
### ccusage integration
|
||||||
|
|
||||||
|
Two shapes, pick one.
|
||||||
|
|
||||||
|
**B1 (simplest)**: periodic CLI subprocess.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx ccusage@latest blocks --json # current 5h block
|
||||||
|
npx ccusage@latest daily --json # per-day aggregates
|
||||||
|
```
|
||||||
|
|
||||||
|
**B2 (persistent)**: ccusage MCP HTTP server. Exposes four tools
|
||||||
|
(`daily`, `monthly`, `session`, `blocks`) at `POST /` over
|
||||||
|
StreamableHTTP MCP transport.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bunx @ccusage/mcp@latest --type http --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
B1 is the default. Switch to B2 only if you also want the MCP
|
||||||
|
surface available to other local agents.
|
||||||
|
|
||||||
|
### Short-window tach via watchdog
|
||||||
|
|
||||||
|
`ccusage` aggregates are too coarse for a responsive tach. The
|
||||||
|
daemon keeps a 60-second ring buffer by tailing
|
||||||
|
`~/.claude/projects/**/*.jsonl` directly.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
from collections import deque
|
||||||
|
import json, time
|
||||||
|
|
||||||
|
class JsonlTail(FileSystemEventHandler):
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
self.offsets = {}
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
p = Path(event.src_path)
|
||||||
|
if p.suffix != ".jsonl":
|
||||||
|
return
|
||||||
|
off = self.offsets.get(p, 0)
|
||||||
|
with p.open() as f:
|
||||||
|
f.seek(off)
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
d = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if d.get("type") == "assistant":
|
||||||
|
u = d["message"].get("usage", {})
|
||||||
|
tokens = sum(u.get(k, 0) for k in (
|
||||||
|
"input_tokens", "output_tokens",
|
||||||
|
"cache_read_input_tokens",
|
||||||
|
"cache_creation_input_tokens",
|
||||||
|
))
|
||||||
|
thinking = extract_thinking_tokens(d)
|
||||||
|
model = d["message"].get("model", "")
|
||||||
|
self.bus.push(time.time(), tokens, thinking, model)
|
||||||
|
self.offsets[p] = f.tell()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daemon (B)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/claude_gauge/
|
||||||
|
daemon_ccusage.py FastAPI, ccusage subprocess, /usage
|
||||||
|
tail.py watchdog + RateBus
|
||||||
|
config.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def ccusage(cmd):
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"npx", "ccusage@latest", cmd, "--json",
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
out, _ = await proc.communicate()
|
||||||
|
return json.loads(out)
|
||||||
|
|
||||||
|
@app.get("/usage")
|
||||||
|
async def usage():
|
||||||
|
rate = bus.rate_per_min()
|
||||||
|
blocks = await ccusage("blocks")
|
||||||
|
cur = next((b for b in blocks["blocks"] if b.get("isActive")), None)
|
||||||
|
w5 = cur["totalTokens"] if cur else 0
|
||||||
|
return {
|
||||||
|
"rate_1m": rate,
|
||||||
|
"window_5h_tokens": w5,
|
||||||
|
"window_5h_pct": min(1.0, w5 / CEIL_5H),
|
||||||
|
"thinking_ratio": bus.thinking_ratio(),
|
||||||
|
"cache_hit_rate": bus.cache_hit_rate(),
|
||||||
|
"last_model": bus.last_model(),
|
||||||
|
"hot": rate > RED,
|
||||||
|
"warn": (w5 / CEIL_5H) > 0.8,
|
||||||
|
"stall": bus.silent_for_minutes() > STALL_MIN,
|
||||||
|
"idle": True,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cache `ccusage` output with a 10s TTL so 1 Hz firmware polling does
|
||||||
|
not spawn a subprocess every request.
|
||||||
|
|
||||||
|
### Pros and cons
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
* Single process, one dependency tree
|
||||||
|
* Sub-second tach out of the box
|
||||||
|
* No Docker, no collector, no env vars on the Claude Code side
|
||||||
|
* `ccusage` has solved the JSONL edge cases already
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
* No free Grafana dashboard; deep stats require the `ccusage` TUI or
|
||||||
|
a custom surface
|
||||||
|
* Node required on the runtime path
|
||||||
|
* JSONL format is an implementation detail; upstream changes can
|
||||||
|
break parsing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Architecture A** for homelab-as-enterprise framing. OTEL is the
|
||||||
|
platform feature, Prometheus integrates with the rest of the homelab
|
||||||
|
stack, and Grafana dashboard 25052 is free deep-stats. Scrape
|
||||||
|
interval cap is the only real concession; hybrid with the JSONL
|
||||||
|
tail from B if the tach feels sluggish after calibration.
|
||||||
|
|
||||||
|
**Architecture B** if Prometheus is not already running and you want
|
||||||
|
a working needle sooner. Ship it, migrate to A later if OTEL becomes
|
||||||
|
useful for other things.
|
||||||
|
|
||||||
|
Firmware and cluster are identical either way.
|
||||||
190
Hardware.md
Normal file
190
Hardware.md
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
# Hardware
|
||||||
|
|
||||||
|
Four analog needles, five annunciator lamps, brushed-aluminium bezel,
|
||||||
|
mounted under the monitor. ESP32 polls the daemon's `/usage` endpoint
|
||||||
|
at ~1 Hz and updates the cluster.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Cluster layout
|
||||||
|
|
||||||
|
Four round gauges in a single bezel, reading left to right:
|
||||||
|
|
||||||
|
| # | Label | Metric | Scale |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 5H PLAN / Left fuel | % of 5h plan window used | 0 - 100% |
|
||||||
|
| 2 | TOKENS/MIN / Tach | Short-window burn rate | 0 - redline |
|
||||||
|
| 3 | THINKING/OUTPUT / Temp | Thinking vs output tokens | low - high, colour-coded zones |
|
||||||
|
| 4 | CACHE HIT / Boost | Cache read as fraction of input | 0 - 100% |
|
||||||
|
|
||||||
|
Annunciator row below the gauges, left to right:
|
||||||
|
|
||||||
|
| Lamp | Colour | Condition |
|
||||||
|
|---|---|---|
|
||||||
|
| MODEL | RGB | Red for Opus, amber for Sonnet, green for Haiku; reflects the most recent assistant message |
|
||||||
|
| HOT | Red | Tach above redline; optionally flashes |
|
||||||
|
| WARN | Amber | Fuel above 80% or cache-hit-rate below a floor |
|
||||||
|
| STALL | Blue | No telemetry for the configured silence window |
|
||||||
|
| IDLE | Green | Daemon reachable, no current activity (pulses softly) |
|
||||||
|
|
||||||
|
The 7-day window is not on the cluster. It moves too slowly to warrant
|
||||||
|
a needle and lives on the deep-stats dashboard instead.
|
||||||
|
|
||||||
|
## Movement
|
||||||
|
|
||||||
|
**Switec X27.168** automotive stepper motor.
|
||||||
|
|
||||||
|
* 315-degree sweep
|
||||||
|
* 600 steps (~2 degrees per step with 3-degree/step gearing)
|
||||||
|
* Direct drive, no microstepping required
|
||||||
|
* ~$8 per unit from Adafruit / AliExpress
|
||||||
|
* Used in car dashboards, so bezels and faces are off-the-shelf
|
||||||
|
|
||||||
|
Sibling parts in the same family (X25, VID28, VID29, BKA30D-R5) all
|
||||||
|
work with the same driver library. The X27.168 has the longest sweep
|
||||||
|
and the most tutorial coverage, which is why it wins.
|
||||||
|
|
||||||
|
## Driver library
|
||||||
|
|
||||||
|
**`clearwater/SwitecX25`** Arduino C++ library. Despite the name it
|
||||||
|
covers X27.168 without modification. Drives four GPIO pins per
|
||||||
|
motor. No external driver IC strictly required for short wiring
|
||||||
|
runs; add ULN2003A darlington arrays if you want cleaner current
|
||||||
|
handling or longer wire runs.
|
||||||
|
|
||||||
|
No maintained MicroPython port exists. Firmware is therefore Arduino
|
||||||
|
C++, not MicroPython. Not the original plan, but the right trade
|
||||||
|
given how mature the Arduino ecosystem is for this motor.
|
||||||
|
|
||||||
|
## Board
|
||||||
|
|
||||||
|
**ESP32 DevKit V1** (generic).
|
||||||
|
|
||||||
|
* WiFi built in
|
||||||
|
* Enough GPIO for 3+ steppers (12 pins), 5 annunciator LEDs, one
|
||||||
|
reset button
|
||||||
|
* Arduino framework via PlatformIO
|
||||||
|
* ~$8 per board
|
||||||
|
|
||||||
|
Alternatives:
|
||||||
|
|
||||||
|
* Raspberry Pi Pico W with CircuitPython — fewer tutorials for the
|
||||||
|
stepper, but friendlier to iterate
|
||||||
|
* ESP32-S3 if you want USB-CDC for serial debugging without an
|
||||||
|
extra USB-to-UART chip
|
||||||
|
|
||||||
|
Picking the classic ESP32 DevKit keeps part count low.
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
|
Concrete GPIO assignments for a four-gauge cluster. Adjust to suit
|
||||||
|
the specific board variant.
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32 DevKit V1
|
||||||
|
+---------------- 5V -> stepper common (via regulator if needed)
|
||||||
|
+---------------- GND -> common ground
|
||||||
|
|
||||||
|
Gauge 1 (5h PLAN)
|
||||||
|
GPIO 13, 14, 27, 26 -> X27.168 #1 coil pins
|
||||||
|
|
||||||
|
Gauge 2 (Tach)
|
||||||
|
GPIO 25, 33, 32, 35 -> X27.168 #2 coil pins
|
||||||
|
|
||||||
|
Gauge 3 (Thinking ratio)
|
||||||
|
GPIO 34, 39, 36, 22 -> X27.168 #3 coil pins
|
||||||
|
|
||||||
|
Gauge 4 (Cache hit)
|
||||||
|
GPIO 23, 18, 19, 5 -> X27.168 #4 coil pins
|
||||||
|
|
||||||
|
Annunciator LEDs (220R series resistors)
|
||||||
|
GPIO 16 (R), 17 (G), 21 (B) -> MODEL (common-cathode RGB)
|
||||||
|
GPIO 4 -> HOT (red, PWM for flash)
|
||||||
|
GPIO 2 -> WARN (amber)
|
||||||
|
GPIO 15 -> STALL (blue)
|
||||||
|
GPIO 12 -> IDLE (green, PWM for pulse)
|
||||||
|
|
||||||
|
Reset button
|
||||||
|
GPIO 0 (boot button dual-use) or GPIO 35 with pull-up
|
||||||
|
```
|
||||||
|
|
||||||
|
Separate 5V rail for the steppers if you see brownouts when all four
|
||||||
|
move at once. The onboard regulator is marginal under combined motor
|
||||||
|
current.
|
||||||
|
|
||||||
|
## Firmware structure
|
||||||
|
|
||||||
|
```
|
||||||
|
firmware/
|
||||||
|
platformio.ini
|
||||||
|
src/
|
||||||
|
main.cpp setup() + loop()
|
||||||
|
wifi.cpp connect + reconnect
|
||||||
|
gauge.cpp SwitecX25 wrapper; map 0-1000 to 0-600 steps
|
||||||
|
annunciator.cpp LED state machine
|
||||||
|
poll.cpp HTTP GET /usage every 1s
|
||||||
|
config.h daemon URL, thresholds
|
||||||
|
```
|
||||||
|
|
||||||
|
Poll loop:
|
||||||
|
|
||||||
|
1. Every 1000 ms, HTTP GET `http://<daemon>:8080/usage`
|
||||||
|
2. Parse JSON with ArduinoJson
|
||||||
|
3. For each gauge, set target step from scaled value
|
||||||
|
4. Update LED states from boolean flags and `last_model`
|
||||||
|
5. `gauge.update()` runs per-loop-tick; stepper moves toward target
|
||||||
|
non-blocking
|
||||||
|
|
||||||
|
Firmware does not interpret: no PromQL, no ring buffers, no rolling
|
||||||
|
windows. That is all daemon work. Firmware maps pre-computed floats
|
||||||
|
to stepper positions and LED states.
|
||||||
|
|
||||||
|
## Enclosure
|
||||||
|
|
||||||
|
V1 target:
|
||||||
|
|
||||||
|
* Brushed-aluminium bezel (3D-printed PLA + metallic spray, upgrade
|
||||||
|
later)
|
||||||
|
* Cream faces with a hairline burgundy redline zone (optional house
|
||||||
|
palette match with quartermaster)
|
||||||
|
* Hexagonal screws around each gauge bezel (visible in the render)
|
||||||
|
* Smoked acrylic covering the annunciator row so LEDs only appear
|
||||||
|
when lit
|
||||||
|
* Matte black mounting bracket clipped to the monitor bezel
|
||||||
|
* Roughly 200 mm wide by 95 mm tall
|
||||||
|
|
||||||
|
Faces and bezels can be printed on a home 3D printer at V1 fidelity;
|
||||||
|
if the project earns a V2, commission a machinist for a proper
|
||||||
|
aluminium bezel.
|
||||||
|
|
||||||
|
## Bill of materials (V1)
|
||||||
|
|
||||||
|
| Item | Qty | ~Cost |
|
||||||
|
|---|---|---|
|
||||||
|
| Switec X27.168 stepper | 4 | $32 |
|
||||||
|
| ESP32 DevKit V1 | 1 | $8 |
|
||||||
|
| ULN2003A driver array | 1-2 | $2 |
|
||||||
|
| RGB LED (common cathode) | 1 | $1 |
|
||||||
|
| 5 mm LEDs (red, amber, blue, green, red flash) | 4 | $2 |
|
||||||
|
| 220R resistors | 10 | $1 |
|
||||||
|
| 5 V 2 A supply | 1 | $10 |
|
||||||
|
| PLA filament + spray paint | n/a | $10 |
|
||||||
|
| Hook-up wire, headers, JST connectors | | $10 |
|
||||||
|
| Smoked acrylic scrap | 1 | $5 |
|
||||||
|
|
||||||
|
~$80-90 for V1 materials.
|
||||||
|
|
||||||
|
## Calibration
|
||||||
|
|
||||||
|
After the cluster is physically assembled, run the daemon for a week
|
||||||
|
and compare:
|
||||||
|
|
||||||
|
* Left fuel steady-state against `ccusage blocks` or Claude's
|
||||||
|
`/usage` output; tune `CLAUDE_GAUGE_5H_CEILING`
|
||||||
|
* Tach peaks against "Claude was really cooking" sessions; tune
|
||||||
|
`CLAUDE_GAUGE_TACH_REDLINE`
|
||||||
|
* Cache hit rate against a session you know was cache-warm vs a
|
||||||
|
cold session; confirm the needle is responsive
|
||||||
|
|
||||||
|
Recalibrate when Anthropic changes plan limits or when your daily
|
||||||
|
workload shifts materially.
|
||||||
54
Home.md
Normal file
54
Home.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# claude-gauge
|
||||||
|
|
||||||
|
Hardware instrument cluster displaying Claude Code session telemetry
|
||||||
|
in real time. Four analog needle gauges plus an annunciator row,
|
||||||
|
driven by an ESP32 polling a local Python daemon. The daemon reads
|
||||||
|
either Claude Code's native OpenTelemetry feed through Prometheus
|
||||||
|
(architecture A) or `ccusage` CLI aggregates with a direct JSONL
|
||||||
|
tail for the tach (architecture B). Firmware and cluster are
|
||||||
|
identical across both.
|
||||||
|
|
||||||
|
Fighter-jet / race-car aesthetic. Physical-first. The deep stats
|
||||||
|
live in Grafana (A) or `ccusage`'s own surfaces (B); the cluster is
|
||||||
|
the ambient summary.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
* [Architecture](Architecture) — two-architecture tradeoff, daemon
|
||||||
|
shape, shared HTTP surface
|
||||||
|
* [DataSources](DataSources) — Docker Compose + PromQL for A,
|
||||||
|
`ccusage` integration for B, Claude Code env vars
|
||||||
|
* [Hardware](Hardware) — cluster layout, X27.168 steppers, ESP32
|
||||||
|
wiring, firmware structure, enclosure
|
||||||
|
* [Roadmap](Roadmap) — phases, shipped, deferred
|
||||||
|
* [Ideas](Ideas) — exotic variants, metrics brainstorm, parked
|
||||||
|
thoughts
|
||||||
|
|
||||||
|
## At a glance
|
||||||
|
|
||||||
|
Four round gauges in a brushed-aluminium bezel, mounted under the
|
||||||
|
monitor. Each drives from one primary metric.
|
||||||
|
|
||||||
|
| Gauge | Metric | Scale |
|
||||||
|
|---|---|---|
|
||||||
|
| 5H PLAN / Left fuel | % of 5h plan window used | 0 - 100% |
|
||||||
|
| TOKENS/MIN / Tach | Short-window burn rate | 0 - redline (configurable) |
|
||||||
|
| THINKING/OUTPUT / Temp | Ratio of thinking to output tokens | low - high (colour-coded) |
|
||||||
|
| CACHE HIT / Boost | Cache read as fraction of input | 0 - 100% |
|
||||||
|
|
||||||
|
Annunciator row below: **MODEL** (RGB, colour-coded per model),
|
||||||
|
**HOT** (tach above redline), **WARN** (fuel or cache anomaly),
|
||||||
|
**STALL** (no telemetry for N minutes), **IDLE** (daemon reachable,
|
||||||
|
no activity).
|
||||||
|
|
||||||
|
The 7-day window is deliberately absent from the physical cluster.
|
||||||
|
It moves too slowly to warrant a needle and lives on the deep-stats
|
||||||
|
dashboard (Grafana panel or `ccusage` TUI) instead.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Scaffolded. Phase A pending architecture decision.
|
||||||
|
|
||||||
|
See the [Roadmap](Roadmap) for phase-by-phase plan.
|
||||||
186
Ideas.md
Normal file
186
Ideas.md
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
# Ideas
|
||||||
|
|
||||||
|
Parked thoughts and exotic variants. Nothing here is committed; this
|
||||||
|
is the "capture the shiny object so we can return to the main task"
|
||||||
|
page.
|
||||||
|
|
||||||
|
## Exotic variants
|
||||||
|
|
||||||
|
Four directions the enclosure could take beyond the classic
|
||||||
|
brushed-aluminium dashboard. The daemon, firmware contract, and
|
||||||
|
annunciator semantics stay the same; only the physical shell changes.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Steampunk automaton chronometer
|
||||||
|
|
||||||
|
Wooden case with visible gears, brass piping, vertical glass tubes
|
||||||
|
as fluid-column gauges. The tach becomes a swinging pendulum; the
|
||||||
|
fuel gauges become liquid levels in tubes lit from below. Models
|
||||||
|
are indicated by a rotating brass disc behind a window. Victorian
|
||||||
|
scientific-instrument vibe.
|
||||||
|
|
||||||
|
Build cost: medium-high. Requires woodworking and brass hardware
|
||||||
|
sourcing. Steppers hide inside the case; tube visuals driven by
|
||||||
|
RGB LED strips scaled to the fuel percentage.
|
||||||
|
|
||||||
|
### Retro-future VFD array
|
||||||
|
|
||||||
|
Black panel with cyan/teal vacuum-fluorescent displays. Large VFD
|
||||||
|
readout for TOKEN/MIN, segmented bar graphs for the fuel windows,
|
||||||
|
a small 3D wireframe terrain display corner animated to reflect
|
||||||
|
thinking ratio. Blade Runner, 80s mainframe, CRT TV from the future.
|
||||||
|
|
||||||
|
Build cost: medium. VFD modules are available but not cheap; needs
|
||||||
|
a driver IC per display and a 5 V rail that can swing the filament
|
||||||
|
bias. All character-based, no moving needles.
|
||||||
|
|
||||||
|
### Minimalist birch e-ink interface
|
||||||
|
|
||||||
|
Birch plywood case with four e-ink round displays rendering
|
||||||
|
minimalist graphs. Matte, quiet, Nordic. Each display is a full
|
||||||
|
round e-ink rather than a stepper and needle. Less showy than the
|
||||||
|
classic dash, more desk-neighbour-friendly if you work in shared
|
||||||
|
space.
|
||||||
|
|
||||||
|
Build cost: high per-unit; round e-ink panels run $30-60 each.
|
||||||
|
Firmware is more complex: per-panel rendering instead of stepper
|
||||||
|
control. Update rate is slow, which suits the fuel gauges more than
|
||||||
|
the tach. May need to fall back to an OLED for the tach specifically.
|
||||||
|
|
||||||
|
### Bio-digital cybernetic cluster
|
||||||
|
|
||||||
|
Organic bioluminescent tendrils in magenta/cyan, plasma-globe central
|
||||||
|
sphere for the tach, neural-network-like filaments for the fuels.
|
||||||
|
H. R. Giger meets a Phillip K. Dick novel.
|
||||||
|
|
||||||
|
Build cost: low (if done with EL wire and a plasma globe) to
|
||||||
|
ridiculous (if done properly with electroluminescent silicone and a
|
||||||
|
custom bioreactor substrate). Mostly a vibes build. Would be fun as
|
||||||
|
a Halloween-only seasonal swap-out. Not a daily driver.
|
||||||
|
|
||||||
|
## Shortlist if building a second cluster
|
||||||
|
|
||||||
|
1. VFD array for a secondary desk (aesthetic pairs well with a
|
||||||
|
terminal-heavy workflow)
|
||||||
|
2. Birch e-ink as a quiet hallway ambient display
|
||||||
|
3. Steampunk as a living-room "ambient AI" piece for non-technical
|
||||||
|
guests
|
||||||
|
4. Bio-digital as a conversation-starter for Halloween
|
||||||
|
|
||||||
|
## Metrics brainstorm (for Phase G / H)
|
||||||
|
|
||||||
|
Derivable from OTEL (A) or JSONL (B). Not on the primary cluster;
|
||||||
|
lands on Grafana panels or a custom web page later.
|
||||||
|
|
||||||
|
### Cost and tokens
|
||||||
|
|
||||||
|
* Cache hit rate and cache-savings dollar value
|
||||||
|
* Cost per session at published pricing
|
||||||
|
* Projected monthly spend at current burn rate
|
||||||
|
* Opus / Sonnet / Haiku token split
|
||||||
|
* Server tool use (web search / web fetch) counts
|
||||||
|
* Service tier distribution (standard vs priority)
|
||||||
|
|
||||||
|
### Time and rhythm
|
||||||
|
|
||||||
|
* Session count, duration distribution
|
||||||
|
* Time-of-day heatmap (circadian work pattern)
|
||||||
|
* Day-of-week heatmap
|
||||||
|
* Think-time vs work-time ratio
|
||||||
|
* Streak tracking (consecutive days used)
|
||||||
|
* All-nighter detector (session crossing 2am local)
|
||||||
|
* Longest continuous session
|
||||||
|
|
||||||
|
### Work shape
|
||||||
|
|
||||||
|
* Thinking-to-output ratio trend over time
|
||||||
|
* Stop-reason distribution (watch for rising `max_tokens`)
|
||||||
|
* Messages per session
|
||||||
|
* Tool calls per assistant response (parallelism indicator)
|
||||||
|
* User interrupt rate (sessions ending on cancel)
|
||||||
|
* Iteration count per task
|
||||||
|
|
||||||
|
### Tool usage
|
||||||
|
|
||||||
|
* Top tools by count (Bash, Edit, Read, Grep)
|
||||||
|
* Tool success vs failure rate
|
||||||
|
* Bash command distribution parsed by root executable
|
||||||
|
* File reads vs edits vs writes
|
||||||
|
* Hottest files across all sessions
|
||||||
|
* Agent / subagent counts (`isSidechain=true`), depth
|
||||||
|
* Web search / web fetch counts
|
||||||
|
|
||||||
|
### Project and context
|
||||||
|
|
||||||
|
* Tokens per project
|
||||||
|
* Time per project
|
||||||
|
* Project switching rate within a session
|
||||||
|
* Dormant project detector (no activity in N days)
|
||||||
|
* Languages touched, by file extension
|
||||||
|
* Last file edited per project (resume-where-you-left-off)
|
||||||
|
|
||||||
|
### Friction and quality
|
||||||
|
|
||||||
|
* User message length distribution (terse vs prose)
|
||||||
|
* Rough correction reflex count ("no", "wrong", "stop", "actually")
|
||||||
|
* Permission denial frequency
|
||||||
|
* Retry / regenerate patterns
|
||||||
|
* File-history-snapshot count per session
|
||||||
|
|
||||||
|
### Character and fun
|
||||||
|
|
||||||
|
* **Em-dash violator count** against the CLAUDE.md rule. Per-week
|
||||||
|
needle ("rule violations") with its own LED annunciator
|
||||||
|
* Emoji leakage count
|
||||||
|
* Most-used phrase by Claude in your transcripts
|
||||||
|
* Most-used phrase by you
|
||||||
|
* "Dude, chill" detector (explicit pushback events)
|
||||||
|
* Thank-you rate per session
|
||||||
|
* Silent sessions (ended without `/compact` or `/clear`)
|
||||||
|
|
||||||
|
### Cross-system correlations
|
||||||
|
|
||||||
|
* Git: commits produced per session, lines changed per token spent
|
||||||
|
* Forgejo: PRs opened, merged, closed per session
|
||||||
|
* Quartermaster: did long Claude sessions correlate with
|
||||||
|
budget-editing days
|
||||||
|
* Home automation: Claude usage vs espresso machine activations
|
||||||
|
* Fitbit / health data: Claude usage vs heart rate (do you actually
|
||||||
|
stress out when Claude is slow?)
|
||||||
|
|
||||||
|
## Hardware wild ideas
|
||||||
|
|
||||||
|
* **Audio feedback**: a quiet relay-click when the tach crosses
|
||||||
|
redline; a chime when a 5h block resets. Could be annoying;
|
||||||
|
prototype with speaker off first.
|
||||||
|
* **Tactile feedback**: a small vibration motor in the bezel that
|
||||||
|
pulses when STALL lights (useful if the cluster is out of your
|
||||||
|
peripheral vision).
|
||||||
|
* **Per-project LEDs**: a strip beside the cluster where the lit
|
||||||
|
LED indicates which project is currently active. Fun for homelab
|
||||||
|
archaeology.
|
||||||
|
* **Second cluster on the other monitor** showing a 24-hour sparkline
|
||||||
|
rather than live gauges. Historical companion.
|
||||||
|
* **Cluster-to-cluster sync**: if claude-gauge becomes popular enough
|
||||||
|
to build for friends, a single OTEL collector could drive multiple
|
||||||
|
clusters in multiple homes. Niche but fun.
|
||||||
|
|
||||||
|
## Software wild ideas
|
||||||
|
|
||||||
|
* Voice announcement on stall ("Claude appears to be thinking. Or
|
||||||
|
possibly dead.")
|
||||||
|
* "Sobriety score" needle that gauges how many consecutive tool
|
||||||
|
failures are happening
|
||||||
|
* Integration with a physical "commit" button on the cluster that
|
||||||
|
accepts pending changes with a satisfying mechanical click
|
||||||
|
* RGB LED strip behind the cluster that shifts colour based on
|
||||||
|
thinking-ratio (cool blue when cruising, deep red when grinding)
|
||||||
|
|
||||||
|
## Unlikely but noted
|
||||||
|
|
||||||
|
* Smartwatch complication showing 5h fuel. Nice for away-from-desk
|
||||||
|
awareness but duplicates what a phone notification already does.
|
||||||
|
* Discord bot that posts "claude-gauge is hot!" to a channel when
|
||||||
|
the tach crosses redline. Fun in a team setting, overkill solo.
|
||||||
|
* Tattoo. No.
|
||||||
104
Roadmap.md
Normal file
104
Roadmap.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
One phase per issue. No scope bleed between phases.
|
||||||
|
|
||||||
|
## Shipped
|
||||||
|
|
||||||
|
| # | Title | Merged |
|
||||||
|
|---|---|---|
|
||||||
|
| - | Initial scaffold and PLAN.md | 2026-04-17 |
|
||||||
|
| 1 | Flesh out PLAN.md with two-architecture implementation detail | PR #2 open |
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
### Phase A — daemon prints to stdout
|
||||||
|
|
||||||
|
Tail the telemetry source (OTEL-Prom or ccusage), maintain the five
|
||||||
|
primary values in memory, print them to stdout every second. No HTTP,
|
||||||
|
no hardware, no dashboard. First proof the numbers land.
|
||||||
|
|
||||||
|
Architecture A or B must be chosen before starting; the daemon
|
||||||
|
implementation differs.
|
||||||
|
|
||||||
|
**Exit criteria**: Claude Code activity makes the values tick in a
|
||||||
|
terminal.
|
||||||
|
|
||||||
|
### Phase B — HTTP endpoint
|
||||||
|
|
||||||
|
Stand up FastAPI, expose `GET /usage` returning the documented JSON
|
||||||
|
shape. Verify with `curl` from another machine on the LAN.
|
||||||
|
|
||||||
|
**Exit criteria**: `curl http://<host>:8080/usage` returns valid JSON
|
||||||
|
with all fields populated.
|
||||||
|
|
||||||
|
### Phase C — single needle
|
||||||
|
|
||||||
|
Minimal ESP32 firmware. Polls `/usage`, drives one stepper (the tach)
|
||||||
|
and one LED (MODEL). Proves the hardware path end to end.
|
||||||
|
|
||||||
|
**Exit criteria**: the tach needle moves in response to typing into
|
||||||
|
Claude Code.
|
||||||
|
|
||||||
|
### Phase D — full cluster
|
||||||
|
|
||||||
|
Four steppers, full annunciator row. Wiring on a breadboard or
|
||||||
|
solder-perf. No enclosure yet.
|
||||||
|
|
||||||
|
**Exit criteria**: all four gauges and all five lamps behave
|
||||||
|
according to their specifications in [Hardware](Hardware).
|
||||||
|
|
||||||
|
### Phase E — calibration
|
||||||
|
|
||||||
|
Run the cluster for a week of real use. Tune the three environment
|
||||||
|
variables against observed behaviour. Document chosen values in
|
||||||
|
this wiki.
|
||||||
|
|
||||||
|
**Exit criteria**: the fuel gauge reads roughly what `ccusage blocks`
|
||||||
|
reports; the tach redline fires when you expect it to.
|
||||||
|
|
||||||
|
### Phase F — enclosure V1
|
||||||
|
|
||||||
|
3D-printed bezel, cream faces, annunciator smoked-acrylic cover,
|
||||||
|
monitor-mount bracket. Permanent install under the monitor.
|
||||||
|
|
||||||
|
**Exit criteria**: the cluster is a desk object you would show
|
||||||
|
someone, not a breadboard.
|
||||||
|
|
||||||
|
### Phase G — dashboard
|
||||||
|
|
||||||
|
Architecture A: import Grafana Labs dashboard 25052 against the
|
||||||
|
Prometheus data source. Link it from the wiki Home.
|
||||||
|
|
||||||
|
Architecture B: decide whether to build a custom deep-stats page or
|
||||||
|
just live in the `ccusage` TUI.
|
||||||
|
|
||||||
|
**Exit criteria**: a "look at everything" surface exists somewhere.
|
||||||
|
|
||||||
|
### Phase H — character and cross-system metrics
|
||||||
|
|
||||||
|
Em-dash counter, phrase extractor, git correlation, Quartermaster
|
||||||
|
correlation. Lowest priority, highest amusement. Ships as Grafana
|
||||||
|
panels (A) or as a small web page (B).
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
* True stream-time tach. Requires a Claude Code hook
|
||||||
|
(`SessionStart` / `Stop` / `PostToolUse`) pushing heartbeats.
|
||||||
|
Dramatic work for modest gain over the JSONL tail.
|
||||||
|
* Thinking-token metric via OTEL. Currently derivable only from
|
||||||
|
events + `prompt.id` correlation; simpler to extract from JSONL
|
||||||
|
content blocks directly in the tail.
|
||||||
|
* Hybrid (A + B) daemon where fuel gauges come from Prometheus and
|
||||||
|
tach comes from JSONL tail. Worth it only if Phase E shows the
|
||||||
|
15 s scrape interval feels sluggish.
|
||||||
|
* Multi-machine cluster. If other homelab hosts start running Claude
|
||||||
|
Code, point them at the same OTEL collector (architecture A) and
|
||||||
|
the gauges become per-host aggregates.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
* MicroPython firmware. No maintained SwitecX25 port exists. Not
|
||||||
|
worth writing one.
|
||||||
|
* Battery operation. Always desk-powered.
|
||||||
|
* Alexa / voice interfaces. No.
|
||||||
|
* Cloud sync of gauge state. No.
|
||||||
BIN
images/exotic-dashes.png
Normal file
BIN
images/exotic-dashes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 MiB |
BIN
images/monitor-dash.png
Normal file
BIN
images/monitor-dash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 MiB |
Loading…
Reference in a new issue