Repository navigation
Add manual switch, threshold control, and web dashboard - #1
Merged
Merged
Conversation
When teamclaude runs as a long-lived service (systemd, container), the interactive TUI never renders — its keyboard controls require a TTY. This adds three out-of-band control surfaces that don't depend on the TUI being active: - POST /teamclaude/switch — pin the active account by name - POST /teamclaude/threshold — change the rotation cutoff (0..1) and persist it via the existing atomicConfigUpdate path - GET /ui — single-page dashboard served on the same proxy port, polling /teamclaude/status every 2s with per-account quota bars, click-to-pin account selection, and a live threshold slider Two thin CLI wrappers map onto the new endpoints: - teamclaude switch <name> - teamclaude threshold <value> (accepts "85", "85%", or "0.85") Dashboard uses createElement + textContent throughout (no innerHTML) so account names from the API can't smuggle markup into the DOM. Dark/light themes via prefers-color-scheme. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a web dashboard and new CLI commands (switch and threshold) to manage account rotation and pinning. The dashboard is served via a new /ui endpoint. Feedback suggests improving the routing logic to handle query strings correctly and caching the static dashboard HTML in memory to optimize performance.
| } | ||
|
|
||
| // GET /ui — serve the dashboard (single-page HTML) | ||
| if (req.method === 'GET' && (req.url === '/ui' || req.url === '/ui/' || req.url === '/ui/index.html')) { |
There was a problem hiding this comment.
The current routing logic for the dashboard uses exact string matching on req.url. This will fail if the user includes a query string (e.g., http://localhost:3456/ui?refresh=true). It is safer to check the pathname specifically.
const url = new URL(req.url, 'http://localhost');
if (req.method === 'GET' && (url.pathname === '/ui' || url.pathname === '/ui/' || url.pathname === '/ui/index.html')) {| // GET /ui — serve the dashboard (single-page HTML) | ||
| if (req.method === 'GET' && (req.url === '/ui' || req.url === '/ui/' || req.url === '/ui/index.html')) { | ||
| try { | ||
| const html = await readFile(join(__dirname, 'web', 'index.html'), 'utf-8'); |
- Parse req.url with new URL() once per request and match on .pathname so /ui?foo=bar (or /teamclaude/status?refresh=1) still hits the right handler. Previous exact-string equality silently fell through to the upstream proxy on any query string. - Cache dashboardHtml in a closure variable after first read. The file is static for the lifetime of the process, so subsequent /ui requests skip the readFile() round-trip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jphein
pushed a commit
that referenced
this pull request
Jun 23, 2026
Passive remains the default; this adds an explicitly opt-in active quota refresh for users who need idle accounts' quota kept fresh. - src/prober.js: reschedulable background probe, OFF by default. When on, reads each OAuth account's utilization from the zero-spend /api/oauth/usage endpoint (no message quota consumed), with per-probe timeout, 401 refresh-retry, and overlap guard. API-key accounts are skipped. - oauth.js: fetchUsage + normalizeUsageBucket (tolerant of pct/fraction and sec/ms reset formats). - account-manager.js: applyUsageData updates 5h/7d/Sonnet-7d quota without counting a request; new unified7dSonnet(+Reset) fields (cleared on expiry, persisted across restarts). - index.js: start/stop the prober from config.quotaProbeSeconds; `teamclaude probe <off|seconds>` (min 30s) writes the config and notifies a running server; reload reschedules live. Help updated. - TUI/status: show a Sonnet weekly bar/line when the probe has populated it — a no-op when absent. Closes #1. - config.example + README documented; tests for normalize/applyUsageData/prober. Default stays passive (quotaProbeSeconds 0). The probe is the one sanctioned active-upstream feature, gated behind explicit opt-in. 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.
Summary
s/a/r/Rkeys all require a TTY. This adds three out-of-band control surfaces.POST /teamclaude/switch(pin account by name),POST /teamclaude/threshold(change rotation cutoff 0..1, persisted via existingatomicConfigUpdate),GET /ui(dashboard SPA served on same port).teamclaude switch <name>andteamclaude threshold <value>— value accepts"85","85%", or"0.85".src/web/index.html, polls/teamclaude/statusevery 2s, per-account 5h/7d quota bars, click any non-active card to pin, threshold slider live-updates rotation cutoff. UsescreateElement+textContentthroughout (noinnerHTML) so API-returned account names can't inject markup. Dark/light themes viaprefers-color-scheme.Why
I'm running teamclaude as a systemd
--userservice that has no TTY, so the TUI's keyboard controls aren't reachable. Rather than spawning a separate TUI client against the daemon, the proxy itself now exposes manual-control endpoints on its existing port. Same loopback auth-skip rule as/teamclaude/status— no additional listener, no new auth model.Test plan
curl -s http://localhost:3456/teamclaude/statusstill returns full status JSONcurl http://localhost:3456/uireturns 200 + HTMLteamclaude switch claude2flipscurrentAccountteamclaude threshold 85setsswitchThreshold: 0.85and persists to~/.config/teamclaude.jsonchangeand the new value survives a server restartKarpelesLabreview (this PR is on the fork for review only — not intended to land upstream as-is unless wanted)🤖 Generated with Claude Code