Skip to content

[feat] manage dashboard ui and deploy coolify with docker#29

Open
vuluu2k wants to merge 39 commits into
motiful:mainfrom
vuluu2k:main
Open

[feat] manage dashboard ui and deploy coolify with docker#29
vuluu2k wants to merge 39 commits into
motiful:mainfrom
vuluu2k:main

Conversation

@vuluu2k
Copy link
Copy Markdown

@vuluu2k vuluu2k commented May 10, 2026

No description provided.

root and others added 30 commits May 5, 2026 11:59
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>
vuluu2k and others added 9 commits May 27, 2026 11:01
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant