Real-time dashboard for monitoring CLIProxy usage, token consumption, estimated cost, and credential health.
- Collector (Python/Flask): polls CLIProxy Management API, computes deltas/costs, writes to PostgreSQL
- Frontend (React + Nginx): charts and analytics UI
- PostgreSQL: self-hosted DB initialized from
init-db/schema.sql - PostgREST: read-only API layer for frontend
- Skill tracker plugin distribution via marketplace + submodule (
plugin/claude-skills-tracker)
CLIProxy API / CPA-Manager Usage Service → Collector (Python) → PostgreSQL
Browser → Nginx:8417
├── /rest/v1/* → PostgREST:3000 → PostgreSQL (read)
└── /api/collector/* → collector:5001 (write/trigger)
- Docker + Docker Compose v2
- CLIProxy with remote management enabled
Ensure your CLIProxy config includes:
remote-management:
allow-remote: true
secret: "<your-management-secret>"For CLIProxyAPI v6.10+ / v7, usage statistics are served by an external usage service such as CPA-Manager. Keep the dashboard management URL pointed at CLIProxyAPI, and point the usage URL at the usage service:
usage-statistics-enabled: true
redis-usage-queue-retention-seconds: 3600Quick verification:
curl -H "Authorization: Bearer <your-management-secret>" \
http://localhost:18317/v0/management/usageYou should receive a JSON usage response. On older CLIProxyAPI versions that
still expose /v0/management/usage, CLIPROXY_URL and CLIPROXY_USAGE_URL
can be the same URL.
git clone https://github.com/leolionart/CLIProxyAPI-Dashboard.git
cd CLIProxyAPI-Dashboard
git submodule update --init --recursivecp .env.example .envEdit .env:
DB_PASSWORD=your_secure_password_here
CLIPROXY_URL=http://host.docker.internal:8317
# Optional: set this to CPA-Manager Usage Service for CLIProxyAPI v6.10+ / v7.
CLIPROXY_USAGE_URL=http://host.docker.internal:18317
CLIPROXY_MANAGEMENT_KEY=<your-management-secret>
# Recommended for CPA-Manager on the same Docker host.
# Replace the volume name if your CPA-Manager stack uses a different one.
CPA_USAGE_DATA_DIR=/var/lib/docker/volumes/cli-proxy-api_cpa_manager_data/_data
CPA_USAGE_DB_PATH=/cpa-data/usage.sqlite
# Optional
COLLECTOR_INTERVAL_SECONDS=300
TIMEZONE_OFFSET_HOURS=7
RAW_SNAPSHOT_ENABLED=true
RAW_SNAPSHOT_RETENTION_DAYS=3
RAW_SNAPSHOT_MIN_INTERVAL_HOURS=24
RAW_SNAPSHOT_CLEANUP_BATCH_SIZE=1000
RAW_SNAPSHOT_CLEANUP_MAX_BATCHES=50
ADMIN_AUTH_REQUIRED=false
ADMIN_PASSWORD=change-me
ADMIN_SESSION_TTL_DAYS=30
ADMIN_SESSION_SECURE_COOKIE=false
ADMIN_SESSION_SAMESITE=LaxNotes:
- Dashboard is public by default. Set
ADMIN_AUTH_REQUIRED=trueandADMIN_PASSWORD=...to require admin login before loading UI or/rest/v1/*data. - The browser stores only an
HttpOnlysession cookie; the password is never stored in browser storage. - If you deploy behind HTTPS, set
ADMIN_SESSION_SECURE_COOKIE=true. - Default host port for PostgREST is now
8418to avoid common conflicts on3000. Override withPOSTGREST_HOST_PORTif needed. ADMIN_ALLOWED_ORIGINSis optional. Leave it empty for the default same-compose setup; set it only if you want stricter Origin/Referer enforcement.CLIPROXY_URLis used for CLIProxyAPI management endpoints such as auth files.CLIPROXY_USAGE_URLis used only for/v0/management/usage. For CPA-Manager, set it to the usage service URL.CPA_USAGE_DATA_DIRshould point to the host directory that contains CPA-Managerusage.sqlite. When this SQLite file is mounted, the collector uses it as the source of truth for inbound API-key attribution, so the API Keys dashboard can show key aliases/names instead of raw hashes.- Dashboard/statistics reads use normalized rows (
daily_stats,credential_daily_stats,credential_hourly_stats,model_usage,skill_daily_stats,skill_runs).usage_snapshots.raw_datais optional debug data only. RAW_SNAPSHOT_ENABLED=truekeeps raw debug payloads, but the defaultRAW_SNAPSHOT_MIN_INTERVAL_HOURS=24stores roughly one raw payload per day instead of every 5-minute run.RAW_SNAPSHOT_RETENTION_DAYS=3means expiredusage_snapshots.raw_datavalues are set toNULL; snapshot rows are not deleted, somodel_usagehistory is preserved. If the path is configured but unreadable,/api/collector/healthand collector logs report the problem before falling back to the management usage API.
docker compose up -dOpen dashboard at: http://localhost:8417
Expected startup order:
postgreshealthycollectorhealthy (DB init + migrations)postgreststartsfrontendstarts
First data usually appears after the first collector interval.
docker compose ps
docker compose logs -f collector
curl -X POST http://localhost:8417/api/collector/triggerSuccess signals:
- collector logs periodic snapshot collection
- collector health endpoint responds
- manual trigger returns success
If you don't want to clone the full repo:
mkdir cliproxy-dashboard && cd cliproxy-dashboard
curl -O https://raw.githubusercontent.com/leolionart/CLIProxyAPI-Dashboard/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/leolionart/CLIProxyAPI-Dashboard/main/.env.example
cp .env.example .env
# then edit .env and run:
docker compose up -dTracker plugin is now distributed from the shared Claude skills marketplace.
- Marketplace repo:
leolionart/claude-skills - Plugin install ID:
claude-skill-tracker
Inside Claude Code:
/plugin marketplace add leolionart/claude-skills
/plugin install claude-skill-tracker
/reload-plugins
Optional endpoint override (if dashboard is not local):
export CLIPROXY_COLLECTOR_URL="https://your-domain/api/collector/skill-events"Dedupe note: do not run both marketplace plugin hook and a manual PostToolUse: Skill hook at the same time.
Codex skill tracking is best-effort because Codex does not currently emit a dedicated Skill tool event. CLIProxyDash supports an inferred Stop hook that reads the Codex session JSONL and sends rows to the existing skill endpoint with source=codex-hook.
On every machine that should report Codex skill usage, run the one-step installer:
curl -fsSL \
https://raw.githubusercontent.com/leolionart/CLIProxyAPI-Dashboard/main/scripts/setup_codex_tracking.py \
| python3 - --collector-url "https://your-domain/api/collector/skill-events"The installer downloads the hook script, creates a wrapper with the dashboard URL, enables codex_hooks, and appends the Stop hook without removing existing hooks.
Manual setup, if needed:
mkdir -p ~/.codex/hooks
curl -fsSL \
https://raw.githubusercontent.com/leolionart/CLIProxyAPI-Dashboard/main/scripts/codex_skill_usage_hook.py \
-o ~/.codex/hooks/codex_skill_usage_hook.py
chmod +x ~/.codex/hooks/codex_skill_usage_hook.pySet the dashboard endpoint in the shell environment used to launch Codex:
export CLIPROXY_COLLECTOR_URL="https://your-domain/api/collector/skill-events"Enable hooks in ~/.codex/config.toml:
[features]
codex_hooks = trueAdd this command to your Codex Stop hooks in ~/.codex/hooks.json, preserving any existing Stop hooks:
{
"type": "command",
"command": "python3 \"$HOME/.codex/hooks/codex_skill_usage_hook.py\"",
"timeout": 10
}Dry-run against a known session:
CLIPROXY_DRY_RUN=1 python3 ~/.codex/hooks/codex_skill_usage_hook.py <<'JSON'
{"session_path":"$HOME/.codex/sessions/YYYY/MM/DD/rollout-...jsonl"}
JSONCurrent Codex coverage: this hook collects inferred skill usage only. It does not collect Codex sub-agent / agent lifecycle yet; that requires a separate Codex agent event pipeline, endpoint, and schema.
This repo now includes templates to enable Lark task data access from Claude Code.
cp .mcp.json.example .mcp.json.mcp.json is ignored by git in this repo, so keep real credentials there.
Use your shell profile (or export in current terminal):
export LARK_APP_ID="cli_xxx"
export LARK_APP_SECRET="your-lark-app-secret"
export LARK_DOMAIN="https://open.larksuite.com"
export LARK_TOOLSETS="preset.base,preset.task,task.v2.task.get,task.v2.task.list,task.v2.tasklist.list,task.v2.tasklist.tasks"After saving .mcp.json and env vars, restart Claude Code (or reload) so lark-mcp can start.
Skill file: .claude/skills/lark-suite/SKILL.md
Ask naturally, for example:
- "Lấy danh sách task đang open trong Lark"
- "Lấy chi tiết task theo ID ..."
- "Tóm tắt task theo trạng thái"
docker compose pull
docker compose up -ddocker compose ps
docker compose logs --tail=200 collector postgrest frontend
curl http://localhost:8417/api/collector/health
curl "http://localhost:8417/rest/v1/daily_stats?select=date,total_requests&order=date.desc&limit=1"
curl -X POST http://localhost:8417/api/collector/triggerdocker-compose.override.yml is the local dev override and is loaded automatically by docker compose.
For source-only changes, prefer bind mounts + service restart. Rebuild images only when Dockerfile or dependencies changed.
docker compose up -d postgres postgrest
cd frontend
npm install
POSTGREST_HOST_PORT=8418 npm run devOpen Vite dev UI at http://localhost:5173.
Keep the local collector running too. Vite dev proxy now checks the same auth session flow as production, so
/rest/v1/*stays locked until you log in.
cd collector
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python main.pyThe collector keeps usage_snapshots rows as the cumulative counter timeline used
for delta calculation and model joins. It no longer stores the full cumulative
CLIProxy payload on every run.
Default behavior:
- Every collector run inserts compact normalized snapshot counters and
model_usagerows. raw_datais written only whenRAW_SNAPSHOT_ENABLED=trueand the newest retained raw snapshot is at leastRAW_SNAPSHOT_MIN_INTERVAL_HOURSold.- Expired
raw_datais set toNULLin bounded batches. Rows are never deleted becausemodel_usage.snapshot_idhasON DELETE CASCADE. - Cleanup runs at collector startup and then at most once per local day. Logs include
rows_nullified,batches,duration_ms,cutoff,batch_size, andmax_batches.
Production deployment:
# 1. Backup first because this changes large TOAST values.
docker exec cliproxy-postgres pg_dump -U cliproxy -d cliproxy -Fc -f /tmp/cliproxy-before-raw-retention.dump
docker cp cliproxy-postgres:/tmp/cliproxy-before-raw-retention.dump ./cliproxy-before-raw-retention.dump
# 2. Pull and restart through compose so collector applies migration 0009.
docker compose pull collector frontend
docker compose up -d
# 3. Watch collector migration and cleanup logs.
docker compose logs -f collectorRollback:
# Roll back app images if needed. The migration only adds indexes, so it is backward-compatible.
docker compose pull collector frontend
docker compose up -d collector frontendIf you need to pause raw cleanup during investigation, set:
RAW_SNAPSHOT_RETENTION_DAYS=3650
RAW_SNAPSHOT_CLEANUP_MAX_BATCHES=1SQL verification:
-- Confirm migration indexes exist.
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'usage_snapshots'
AND indexname IN (
'idx_usage_snapshots_collected_at',
'idx_usage_snapshots_raw_data_retention'
);
-- Current raw debug footprint.
WITH raw_counts AS (
SELECT
count(*) FILTER (WHERE raw_data IS NOT NULL) AS rows_with_raw_data,
count(*) FILTER (WHERE raw_data IS NULL) AS rows_without_raw_data
FROM usage_snapshots
)
SELECT
raw_counts.rows_with_raw_data,
raw_counts.rows_without_raw_data,
pg_size_pretty(pg_total_relation_size('usage_snapshots')) AS usage_snapshots_total,
CASE
WHEN toast.relid = 0 THEN '0 bytes'
ELSE pg_size_pretty(pg_total_relation_size(toast.relid))
END AS usage_snapshots_toast
FROM raw_counts
CROSS JOIN (
SELECT c.reltoastrelid AS relid
FROM pg_class c
WHERE c.oid = 'usage_snapshots'::regclass
) toast;
-- Expired raw values remaining after a cleanup cycle.
SELECT count(*) AS expired_raw_rows
FROM usage_snapshots
WHERE raw_data IS NOT NULL
AND collected_at < now() - interval '3 days';
-- Recent normalized rows still exist and keep model_usage joins valid.
SELECT s.id, s.collected_at, s.raw_data IS NOT NULL AS has_raw_data, count(mu.id) AS model_rows
FROM usage_snapshots s
LEFT JOIN model_usage mu ON mu.snapshot_id = s.id
GROUP BY s.id, s.collected_at, s.raw_data IS NOT NULL
ORDER BY s.collected_at DESC
LIMIT 10;Expected storage growth:
- Before: with a full cumulative ledger written every 5 minutes, production observed roughly 600-680 MB/day.
- After: normal runs add compact counters plus
model_usagerows. Raw payload growth is about one retained payload/day, capped byRAW_SNAPSHOT_RETENTION_DAYS. - With the defaults, steady-state raw debug storage is roughly 3 raw payloads plus normalized rows, so growth should no longer scale with the full historical ledger.
- Check
remote-management.allow-remote: truein CLIProxy config - Ensure
CLIPROXY_MANAGEMENT_KEYmatches CLIProxysecret - Ensure
CLIPROXY_URLis reachable from the collector container
- Wait until first collection interval
- Check collector logs:
docker compose logs -f collector - Trigger manually after logging in:
curl -X POST http://localhost:8417/api/collector/trigger
- Ensure
.envhasADMIN_AUTH_REQUIRED=true - Ensure
.envcontainsADMIN_PASSWORDand that it matches what you enter on the login screen - For HTTPS deployments, set
ADMIN_SESSION_SECURE_COOKIE=true; for local HTTP keep itfalse - If you use a custom origin or reverse proxy, set
ADMIN_ALLOWED_ORIGINSto the public dashboard origin
- Confirm postgres is healthy before postgrest starts:
docker compose ps - If using an old pre-initialized volume, apply schema manually from
init-db/schema.sql
- PostgREST now defaults to host port
8418instead of3000 - If you want a different host port, set
POSTGREST_HOST_PORTin.env - If Vite dev is already running, restart it after changing
POSTGREST_HOST_PORT
collector/main.py– collector + Flask endpointscollector/db.py– PostgreSQL client + migrations runnercollector/migrations/– DB migrations (required for schema changes)frontend/src/– dashboard UIplugin/claude-skills-tracker/– tracker plugin submodule (source mirror for dashboard development)- Tracker marketplace source of truth:
leolionart/claude-skills
MIT — see LICENSE.
