Skip to content

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

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

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

Conversation

@vuluu2k

@vuluu2k vuluu2k commented May 10, 2026

Copy link
Copy Markdown

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 30 commits May 27, 2026 14:21
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>
Add step 5 with ccg hijack-gui snippet so users know how to route the
VS Code / Cursor extension through the gateway, and a step 2 callout
warning that Claude Code's "Yes / No (recommended)" prompt on first
run must be answered Yes — picking the recommended option drops the
gateway env vars and falls back to the native endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…to official prices

Opus 4.5/4.6/4.7 were stale at the old $15/$75 tier; per the official
pricing page they are $5/$25 like 4.8. Haiku 4.5 was using Haiku 3.5
rates ($0.80/$4); corrected to $1/$5. Adds claude-fable-5 ($10/$50) and
a fable family fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the "Client created" steps on the main page with client/platform
selectors and copy buttons, so instructions stay accessible after the
modal closes. Shares command generation via launcherSteps().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t landing

Add a token-free client portal so each client can self-manage:
- Log in with client name + admin-generated password (separate ccg_portal
  session cookie; scrypt hashing shared via new password.ts)
- View granted credit (cost limit), used/remaining per period, and usage stats
- Download their personal launcher (bash / PowerShell)
- Change their own password; admin can reset it from the dashboard

Routing: / is now a public product landing (client-only CTA, admin not
advertised); admin login moved to /admin (/login redirects for back-compat),
dashboard stays at /dashboard.

New: portal.ts, landing.ts, client-auth.ts, password.ts, client_credentials
table. Admin "Add client" now surfaces the one-time portal password
(X-Portal-Password header / JSON) and a per-client Reset password action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… install guide

Landing: full marketing page (sticky header, gradient hero with terminal mock,
how-it-works, features grid, portal preview, FAQ, CTA band, footer). No admin
links — admin stays at /admin, unadvertised.

Portal: tabbed layout (Overview / Profile / Usage / Install / Security).
- Editable personal profile (display name + email) via /portal/profile
- Account view: client name, plan/limit, member since, password set; API token
  with reveal + copy
- Usage by period, by model, and a recent-activity feed (per client)
- Detailed platform-aware install guide: prerequisite, 5 steps, full ccg
  subcommand table, and manual env-var setup prefilled with token + gateway URL

Backend: client_profiles table + client-profile.ts; metrics getClientModels /
getClientRecent; client-auth getClientCredentialMeta; /portal/me enriched with
token, gateway_addr, profile, account, models, recent; client delete cleans up
its profile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One-line install (no permissions needed):
- New /portal/install.sh and /portal/install.ps1 endpoints, authorized by a
  short-lived signed grant in the URL (createInstallGrant, 30 min), so the
  command runs under `curl … | bash` / `irm … | iex` — no Gatekeeper quarantine,
  no chmod, no execution-policy change, no sudo (installs to ~/.local/bin /
  %LOCALAPPDATA%). buildUnixInstaller embeds the launcher via a quoted heredoc;
  buildPowerShellInstaller embeds it base64-encoded. Portal surfaces the command
  (install_token added to /portal/me).

i18n (English / Tiếng Việt / 中文 via ?lang=):
- New i18n.ts: message catalog (en/vi/zh) + tiny client runtime that resolves
  the language from ?lang= → localStorage → browser → en, translates
  data-i18n / -html / -ph / -title nodes, exposes window.t() for dynamic
  strings, and renders a <select id="langSel"> switcher.
- Fully translated landing, portal (login + portal) and the admin dashboard
  (login + dashboard, modals, install guide, command tables, JS messages).
  Login error strings now passed as i18n keys.

Icons:
- Landing feature icons are now inline Lucide-style SVGs (no CDN, privacy-friendly)
  instead of emoji.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The quick-install section now lists both commands at once, each labelled with
its OS and its own copy button, so Windows users see `irm … | iex` without
switching the platform selector. Long command boxes wrap instead of scrolling
horizontally (pre-wrap + room for the copy button) and snippets get a thinner
scrollbar. Adds os.unix / os.win i18n keys (en/vi/zh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Portal:
- Freeze the install grant token after first load so the one-line command no
  longer changes on every background poll; don't reseed profile inputs on
  refresh (was wiping in-progress edits); poll every 30s instead of 15s.

Installer:
- buildUnixInstaller / buildPowerShellInstaller now check for `claude` and run
  `npm install -g @anthropic-ai/claude-code` when it's missing; if that's not
  possible they print a clear reminder with the command. Quick-install note
  mentions Claude Code is installed automatically (en/vi/zh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anthropic returns 429 (rate_limit_error) or 529 (overloaded) when the
shared OAuth account hits its rate cap; the client retries per retry-after
and usually gets 200 next. Log these explicitly so shared-account
throttling is distinguishable from the gateway's own cost-limit 429.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The forwarded user-agent + cc_version were pinned to a hardcoded 2.1.81,
which can drop requests into a stricter upstream rate-limit bucket. Now
the gateway tracks the latest released Claude Code automatically:

- On every deploy/start, fetch the latest version from the npm registry
  (@anthropic-ai/claude-code) and update env.version/version_base in the
  running config + persist to config.yaml. Opt out: CCG_DISABLE_VERSION_AUTOUPDATE=1.
- Built-in daily scheduler re-syncs every 24h between deploys
  (override cadence: CCG_VERSION_SYNC_INTERVAL_MS).
- `npm run update-version` standalone command for OS-level cron; running
  gateway picks up the change live via the config.yaml watcher (no restart).
- Best-effort: registry/network failure keeps the configured version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agent commands

Path masking rewrote the real cwd/home paths in the request prompt to a
canonical working_dir (e.g. /Users/jack/projects), but the rewrite is
one-directional — there's no reverse rewrite of the model's response. The
model echoed the fake path straight into bash/file tool calls (`cd
/Users/jack/projects`), which fail on the real machine (user is `mac`).
Hiding the path also adds ~no privacy since the API already receives full
file contents.

Make it opt-in via prompt_env.mask_paths (default off). Existing deployed
configs auto-fix on redeploy. Identity/env/telemetry masking is untouched.

- rewriter: guard working-dir + home-path rewrites behind pe.mask_paths
- config: add optional prompt_env.mask_paths
- generated configs (example/bootstrap/gen-config.sh): mask_paths: false,
  align stale /Users/jack default to /Users/dev
- tests: opt the masking tests into mask_paths:true; add a default-off test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Path masking rewrites the real cwd/home to a canonical working_dir in the
request, but the model then echoes that fake path into bash/file tool calls
which fail on the real machine. This adds the missing reverse direction so
masking stays on (privacy) AND tool calls hit real paths.

- extractReversePathMap: capture real cwd + home prefix from the original
  request body (before masking) and build canonical→real pairs
- createPathReplacer: streaming literal replacer, boundary-safe via a
  prefix-aware holdback (handles a path split across network chunks and the
  /Users/jack/ vs /Users/jack/projects prefix collision)
- proxy: new response mode that decodes (gzip/br/deflate) → reverses paths →
  streams identity bytes; strips content-encoding/length. Usage parsing and
  the raw-passthrough fast path are preserved
- config: prompt_env.reverse_paths kill-switch (default on)
- tests: reverse map extraction, single-chunk, byte-split boundary, disable

Verified end-to-end: gzipped SSE split into 7-byte chunks round-trips with
all /Users/jack paths reversed to the real /Users/mac paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing gzip

The path reverse-rewrite previously decoded the response and sent it to the
client as identity (uncompressed). Keep the wire compressed: re-encode the
rewritten bytes with the SAME content-encoding upstream used
(gzip/br/deflate) so content-encoding is preserved; only content-length is
dropped (length changes → chunked). When upstream sent identity, decoder and
encoder are both null and plaintext flows through unchanged.

Flow: upstream → [decoder] → reverse paths → [encoder] → client.

Verified: gzipped SSE fed in 5-byte chunks round-trips through
gunzip→rewrite→gzip and the wire is still valid gzip the client decodes to
the real /Users/mac paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… exact

Step 3 collapsed every "Working directory: /X" to a single canonical
working_dir — many→one and irreversible, so a request touching multiple real
cwds could only reverse one of them. Project names were already exposed via
every file path (Step 4 preserves structure), so the collapse added ~no
privacy anyway.

Now a home-based cwd (/Users/x/… or /home/x/…) is left to Step 4, which swaps
only the username and keeps the project subpath. The single home-prefix
reverse pair then un-masks ANY number of distinct projects/cwds under that
home, unambiguously. Only non-home cwds (no username) are still collapsed to
working_dir and reversed best-effort.

- rewriter: Step 3 skips home cwds; extractReversePathMap builds the
  working_dir pair only for a non-home cwd
- tests: home structural mask, non-home collapse, and a multi-cwd reverse
  proving two projects under one home both map back correctly

Per-request isolation already made concurrent users safe; this fixes the
remaining within-request multi-cwd case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extractReversePathMap derived the real home via a first-match scan over the
whole request body. In a multi-turn session, messages (history) serialize
before the system env block, so once any masked /Users/jack/ path landed in
history it became the first match — home resolved to the canonical prefix,
the reverse pair was dropped, and un-masking silently turned off for the rest
of the session (model emitted /Users/jack/... and bash `ls`/`cd` hit a path
that does not exist on the real machine).

Anchor the real home to the authoritative "Working directory:" env line(s),
preferring a prefix that differs from the canonical one; fall back to the old
first-match scan only when no env line is present. Add a regression test that
reproduces the polluted-history shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… windows)

Forward mask and reverse map now derive from one shared buildPathContext so the
two directions are exact inverses and cannot drift.

- Multiple non-home cwds: discover both the primary `Working directory:` line
  and the `Additional working directories:` bullet list; each distinct root maps
  to a distinct canonical (working_dir, working_dir-cwd1, ...) instead of being
  collapsed lossily, so every project reverses to its own real path.
- Bare home (`/Users/jack`, no trailing slash): was leaking the real username
  upstream and was never reversed; now boundary-masked and reversed via a
  boundary-guarded pair in the streaming replacer (won't hit /Users/jackson).
- Windows home (`C:\Users\bob\...`): username masked forward-only (no reverse).
- Unified PATH_BOUNDARY feeds both the lookahead and the username char class so
  delimiters (: ; , ) ] }) are no longer consumed and dropped from masked text.
