[feat] manage dashboard ui and deploy coolify with docker#29
Open
vuluu2k wants to merge 65 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>
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>
… broken agent commands" This reverts commit 2f2dc74.
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>
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.