[feat] manage dashboard ui and deploy coolify with docker#29
Open
vuluu2k wants to merge 39 commits into
Open
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SQLite-backed dashboard with scrypt password auth and HMAC session cookies - Per-client request metrics (rate, status, duration) persisted across restarts - Add/remove clients from the dashboard, launcher script auto-downloaded - Coolify-friendly compose using SERVICE_FQDN_GATEWAY magic variable - npm run add-user CLI for creating dashboard accounts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bind mount './config.yaml:/app/config.yaml' caused EISDIR on Coolify because config.yaml is gitignored — Docker created an empty directory at the missing source path. Provide the file via Coolify's Storage tab File Mount instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ogin Server-friendly variant of quick-setup.sh that prints YAML to stdout or writes directly to a target path (--out), without trying to start the gateway. Supports both macOS Keychain and Linux credentials.json sources, and includes the new db.path field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Container now generates config.yaml on first start if missing, sourcing the OAuth refresh token from CCG_REFRESH_TOKEN env or a mounted credentials.json (CCG_CREDENTIALS_PATH or default /app/data/...). The file lands in the persistent ccg_data volume so device_id and tokens survive restarts. Coolify deploy now needs only: an env var or credentials mount — no more manual File Mount setup, no more EISDIR/ENOENT. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps /root/.claude/.credentials.json (read-only) into the container so the first-start bootstrap can read the OAuth refresh token without any env var setup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anthropic rotates the refresh_token on every refresh — the previous token is invalidated immediately. Before this change, the gateway only held the new token in memory; on container restart it would replay the already-consumed token from disk and crash with invalid_grant. Two fixes: - After every successful refresh, write access_token / refresh_token / expires_at back into config.yaml via yaml.parseDocument round-trip. - On startup, if a mounted credentials.json has a different refresh token than the one in config.yaml (host did its own claude login and rotated), copy it across before initOAuth runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anthropic's API rejects OAuth access tokens (sk-ant-oat01-) when sent through the x-api-key header — that header is for static API keys (sk-ant-api03-) only. The previous code's comment was wrong; the actual upstream behaviour is 401 "Invalid authentication credentials". Switch to Authorization: Bearer and ensure the request carries anthropic-beta: oauth-2025-04-20 (merging with any client-provided beta flags rather than overwriting). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…image The Docker runtime image only contains dist/ (no src/), so 'tsx src/scripts/add-user.ts' fails with ERR_MODULE_NOT_FOUND. Switch the 'add-user' script to 'node dist/scripts/add-user.js' and add a separate 'add-user:dev' for local TS workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restored the verbose install/uninstall/hijack/release/status/help text that scripts/add-client.sh emits, so the file dashboard generates is byte-equivalent to the bash version (only difference is the header comment substitutes the real client name instead of a <name> placeholder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Collapsible 'How to use this dashboard' section explains each card (stats / charts / clients / recent) and the post-add flow. - After 'Add client' succeeds, the modal switches to a success view with copy-to-clipboard snippets for both 'chmod +x ... && ./cc-name' and the install variant, instead of closing silently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tee /v1/messages response stream into an SSE parser that extracts input/output/cache token counts and the model id from message_start + message_delta events (also handles non-streaming JSON fallback). Each request is recorded with usage + USD cost computed from a per-model pricing table. Schema migrates additively (ALTER TABLE ADD COLUMN) so existing data stays intact. Dashboard now shows: total cost / total tokens in the top stats row, a 'By model' card with per-model token breakdown and cost, and tokens / cost columns in the clients table and recent requests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top stats row now has dedicated cards for input, output, cache read and cache write totals (was one merged 'tokens' figure). Clients table and recent requests table split tokens into Input / Output / Cache columns with hover tooltips showing exact counts. Recent table also gains a Model column so the per-row pricing context is visible without hovering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Cost & usage by period' card right under the lifetime totals. Each row shows calls, input/output/cache tokens and cost for a rolling window so the running spend on Anthropic is easy to track at a glance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit fbe7903 (per-request token tracking) was based on an older snapshot of proxy.ts and silently reverted the OAuth header fix from 497f46f. The gateway went back to sending the OAuth access_token (sk-ant-oat01-) via x-api-key, which Anthropic rejects with 401 "Invalid authentication credentials". Re-apply: send the token via Authorization: Bearer and merge the anthropic-beta: oauth-2025-04-20 flag with any client-provided one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recent requests table now lives in a 480px-tall scroll container with a sticky header. Rows are flipped so the newest request sits at the bottom (chat/log style), and the view auto-scrolls to the bottom on each refresh — but only when the user was already near the bottom. If they scrolled up to inspect an older request, their scroll position is preserved instead of being yanked back down every 5s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SSE/JSON usage parser feeds raw response bytes through a UTF-8 decode and expects either SSE event blocks or a JSON document. When clients sent Accept-Encoding: gzip we forwarded that to Anthropic, which then gzipped the response — the parser saw binary garbage, JSON.parse silently failed, and every persisted row had model='' and token counts of 0, so the dashboard showed no usage or cost data. Strip any inbound Accept-Encoding and pin it to 'identity' on the upstream request. The bandwidth hit is negligible for typical Claude Code traffic and makes usage tracking deterministic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The scrollable wrapper previously had no background, so the scrollbar gutter rendered with the browser's default light track inside the otherwise dark dashboard. Set explicit panel/fg colors on the container, declare color-scheme: dark, and style both the standard scrollbar (scrollbar-color/width) and the WebKit scrollbar pseudo-elements so the track and thumb match the rest of the UI. Sticky header now uses panel-2 to read as a header band against the panel-coloured rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The launcher hard-coded INSTALL_PATH=/usr/local/bin/ccg, which doesn't exist on a default Apple Silicon macOS install (Homebrew lives at /opt/homebrew/bin and /usr/local/bin is never created). cp + sudo cp both failed with "No such file or directory" and chmod failed right after, leaving the user with a confusing half-broken install flow. Detect at runtime: prefer /opt/homebrew/bin, fall back to /usr/local/bin, and finally to ~/.local/bin (created on demand, no sudo). After install, warn if the chosen dir is not on PATH and show the exact line to add. Mirrored in both src/clients.ts (dashboard 'Add client' template) and scripts/add-client.sh (CLI script). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e parser Stop forcing 'identity' upstream. Forward whatever Accept-Encoding the client sent so the response bytes returned to the client are byte-identical to a direct Anthropic call (preserves the transparent-proxy property). For /v1/messages, tee the response: write raw upstream bytes to the client unchanged, and feed a local zlib decoder (gzip/br/deflate) into the usage parser so token counts stay readable regardless of how upstream compresses. Decoder errors only drop the usage row — they never affect the client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote the macOS Gatekeeper xattr hint from inline text to its own snippet block with a Copy button, rendered with the actual client name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Recent requests table gains a "Message" column with the last user-authored prompt (truncated to 200 chars, full text in tooltip). Tool_result blocks are skipped so the displayed text is the actual prompt. Body is parsed read-only — upstream payload is unchanged. - Per-client cost cap with optional window (lifetime/monthly/daily, UTC). Limits live in config.yaml under each token. Enforced only on /v1/messages so free endpoints (event_logging, settings, etc.) keep working. Over-limit requests return 429 with used/limit/period. - Add/edit limits from the dashboard: limit fields in the Add Client modal, plus a "Set limit" button + modal per client. PATCH /api/clients/:name updates the cap; POST accepts limits at creation. In-memory token map reloads after every mutation so changes take effect without restarting the gateway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…entials.json `syncOAuthFromCredentialsIfChanged` previously copied any mounted credentials.json refresh_token into config.yaml whenever the two differed. After the gateway has rotated tokens at runtime, the mounted file is usually older than config.yaml, so the sync would replay a consumed refresh_token and brick auth on the next restart — forcing a re-login and redeploy every time. Now compare expiresAt and only adopt the mounted creds when they're actually newer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eview The Recent Requests dashboard column was showing walls of <system-reminder>, <command-name>, <local-command-stdout> etc. that Claude Code injects into the user message stream — none of which is text the human typed. Filter those blocks out both at write time (extractLastUserMessage) and at read time (so historical rows already in SQLite also display cleanly without a migration). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two UX fixes: - Add a sticky left sidebar with section anchors (Overview, Cost, Traffic, Models, Clients, Recent, How-to). Active link tracks the visible section via IntersectionObserver and the page title syncs. - Recent Requests no longer re-renders the whole table every 5s. New rows are diffed by ts and appended at the bottom (chat-style); the view stays anchored to the latest unless the user has scrolled up. Hovering the table pauses inserts and shows a "paused · N new" hint so rows don't shift under the cursor while reading. Relative-time cells tick separately every 15s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidebar nav refactor renamed the stats container from #topStats to #stats at runtime, but renderTopStats still wrote into #topStats by id. The lookup returned null, the first refresh threw, and every subsequent render in the same tick (periods, charts, models, clients, recent) was skipped — so the dashboard came up blank. Point the sidebar anchor at #topStats directly and drop the rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tablet (≤800px): sidebar collapses into a sticky horizontal nav bar at the top with brand on the left and scrollable section links to the right. Header un-stickies so the nav bar takes that role. Phone (≤600px): tighten padding, font sizes, stat number, and column widths so the layout breathes on a 360-400px viewport. Modal becomes full-screen instead of a tiny floating box. Recent table cells (msg, path) get smaller max-widths. Tables: every table container now has overflow-x: auto with a min-width on the inner table, so wide tables scroll horizontally inside their card instead of squishing columns or breaking the page layout. Logout moves back into the header toolbar so it stays reachable on mobile (the sidebar footer is hidden in horizontal nav mode). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wide data tables (periods, models, clients, recent, client config) now collapse into per-row cards on phones via td[data-label] pseudo-labels, so each row reads top-to-bottom without horizontal scroll. Header stacks with full-width touch toolbar; stats grid drops to 2 columns; modal inputs use 16px to suppress iOS zoom. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…reated modal Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lient, model, status, method) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tep 1 Match the actual terminal-execution order on macOS — the quarantine attribute must be removed before chmod/run, otherwise step 2 fails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add buildPowerShellLauncherScript with full feature parity to the bash version (install/uninstall/hijack/release/native/status/help). POST /api/clients accepts platform="windows" to serve a cc-NAME.ps1 instead of the bash file. Dashboard "Add client" form has a target-platform select, and the success modal swaps step labels + commands to match (Set-ExecutionPolicy + Unblock-File, .\cc-NAME.ps1 install, etc). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\\ inside the renderDashboard() template literal collapses to a single \, which then terminated the inline JS string with \' — the script failed to parse and the entire dashboard rendered blank. Use \\\\ in source so the inline JS receives a valid \\ escape and prints .\cc-name.ps1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows npm installs both 'claude' (no ext) and 'claude.cmd', so Get-Command -CommandType Application returned an array. $app.Source then became an array of paths and '& $app.Source' tried to invoke the joined string, producing CommandNotFoundException. Pipe through Select-Object -First 1 in both Invoke-Native and the main launch path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ension Process-scoped env vars in the launcher don't reach GUI apps spawned by the OS (VS Code extension, Cursor). Add opt-in subcommands that persist ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL at user scope so the Claude Code extension routes through the gateway too. - macOS: ~/Library/LaunchAgents/com.ccg.env.plist + launchctl setenv - Linux: ~/.config/environment.d/ccg.conf - Windows: [Environment]::SetEnvironmentVariable(..., "User") uninstall now also clears GUI hijack. status splits Hijack into Shell / GUI rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing clients receive their launcher script once via "+ Add client" — there was no way to regenerate when the script template grows new features (e.g. ccg hijack-gui / release-gui added in 1eb88a2). - GET /api/clients/:name/launcher reuses the token from config.yaml, so billing, cost limit, and request history are all preserved. - Dashboard adds a "Re-download" button per client row that opens a small modal for platform / scheme / gateway address, then downloads the freshly generated launcher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.