- Trim trailing punctuation off discovered cwd roots so subpaths still match.
- Non-home canon takes a -cwd0 suffix when a home is also masked, avoiding alias
  collisions with a real home subpath sharing working_dir's basename.

tests/rewriter.test.ts: 36/36 pass; build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When upstream returns 429/529, log all anthropic-ratelimit-* headers
(incl. unified 5h/weekly windows) so the exhausted bucket is named
instead of guessed. For any 4xx/5xx, buffer a capped copy while
forwarding raw bytes byte-identical, then decode and log Anthropic's
actual error payload (rate_limit_error vs invalid_request vs overloaded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two multi-gateway friction fixes:

- Launchers (Unix, PowerShell, add-client.sh) pre-seed the token's last
  20 chars into customApiKeyResponses.approved in ~/.claude.json, so
  Claude Code never shows the "Detected a custom API key — use it?"
  prompt when switching gateways/tokens. Unix uses node with a python3
  fallback (native claude installs may lack node); PowerShell requires
  node and skips otherwise, since ConvertTo-Json can silently truncate
  deep structures in the user's ~/.claude.json.

- Installers and the `install` subcommand now overwrite stale ccg
  copies in /opt/homebrew/bin, /usr/local/bin, and ~/.local/bin so the
  newly installed gateway wins regardless of PATH order (previously an
  old launcher earlier on PATH kept routing to the previous gateway).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Message column now previews each request's actual last message (tool_result payloads, tool_use calls, classifier text) instead of repeating the turn's human prompt. Record error_source (gateway|upstream) plus decoded error_detail per request: new dashboard column with tooltip, portal status badge, x-ccg-error header on gateway-generated responses, and model attribution for failed requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… rate-limit headers

Capture the anthropic-ratelimit-unified-* headers Anthropic returns on every
/v1/messages response (previously only read inside the 429 log branch and
discarded) into a live in-memory snapshot, and surface the shared subscription
account's quota on the admin dashboard:

- ratelimit.ts: capture-all + parse 5h/weekly windows (utilization → used %,
  reset → countdown, status, representative-claim); raw map kept as fallback.
- proxy.ts: snapshot headers on every response (200 and 429).
- metrics.ts: getSessionModels() + quota/sessionStart/sessionModels in snapshot.
- dashboard.ts: "Session quota" panel (per-window % bar + live reset countdown +
  raw headers) and "Models used this session (5h)" table; new nav entry.
- i18n.ts: en/vi/zh keys for the new panels.
- tests: 9 unit tests for the header parser (58/58 passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erse mapping

The response reverse-map re-guessed each client's real home from the request's
env block, so it silently failed (empty map → canonical path leaks to the client)
whenever that line was missing or already canonical. Make it deterministic and
cross-platform by storing each client's real home once.

- config/clients: new auth.tokens[].home_dir (native form: /Users/x, /home/x,
  or C:\Users\x); normalizeHomeDir validation + setClientHomeDir YAML writer.
- rewriter: extractReversePathMap(body, config, clientHomeDir?) prefers the
  stored home as an authoritative anchor over env auto-detection. Adds Windows
  reverse in the JSON-escaped wire form (C:\\Users\\dev\\ → real) — the reason
  Windows paths never reversed before. 6 new tests (POSIX/macOS/Windows/Case-B/
  chunk-split).
- proxy: pass tokenEntry.home_dir; /api/clients GET+PATCH and portal /me+/profile
  read/write home_dir.
- dashboard: admin Workdir column + edit modal with macOS/Linux/Windows guidance.
- portal: clients self-set their home folder in Profile, with the same guidance.
- scripts/set-workdir.ts + npm run set-workdir: set the canonical working_dir
  (e.g. /Users/dev) in config.yaml.
- i18n en/vi/zh for all new UI.

Build clean; 64/64 tests pass. set-workdir + setClientHomeDir verified end-to-end
(Windows backslashes round-trip through YAML; invalid paths rejected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.pct had line-height:1 and no margin, so "41% đã dùng" sat flush against the
quota bar. Add margin-bottom on .pct and a small left margin on the unit label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fmtCountdown only emitted hours, so the 7-day quota window read as
"154h 31m". Add a day component so it renders "6d 10h 31m" — far easier
to parse at a glance. Applies to the live countdown ticker too.

Co-Authored-By: Claude Opus 4.8 (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