From 6a69f88b22394efd7c3aa35267985e5b0fba4a88 Mon Sep 17 00:00:00 2001 From: ctt062 Date: Tue, 28 Jul 2026 11:06:41 +0800 Subject: [PATCH 1/6] Improve Agent Deck for daily use, usage resets, and LAN phone access Bring the feature branch onto cleaned main after reverting mistaken commits, keeping per-provider usage resets, This month range, Local Mac LAN/phone mode, and related collector/UI hardening. --- .github/workflows/ci.yml | 20 ++ README.md | 84 +++++-- docs/Screenshot.png | Bin 0 -> 308665 bytes index.html | 7 +- package-lock.json | 100 +------- package.json | 10 +- public/manifest.webmanifest | 17 ++ scripts/install-launchagent.sh | 77 ++++++ scripts/uninstall-launchagent.sh | 15 ++ server/collectors/claude.ts | 79 ++++++- server/collectors/codex.ts | 78 ++++-- server/collectors/cursor.ts | 65 ++++- server/collectors/github.ts | 49 +++- server/collectors/usageResets.ts | 391 +++++++++++++++++++++++++++++++ server/index.ts | 171 +++++++++++--- server/lib/agents.ts | 191 +++++++++++++++ server/lib/lan-intent.test.ts | 49 ++++ server/lib/range.test.ts | 35 +++ server/lib/range.ts | 52 ++++ server/types.ts | 47 +++- src/App.tsx | 110 +++++++-- src/components/AgentPanel.tsx | 368 +++++++++++++++++++++++++++-- src/components/GithubPanel.tsx | 15 +- src/index.css | 4 + src/lib/types.test.ts | 17 ++ src/lib/types.ts | 56 ++++- tsconfig.app.json | 3 +- vite.config.ts | 5 +- 28 files changed, 1869 insertions(+), 246 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/Screenshot.png create mode 100644 public/manifest.webmanifest create mode 100755 scripts/install-launchagent.sh create mode 100755 scripts/uninstall-launchagent.sh create mode 100644 server/collectors/usageResets.ts create mode 100644 server/lib/agents.ts create mode 100644 server/lib/lan-intent.test.ts create mode 100644 server/lib/range.test.ts create mode 100644 server/lib/range.ts create mode 100644 src/lib/types.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..79165c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run build diff --git a/README.md b/README.md index 761ad01..05f0136 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Agent Deck -![Agent Deck screenshot](./src/assets/Screenshot.png) +![Agent Deck screenshot](./docs/Screenshot.png) Local macOS dashboard for AI agent usage (Cursor, Claude Code, Codex), Mac resource meters, and your GitHub contribution calendar. -Runs entirely on your machine. Nothing is uploaded. +Runs entirely on your machine. Usage-reset lookups send your local Cursor, Codex, and Claude auth tokens from this Mac only to those vendors' usage APIs (api2.cursor.sh, auth.openai.com / chatgpt.com, api.anthropic.com) - not to Agent Deck or any other service. ![Agent Deck](https://img.shields.io/badge/platform-macOS-black) ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) -## Why localhost (not a public web host) +## Why localhost (not Vercel / public web) Your machine is the source of truth: @@ -17,7 +17,7 @@ Your machine is the source of truth: - **CPU / GPU / Memory** only exist on this Mac - **GitHub** calendar is fetched with your local `gh` auth -A remote host cannot see those safely. Keep the app on `127.0.0.1`. This GitHub repo is only the source code. +A remote host like Vercel cannot see those safely. This GitHub repo is only the source code. For phone viewing, keep the API on your Mac and open it over your LAN (below) - do not deploy the collectors to a public cloud. ## Download @@ -43,15 +43,50 @@ npm run dev Open **http://127.0.0.1:5174** -### Production-style start (optional) +### One-command production serve + +Builds the UI and serves API + static UI from one localhost port: ```bash npm install -npm run build -npm start +npm run serve +``` + +Open **http://127.0.0.1:3847** + +`npm start` alone also works after `npm run build` (serves `dist/` when present). + +## View on your phone (same Wi-Fi) + +Agent Deck is a local web app. Your Mac still runs the API; your phone just opens it in Safari/Chrome. + +```bash +npm run serve:lan +``` + +The terminal prints a LAN URL like `http://192.168.x.x:3847`. Open that on your phone (same Wi-Fi). On iOS you can use **Share → Add to Home Screen** for an app-like icon. + +Dev equivalent: + +```bash +npm run dev:lan +``` + +Then open the printed Vite URL (port `5174`) on your phone. + +Only do this on a trusted network - LAN mode exposes local agent + Mac metrics to devices on that Wi-Fi. + +## Start at login (macOS) + +```bash +npm run launchagent:install ``` -`npm start` serves the API on `http://127.0.0.1:3847`. For the UI, use `npm run preview` (or keep using `npm run dev`). +This installs a LaunchAgent that keeps Agent Deck running on `http://127.0.0.1:3847` and opens it once. Remove with: + +```bash +npm run launchagent:uninstall +``` ## Requirements @@ -63,45 +98,56 @@ npm start - Claude Code logs under `~/.claude/projects/` - Codex sessions under `~/.codex/sessions/` -Missing collectors degrade gracefully - panels show empty or partial data instead of crashing. +Missing collectors degrade gracefully - each panel shows a short hint instead of crashing. + +## Features + +- **Date range**: Today / 7 days / 30 days / This month for agent share and charts +- **Usage resets**: Per-provider token/limit reset times (Cursor billing cycle via local dashboard API, Codex ChatGPT wham/usage windows, Claude rolling 5h/weekly with `/usage` guidance when exact times are unavailable) +- **Detailed agent stats**: period total, avg/day, active days, peak day, acceptance rate (Cursor), input/output tokens +- **Dual-series charts** plus a cross-agent comparison chart +- **Local web app**: `npm run serve` on loopback, or `npm run serve:lan` for phone access on the same Wi-Fi +- **Cached collectors** (~10s TTL) with parallel collection; usage-reset lookups cache separately (~3 min). Refresh bypasses the collector cache ## Stack - Vite + React + TypeScript UI -- Express API on port `3847` (localhost only) +- Express API on port `3847` (Local Mac; also serves `dist/` after build) - Collectors read local files / `top` / `ioreg` / `gh api` ## What the percentages mean -Agent % is **relative share** of a local activity score across Cursor, Claude Code, and Codex on this Mac - not a vendor billing percentage. +Agent % is **relative share** of a local activity score across Cursor, Claude Code, and Codex for the selected date range - not a vendor billing percentage. | Agent | Primary signal | |-------|----------------| -| Cursor | Accepted AI lines (`aiCodeTracking.dailyStats`) + chat volume | +| Cursor | Accepted AI lines (`aiCodeTracking.dailyStats`) | | Claude Code | Tokens from `~/.claude/projects/**/*.jsonl`, else message volume | | Codex | Tokens from `~/.codex/sessions/**/*.jsonl`, else event volume | ## API -Bound to `127.0.0.1` only: +Default bind is `127.0.0.1`. Use `HOST=0.0.0.0` (or `npm run serve:lan`) for LAN/phone access. | Endpoint | Description | |----------|-------------| -| `GET /api/dashboard` | Full payload (agents + system + GitHub) | +| `GET /api/dashboard?range=1d\|7d\|30d\|month` | Full payload (agents + system + GitHub). Add `refresh=1` to bypass cache. | | `GET /api/system` | Mac snapshot only | | `GET /api/health` | Liveness check | -Override the API port with `PORT` if needed: +Override host/port if needed: ```bash -PORT=4000 npm run dev:api +HOST=0.0.0.0 PORT=4000 npm start ``` ## Privacy -- Nothing is uploaded by this app -- Do not expose port `3847` beyond localhost -- Stats come from files and tools already on your Mac +- Dashboard stats come from files and tools already on your Mac +- Usage-reset times use local Cursor / Codex / Claude credentials on this machine only to call those vendors' usage APIs; tokens are not sent to Agent Deck or any other service +- Default bind is localhost; `serve:lan` exposes the dashboard on your LAN - use only on trusted Wi-Fi +- The API has no open CORS; the Vite proxy and same-origin `serve` are enough for the UI +- Do not deploy this app to Vercel/public cloud - collectors require your Mac ## License diff --git a/docs/Screenshot.png b/docs/Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..53f5a1fbd257dab17f1a6da2126cb7cc7b6a0cad GIT binary patch literal 308665 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%0AIlI6ZfU2O9GR>-M3l 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 - + + + + + + Agent Deck diff --git a/package-lock.json b/package-lock.json index dc6ace2..a1c1ff7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,21 @@ { "name": "agent-dashboard", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-dashboard", - "version": "0.0.0", + "version": "0.1.0", + "license": "MIT", "dependencies": { "concurrently": "^10.0.4", - "cors": "^2.8.6", "express": "^5.2.1", "react": "^19.2.7", "react-dom": "^19.2.7", "recharts": "^3.10.1" }, "devDependencies": { - "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^24.13.3", "@types/react": "^19.2.17", @@ -26,6 +25,9 @@ "tsx": "^4.23.1", "typescript": "~6.0.2", "vite": "^8.1.1" + }, + "engines": { + "node": ">=22" } }, "node_modules/@emnapi/core": { @@ -660,9 +662,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -680,9 +679,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -700,9 +696,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -720,9 +713,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -740,9 +730,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -760,9 +747,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -780,9 +764,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -800,9 +781,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -999,9 +977,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1019,9 +994,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1039,9 +1011,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1059,9 +1028,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1079,9 +1045,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1099,9 +1062,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1232,16 +1192,6 @@ "@types/node": "*" } }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -1645,23 +1595,6 @@ "node": ">=6.6.0" } }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2417,9 +2350,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2441,9 +2371,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2465,9 +2392,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2489,9 +2413,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2640,15 +2561,6 @@ "node": ">= 0.6" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index fa58d1a..1d071ef 100644 --- a/package.json +++ b/package.json @@ -30,21 +30,25 @@ "dev": "concurrently -k -n api,web -c gray.bold,white.bold \"npm:dev:api\" \"npm:dev:web\"", "dev:api": "tsx watch server/index.ts", "dev:web": "vite", + "dev:lan": "concurrently -k -n api,web -c gray.bold,white.bold \"HOST=0.0.0.0 npm run dev:api\" \"VITE_HOST=0.0.0.0 npm run dev:web\"", "build": "tsc -b && vite build", "start": "tsx server/index.ts", + "serve": "npm run build && npm start", + "serve:lan": "npm run build && HOST=0.0.0.0 npm start", "preview": "vite preview", - "lint": "oxlint" + "lint": "oxlint", + "test": "node --test server/lib/range.test.ts server/lib/lan-intent.test.ts src/lib/types.test.ts", + "launchagent:install": "bash scripts/install-launchagent.sh", + "launchagent:uninstall": "bash scripts/uninstall-launchagent.sh" }, "dependencies": { "concurrently": "^10.0.4", - "cors": "^2.8.6", "express": "^5.2.1", "react": "^19.2.7", "react-dom": "^19.2.7", "recharts": "^3.10.1" }, "devDependencies": { - "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^24.13.3", "@types/react": "^19.2.17", diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..7188593 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Agent Deck", + "short_name": "Agent Deck", + "description": "Local macOS AI agent dashboard", + "start_url": "/", + "display": "standalone", + "background_color": "#050505", + "theme_color": "#050505", + "icons": [ + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +} diff --git a/scripts/install-launchagent.sh b/scripts/install-launchagent.sh new file mode 100755 index 0000000..82bdd63 --- /dev/null +++ b/scripts/install-launchagent.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Install a LaunchAgent that starts Agent Deck at login and opens the UI. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +LABEL="com.ctt062.agent-deck" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG_DIR="$HOME/Library/Logs/agent-deck" +NODE_BIN="$(command -v node)" +NPM_BIN="$(command -v npm)" +OPEN_BIN="$(command -v open)" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "LaunchAgent install is macOS-only." >&2 + exit 1 +fi + +if [[ -z "$NODE_BIN" || -z "$NPM_BIN" ]]; then + echo "node and npm must be on PATH." >&2 + exit 1 +fi + +mkdir -p "$HOME/Library/LaunchAgents" "$LOG_DIR" + +# Prefer a built UI+API serve; build once if dist is missing. +if [[ ! -d "$ROOT/dist" ]]; then + echo "Building UI (one-time)…" + (cd "$ROOT" && "$NPM_BIN" run build) +fi + +NODE_DIR="$(dirname "$NODE_BIN")" +NPM_DIR="$(dirname "$NPM_BIN")" +LAUNCH_PATH="${NODE_DIR}:${NPM_DIR}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin" + +cat >"$PLIST" < + + + + Label + ${LABEL} + WorkingDirectory + ${ROOT} + ProgramArguments + + ${NPM_BIN} + start + + RunAtLoad + + KeepAlive + + StandardOutPath + ${LOG_DIR}/stdout.log + StandardErrorPath + ${LOG_DIR}/stderr.log + EnvironmentVariables + + PATH + ${LAUNCH_PATH} + + + +EOF + +launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true +launchctl bootstrap "gui/$(id -u)" "$PLIST" +launchctl enable "gui/$(id -u)/${LABEL}" 2>/dev/null || true + +# Give the server a moment, then open the UI. +sleep 2 +"$OPEN_BIN" "http://127.0.0.1:3847" >/dev/null 2>&1 || true + +echo "Installed ${PLIST}" +echo "Agent Deck starts at login and stays running on http://127.0.0.1:3847" +echo "Logs: ${LOG_DIR}" +echo "Uninstall: npm run launchagent:uninstall" diff --git a/scripts/uninstall-launchagent.sh b/scripts/uninstall-launchagent.sh new file mode 100755 index 0000000..688611e --- /dev/null +++ b/scripts/uninstall-launchagent.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Remove the Agent Deck LaunchAgent. +set -euo pipefail + +LABEL="com.ctt062.agent-deck" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "LaunchAgent uninstall is macOS-only." >&2 + exit 1 +fi + +launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true +rm -f "$PLIST" +echo "Removed LaunchAgent ${LABEL}" diff --git a/server/collectors/claude.ts b/server/collectors/claude.ts index 94f38ef..05ea217 100644 --- a/server/collectors/claude.ts +++ b/server/collectors/claude.ts @@ -2,10 +2,15 @@ import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' import { createInterface } from 'node:readline' +import { buildStats } from '../lib/agents.js' +import { localDateFromTimestamp } from '../lib/range.js' import type { AgentUsage, DailyPoint } from '../types.js' const PROJECTS = join(homedir(), '.claude', 'projects') +const HINT = + 'Run Claude Code so session JSONL logs appear under ~/.claude/projects/.' + function walkJsonl(dir: string, out: string[] = []): string[] { if (!existsSync(dir)) return out for (const name of readdirSync(dir)) { @@ -20,13 +25,35 @@ function walkJsonl(dir: string, out: string[] = []): string[] { export async function collectClaude(): Promise { const byDay = new Map< string, - { tokens: number; messages: number; input: number; output: number } + { + tokens: number + messages: number + input: number + output: number + cacheTokens: number + } >() let sessions = 0 let messages = 0 let inputTokens = 0 let outputTokens = 0 + let cacheTokens = 0 let note: string | undefined + const rootExists = existsSync(PROJECTS) + + if (!rootExists) { + return { + id: 'claude', + name: 'Claude Code', + score: 0, + available: false, + metrics: {}, + daily: [], + stats: buildStats([], 30), + note: 'Claude Code projects folder not found.', + hint: HINT, + } + } try { const files = walkJsonl(PROJECTS) @@ -44,7 +71,10 @@ export async function collectClaude(): Promise { } catch { continue } - const ts = typeof o.timestamp === 'string' ? o.timestamp.slice(0, 10) : null + const ts = + typeof o.timestamp === 'string' + ? localDateFromTimestamp(o.timestamp) + : null const type = o.type if (type === 'user' || type === 'assistant') { messages += 1 @@ -54,6 +84,7 @@ export async function collectClaude(): Promise { messages: 0, input: 0, output: 0, + cacheTokens: 0, } cur.messages += 1 byDay.set(ts, cur) @@ -74,16 +105,19 @@ export async function collectClaude(): Promise { const total = inn + outn + cache inputTokens += inn outputTokens += outn + cacheTokens += cache if (ts && total > 0) { const cur = byDay.get(ts) ?? { tokens: 0, messages: 0, input: 0, output: 0, + cacheTokens: 0, } cur.tokens += total cur.input += inn cur.output += outn + cur.cacheTokens += cache byDay.set(ts, cur) } } @@ -92,35 +126,54 @@ export async function collectClaude(): Promise { note = err instanceof Error ? err.message : String(err) } + const totalTokens = inputTokens + outputTokens + cacheTokens const daily: DailyPoint[] = [...byDay.entries()] .sort(([a], [b]) => a.localeCompare(b)) - .slice(-60) - .map(([date, v]) => ({ - date, - value: v.tokens > 0 ? v.tokens : v.messages, - label: v.tokens > 0 ? 'tokens' : 'messages', - })) + .slice(-90) + .map(([date, v]) => { + const useTokens = v.tokens > 0 + return { + date, + primary: useTokens ? v.tokens : v.messages, + secondary: useTokens ? v.output : undefined, + primaryLabel: useTokens ? 'tokens' : 'messages', + secondaryLabel: useTokens ? 'output' : undefined, + extras: { + tokens: v.tokens, + messages: v.messages, + input: v.input, + output: v.output, + cacheTokens: v.cacheTokens, + }, + } + }) - const totalTokens = inputTokens + outputTokens - // Prefer tokens; fall back to message volume when token logs are empty + const stats = buildStats(daily, 30) const score = totalTokens > 0 ? totalTokens : messages * 800 + sessions * 2000 + const available = !note && (sessions > 0 || messages > 0) return { id: 'claude', name: 'Claude Code', score, + available, metrics: { sessions, messages, inputTokens, outputTokens, + cacheTokens, totalTokens, }, daily, + stats, note: note ?? - (totalTokens === 0 && messages > 0 - ? 'Local sessions found; token fields mostly empty (billing/API gaps). Score uses message volume.' - : undefined), + (!available + ? 'No Claude Code session logs yet.' + : totalTokens === 0 && messages > 0 + ? 'Sessions found; token fields mostly empty. Score uses message volume.' + : undefined), + hint: !available && !note ? HINT : undefined, } } diff --git a/server/collectors/codex.ts b/server/collectors/codex.ts index a1f8a35..96cb514 100644 --- a/server/collectors/codex.ts +++ b/server/collectors/codex.ts @@ -2,10 +2,15 @@ import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' import { createInterface } from 'node:readline' +import { buildStats } from '../lib/agents.js' +import { localDateFromTimestamp } from '../lib/range.js' import type { AgentUsage, DailyPoint } from '../types.js' const SESSIONS = join(homedir(), '.codex', 'sessions') +const HINT = + 'Run Codex so session JSONL rollouts appear under ~/.codex/sessions/.' + function walkJsonl(dir: string, out: string[] = []): string[] { if (!existsSync(dir)) return out for (const name of readdirSync(dir)) { @@ -34,13 +39,28 @@ function pickUsage(obj: unknown): Record | null { export async function collectCodex(): Promise { const byDay = new Map< string, - { tokens: number; events: number } + { tokens: number; events: number; input: number; output: number } >() let sessions = 0 let events = 0 let inputTokens = 0 let outputTokens = 0 let note: string | undefined + const rootExists = existsSync(SESSIONS) + + if (!rootExists) { + return { + id: 'codex', + name: 'Codex', + score: 0, + available: false, + metrics: {}, + daily: [], + stats: buildStats([], 30), + note: 'Codex sessions folder not found.', + hint: HINT, + } + } try { const files = walkJsonl(SESSIONS) @@ -62,10 +82,15 @@ export async function collectCodex(): Promise { events += 1 const ts = typeof o.timestamp === 'string' - ? o.timestamp.slice(0, 10) + ? localDateFromTimestamp(o.timestamp) ?? dayFromName : dayFromName if (ts) { - const cur = byDay.get(ts) ?? { tokens: 0, events: 0 } + const cur = byDay.get(ts) ?? { + tokens: 0, + events: 0, + input: 0, + output: 0, + } cur.events += 1 byDay.set(ts, cur) } @@ -86,8 +111,15 @@ export async function collectCodex(): Promise { inputTokens += Number(inn) || 0 outputTokens += Number(outn) || 0 if (ts && total > 0) { - const cur = byDay.get(ts) ?? { tokens: 0, events: 0 } + const cur = byDay.get(ts) ?? { + tokens: 0, + events: 0, + input: 0, + output: 0, + } cur.tokens += total + cur.input += Number(inn) || 0 + cur.output += Number(outn) || 0 byDay.set(ts, cur) } } @@ -96,23 +128,37 @@ export async function collectCodex(): Promise { note = err instanceof Error ? err.message : String(err) } + const totalTokens = inputTokens + outputTokens const daily: DailyPoint[] = [...byDay.entries()] .sort(([a], [b]) => a.localeCompare(b)) - .slice(-60) - .map(([date, v]) => ({ - date, - value: v.tokens > 0 ? v.tokens : v.events, - label: v.tokens > 0 ? 'tokens' : 'events', - })) + .slice(-90) + .map(([date, v]) => { + const useTokens = v.tokens > 0 + return { + date, + primary: useTokens ? v.tokens : v.events, + secondary: useTokens ? v.output : undefined, + primaryLabel: useTokens ? 'tokens' : 'events', + secondaryLabel: useTokens ? 'output' : undefined, + extras: { + tokens: v.tokens, + events: v.events, + input: v.input, + output: v.output, + }, + } + }) - const totalTokens = inputTokens + outputTokens + const stats = buildStats(daily, 30) const score = totalTokens > 0 ? totalTokens : events * 120 + sessions * 1500 + const available = !note && (sessions > 0 || events > 0) return { id: 'codex', name: 'Codex', score, + available, metrics: { sessions, events, @@ -121,10 +167,14 @@ export async function collectCodex(): Promise { totalTokens, }, daily, + stats, note: note ?? - (totalTokens === 0 && sessions > 0 - ? 'Session logs found; token usage sparse in local rollouts. Score uses event volume.' - : undefined), + (!available + ? 'No Codex session logs yet.' + : totalTokens === 0 && sessions > 0 + ? 'Sessions found; token usage sparse in local rollouts. Score uses event volume.' + : undefined), + hint: !available && !note ? HINT : undefined, } } diff --git a/server/collectors/cursor.ts b/server/collectors/cursor.ts index 35d820a..df095ff 100644 --- a/server/collectors/cursor.ts +++ b/server/collectors/cursor.ts @@ -1,6 +1,8 @@ +import { existsSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' import { DatabaseSync } from 'node:sqlite' +import { buildStats } from '../lib/agents.js' import type { AgentUsage, DailyPoint } from '../types.js' const DB = join( @@ -8,14 +10,34 @@ const DB = join( 'Library/Application Support/Cursor/User/globalStorage/state.vscdb', ) +const INSTALL_HINT = + 'Install Cursor and use Agent / Tab / Chat so AI line stats land in the local SQLite DB.' +const EMPTY_HINT = + 'Use Agent / Tab / Chat so AI line stats land in the local SQLite DB.' + export function collectCursor(): AgentUsage { - const daily: DailyPoint[] = [] + const dailyMap = new Map() let acceptedLines = 0 let suggestedLines = 0 let composers = 0 let bubbles = 0 let costCents = 0 let note: string | undefined + const dbExists = existsSync(DB) + + if (!dbExists) { + return { + id: 'cursor', + name: 'Cursor', + score: 0, + available: false, + metrics: {}, + daily: [], + stats: buildStats([], 30), + note: 'Cursor state database not found.', + hint: INSTALL_HINT, + } + } try { const db = new DatabaseSync(DB, { readOnly: true }) @@ -41,11 +63,10 @@ export function collectCursor(): AgentUsage { (d.composerSuggestedLines ?? 0) + (d.tabSuggestedLines ?? 0) acceptedLines += accepted suggestedLines += suggested - daily.push({ - date, - value: accepted, - label: 'accepted lines', - }) + const cur = dailyMap.get(date) ?? { accepted: 0, suggested: 0 } + cur.accepted += accepted + cur.suggested += suggested + dailyMap.set(date, cur) } catch { /* skip bad row */ } @@ -88,21 +109,45 @@ export function collectCursor(): AgentUsage { note = err instanceof Error ? err.message : String(err) } - // Activity score: accepted AI lines (primary) + chat volume weight + const daily: DailyPoint[] = [...dailyMap.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .slice(-90) + .map(([date, v]) => ({ + date, + primary: v.accepted, + secondary: v.suggested, + primaryLabel: 'accepted', + secondaryLabel: 'suggested', + extras: { accepted: v.accepted, suggested: v.suggested }, + })) + + const stats = buildStats(daily, 30) const score = acceptedLines + composers * 50 + bubbles * 0.05 + const available = !note && (daily.length > 0 || composers > 0 || bubbles > 0) return { id: 'cursor', name: 'Cursor', score, + available, metrics: { acceptedLines, suggestedLines, + acceptanceRate: + suggestedLines > 0 + ? Math.round((acceptedLines / suggestedLines) * 1000) / 10 + : 0, composers, messages: bubbles, - costUsd: costCents / 100, + costUsd: Math.round((costCents / 100) * 100) / 100, }, - daily: daily.slice(-60), - note, + daily, + stats, + note: + note ?? + (daily.length === 0 + ? 'Cursor is installed but AI daily stats are empty yet.' + : undefined), + hint: !available && !note ? EMPTY_HINT : undefined, } } diff --git a/server/collectors/github.ts b/server/collectors/github.ts index 48a9c77..3808a89 100644 --- a/server/collectors/github.ts +++ b/server/collectors/github.ts @@ -1,15 +1,48 @@ import { execFileSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { homedir } from 'node:os' +import { delimiter, join } from 'node:path' import type { GithubSnapshot } from '../types.js' const QUERY = 'query { viewer { login contributionsCollection { contributionCalendar { totalContributions weeks { contributionDays { date contributionCount color } } } } } }' +const INSTALL_HINT = + 'Install GitHub CLI and run `gh auth login`, then refresh. Agent Deck reads your contribution calendar via local gh auth.' + +const AUTH_HINT = + 'Run `gh auth login`, then refresh. Agent Deck reads your contribution calendar via local gh auth.' + +function ghEnv(): NodeJS.ProcessEnv { + const extras = [ + join(homedir(), '.local/bin'), + '/opt/homebrew/bin', + '/usr/local/bin', + ] + const path = [...extras, process.env.PATH ?? ''].filter(Boolean).join(delimiter) + return { ...process.env, PATH: path } +} + +function resolveGh(): string { + if (process.env.GH_PATH && existsSync(process.env.GH_PATH)) { + return process.env.GH_PATH + } + for (const candidate of [ + join(homedir(), '.local/bin/gh'), + '/opt/homebrew/bin/gh', + '/usr/local/bin/gh', + ]) { + if (existsSync(candidate)) return candidate + } + return 'gh' +} + export function collectGithub(): GithubSnapshot { try { const raw = execFileSync( - 'gh', + resolveGh(), ['api', 'graphql', '-f', `query=${QUERY}`], - { encoding: 'utf8', timeout: 20000 }, + { encoding: 'utf8', timeout: 20000, env: ghEnv() }, ) const data = JSON.parse(raw) as { data?: { @@ -35,10 +68,11 @@ export function collectGithub(): GithubSnapshot { if (!viewer) { return { ok: false, - error: 'GitHub GraphQL returned no viewer (check gh auth)', + error: 'GitHub GraphQL returned no viewer (check gh auth).', login: null, totalContributions: 0, days: [], + hint: AUTH_HINT, } } const cal = viewer.contributionsCollection.contributionCalendar @@ -57,12 +91,19 @@ export function collectGithub(): GithubSnapshot { days, } } catch (err) { + const message = err instanceof Error ? err.message : String(err) + const missingGh = + /ENOENT|not found|command not found/i.test(message) || + message.includes('spawn gh') return { ok: false, - error: err instanceof Error ? err.message : String(err), + error: missingGh + ? 'GitHub CLI (`gh`) is not installed or not on PATH.' + : message, login: null, totalContributions: 0, days: [], + hint: missingGh ? INSTALL_HINT : AUTH_HINT, } } } diff --git a/server/collectors/usageResets.ts b/server/collectors/usageResets.ts new file mode 100644 index 0000000..5685867 --- /dev/null +++ b/server/collectors/usageResets.ts @@ -0,0 +1,391 @@ +import { execFileSync } from 'node:child_process' +import { readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs' +import { homedir } from 'node:os' +import { dirname, join } from 'node:path' +import { DatabaseSync } from 'node:sqlite' +import type { UsageReset, UsageResetWindow } from '../types.js' + +const CURSOR_DB = join( + homedir(), + 'Library/Application Support/Cursor/User/globalStorage/state.vscdb', +) +const CODEX_AUTH = join(homedir(), '.codex', 'auth.json') + +function msOrSecToIso(value: unknown): string | null { + const n = typeof value === 'string' ? Number(value) : Number(value) + if (!Number.isFinite(n) || n <= 0) return null + // Cursor sends ms; ChatGPT sends seconds + const ms = n > 1e12 ? n : n * 1000 + return new Date(ms).toISOString() +} + +async function fetchJson( + url: string, + init: RequestInit, +): Promise<{ ok: boolean; status: number; json: unknown; text: string }> { + try { + const res = await fetch(url, { + ...init, + signal: init.signal ?? AbortSignal.timeout(8_000), + }) + const text = await res.text() + let json: unknown = null + try { + json = JSON.parse(text) as unknown + } catch { + json = null + } + return { ok: res.ok, status: res.status, json, text } + } catch (err) { + return { + ok: false, + status: 0, + json: null, + text: err instanceof Error ? err.message : String(err), + } + } +} + +function cursorAccessToken(): string | null { + if (!existsSync(CURSOR_DB)) return null + try { + const db = new DatabaseSync(CURSOR_DB, { readOnly: true }) + const row = db + .prepare(`SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'`) + .get() as { value: string } | undefined + db.close() + return row?.value ?? null + } catch { + return null + } +} + +export async function collectCursorUsageReset(): Promise { + const token = cursorAccessToken() + if (!token) { + return { + ok: false, + windows: [], + error: 'Cursor auth token not found locally.', + } + } + const res = await fetchJson( + 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage', + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: '{}', + }, + ) + if (!res.ok || !res.json || typeof res.json !== 'object') { + return { + ok: false, + windows: [], + error: `Cursor usage API HTTP ${res.status}`, + } + } + const data = res.json as { + billingCycleStart?: string | number + billingCycleEnd?: string | number + planUsage?: { totalPercentUsed?: number; includedSpend?: number; limit?: number } + } + const at = msOrSecToIso(data.billingCycleEnd) + const start = msOrSecToIso(data.billingCycleStart) + const used = data.planUsage?.totalPercentUsed + const windows: UsageResetWindow[] = [ + { + label: 'Billing cycle', + at, + usedPercent: + typeof used === 'number' ? Math.round(used * 10) / 10 : undefined, + note: start + ? `Cycle started ${new Date(start).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + })}` + : 'Monthly Cursor included usage', + }, + ] + return { ok: Boolean(at), windows } +} + +type CodexAuthFile = { + tokens?: { + access_token?: string + refresh_token?: string + id_token?: string + } + last_refresh?: string +} + +async function refreshCodexAccessToken( + refreshToken: string, +): Promise { + const res = await fetchJson('https://auth.openai.com/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + client_id: 'app_EMoamEEZ73f0CkXaXp7hrann', + grant_type: 'refresh_token', + redirect_uri: 'http://localhost:1455/auth/callback', + refresh_token: refreshToken, + }), + }) + if (!res.ok || !res.json || typeof res.json !== 'object') return null + const data = res.json as { + access_token?: string + refresh_token?: string + id_token?: string + } + if (!data.access_token) return null + + // Persist refreshed tokens so subsequent polls stay authenticated. + try { + if (existsSync(CODEX_AUTH)) { + const raw = JSON.parse(readFileSync(CODEX_AUTH, 'utf8')) as CodexAuthFile + raw.tokens = { + ...raw.tokens, + access_token: data.access_token, + refresh_token: data.refresh_token ?? raw.tokens?.refresh_token, + id_token: data.id_token ?? raw.tokens?.id_token, + } + raw.last_refresh = new Date().toISOString() + const tmp = join(dirname(CODEX_AUTH), `.auth.json.${process.pid}.tmp`) + writeFileSync(tmp, `${JSON.stringify(raw, null, 2)}\n`, { + encoding: 'utf8', + mode: 0o600, + }) + renameSync(tmp, CODEX_AUTH) + } + } catch { + /* ignore persist failures */ + } + return data.access_token +} + +function codexAccessToken(): { access: string | null; refresh: string | null } { + if (!existsSync(CODEX_AUTH)) return { access: null, refresh: null } + try { + const raw = JSON.parse(readFileSync(CODEX_AUTH, 'utf8')) as CodexAuthFile + return { + access: raw.tokens?.access_token ?? null, + refresh: raw.tokens?.refresh_token ?? null, + } + } catch { + return { access: null, refresh: null } + } +} + +function windowFromCodex(raw: unknown, fallbackLabel: string): UsageResetWindow | null { + if (!raw || typeof raw !== 'object') return null + const w = raw as { + used_percent?: number + limit_window_seconds?: number + reset_after_seconds?: number + reset_at?: number + } + const at = msOrSecToIso(w.reset_at) + const secs = w.limit_window_seconds + let label = fallbackLabel + if (secs === 18_000) label = '5h window' + else if (secs === 604_800) label = 'Weekly' + else if (secs === 2_592_000) label = 'Monthly' + else if (typeof secs === 'number' && secs > 0) { + const h = Math.round(secs / 3600) + label = h >= 24 ? `${Math.round(h / 24)}d window` : `${h}h window` + } + return { + label, + at, + usedPercent: + typeof w.used_percent === 'number' + ? Math.round(w.used_percent * 10) / 10 + : undefined, + note: + typeof w.reset_after_seconds === 'number' + ? `Resets in ~${Math.max(0, Math.round(w.reset_after_seconds / 3600))}h` + : undefined, + } +} + +export async function collectCodexUsageReset(): Promise { + let { access, refresh } = codexAccessToken() + if (!access && refresh) { + access = await refreshCodexAccessToken(refresh) + } + if (!access) { + return { + ok: false, + windows: [], + error: 'Codex/ChatGPT auth not found. Run `codex login`.', + } + } + + let res = await fetchJson('https://chatgpt.com/backend-api/wham/usage', { + headers: { + Authorization: `Bearer ${access}`, + Accept: 'application/json', + }, + }) + if ((!res.ok || res.status === 401) && refresh) { + access = await refreshCodexAccessToken(refresh) + if (access) { + res = await fetchJson('https://chatgpt.com/backend-api/wham/usage', { + headers: { + Authorization: `Bearer ${access}`, + Accept: 'application/json', + }, + }) + } + } + if (!res.ok || !res.json || typeof res.json !== 'object') { + return { + ok: false, + windows: [], + error: `Codex usage API HTTP ${res.status}`, + } + } + const data = res.json as { + plan_type?: string + rate_limit?: { + primary_window?: unknown + secondary_window?: unknown + } + } + const windows: UsageResetWindow[] = [] + const primary = windowFromCodex(data.rate_limit?.primary_window, 'Primary') + const secondary = windowFromCodex( + data.rate_limit?.secondary_window, + 'Secondary', + ) + if (primary) windows.push(primary) + if (secondary) windows.push(secondary) + if (windows.length === 0) { + return { + ok: false, + windows: [], + error: 'Codex usage payload had no reset windows.', + } + } + if (data.plan_type) { + windows[0] = { + ...windows[0], + note: [windows[0].note, `Plan: ${data.plan_type}`] + .filter(Boolean) + .join(' · '), + } + } + return { ok: true, windows } +} + +function claudeOAuthAccessToken(): string | null { + try { + const out = execFileSync( + 'security', + ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], + { encoding: 'utf8', timeout: 3000 }, + ).trim() + if (!out) return null + const parsed = JSON.parse(out) as { + claudeAiOauth?: { accessToken?: string } + } + return parsed.claudeAiOauth?.accessToken ?? null + } catch { + return null + } +} + +function claudeWindowUsedPercent(block: { + utilization?: number + utilized?: number + usage?: number +}): number | undefined { + if (typeof block.utilization === 'number') { + return Math.round(block.utilization * 10) / 10 + } + if (typeof block.utilized === 'number') { + return Math.round(block.utilized * 10) / 10 + } + if (typeof block.usage === 'number') { + return Math.round(block.usage * 10) / 10 + } + return undefined +} + +export async function collectClaudeUsageReset(): Promise { + // Prefer Claude Code / Anthropic OAuth-style usage when available. + const accessToken = claudeOAuthAccessToken() + if (accessToken) { + const res = await fetchJson('https://api.anthropic.com/api/oauth/usage', { + headers: { + Authorization: `Bearer ${accessToken}`, + 'anthropic-beta': 'oauth-2025-04-20', + 'anthropic-version': '2023-06-01', + 'User-Agent': 'claude-code/2.1.212', + Accept: 'application/json', + }, + }) + if (res.ok && res.json && typeof res.json === 'object') { + const data = res.json as Record + const windows: UsageResetWindow[] = [] + for (const [label, keyName] of [ + ['5h session', 'five_hour'], + ['Weekly', 'seven_day'], + ['Weekly', 'weekly'], + ] as const) { + const block = data[keyName] + if (block && typeof block === 'object') { + const b = block as { + resets_at?: string + reset_at?: string + utilization?: number + utilized?: number + usage?: number + } + windows.push({ + label, + at: b.resets_at ?? b.reset_at ?? null, + usedPercent: claudeWindowUsedPercent(b), + }) + } + } + if (windows.length > 0) return { ok: true, windows } + } + } + + // Claude Code Pro/Max limits are rolling windows; exact times live in `/usage`. + return { + ok: true, + windows: [ + { + label: '5h session', + at: null, + note: 'Rolling window · run `/usage` in Claude Code for exact time', + }, + { + label: 'Weekly', + at: null, + note: 'Rolling 7-day cap · not calendar Monday', + }, + ], + } +} + +export async function collectUsageResets(): Promise<{ + cursor: UsageReset + claude: UsageReset + codex: UsageReset +}> { + const [cursor, claude, codex] = await Promise.all([ + collectCursorUsageReset(), + collectClaudeUsageReset(), + collectCodexUsageReset(), + ]) + return { cursor, claude, codex } +} diff --git a/server/index.ts b/server/index.ts index 44400c2..74b2602 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,52 +1,136 @@ -import cors from 'cors' import express from 'express' +import { existsSync } from 'node:fs' +import { networkInterfaces } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import { collectClaude } from './collectors/claude.js' import { collectCodex } from './collectors/codex.js' import { collectCursor } from './collectors/cursor.js' import { collectGithub } from './collectors/github.js' import { collectSystem } from './collectors/system.js' -import type { AgentShare, DashboardPayload } from './types.js' +import { collectUsageResets } from './collectors/usageResets.js' +import { applyRange, withShares } from './lib/agents.js' +import { parseRange, rangeStartDate } from './lib/range.js' +import type { DashboardPayload, RawCollectors, UsageReset } from './types.js' const PORT = Number(process.env.PORT ?? 3847) +/** Default loopback. Set HOST=0.0.0.0 to reach from phone on the same LAN. */ +const HOST = process.env.HOST ?? '127.0.0.1' +const CACHE_TTL_MS = Number(process.env.CACHE_TTL_MS ?? 10_000) +const USAGE_RESETS_TTL_MS = Number(process.env.USAGE_RESETS_TTL_MS ?? 180_000) +const __dirname = dirname(fileURLToPath(import.meta.url)) +const DIST = join(__dirname, '../dist') -function withShares( - agents: Awaited>[], -): AgentShare[] { - const total = agents.reduce((s, a) => s + Math.max(0, a.score), 0) - return agents.map((a) => ({ - ...a, - percent: - total > 0 - ? Math.round((Math.max(0, a.score) / total) * 1000) / 10 - : 0, - })) +type UsageResetsPayload = { + cursor: UsageReset + claude: UsageReset + codex: UsageReset } -async function buildPayload(): Promise { - const [claude, codex] = await Promise.all([ - collectClaude(), - collectCodex(), - ]) - const cursor = collectCursor() - const agents = withShares([cursor, claude, codex]) +let cache: { at: number; data: RawCollectors } | null = null +let inflight: Promise | null = null +let usageResetsCache: { at: number; data: UsageResetsPayload } | null = null +let usageResetsInflight: Promise | null = null + +function lanUrls(port: number): string[] { + const out: string[] = [] + const nets = networkInterfaces() + for (const entries of Object.values(nets)) { + for (const net of entries ?? []) { + if (net.family === 'IPv4' && !net.internal) { + out.push(`http://${net.address}:${port}`) + } + } + } + return out +} + +async function getUsageResets(): Promise { + if ( + usageResetsCache && + Date.now() - usageResetsCache.at < USAGE_RESETS_TTL_MS + ) { + return usageResetsCache.data + } + if (!usageResetsInflight) { + usageResetsInflight = collectUsageResets() + .then((data) => { + usageResetsCache = { at: Date.now(), data } + return data + }) + .finally(() => { + usageResetsInflight = null + }) + } + return usageResetsInflight +} + +async function collectRaw(force = false): Promise<{ data: RawCollectors; cached: boolean }> { + if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) { + return { data: cache.data, cached: true } + } + if (!inflight) { + inflight = (async () => { + const [cursor, claude, codex, system, github, resets] = await Promise.all([ + Promise.resolve().then(() => collectCursor()), + collectClaude(), + collectCodex(), + Promise.resolve().then(() => collectSystem()), + Promise.resolve().then(() => collectGithub()), + getUsageResets(), + ]) + cursor.usageReset = resets.cursor + claude.usageReset = resets.claude + codex.usageReset = resets.codex + const data: RawCollectors = { cursor, claude, codex, system, github } + cache = { at: Date.now(), data } + return data + })().finally(() => { + inflight = null + }) + } + const data = await inflight + return { data, cached: false } +} + +async function buildPayload( + rangeParam: unknown, + force = false, +): Promise { + const range = parseRange(rangeParam) + const since = rangeStartDate(range) + const { data, cached } = await collectRaw(force) + const agents = withShares( + [data.cursor, data.claude, data.codex].map((a) => + applyRange(a, range, since), + ), + ) return { generatedAt: new Date().toISOString(), + range, + cached, agents, - system: collectSystem(), - github: collectGithub(), + system: data.system, + github: data.github, } } const app = express() -app.use(cors()) app.get('/api/health', (_req, res) => { - res.json({ ok: true }) + res.json({ + ok: true, + dist: existsSync(DIST), + host: HOST, + cacheTtlMs: CACHE_TTL_MS, + usageResetsTtlMs: USAGE_RESETS_TTL_MS, + }) }) -app.get('/api/dashboard', async (_req, res) => { +app.get('/api/dashboard', async (req, res) => { try { - const payload = await buildPayload() + const force = req.query.refresh === '1' || req.query.refresh === 'true' + const payload = await buildPayload(req.query.range, force) res.json(payload) } catch (err) { res.status(500).json({ @@ -59,6 +143,37 @@ app.get('/api/system', (_req, res) => { res.json(collectSystem()) }) -app.listen(PORT, '127.0.0.1', () => { - console.log(`agent-dashboard API on http://127.0.0.1:${PORT}`) +if (existsSync(DIST)) { + app.use(express.static(DIST)) + app.get('/{*path}', (req, res, next) => { + if (req.path.startsWith('/api')) return next() + res.sendFile(join(DIST, 'index.html')) + }) +} + +app.listen(PORT, HOST, () => { + const servingUi = existsSync(DIST) + const mode = servingUi ? 'API + UI' : 'API only' + console.log(`agent-dashboard ${mode} on http://${HOST === '0.0.0.0' ? '127.0.0.1' : HOST}:${PORT}`) + if (HOST === '0.0.0.0' || HOST === '::') { + const urls = lanUrls(PORT) + if (urls.length > 0) { + if (servingUi) { + console.log('Phone / LAN (same Wi-Fi):') + for (const url of urls) console.log(` ${url}`) + } else { + console.log('API only (LAN, same Wi-Fi):') + for (const url of urls) console.log(` ${url}`) + console.log( + 'Phone UI: open the Vite network URL on port 5174 (from `npm run dev:lan`), not these API URLs.', + ) + } + } else { + console.log('LAN bind enabled, but no non-loopback IPv4 address was found.') + } + console.log('Only use on a trusted network. This exposes local agent + Mac metrics.') + } + if (!servingUi) { + console.log('Tip: run `npm run build` (or `npm run serve`) to serve the UI from this port.') + } }) diff --git a/server/lib/agents.ts b/server/lib/agents.ts new file mode 100644 index 0000000..22e3659 --- /dev/null +++ b/server/lib/agents.ts @@ -0,0 +1,191 @@ +import type { AgentShare, AgentStats, AgentUsage, DailyPoint } from '../types.js' +import { daysInRange, type DateRange } from './range.js' + +function emptyStats(): AgentStats { + return { + activeDays: 0, + avgPerDay: 0, + peakDay: null, + peakValue: 0, + periodTotal: 0, + } +} + +export function buildStats(daily: DailyPoint[], rangeDays: number): AgentStats { + if (daily.length === 0) return emptyStats() + let periodTotal = 0 + let activeDays = 0 + let peakDay: string | null = null + let peakValue = 0 + for (const d of daily) { + periodTotal += d.primary + if (d.primary > 0) activeDays += 1 + if (d.primary > peakValue) { + peakValue = d.primary + peakDay = d.date + } + } + return { + activeDays, + avgPerDay: Math.round((periodTotal / Math.max(rangeDays, 1)) * 10) / 10, + peakDay, + peakValue, + periodTotal, + } +} + +function sumExtra(daily: DailyPoint[], key: string): number { + return daily.reduce((s, d) => s + (d.extras?.[key] ?? 0), 0) +} + +/** + * Period activity score for the filtered window. + * Cursor: accepted AI lines only. Claude/Codex: tokens, else message/event volume. + */ +function computePeriodScore(agent: AgentUsage, daily: DailyPoint[]): number { + if (agent.id === 'cursor') { + const sumPrimary = daily.reduce((s, d) => s + d.primary, 0) + return sumExtra(daily, 'accepted') || sumPrimary + } + + const totalTokens = sumExtra(daily, 'tokens') + if (agent.id === 'claude') { + return totalTokens > 0 ? totalTokens : sumExtra(daily, 'messages') * 800 + } + + return totalTokens > 0 ? totalTokens : sumExtra(daily, 'events') * 120 +} + +function hasInRangeActivity(daily: DailyPoint[]): boolean { + return daily.some( + (d) => + d.primary > 0 || + (d.secondary ?? 0) > 0 || + Object.values(d.extras ?? {}).some((v) => v > 0), + ) +} + +/** Align Claude/Codex day primaries to one unit for the filtered window. */ +function normalizePeriodDaily(agent: AgentUsage, daily: DailyPoint[]): DailyPoint[] { + if (agent.id === 'cursor' || daily.length === 0) return daily + + const useTokens = sumExtra(daily, 'tokens') > 0 + const volumeKey = agent.id === 'claude' ? 'messages' : 'events' + + return daily.map((d) => { + if (useTokens) { + return { + ...d, + primary: d.extras?.tokens ?? 0, + secondary: d.extras?.output, + primaryLabel: 'tokens', + secondaryLabel: 'output', + } + } + return { + ...d, + primary: d.extras?.[volumeKey] ?? 0, + secondary: undefined, + primaryLabel: volumeKey, + secondaryLabel: undefined, + } + }) +} + +function periodMetrics(agent: AgentUsage, daily: DailyPoint[]): Record { + const score = computePeriodScore(agent, daily) + const base = { ...agent.metrics } + + if (agent.id === 'cursor') { + const sumPrimary = daily.reduce((s, d) => s + d.primary, 0) + const accepted = sumExtra(daily, 'accepted') || sumPrimary + const suggested = sumExtra(daily, 'suggested') + return { + ...base, + acceptedLines: accepted, + suggestedLines: suggested, + acceptanceRate: + suggested > 0 ? Math.round((accepted / suggested) * 1000) / 10 : 0, + periodScore: score, + } + } + + if (agent.id === 'claude') { + const inputTokens = sumExtra(daily, 'input') + const outputTokens = sumExtra(daily, 'output') + const totalTokens = sumExtra(daily, 'tokens') + const cacheTokens = sumExtra(daily, 'cacheTokens') + const messages = sumExtra(daily, 'messages') + return { + ...base, + inputTokens, + outputTokens, + totalTokens, + cacheTokens, + messages, + periodScore: score, + } + } + + const inputTokens = sumExtra(daily, 'input') + const outputTokens = sumExtra(daily, 'output') + const totalTokens = sumExtra(daily, 'tokens') + const events = sumExtra(daily, 'events') + return { + ...base, + inputTokens, + outputTokens, + totalTokens, + events, + periodScore: score, + } +} + +/** Filter full-history agent data down to the selected date range and recompute score/stats. */ +export function applyRange(agent: AgentUsage, range: DateRange, since: string): AgentUsage { + if (!agent.available) { + return { + ...agent, + score: 0, + metrics: {}, + daily: [], + stats: emptyStats(), + usageReset: agent.usageReset, + note: agent.note ?? 'No local data found for this agent.', + hint: agent.hint, + } + } + + const filtered = agent.daily.filter((d) => d.date >= since) + const daily = normalizePeriodDaily(agent, filtered) + const stats = buildStats(daily, daysInRange(range)) + const metrics = periodMetrics(agent, daily) + + const note = + agent.note ?? + (!hasInRangeActivity(daily) + ? `No activity in the selected ${ + range === '1d' ? 'day' : range === 'month' ? 'month' : 'period' + }.` + : undefined) + + return { + ...agent, + score: metrics.periodScore, + metrics, + daily, + stats, + usageReset: agent.usageReset, + note, + hint: agent.hint, + } +} + +export function withShares(agents: AgentUsage[]): AgentShare[] { + const total = agents.reduce((s, a) => s + Math.max(0, a.score), 0) + return agents.map((a) => ({ + ...a, + percent: + total > 0 ? Math.round((Math.max(0, a.score) / total) * 1000) / 10 : 0, + })) +} diff --git a/server/lib/lan-intent.test.ts b/server/lib/lan-intent.test.ts new file mode 100644 index 0000000..1d8b345 --- /dev/null +++ b/server/lib/lan-intent.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { describe, it } from 'node:test' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '../..') + +describe('LAN phone webapp intent', () => { + it('exposes serve:lan and dev:lan with HOST=0.0.0.0', () => { + const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { + scripts: Record + } + assert.match(pkg.scripts['serve:lan'], /HOST=0\.0\.0\.0/) + assert.match(pkg.scripts['dev:lan'], /HOST=0\.0\.0\.0/) + assert.match(pkg.scripts['dev:lan'], /VITE_HOST=0\.0\.0\.0/) + }) + + it('ships a standalone web manifest for Add to Home Screen', () => { + const manifest = JSON.parse( + readFileSync(join(root, 'public/manifest.webmanifest'), 'utf8'), + ) as { + name: string + short_name: string + display: string + start_url: string + } + assert.equal(manifest.name, 'Agent Deck') + assert.equal(manifest.short_name, 'Agent Deck') + assert.equal(manifest.display, 'standalone') + assert.equal(manifest.start_url, '/') + }) + + it('wires mobile PWA meta + manifest into index.html', () => { + const html = readFileSync(join(root, 'index.html'), 'utf8') + assert.match(html, /rel="manifest" href="\/manifest\.webmanifest"/) + assert.match(html, /name="apple-mobile-web-app-capable" content="yes"/) + assert.match(html, /viewport-fit=cover/) + }) + + it('documents why Vercel is the wrong host and how to use LAN instead', () => { + const readme = readFileSync(join(root, 'README.md'), 'utf8') + assert.match(readme, /Why localhost \(not Vercel \/ public web\)/) + assert.match(readme, /Do not deploy this app to Vercel/) + assert.match(readme, /npm run serve:lan/) + assert.match(readme, /npm run dev:lan/) + assert.match(readme, /Add to Home Screen/) + }) +}) diff --git a/server/lib/range.test.ts b/server/lib/range.test.ts new file mode 100644 index 0000000..70e96fd --- /dev/null +++ b/server/lib/range.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + DATE_RANGES, + daysInRange, + parseRange, + rangeLabel, + rangeStartDate, +} from './range.ts' + +describe('date range helpers', () => { + it('includes This month alongside Today/7d/30d', () => { + assert.deepEqual(DATE_RANGES, ['1d', '7d', '30d', 'month']) + assert.equal(rangeLabel('1d'), 'Today') + assert.equal(rangeLabel('7d'), '7 days') + assert.equal(rangeLabel('30d'), '30 days') + assert.equal(rangeLabel('month'), 'This month') + }) + + it('parses month and rejects unknown values', () => { + assert.equal(parseRange('month'), 'month') + assert.equal(parseRange('7d'), '7d') + assert.equal(parseRange('nope'), '7d') + assert.equal(parseRange(undefined), '7d') + }) + + it('starts This month on the 1st of the local calendar month', () => { + const now = new Date(2026, 6, 28, 15, 30, 0) // Jul 28, 2026 local + assert.equal(rangeStartDate('month', now), '2026-07-01') + assert.equal(daysInRange('month', now), 28) + assert.equal(rangeStartDate('1d', now), '2026-07-28') + assert.equal(rangeStartDate('7d', now), '2026-07-22') + assert.equal(rangeStartDate('30d', now), '2026-06-29') + }) +}) diff --git a/server/lib/range.ts b/server/lib/range.ts new file mode 100644 index 0000000..506cab0 --- /dev/null +++ b/server/lib/range.ts @@ -0,0 +1,52 @@ +export const DATE_RANGES = ['1d', '7d', '30d', 'month'] as const +export type DateRange = (typeof DATE_RANGES)[number] + +export function parseRange(value: unknown): DateRange { + if (typeof value === 'string' && (DATE_RANGES as readonly string[]).includes(value)) { + return value as DateRange + } + return '7d' +} + +/** Inclusive day count for avg/day math. */ +export function daysInRange(range: DateRange, now = new Date()): number { + if (range === '1d') return 1 + if (range === '7d') return 7 + if (range === '30d') return 30 + // This month: from the 1st through today (inclusive) + return Math.max(1, now.getDate()) +} + +export function formatLocalDate(d: Date): string { + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +/** Local calendar YYYY-MM-DD for an ISO/RFC3339 (or Date-parseable) timestamp. */ +export function localDateFromTimestamp(value: string): string | null { + const d = new Date(value) + if (Number.isNaN(d.getTime())) return null + return formatLocalDate(d) +} + +/** Inclusive start date (YYYY-MM-DD) for the selected range ending today. */ +export function rangeStartDate(range: DateRange, now = new Date()): string { + const d = new Date(now) + d.setHours(0, 0, 0, 0) + if (range === 'month') { + d.setDate(1) + return formatLocalDate(d) + } + const days = daysInRange(range, now) + d.setDate(d.getDate() - (days - 1)) + return formatLocalDate(d) +} + +export function rangeLabel(range: DateRange): string { + if (range === '1d') return 'Today' + if (range === '7d') return '7 days' + if (range === '30d') return '30 days' + return 'This month' +} diff --git a/server/types.ts b/server/types.ts index 56a3667..feefca4 100644 --- a/server/types.ts +++ b/server/types.ts @@ -1,7 +1,36 @@ +import type { DateRange } from './lib/range.js' + export type DailyPoint = { date: string - value: number + primary: number + secondary?: number + primaryLabel: string + secondaryLabel?: string + /** Extra numeric breakdown for tooltips and period rollups */ + extras?: Record +} + +export type AgentStats = { + activeDays: number + avgPerDay: number + peakDay: string | null + peakValue: number + periodTotal: number +} + +/** Provider-specific token/usage limit reset windows. */ +export type UsageResetWindow = { label: string + /** ISO timestamp when this window resets; null if unknown / N/A */ + at: string | null + usedPercent?: number + note?: string +} + +export type UsageReset = { + ok: boolean + windows: UsageResetWindow[] + error?: string } export type AgentUsage = { @@ -9,9 +38,14 @@ export type AgentUsage = { name: string /** Comparable activity score used for percentage share */ score: number + available: boolean metrics: Record daily: DailyPoint[] + stats: AgentStats + usageReset?: UsageReset note?: string + /** How to enable this collector when data is missing */ + hint?: string } export type AgentShare = AgentUsage & { @@ -45,11 +79,22 @@ export type GithubSnapshot = { login: string | null totalContributions: number days: GithubDay[] + hint?: string } export type DashboardPayload = { generatedAt: string + range: DateRange + cached: boolean agents: AgentShare[] system: SystemSnapshot github: GithubSnapshot } + +export type RawCollectors = { + cursor: AgentUsage + claude: AgentUsage + codex: AgentUsage + system: SystemSnapshot + github: GithubSnapshot +} diff --git a/src/App.tsx b/src/App.tsx index c892106..aef4931 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,52 +1,82 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { AgentPanel } from './components/AgentPanel' import { GithubPanel } from './components/GithubPanel' import { SystemPanel } from './components/SystemPanel' -import type { DashboardPayload } from './lib/types' +import type { DashboardPayload, DateRange } from './lib/types' const REFRESH_MS = 15_000 export default function App() { + const [range, setRange] = useState('7d') const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) + const abortRef = useRef(null) + const inFlightRef = useRef(false) - const load = useCallback(async () => { - try { - const res = await fetch('/api/dashboard') - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const json = (await res.json()) as DashboardPayload - setData(json) - setError(null) - } catch (err) { - setError(err instanceof Error ? err.message : String(err)) - } finally { - setLoading(false) - } - }, []) + const load = useCallback( + async (opts?: { refresh?: boolean; mode?: 'replace' | 'poll' }) => { + const mode = opts?.mode ?? 'replace' + + if (mode === 'poll') { + if (inFlightRef.current) return + } else { + abortRef.current?.abort() + } + + const controller = new AbortController() + abortRef.current = controller + inFlightRef.current = true + try { + const params = new URLSearchParams({ range }) + if (opts?.refresh) params.set('refresh', '1') + const res = await fetch(`/api/dashboard?${params}`, { + signal: controller.signal, + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const json = (await res.json()) as DashboardPayload + if (controller.signal.aborted) return + setData(json) + setError(null) + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') return + setError(err instanceof Error ? err.message : String(err)) + } finally { + if (abortRef.current === controller) inFlightRef.current = false + if (!controller.signal.aborted) setLoading(false) + } + }, + [range], + ) useEffect(() => { - void load() - const id = window.setInterval(() => void load(), REFRESH_MS) - return () => window.clearInterval(id) + setLoading(true) + void load({ mode: 'replace' }) + const id = window.setInterval(() => void load({ mode: 'poll' }), REFRESH_MS) + return () => { + window.clearInterval(id) + abortRef.current?.abort() + } }, [load]) return (
-

Localhost only

+

Local Mac

Agent Deck

{data - ? `Updated ${new Date(data.generatedAt).toLocaleTimeString()}` + ? `Updated ${new Date(data.generatedAt).toLocaleTimeString()}${ + data.cached ? ' · cached' : '' + }` : loading ? 'Loading…' : 'Offline'}

-
@@ -55,13 +85,17 @@ export default function App() { {error ? (

API unreachable ({error}). Start with{' '} - npm run dev so both Vite and the local collector are up. + npm run dev or npm run serve.

) : null} {data ? (
- +
@@ -71,18 +105,31 @@ export default function App() {
Runs only on this Mac. Agent % is relative share of local activity - scores (Cursor accepted lines / Claude & Codex tokens or session volume). + scores for the selected date range (Cursor accepted lines / Claude & + Codex tokens or session volume).
diff --git a/src/components/GithubPanel.tsx b/src/components/GithubPanel.tsx index a418a99..b514bbd 100644 --- a/src/components/GithubPanel.tsx +++ b/src/components/GithubPanel.tsx @@ -53,10 +53,15 @@ export function GithubPanel({ github }: Props) { ))}
) : ( -

- Run gh auth login so this machine can read your - contribution calendar. -

+
+

{github.error ?? 'Unavailable'}

+ {github.hint ?

{github.hint}

: ( +

+ Run gh auth login so this machine can read your + contribution calendar. +

+ )} +
)} + + ) +} From d230af784189126d6f38a243b7cf73488191d7a4 Mon Sep 17 00:00:00 2001 From: ctt062 Date: Tue, 28 Jul 2026 11:25:16 +0800 Subject: [PATCH 3/6] no-mistakes(review): Add PIN LAN auth, Vercel UI, review fixes --- .env.example | 9 +- README.md | 50 ++++++-- index.html | 1 + package.json | 2 +- public/runtime-config.js | 3 + scripts/install-launchagent.sh | 37 +++++- server/auth.ts | 214 +++++++++++++++++++++++++++---- server/collectors/usageResets.ts | 33 +++-- server/index.ts | 69 +++++++--- server/lib/auth-mode.test.ts | 69 ++++++++++ server/lib/auth-mode.ts | 77 +++++++++++ server/lib/lan-intent.test.ts | 13 +- src/App.tsx | 17 ++- src/components/Login.tsx | 173 ++++++++++++++++++++----- src/lib/api.ts | 39 ++++++ vercel.json | 3 + 16 files changed, 690 insertions(+), 119 deletions(-) create mode 100644 public/runtime-config.js create mode 100644 server/lib/auth-mode.test.ts create mode 100644 server/lib/auth-mode.ts create mode 100644 src/lib/api.ts create mode 100644 vercel.json diff --git a/.env.example b/.env.example index e85d376..f93677d 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,15 @@ GOOGLE_CLIENT_ID= -# Optional: comma-separated allowlist (lowercase). Empty = any verified Google account. +# Required whenever HOST binds beyond loopback (e.g. 0.0.0.0 for phone/LAN). ALLOWED_EMAILS= +# Required for phone/LAN (raw IP) access. Google GIS cannot use raw IP origins. +DASHBOARD_PIN= +# Google-compatible public UI origin (Vercel static host). Default below. +PUBLIC_ORIGIN=https://agent-dashboard-ctt.vercel.app # Optional: override auto-generated cookie secret in ~/.config/agent-deck/session-secret SESSION_SECRET= # Optional LAN bind for phone access (LaunchAgent / serve:lan also set this) # HOST=0.0.0.0 # PORT=3847 +# UI → Mac API base for a Vercel (or other) static build. Use an HTTPS tunnel URL +# when the UI is on HTTPS; leave empty for same-origin Mac serve. +# VITE_API_BASE=https://your-mac-api.example diff --git a/README.md b/README.md index f51b815..dd18b58 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Runs entirely on your machine. Usage-reset lookups send your local Cursor, Codex ![Agent Deck](https://img.shields.io/badge/platform-macOS-black) ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) -## Why localhost (not Vercel / public web) +## Why the Mac still runs the API Your machine is the source of truth: @@ -17,9 +17,9 @@ Your machine is the source of truth: - **CPU / GPU / Memory** only exist on this Mac - **GitHub** calendar is fetched with your local `gh` auth -A remote host like Vercel cannot see those safely. This GitHub repo is only the source code. For phone viewing, keep the API on your Mac and open it over your LAN (below) - do not deploy the collectors to a public cloud. +Collectors must stay on this Mac. You can host a **static UI** on Vercel (see below) that talks to the Mac API over a tunnel or LAN URL - do not deploy the collectors to a public cloud. -## Recommended setup (auto-start + Google login) +## Recommended setup (auto-start + dual auth) You do **not** need `npm run dev` day to day. That is only for developers hacking on the UI. @@ -32,17 +32,19 @@ npm install cp .env.example .env ``` -2. Create a Google Cloud **OAuth Web client** and put the client ID in `.env`: +2. Configure `.env`: ```bash GOOGLE_CLIENT_ID=your-id.apps.googleusercontent.com ALLOWED_EMAILS=you@gmail.com +DASHBOARD_PIN=your-phone-pin +PUBLIC_ORIGIN=https://agent-dashboard-ctt.vercel.app ``` -Authorized JavaScript origins (Google Cloud Console): +Authorized JavaScript origins (Google Cloud Console) - Google GIS does **not** accept raw LAN IPs: - `http://127.0.0.1:3847` -- optional LAN origin like `http://192.168.x.x:3847` for phone +- `https://agent-dashboard-ctt.vercel.app` (or your `PUBLIC_ORIGIN`) 3. Build + install login auto-start: @@ -50,10 +52,28 @@ Authorized JavaScript origins (Google Cloud Console): npm run setup ``` -After that, Agent Deck starts when you log into your Mac. Opening `http://127.0.0.1:3847` shows **Continue with Google**. Google verifies your email, then the dashboard loads. +`npm run setup` defaults to LAN bind (`HOST=0.0.0.0`) and refuses to install unless `GOOGLE_CLIENT_ID`, `ALLOWED_EMAILS`, and `DASHBOARD_PIN` are set. + +After that, Agent Deck starts when you log into your Mac: + +- Desktop / localhost and the Vercel UI origin use **Google** sign-in (verified email + allowlist). +- Phone on a raw LAN IP uses **PIN** sign-in (`DASHBOARD_PIN`). `npm run dev` is optional (hot reload for coding). Prefer `npm run setup` for normal use. +## Optional: static UI on Vercel + +Public UI: [https://agent-dashboard-ctt.vercel.app/](https://agent-dashboard-ctt.vercel.app/) + +1. Deploy the Vite `dist/` (SPA rewrite is in `vercel.json`). +2. Point the UI at your Mac API with either: + - build-time `VITE_API_BASE=https://your-mac-tunnel.example`, or + - runtime `public/runtime-config.js` → `window.__AGENT_DECK_API_BASE__` +3. Prefer an **HTTPS tunnel** to the Mac when the UI is on HTTPS (browsers block mixed content to bare `http://192.168.x.x`). +4. On the Mac, keep `PUBLIC_ORIGIN=https://agent-dashboard-ctt.vercel.app` so CORS allows that origin + localhost only (no wildcard, no raw LAN origins). + +Collectors and `/api/*` still run on the Mac. + ## Download ### Option A - Clone with Git @@ -93,13 +113,13 @@ Open **http://127.0.0.1:3847** ## View on your phone (same Wi-Fi) -Agent Deck is a local web app. Your Mac still runs the API; your phone just opens it in Safari/Chrome. +Agent Deck is a local web app. Your Mac still runs the API; your phone opens it in Safari/Chrome and signs in with the **PIN** (Google cannot authorize raw IP origins). ```bash npm run serve:lan ``` -The terminal prints a LAN URL like `http://192.168.x.x:3847`. Open that on your phone (same Wi-Fi). On iOS you can use **Share → Add to Home Screen** for an app-like icon. +The terminal prints a LAN URL like `http://192.168.x.x:3847`. Open that on your phone (same Wi-Fi) and enter `DASHBOARD_PIN`. On iOS you can use **Share → Add to Home Screen** for an app-like icon. Dev equivalent: @@ -147,9 +167,9 @@ Missing collectors degrade gracefully - each panel shows a short hint instead of - **Usage resets**: Per-provider token/limit reset times (Cursor billing cycle via local dashboard API, Codex ChatGPT wham/usage windows, Claude rolling 5h/weekly with `/usage` guidance when exact times are unavailable) - **Detailed agent stats**: period total, avg/day, active days, peak day, acceptance rate (Cursor), input/output tokens - **Dual-series charts** plus a cross-agent comparison chart -- **Google sign-in**: verified Gmail/Google account required before the dashboard (optional `ALLOWED_EMAILS` allowlist) +- **Dual auth**: Google on localhost / `PUBLIC_ORIGIN`; PIN on LAN IPs; `ALLOWED_EMAILS` + `DASHBOARD_PIN` required for LAN bind - **Local web app**: `npm run setup` auto-starts at login; `serve:lan` / LaunchAgent for phone on the same Wi-Fi -- **Cached collectors** (~10s TTL) with parallel collection; usage-reset lookups cache separately (~3 min). Refresh bypasses the collector cache +- **Cached collectors** (~10s TTL) with parallel collection; usage-reset lookups cache separately (~3 min). Refresh bypasses both caches ## Stack @@ -176,6 +196,7 @@ Default bind is `127.0.0.1`. Use `HOST=0.0.0.0` (or `npm run serve:lan`) for LAN | `GET /api/dashboard?range=1d\|7d\|30d\|month` | Full payload (agents + system + GitHub). Add `refresh=1` to bypass cache. | | `GET /api/system` | Mac snapshot only | | `GET /api/health` | Liveness check | +| `GET /api/auth/config` | Auth mode for this Host/Origin (`google` or `pin`) | Override host/port if needed: @@ -186,11 +207,12 @@ HOST=0.0.0.0 PORT=4000 npm start ## Privacy - Dashboard stats come from files and tools already on your Mac -- Google sign-in uses Google Identity Services; only verified emails can open the dashboard (optionally restricted by `ALLOWED_EMAILS`) +- Google sign-in uses Google Identity Services on localhost and `PUBLIC_ORIGIN`; LAN IPs use PIN instead +- When LAN bind is enabled, `ALLOWED_EMAILS` and `DASHBOARD_PIN` are required - Usage-reset times use local Cursor / Codex / Claude credentials on this machine only to call those vendors' usage APIs; tokens are not sent to Agent Deck or any other service - Default LaunchAgent bind is LAN-capable (`HOST=0.0.0.0`) - use only on trusted Wi-Fi -- The API has no open CORS; the Vite proxy and same-origin `serve` are enough for the UI -- Do not deploy this app to Vercel/public cloud - collectors require your Mac +- CORS allowlists localhost + `PUBLIC_ORIGIN` only (no wildcard) +- Static UI may live on Vercel; collectors and the API stay on your Mac ## License diff --git a/index.html b/index.html index cdd9f74..4af8db8 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,7 @@
+ diff --git a/package.json b/package.json index b957e0f..5f7aea7 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "serve:lan": "npm run build && HOST=0.0.0.0 npm start", "preview": "vite preview", "lint": "oxlint", - "test": "node --test server/lib/range.test.ts server/lib/lan-intent.test.ts src/lib/types.test.ts", + "test": "node --test server/lib/range.test.ts server/lib/lan-intent.test.ts server/lib/auth-mode.test.ts src/lib/types.test.ts", "setup": "npm run build && npm run launchagent:install", "launchagent:install": "bash scripts/install-launchagent.sh", "launchagent:uninstall": "bash scripts/uninstall-launchagent.sh" diff --git a/public/runtime-config.js b/public/runtime-config.js new file mode 100644 index 0000000..204de5a --- /dev/null +++ b/public/runtime-config.js @@ -0,0 +1,3 @@ +// Optional runtime override for static hosts (Vercel). Set before the app bundle loads. +// Example: window.__AGENT_DECK_API_BASE__ = 'https://your-mac-tunnel.example'; +window.__AGENT_DECK_API_BASE__ = window.__AGENT_DECK_API_BASE__ || ''; diff --git a/scripts/install-launchagent.sh b/scripts/install-launchagent.sh index b08e841..591e1e2 100755 --- a/scripts/install-launchagent.sh +++ b/scripts/install-launchagent.sh @@ -33,6 +33,20 @@ NODE_DIR="$(dirname "$NODE_BIN")" NPM_DIR="$(dirname "$NPM_BIN")" LAUNCH_PATH="${NODE_DIR}:${NPM_DIR}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin" +strip_env_quotes() { + local val="$1" + if [[ "${#val}" -ge 2 ]]; then + local first="${val:0:1}" + local last="${val: -1}" + if [[ "$first" == '"' && "$last" == '"' ]]; then + val="${val:1:${#val}-2}" + elif [[ "$first" == "'" && "$last" == "'" ]]; then + val="${val:1:${#val}-2}" + fi + fi + printf '%s' "$val" +} + # Default: LAN-capable so phone can open the same always-on server. HOST_VALUE="${HOST:-0.0.0.0}" PORT_VALUE="${PORT:-3847}" @@ -44,7 +58,9 @@ if [[ -f "$ENV_FILE" ]]; then [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// }" ]] && continue if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then - export "${BASH_REMATCH[1]}=${BASH_REMATCH[2]}" + key="${BASH_REMATCH[1]}" + val="$(strip_env_quotes "${BASH_REMATCH[2]}")" + export "${key}=${val}" fi done <"$ENV_FILE" set +a @@ -52,6 +68,17 @@ if [[ -f "$ENV_FILE" ]]; then PORT_VALUE="${PORT:-$PORT_VALUE}" fi +if [[ "$HOST_VALUE" == "0.0.0.0" || "$HOST_VALUE" == "::" ]]; then + missing=() + [[ -z "${GOOGLE_CLIENT_ID:-}" ]] && missing+=("GOOGLE_CLIENT_ID") + [[ -z "${ALLOWED_EMAILS:-}" ]] && missing+=("ALLOWED_EMAILS") + [[ -z "${DASHBOARD_PIN:-}" ]] && missing+=("DASHBOARD_PIN") + if [[ "${#missing[@]}" -gt 0 ]]; then + echo "LAN bind (HOST=${HOST_VALUE}) requires ${missing[*]} in ${ENV_FILE}." >&2 + exit 1 + fi +fi + # Build EnvironmentVariables dict entries from .env (safe subset) ENV_XML="" ENV_XML+=" PATH\n ${LAUNCH_PATH}\n" @@ -63,6 +90,12 @@ fi if [[ -n "${ALLOWED_EMAILS:-}" ]]; then ENV_XML+=" ALLOWED_EMAILS\n ${ALLOWED_EMAILS}\n" fi +if [[ -n "${DASHBOARD_PIN:-}" ]]; then + ENV_XML+=" DASHBOARD_PIN\n ${DASHBOARD_PIN}\n" +fi +if [[ -n "${PUBLIC_ORIGIN:-}" ]]; then + ENV_XML+=" PUBLIC_ORIGIN\n ${PUBLIC_ORIGIN}\n" +fi if [[ -n "${SESSION_SECRET:-}" ]]; then ENV_XML+=" SESSION_SECRET\n ${SESSION_SECRET}\n" fi @@ -109,7 +142,7 @@ echo "Installed ${PLIST}" echo "Agent Deck starts automatically at login." echo "Open http://127.0.0.1:${PORT_VALUE} (sign in with Google)." if [[ "$HOST_VALUE" == "0.0.0.0" || "$HOST_VALUE" == "::" ]]; then - echo "LAN/phone: same Wi-Fi devices can use this Mac's IP on port ${PORT_VALUE}." + echo "LAN/phone: same Wi-Fi devices use this Mac's IP on port ${PORT_VALUE} with PIN sign-in." fi if [[ -z "${GOOGLE_CLIENT_ID:-}" ]]; then echo "Warning: GOOGLE_CLIENT_ID is not set. Add it to ${ENV_FILE} then re-run npm run setup." diff --git a/server/auth.ts b/server/auth.ts index 00ac1e1..ed56825 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -1,15 +1,23 @@ -import { randomBytes } from 'node:crypto' +import { randomBytes, timingSafeEqual } from 'node:crypto' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' import cookieSession from 'cookie-session' import type { Express, NextFunction, Request, Response } from 'express' import { OAuth2Client } from 'google-auth-library' +import { + type AuthMode, + authModeForHostAndOrigin, + corsAllowedOrigins, + isLanBindHost, + publicOriginFromEnv, +} from './lib/auth-mode.js' export type AuthUser = { email: string name: string | null picture: string | null + method: 'google' | 'pin' } declare module 'express-serve-static-core' { @@ -42,11 +50,12 @@ export function googleClientId(): string | null { return id || null } -export function authConfigured(): boolean { - return Boolean(googleClientId()) +export function dashboardPin(): string | null { + const pin = process.env.DASHBOARD_PIN?.trim() + return pin || null } -function allowedEmails(): Set | null { +export function allowedEmails(): Set | null { const raw = process.env.ALLOWED_EMAILS?.trim() if (!raw) return null return new Set( @@ -57,36 +66,135 @@ function allowedEmails(): Set | null { ) } +export function publicOrigin(): string { + return publicOriginFromEnv() +} + +export function authModeForRequest(req: Request): AuthMode { + return authModeForHostAndOrigin({ + hostHeader: req.get('x-forwarded-host') || req.get('host'), + origin: req.get('origin'), + publicOrigin: publicOrigin(), + }) +} + +export function googleAuthReady(): boolean { + return Boolean(googleClientId()) +} + +export function pinAuthReady(): boolean { + return Boolean(dashboardPin()) +} + +/** True when the server can authenticate at least one supported mode. */ +export function authConfigured(): boolean { + return googleAuthReady() || pinAuthReady() +} + +export function assertLanAuthRequirements(host: string): void { + if (!isLanBindHost(host)) return + const missing: string[] = [] + if (!allowedEmails()?.size) missing.push('ALLOWED_EMAILS') + if (!dashboardPin()) missing.push('DASHBOARD_PIN') + if (!googleClientId()) missing.push('GOOGLE_CLIENT_ID') + if (missing.length === 0) return + console.error( + `LAN bind (HOST=${host}) requires ${missing.join(', ')} in .env. Refusing to start.`, + ) + process.exit(1) +} + +function pinMatches(input: string): boolean { + const expected = dashboardPin() + if (!expected) return false + const a = Buffer.from(input) + const b = Buffer.from(expected) + if (a.length !== b.length) { + timingSafeEqual(b, b) + return false + } + return timingSafeEqual(a, b) +} + +function isCrossSitePublicOrigin(req: Request): boolean { + const origin = req.get('origin')?.replace(/\/$/, '') + return Boolean(origin && origin === publicOrigin()) +} + +export function attachCors(app: Express): void { + const allowed = corsAllowedOrigins() + app.use((req, res, next) => { + const origin = req.get('origin') + if (origin && allowed.has(origin.replace(/\/$/, ''))) { + res.setHeader('Access-Control-Allow-Origin', origin) + res.setHeader('Access-Control-Allow-Credentials', 'true') + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization', + ) + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET,POST,OPTIONS', + ) + res.setHeader('Vary', 'Origin') + } + if (req.method === 'OPTIONS') { + res.status(204).end() + return + } + next() + }) +} + export function attachSession(app: Express): void { app.set('trust proxy', 1) - app.use( - cookieSession({ - name: 'agent_deck_session', - keys: [sessionSecret()], - maxAge: 30 * 24 * 60 * 60 * 1000, - httpOnly: true, - sameSite: 'lax', - secure: false, // local / LAN HTTP - }), - ) + const secret = sessionSecret() + const localSession = cookieSession({ + name: 'agent_deck_session', + keys: [secret], + maxAge: 30 * 24 * 60 * 60 * 1000, + httpOnly: true, + sameSite: 'lax', + secure: false, + }) + const crossSession = cookieSession({ + name: 'agent_deck_session', + keys: [secret], + maxAge: 30 * 24 * 60 * 60 * 1000, + httpOnly: true, + sameSite: 'none', + secure: true, + }) + app.use((req, res, next) => { + const mw = isCrossSitePublicOrigin(req) ? crossSession : localSession + mw(req, res, next) + }) } function readSession(req: Request): SessionData { return (req.session ?? {}) as SessionData } +function modeConfigured(mode: AuthMode): boolean { + return mode === 'google' ? googleAuthReady() : pinAuthReady() +} + export function requireAuth(req: Request, res: Response, next: NextFunction): void { - if (!authConfigured()) { + const mode = authModeForRequest(req) + if (!modeConfigured(mode)) { res.status(503).json({ error: 'auth_not_configured', + mode, message: - 'Set GOOGLE_CLIENT_ID (and optional ALLOWED_EMAILS) in .env, then restart Agent Deck.', + mode === 'google' + ? 'Set GOOGLE_CLIENT_ID (and ALLOWED_EMAILS when LAN bind is enabled) in .env, then restart Agent Deck.' + : 'Set DASHBOARD_PIN in .env for LAN / non-Google hosts, then restart Agent Deck.', }) return } const user = readSession(req).user if (!user?.email) { - res.status(401).json({ error: 'unauthorized' }) + res.status(401).json({ error: 'unauthorized', mode }) return } req.user = user @@ -94,34 +202,50 @@ export function requireAuth(req: Request, res: Response, next: NextFunction): vo } export function mountAuthRoutes(app: Express): void { - app.get('/api/auth/config', (_req, res) => { + app.get('/api/auth/config', (req, res) => { + const mode = authModeForRequest(req) + const configured = modeConfigured(mode) res.json({ - configured: authConfigured(), - clientId: googleClientId(), - allowedEmailsConfigured: Boolean(allowedEmails()), + mode, + configured, + clientId: mode === 'google' ? googleClientId() : null, + allowedEmailsConfigured: Boolean(allowedEmails()?.size), + publicOrigin: publicOrigin(), + pinConfigured: pinAuthReady(), }) }) app.get('/api/auth/me', (req, res) => { - if (!authConfigured()) { + const mode = authModeForRequest(req) + if (!modeConfigured(mode)) { res.status(503).json({ error: 'auth_not_configured', configured: false, + mode, }) return } const user = readSession(req).user if (!user?.email) { - res.status(401).json({ error: 'unauthorized', configured: true }) + res.status(401).json({ error: 'unauthorized', configured: true, mode }) return } - res.json({ user, configured: true }) + res.json({ user, configured: true, mode }) }) app.post('/api/auth/google', async (req, res) => { + const mode = authModeForRequest(req) + if (mode !== 'google') { + res.status(400).json({ + error: 'wrong_auth_mode', + message: 'This host uses PIN sign-in, not Google.', + mode, + }) + return + } const clientId = googleClientId() if (!clientId) { - res.status(503).json({ error: 'auth_not_configured' }) + res.status(503).json({ error: 'auth_not_configured', mode }) return } const credential = @@ -152,6 +276,14 @@ export function mountAuthRoutes(app: Express): void { const email = payload.email.toLowerCase() const allow = allowedEmails() + const lanBound = isLanBindHost(process.env.HOST ?? '127.0.0.1') + if (lanBound && !allow?.size) { + res.status(503).json({ + error: 'allowlist_required', + message: 'ALLOWED_EMAILS is required when LAN bind is enabled.', + }) + return + } if (allow && !allow.has(email)) { res.status(403).json({ error: 'email_not_allowed', @@ -164,6 +296,7 @@ export function mountAuthRoutes(app: Express): void { email, name: payload.name ?? null, picture: payload.picture ?? null, + method: 'google', } const session = readSession(req) session.user = user @@ -177,6 +310,37 @@ export function mountAuthRoutes(app: Express): void { } }) + app.post('/api/auth/pin', (req, res) => { + const mode = authModeForRequest(req) + if (mode !== 'pin') { + res.status(400).json({ + error: 'wrong_auth_mode', + message: 'This host uses Google sign-in, not PIN.', + mode, + }) + return + } + if (!dashboardPin()) { + res.status(503).json({ error: 'auth_not_configured', mode }) + return + } + const pin = typeof req.body?.pin === 'string' ? req.body.pin : '' + if (!pinMatches(pin)) { + res.status(401).json({ error: 'invalid_pin', message: 'Incorrect PIN.' }) + return + } + const user: AuthUser = { + email: 'pin@local', + name: 'PIN access', + picture: null, + method: 'pin', + } + const session = readSession(req) + session.user = user + req.session = session + res.json({ user }) + }) + app.post('/api/auth/logout', (req, res) => { req.session = null res.json({ ok: true }) diff --git a/server/collectors/usageResets.ts b/server/collectors/usageResets.ts index 5685867..2dac57e 100644 --- a/server/collectors/usageResets.ts +++ b/server/collectors/usageResets.ts @@ -145,26 +145,25 @@ async function refreshCodexAccessToken( } if (!data.access_token) return null - // Persist refreshed tokens so subsequent polls stay authenticated. + // Fail closed: rotated refresh tokens must land on disk or Codex CLI breaks. + if (!existsSync(CODEX_AUTH)) return null try { - if (existsSync(CODEX_AUTH)) { - const raw = JSON.parse(readFileSync(CODEX_AUTH, 'utf8')) as CodexAuthFile - raw.tokens = { - ...raw.tokens, - access_token: data.access_token, - refresh_token: data.refresh_token ?? raw.tokens?.refresh_token, - id_token: data.id_token ?? raw.tokens?.id_token, - } - raw.last_refresh = new Date().toISOString() - const tmp = join(dirname(CODEX_AUTH), `.auth.json.${process.pid}.tmp`) - writeFileSync(tmp, `${JSON.stringify(raw, null, 2)}\n`, { - encoding: 'utf8', - mode: 0o600, - }) - renameSync(tmp, CODEX_AUTH) + const raw = JSON.parse(readFileSync(CODEX_AUTH, 'utf8')) as CodexAuthFile + raw.tokens = { + ...raw.tokens, + access_token: data.access_token, + refresh_token: data.refresh_token ?? raw.tokens?.refresh_token, + id_token: data.id_token ?? raw.tokens?.id_token, } + raw.last_refresh = new Date().toISOString() + const tmp = join(dirname(CODEX_AUTH), `.auth.json.${process.pid}.tmp`) + writeFileSync(tmp, `${JSON.stringify(raw, null, 2)}\n`, { + encoding: 'utf8', + mode: 0o600, + }) + renameSync(tmp, CODEX_AUTH) } catch { - /* ignore persist failures */ + return null } return data.access_token } diff --git a/server/index.ts b/server/index.ts index 37d78e1..ad35b9d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,9 +5,14 @@ import { networkInterfaces } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { + assertLanAuthRequirements, + attachCors, attachSession, authConfigured, + dashboardPin, + googleAuthReady, mountAuthRoutes, + publicOrigin, requireAuth, } from './auth.js' import { collectClaude } from './collectors/claude.js' @@ -28,6 +33,8 @@ const USAGE_RESETS_TTL_MS = Number(process.env.USAGE_RESETS_TTL_MS ?? 180_000) const __dirname = dirname(fileURLToPath(import.meta.url)) const DIST = join(__dirname, '../dist') +assertLanAuthRequirements(HOST) + type UsageResetsPayload = { cursor: UsageReset claude: UsageReset @@ -52,8 +59,12 @@ function lanUrls(port: number): string[] { return out } -async function getUsageResets(): Promise { +async function getUsageResets(force = false): Promise { + if (force) { + usageResetsCache = null + } if ( + !force && usageResetsCache && Date.now() - usageResetsCache.at < USAGE_RESETS_TTL_MS ) { @@ -76,23 +87,31 @@ async function collectRaw(force = false): Promise<{ data: RawCollectors; cached: if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) { return { data: cache.data, cached: true } } + + const execute = async (): Promise => { + const [cursor, claude, codex, system, github, resets] = await Promise.all([ + Promise.resolve().then(() => collectCursor()), + collectClaude(), + collectCodex(), + Promise.resolve().then(() => collectSystem()), + Promise.resolve().then(() => collectGithub()), + getUsageResets(force), + ]) + cursor.usageReset = resets.cursor + claude.usageReset = resets.claude + codex.usageReset = resets.codex + const data: RawCollectors = { cursor, claude, codex, system, github } + cache = { at: Date.now(), data } + return data + } + + if (force) { + const data = await execute() + return { data, cached: false } + } + if (!inflight) { - inflight = (async () => { - const [cursor, claude, codex, system, github, resets] = await Promise.all([ - Promise.resolve().then(() => collectCursor()), - collectClaude(), - collectCodex(), - Promise.resolve().then(() => collectSystem()), - Promise.resolve().then(() => collectGithub()), - getUsageResets(), - ]) - cursor.usageReset = resets.cursor - claude.usageReset = resets.claude - codex.usageReset = resets.codex - const data: RawCollectors = { cursor, claude, codex, system, github } - cache = { at: Date.now(), data } - return data - })().finally(() => { + inflight = execute().finally(() => { inflight = null }) } @@ -124,6 +143,7 @@ async function buildPayload( const app = express() app.use(express.json({ limit: '32kb' })) +attachCors(app) attachSession(app) mountAuthRoutes(app) @@ -133,6 +153,9 @@ app.get('/api/health', (_req, res) => { dist: existsSync(DIST), host: HOST, authConfigured: authConfigured(), + googleAuthReady: googleAuthReady(), + pinAuthReady: Boolean(dashboardPin()), + publicOrigin: publicOrigin(), cacheTtlMs: CACHE_TTL_MS, usageResetsTtlMs: USAGE_RESETS_TTL_MS, }) @@ -166,18 +189,24 @@ app.listen(PORT, HOST, () => { const servingUi = existsSync(DIST) const mode = servingUi ? 'API + UI' : 'API only' console.log(`agent-dashboard ${mode} on http://${HOST === '0.0.0.0' ? '127.0.0.1' : HOST}:${PORT}`) - if (!authConfigured()) { + console.log(`Public UI origin (Google): ${publicOrigin()}`) + if (!googleAuthReady()) { console.log( 'Auth: GOOGLE_CLIENT_ID is not set. Copy .env.example to .env and add a Google OAuth Web client ID.', ) } else { - console.log('Auth: Google sign-in is required before the dashboard loads.') + console.log('Auth: Google sign-in on localhost and PUBLIC_ORIGIN.') + } + if (dashboardPin()) { + console.log('Auth: PIN sign-in enabled for LAN / non-Google hosts.') + } else { + console.log('Auth: DASHBOARD_PIN is not set (required for phone/LAN IP access).') } if (HOST === '0.0.0.0' || HOST === '::') { const urls = lanUrls(PORT) if (urls.length > 0) { if (servingUi) { - console.log('Phone / LAN (same Wi-Fi):') + console.log('Phone / LAN (same Wi-Fi, PIN sign-in):') for (const url of urls) console.log(` ${url}`) } else { console.log('API only (LAN, same Wi-Fi):') diff --git a/server/lib/auth-mode.test.ts b/server/lib/auth-mode.test.ts new file mode 100644 index 0000000..98b679f --- /dev/null +++ b/server/lib/auth-mode.test.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + DEFAULT_PUBLIC_ORIGIN, + authModeForHostAndOrigin, + corsAllowedOrigins, + isLanBindHost, + isLoopbackHostname, + publicOriginFromEnv, +} from './auth-mode.ts' + +describe('auth-mode', () => { + it('treats loopback as Google-compatible', () => { + assert.equal(isLoopbackHostname('127.0.0.1'), true) + assert.equal(isLoopbackHostname('localhost'), true) + assert.equal(isLoopbackHostname('::1'), true) + assert.equal(isLoopbackHostname('192.168.1.10'), false) + }) + + it('requires LAN extras when binding non-loopback', () => { + assert.equal(isLanBindHost('0.0.0.0'), true) + assert.equal(isLanBindHost('::'), true) + assert.equal(isLanBindHost('127.0.0.1'), false) + assert.equal(isLanBindHost('localhost'), false) + }) + + it('selects google for localhost Host and PUBLIC_ORIGIN Origin', () => { + assert.equal( + authModeForHostAndOrigin({ + hostHeader: '127.0.0.1:3847', + origin: undefined, + }), + 'google', + ) + assert.equal( + authModeForHostAndOrigin({ + hostHeader: '192.168.1.20:3847', + origin: DEFAULT_PUBLIC_ORIGIN, + }), + 'google', + ) + }) + + it('selects pin for raw LAN IP Host without Google-compatible Origin', () => { + assert.equal( + authModeForHostAndOrigin({ + hostHeader: '192.168.1.20:3847', + origin: undefined, + }), + 'pin', + ) + assert.equal( + authModeForHostAndOrigin({ + hostHeader: '192.168.1.20:3847', + origin: 'http://192.168.1.20:3847', + }), + 'pin', + ) + }) + + it('defaults PUBLIC_ORIGIN and CORS allowlist', () => { + assert.equal(publicOriginFromEnv({}), DEFAULT_PUBLIC_ORIGIN) + const origins = corsAllowedOrigins({ PORT: '3847' }) + assert.ok(origins.has(DEFAULT_PUBLIC_ORIGIN)) + assert.ok(origins.has('http://127.0.0.1:3847')) + assert.ok(origins.has('http://127.0.0.1:5174')) + assert.equal(origins.has('http://192.168.1.20:3847'), false) + }) +}) diff --git a/server/lib/auth-mode.ts b/server/lib/auth-mode.ts new file mode 100644 index 0000000..d56fcd4 --- /dev/null +++ b/server/lib/auth-mode.ts @@ -0,0 +1,77 @@ +export const DEFAULT_PUBLIC_ORIGIN = 'https://agent-dashboard-ctt.vercel.app' + +export type AuthMode = 'google' | 'pin' + +export function publicOriginFromEnv( + env: NodeJS.ProcessEnv = process.env, +): string { + const raw = env.PUBLIC_ORIGIN?.trim() || DEFAULT_PUBLIC_ORIGIN + return raw.replace(/\/$/, '') +} + +export function isLoopbackHostname(hostname: string): boolean { + const h = hostname.replace(/^\[|\]$/g, '').toLowerCase() + return h === 'localhost' || h === '127.0.0.1' || h === '::1' +} + +export function isLanBindHost(host: string): boolean { + const h = host.trim().toLowerCase() + if (!h || h === '0.0.0.0' || h === '::' || h === '[::]') return true + if (isLoopbackHostname(h)) return false + return true +} + +function hostnameOf(hostOrUrl: string): string | null { + const raw = hostOrUrl.trim() + if (!raw) return null + try { + if (raw.includes('://')) return new URL(raw).hostname.toLowerCase() + return new URL(`http://${raw}`).hostname.toLowerCase() + } catch { + return null + } +} + +/** Google GIS works on loopback and configured PUBLIC_ORIGIN; everything else uses PIN. */ +export function authModeForHostAndOrigin(input: { + hostHeader: string | null | undefined + origin: string | null | undefined + publicOrigin?: string +}): AuthMode { + const pub = (input.publicOrigin ?? DEFAULT_PUBLIC_ORIGIN).replace(/\/$/, '') + const pubHost = hostnameOf(pub) + + const origin = input.origin?.trim() + if (origin) { + const normalized = origin.replace(/\/$/, '') + if (normalized === pub) return 'google' + const originHost = hostnameOf(origin) + if (originHost && isLoopbackHostname(originHost)) return 'google' + return 'pin' + } + + const host = hostnameOf(input.hostHeader ?? '') + if (!host) return 'pin' + if (isLoopbackHostname(host)) return 'google' + if (pubHost && host === pubHost) return 'google' + return 'pin' +} + +export function corsAllowedOrigins( + env: NodeJS.ProcessEnv = process.env, +): Set { + const pub = publicOriginFromEnv(env) + const origins = new Set([ + pub, + 'http://127.0.0.1:3847', + 'http://localhost:3847', + 'http://127.0.0.1:5174', + 'http://localhost:5174', + ]) + const port = env.PORT?.trim() + if (port && port !== '3847') { + origins.add(`http://127.0.0.1:${port}`) + origins.add(`http://localhost:${port}`) + } + return origins +} diff --git a/server/lib/lan-intent.test.ts b/server/lib/lan-intent.test.ts index 1d8b345..acad4c1 100644 --- a/server/lib/lan-intent.test.ts +++ b/server/lib/lan-intent.test.ts @@ -38,12 +38,19 @@ describe('LAN phone webapp intent', () => { assert.match(html, /viewport-fit=cover/) }) - it('documents why Vercel is the wrong host and how to use LAN instead', () => { + it('documents Mac API + optional Vercel static UI and LAN PIN access', () => { const readme = readFileSync(join(root, 'README.md'), 'utf8') - assert.match(readme, /Why localhost \(not Vercel \/ public web\)/) - assert.match(readme, /Do not deploy this app to Vercel/) + assert.match(readme, /Why the Mac still runs the API/) + assert.match(readme, /do not deploy the collectors to a public cloud/i) + assert.match(readme, /agent-dashboard-ctt\.vercel\.app/) + assert.match(readme, /DASHBOARD_PIN/) assert.match(readme, /npm run serve:lan/) assert.match(readme, /npm run dev:lan/) assert.match(readme, /Add to Home Screen/) + assert.match(readme, /Google GIS does \*\*not\*\* accept raw LAN IPs/) + assert.doesNotMatch( + readme, + /Authorized JavaScript origins[\s\S]{0,400}192\.168/, + ) }) }) diff --git a/src/App.tsx b/src/App.tsx index ae0fd46..892ec30 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { AgentPanel } from './components/AgentPanel' import { GithubPanel } from './components/GithubPanel' import { Login } from './components/Login' import { SystemPanel } from './components/SystemPanel' +import { apiFetch } from './lib/api' import type { DashboardPayload, DateRange } from './lib/types' const REFRESH_MS = 15_000 @@ -11,6 +12,7 @@ type AuthUser = { email: string name: string | null picture: string | null + method?: 'google' | 'pin' } export default function App() { @@ -26,7 +28,7 @@ export default function App() { useEffect(() => { void (async () => { try { - const res = await fetch('/api/auth/me') + const res = await apiFetch('/api/auth/me') if (res.ok) { const json = (await res.json()) as { user: AuthUser } setUser(json.user) @@ -57,7 +59,7 @@ export default function App() { try { const params = new URLSearchParams({ range }) if (opts?.refresh) params.set('refresh', '1') - const res = await fetch(`/api/dashboard?${params}`, { + const res = await apiFetch(`/api/dashboard?${params}`, { signal: controller.signal, }) if (res.status === 401 || res.status === 503) { @@ -65,7 +67,7 @@ export default function App() { setData(null) throw new Error( res.status === 503 - ? 'Google sign-in is not configured on this Mac.' + ? 'Sign-in is not configured on this Mac.' : 'Signed out', ) } @@ -98,7 +100,7 @@ export default function App() { async function logout() { try { - await fetch('/api/auth/logout', { method: 'POST' }) + await apiFetch('/api/auth/logout', { method: 'POST' }) } catch { /* ignore */ } @@ -109,7 +111,7 @@ export default function App() { if (!authChecked) { return (
-

Checking Google sign-in…

+

Checking sign-in…

) } @@ -118,6 +120,9 @@ export default function App() { return } + const identity = + user.method === 'pin' ? 'PIN access' : user.email + return (
@@ -127,7 +132,7 @@ export default function App() {

- {user.email} + {identity} {data ? ` · Updated ${new Date(data.generatedAt).toLocaleTimeString()}${ data.cached ? ' · cached' : '' diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 271f797..93684ed 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -1,15 +1,22 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState, type FormEvent } from 'react' +import { apiFetch } from '../lib/api' + +type AuthMode = 'google' | 'pin' type AuthConfig = { + mode: AuthMode configured: boolean clientId: string | null allowedEmailsConfigured: boolean + publicOrigin?: string + pinConfigured?: boolean } type AuthUser = { email: string name: string | null picture: string | null + method?: 'google' | 'pin' } type Props = { @@ -72,12 +79,13 @@ export function Login({ onSignedIn }: Props) { const [config, setConfig] = useState(null) const [error, setError] = useState(null) const [busy, setBusy] = useState(false) + const [pin, setPin] = useState('') const buttonRef = useRef(null) useEffect(() => { void (async () => { try { - const res = await fetch('/api/auth/config') + const res = await apiFetch('/api/auth/config') const json = (await res.json()) as AuthConfig setConfig(json) } catch (err) { @@ -87,7 +95,8 @@ export function Login({ onSignedIn }: Props) { }, []) useEffect(() => { - if (!config?.configured || !config.clientId || !buttonRef.current) return + if (config?.mode !== 'google') return + if (!config.configured || !config.clientId || !buttonRef.current) return let cancelled = false void (async () => { @@ -101,7 +110,7 @@ export function Login({ onSignedIn }: Props) { setBusy(true) setError(null) try { - const res = await fetch('/api/auth/google', { + const res = await apiFetch('/api/auth/google', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: response.credential }), @@ -145,45 +154,117 @@ export function Login({ onSignedIn }: Props) { } }, [config, onSignedIn]) + async function submitPin(event: FormEvent) { + event.preventDefault() + setBusy(true) + setError(null) + try { + const res = await apiFetch('/api/auth/pin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pin }), + }) + const json = (await res.json()) as { + user?: AuthUser + message?: string + error?: string + } + if (!res.ok || !json.user) { + throw new Error(json.message ?? json.error ?? `HTTP ${res.status}`) + } + onSignedIn(json.user) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setBusy(false) + } + } + + const mode = config?.mode + const publicOrigin = + config?.publicOrigin ?? 'https://agent-dashboard-ctt.vercel.app' + return (

Local Mac

Agent Deck

- Sign in with Google to open your private agent dashboard. Google must - verify your email before access is granted. + {mode === 'pin' + ? 'Enter the dashboard PIN to open Agent Deck on this LAN address. Google sign-in is not available on raw IP hosts.' + : 'Sign in with Google to open your private agent dashboard. Google must verify your email before access is granted.'}

{!config ? (

Checking sign-in…

) : !config.configured ? (
-

- Google sign-in is not configured yet. On this Mac: -

-
    -
  1. - Create an OAuth Web client in Google Cloud - Console -
  2. -
  3. - Add authorized JavaScript origins:{' '} - http://127.0.0.1:3847 (and your LAN URL if using - phone) -
  4. -
  5. - Copy .env.example to .env and set{' '} - GOOGLE_CLIENT_ID -
  6. -
  7. - Optional: set ALLOWED_EMAILS=you@gmail.com -
  8. -
  9. - Restart with npm run setup -
  10. -
+ {mode === 'pin' ? ( + <> +

PIN sign-in is not configured yet. On this Mac:

+
    +
  1. + Copy .env.example to .env +
  2. +
  3. + Set DASHBOARD_PIN (required for LAN / phone) +
  4. +
  5. + When HOST=0.0.0.0, also set{' '} + ALLOWED_EMAILS and GOOGLE_CLIENT_ID +
  6. +
  7. + Restart with npm run setup +
  8. +
+ + ) : ( + <> +

Google sign-in is not configured yet. On this Mac:

+
    +
  1. + Create an OAuth Web client in Google Cloud + Console +
  2. +
  3. + Add authorized JavaScript origins:{' '} + http://127.0.0.1:3847 and{' '} + {publicOrigin} +
  4. +
  5. + Copy .env.example to .env and set{' '} + GOOGLE_CLIENT_ID +
  6. +
  7. + For LAN bind, set ALLOWED_EMAILS and{' '} + DASHBOARD_PIN +
  8. +
  9. + Restart with npm run setup +
  10. +
+ + )}
+ ) : mode === 'pin' ? ( +
void submitPin(e)}> + + setPin(e.target.value)} + disabled={busy} + /> + +
) : ( <>
@@ -251,6 +332,38 @@ export function Login({ onSignedIn }: Props) { flex-direction: column; gap: 0.4rem; } + .pin-form { + display: flex; + flex-direction: column; + gap: 0.55rem; + } + .pin-label { + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + } + .pin-input { + border: 1px solid var(--line); + background: var(--bg); + color: var(--text); + padding: 0.7rem 0.75rem; + font: inherit; + font-size: 1rem; + } + .pin-submit { + border: 1px solid var(--line); + background: var(--text); + color: var(--bg); + padding: 0.7rem 0.9rem; + font: inherit; + font-size: 0.85rem; + cursor: pointer; + } + .pin-submit:disabled { + opacity: 0.5; + cursor: default; + } code { font-family: inherit; border: 1px solid var(--line); diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..e8f5044 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,39 @@ +declare global { + interface Window { + __AGENT_DECK_API_BASE__?: string + } +} + +function trimSlash(value: string): string { + return value.replace(/\/$/, '') +} + +/** Build-time VITE_API_BASE, overridden by runtime `window.__AGENT_DECK_API_BASE__`. */ +export function apiBase(): string { + const runtime = + typeof window !== 'undefined' ? window.__AGENT_DECK_API_BASE__ : undefined + if (typeof runtime === 'string' && runtime.trim()) { + return trimSlash(runtime.trim()) + } + const baked = import.meta.env.VITE_API_BASE + if (typeof baked === 'string' && baked.trim()) { + return trimSlash(baked.trim()) + } + return '' +} + +export function apiUrl(path: string): string { + const base = apiBase() + const p = path.startsWith('/') ? path : `/${path}` + return `${base}${p}` +} + +export function apiFetch( + path: string, + init?: RequestInit, +): Promise { + return fetch(apiUrl(path), { + ...init, + credentials: 'include', + }) +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..0f32683 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} From 26bf563d4ea6e3a25008f96869bd7dc54982e7b8 Mon Sep 17 00:00:00 2001 From: ctt062 Date: Tue, 28 Jul 2026 11:35:15 +0800 Subject: [PATCH 4/6] no-mistakes(review): Add Bearer auth and PIN rate limits --- .env.example | 3 +- README.md | 8 ++- package.json | 2 +- server/auth.ts | 111 +++++++++++++++++++++--------- server/lib/bearer-token.test.ts | 38 ++++++++++ server/lib/bearer-token.ts | 69 +++++++++++++++++++ server/lib/lan-intent.test.ts | 3 + server/lib/pin-rate-limit.test.ts | 43 ++++++++++++ server/lib/pin-rate-limit.ts | 50 ++++++++++++++ src/App.tsx | 4 +- src/components/Login.tsx | 15 ++-- src/lib/api.ts | 29 ++++++++ 12 files changed, 333 insertions(+), 42 deletions(-) create mode 100644 server/lib/bearer-token.test.ts create mode 100644 server/lib/bearer-token.ts create mode 100644 server/lib/pin-rate-limit.test.ts create mode 100644 server/lib/pin-rate-limit.ts diff --git a/.env.example b/.env.example index f93677d..27d926f 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,8 @@ ALLOWED_EMAILS= DASHBOARD_PIN= # Google-compatible public UI origin (Vercel static host). Default below. PUBLIC_ORIGIN=https://agent-dashboard-ctt.vercel.app -# Optional: override auto-generated cookie secret in ~/.config/agent-deck/session-secret +# Optional: override auto-generated secret for cookie + Bearer token signing +# (~/.config/agent-deck/session-secret) SESSION_SECRET= # Optional LAN bind for phone access (LaunchAgent / serve:lan also set this) # HOST=0.0.0.0 diff --git a/README.md b/README.md index dd18b58..332c181 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ PUBLIC_ORIGIN=https://agent-dashboard-ctt.vercel.app Authorized JavaScript origins (Google Cloud Console) - Google GIS does **not** accept raw LAN IPs: -- `http://127.0.0.1:3847` +- `http://127.0.0.1:3847` and `http://localhost:3847` (production serve) +- `http://127.0.0.1:5174` and `http://localhost:5174` (`npm run dev`) - `https://agent-dashboard-ctt.vercel.app` (or your `PUBLIC_ORIGIN`) 3. Build + install login auto-start: @@ -71,6 +72,7 @@ Public UI: [https://agent-dashboard-ctt.vercel.app/](https://agent-dashboard-ctt - runtime `public/runtime-config.js` → `window.__AGENT_DECK_API_BASE__` 3. Prefer an **HTTPS tunnel** to the Mac when the UI is on HTTPS (browsers block mixed content to bare `http://192.168.x.x`). 4. On the Mac, keep `PUBLIC_ORIGIN=https://agent-dashboard-ctt.vercel.app` so CORS allows that origin + localhost only (no wildcard, no raw LAN origins). +5. Sign-in returns a **Bearer token** stored in the browser (`sessionStorage`) and sent as `Authorization: Bearer …`. Do not rely on third-party cookies for Vercel → Mac. Collectors and `/api/*` still run on the Mac. @@ -167,7 +169,7 @@ Missing collectors degrade gracefully - each panel shows a short hint instead of - **Usage resets**: Per-provider token/limit reset times (Cursor billing cycle via local dashboard API, Codex ChatGPT wham/usage windows, Claude rolling 5h/weekly with `/usage` guidance when exact times are unavailable) - **Detailed agent stats**: period total, avg/day, active days, peak day, acceptance rate (Cursor), input/output tokens - **Dual-series charts** plus a cross-agent comparison chart -- **Dual auth**: Google on localhost / `PUBLIC_ORIGIN`; PIN on LAN IPs; `ALLOWED_EMAILS` + `DASHBOARD_PIN` required for LAN bind +- **Dual auth**: Google on localhost / `PUBLIC_ORIGIN`; PIN on LAN IPs; Bearer token for Vercel → Mac API; `ALLOWED_EMAILS` + `DASHBOARD_PIN` required for LAN bind - **Local web app**: `npm run setup` auto-starts at login; `serve:lan` / LaunchAgent for phone on the same Wi-Fi - **Cached collectors** (~10s TTL) with parallel collection; usage-reset lookups cache separately (~3 min). Refresh bypasses both caches @@ -208,7 +210,9 @@ HOST=0.0.0.0 PORT=4000 npm start - Dashboard stats come from files and tools already on your Mac - Google sign-in uses Google Identity Services on localhost and `PUBLIC_ORIGIN`; LAN IPs use PIN instead +- Cross-origin Vercel UI uses Bearer tokens (not third-party cookies); Mac same-origin UI may still use session cookies - When LAN bind is enabled, `ALLOWED_EMAILS` and `DASHBOARD_PIN` are required +- Failed PIN attempts are rate-limited in memory per client IP - Usage-reset times use local Cursor / Codex / Claude credentials on this machine only to call those vendors' usage APIs; tokens are not sent to Agent Deck or any other service - Default LaunchAgent bind is LAN-capable (`HOST=0.0.0.0`) - use only on trusted Wi-Fi - CORS allowlists localhost + `PUBLIC_ORIGIN` only (no wildcard) diff --git a/package.json b/package.json index 5f7aea7..8650941 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "serve:lan": "npm run build && HOST=0.0.0.0 npm start", "preview": "vite preview", "lint": "oxlint", - "test": "node --test server/lib/range.test.ts server/lib/lan-intent.test.ts server/lib/auth-mode.test.ts src/lib/types.test.ts", + "test": "node --test server/lib/range.test.ts server/lib/lan-intent.test.ts server/lib/auth-mode.test.ts server/lib/bearer-token.test.ts server/lib/pin-rate-limit.test.ts src/lib/types.test.ts", "setup": "npm run build && npm run launchagent:install", "launchagent:install": "bash scripts/install-launchagent.sh", "launchagent:uninstall": "bash scripts/uninstall-launchagent.sh" diff --git a/server/auth.ts b/server/auth.ts index ed56825..6d75bff 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -12,6 +12,17 @@ import { isLanBindHost, publicOriginFromEnv, } from './lib/auth-mode.js' +import { + bearerFromAuthorization, + issueBearerToken, + verifyBearerToken, +} from './lib/bearer-token.js' +import { + checkPinRateLimit, + clearPinFailures, + createPinRateLimitStore, + recordPinFailure, +} from './lib/pin-rate-limit.js' export type AuthUser = { email: string @@ -30,6 +41,8 @@ type SessionData = { user?: AuthUser } +const pinAttemptStore = createPinRateLimitStore() + function configDir(): string { const dir = join(homedir(), '.config', 'agent-deck') if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }) @@ -116,9 +129,13 @@ function pinMatches(input: string): boolean { return timingSafeEqual(a, b) } -function isCrossSitePublicOrigin(req: Request): boolean { - const origin = req.get('origin')?.replace(/\/$/, '') - return Boolean(origin && origin === publicOrigin()) +function clientIp(req: Request): string { + const forwarded = req.get('x-forwarded-for') + if (forwarded) { + const first = forwarded.split(',')[0]?.trim() + if (first) return first + } + return req.ip || req.socket.remoteAddress || 'unknown' } export function attachCors(app: Express): void { @@ -149,26 +166,16 @@ export function attachCors(app: Express): void { export function attachSession(app: Express): void { app.set('trust proxy', 1) const secret = sessionSecret() - const localSession = cookieSession({ - name: 'agent_deck_session', - keys: [secret], - maxAge: 30 * 24 * 60 * 60 * 1000, - httpOnly: true, - sameSite: 'lax', - secure: false, - }) - const crossSession = cookieSession({ - name: 'agent_deck_session', - keys: [secret], - maxAge: 30 * 24 * 60 * 60 * 1000, - httpOnly: true, - sameSite: 'none', - secure: true, - }) - app.use((req, res, next) => { - const mw = isCrossSitePublicOrigin(req) ? crossSession : localSession - mw(req, res, next) - }) + app.use( + cookieSession({ + name: 'agent_deck_session', + keys: [secret], + maxAge: 30 * 24 * 60 * 60 * 1000, + httpOnly: true, + sameSite: 'lax', + secure: false, + }), + ) } function readSession(req: Request): SessionData { @@ -179,6 +186,23 @@ function modeConfigured(mode: AuthMode): boolean { return mode === 'google' ? googleAuthReady() : pinAuthReady() } +function resolveUser(req: Request): AuthUser | null { + const bearer = bearerFromAuthorization(req.get('authorization')) + if (bearer) { + const user = verifyBearerToken(bearer, sessionSecret()) + if (user) return user + } + return readSession(req).user ?? null +} + +function issueAuthResponse(req: Request, res: Response, user: AuthUser): void { + const session = readSession(req) + session.user = user + req.session = session + const token = issueBearerToken(user, sessionSecret()) + res.json({ user, token }) +} + export function requireAuth(req: Request, res: Response, next: NextFunction): void { const mode = authModeForRequest(req) if (!modeConfigured(mode)) { @@ -192,7 +216,7 @@ export function requireAuth(req: Request, res: Response, next: NextFunction): vo }) return } - const user = readSession(req).user + const user = resolveUser(req) if (!user?.email) { res.status(401).json({ error: 'unauthorized', mode }) return @@ -225,7 +249,7 @@ export function mountAuthRoutes(app: Express): void { }) return } - const user = readSession(req).user + const user = resolveUser(req) if (!user?.email) { res.status(401).json({ error: 'unauthorized', configured: true, mode }) return @@ -298,10 +322,7 @@ export function mountAuthRoutes(app: Express): void { picture: payload.picture ?? null, method: 'google', } - const session = readSession(req) - session.user = user - req.session = session - res.json({ user }) + issueAuthResponse(req, res, user) } catch (err) { res.status(401).json({ error: 'verify_failed', @@ -324,21 +345,45 @@ export function mountAuthRoutes(app: Express): void { res.status(503).json({ error: 'auth_not_configured', mode }) return } + + const ip = clientIp(req) + const limited = checkPinRateLimit(pinAttemptStore, ip) + if (!limited.ok) { + const retryAfterSec = Math.max(1, Math.ceil(limited.retryAfterMs / 1000)) + res.setHeader('Retry-After', String(retryAfterSec)) + res.status(429).json({ + error: 'pin_rate_limited', + message: `Too many incorrect PIN attempts. Try again in ${retryAfterSec}s.`, + retryAfterMs: limited.retryAfterMs, + }) + return + } + const pin = typeof req.body?.pin === 'string' ? req.body.pin : '' if (!pinMatches(pin)) { + const result = recordPinFailure(pinAttemptStore, ip) + if (result.retryAfterMs > 0) { + const retryAfterSec = Math.max(1, Math.ceil(result.retryAfterMs / 1000)) + res.setHeader('Retry-After', String(retryAfterSec)) + res.status(429).json({ + error: 'pin_rate_limited', + message: `Too many incorrect PIN attempts. Try again in ${retryAfterSec}s.`, + retryAfterMs: result.retryAfterMs, + }) + return + } res.status(401).json({ error: 'invalid_pin', message: 'Incorrect PIN.' }) return } + + clearPinFailures(pinAttemptStore, ip) const user: AuthUser = { email: 'pin@local', name: 'PIN access', picture: null, method: 'pin', } - const session = readSession(req) - session.user = user - req.session = session - res.json({ user }) + issueAuthResponse(req, res, user) }) app.post('/api/auth/logout', (req, res) => { diff --git a/server/lib/bearer-token.test.ts b/server/lib/bearer-token.test.ts new file mode 100644 index 0000000..2dab1ae --- /dev/null +++ b/server/lib/bearer-token.test.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + bearerFromAuthorization, + issueBearerToken, + verifyBearerToken, +} from './bearer-token.ts' + +describe('bearer-token', () => { + const secret = 'test-secret-key' + const user = { + email: 'you@example.com', + name: 'You', + picture: null, + method: 'google' as const, + } + + it('issues a verifiable token', () => { + const token = issueBearerToken(user, secret, 60_000, 1_000) + const parsed = verifyBearerToken(token, secret, 1_500) + assert.deepEqual(parsed, user) + }) + + it('rejects expired, tampered, or wrong-secret tokens', () => { + const token = issueBearerToken(user, secret, 100, 1_000) + assert.equal(verifyBearerToken(token, secret, 1_200), null) + assert.equal(verifyBearerToken(token, 'other', 1_050), null) + assert.equal(verifyBearerToken(`${token}x`, secret, 1_050), null) + assert.equal(verifyBearerToken('not-a-token', secret), null) + }) + + it('parses Authorization Bearer headers', () => { + assert.equal(bearerFromAuthorization('Bearer abc.def'), 'abc.def') + assert.equal(bearerFromAuthorization('bearer abc.def'), 'abc.def') + assert.equal(bearerFromAuthorization('Basic abc'), null) + assert.equal(bearerFromAuthorization(undefined), null) + }) +}) diff --git a/server/lib/bearer-token.ts b/server/lib/bearer-token.ts new file mode 100644 index 0000000..093e9d7 --- /dev/null +++ b/server/lib/bearer-token.ts @@ -0,0 +1,69 @@ +import { createHmac, timingSafeEqual } from 'node:crypto' + +export type BearerUser = { + email: string + name: string | null + picture: string | null + method: 'google' | 'pin' +} + +type TokenPayload = { + u: BearerUser + exp: number +} + +const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000 + +function sign(payload: string, secret: string): string { + return createHmac('sha256', secret).update(payload).digest('base64url') +} + +export function issueBearerToken( + user: BearerUser, + secret: string, + ttlMs = DEFAULT_TTL_MS, + now = Date.now(), +): string { + const body: TokenPayload = { u: user, exp: now + ttlMs } + const payload = Buffer.from(JSON.stringify(body), 'utf8').toString('base64url') + return `${payload}.${sign(payload, secret)}` +} + +export function verifyBearerToken( + token: string, + secret: string, + now = Date.now(), +): BearerUser | null { + const dot = token.indexOf('.') + if (dot <= 0 || dot === token.length - 1) return null + const payload = token.slice(0, dot) + const sig = token.slice(dot + 1) + const expected = sign(payload, secret) + const a = Buffer.from(sig) + const b = Buffer.from(expected) + if (a.length !== b.length || !timingSafeEqual(a, b)) return null + try { + const raw = JSON.parse( + Buffer.from(payload, 'base64url').toString('utf8'), + ) as TokenPayload + if (!raw?.u?.email || typeof raw.exp !== 'number') return null + if (raw.exp < now) return null + if (raw.u.method !== 'google' && raw.u.method !== 'pin') return null + return { + email: raw.u.email, + name: raw.u.name ?? null, + picture: raw.u.picture ?? null, + method: raw.u.method, + } + } catch { + return null + } +} + +export function bearerFromAuthorization( + header: string | null | undefined, +): string | null { + if (!header) return null + const match = /^Bearer\s+(\S+)$/i.exec(header.trim()) + return match?.[1] ?? null +} diff --git a/server/lib/lan-intent.test.ts b/server/lib/lan-intent.test.ts index acad4c1..8843682 100644 --- a/server/lib/lan-intent.test.ts +++ b/server/lib/lan-intent.test.ts @@ -48,6 +48,9 @@ describe('LAN phone webapp intent', () => { assert.match(readme, /npm run dev:lan/) assert.match(readme, /Add to Home Screen/) assert.match(readme, /Google GIS does \*\*not\*\* accept raw LAN IPs/) + assert.match(readme, /127\.0\.0\.1:5174/) + assert.match(readme, /localhost:5174/) + assert.match(readme, /Bearer token/) assert.doesNotMatch( readme, /Authorized JavaScript origins[\s\S]{0,400}192\.168/, diff --git a/server/lib/pin-rate-limit.test.ts b/server/lib/pin-rate-limit.test.ts new file mode 100644 index 0000000..78c35b5 --- /dev/null +++ b/server/lib/pin-rate-limit.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + checkPinRateLimit, + clearPinFailures, + createPinRateLimitStore, + pinLockMsForFails, + recordPinFailure, +} from './pin-rate-limit.ts' + +describe('pin-rate-limit', () => { + it('locks after five failures with progressive backoff', () => { + assert.equal(pinLockMsForFails(4), 0) + assert.equal(pinLockMsForFails(5), 60_000) + assert.equal(pinLockMsForFails(6), 120_000) + assert.equal(pinLockMsForFails(10), 15 * 60_000) + }) + + it('rate-limits an IP after repeated failures and clears on success path', () => { + const store = createPinRateLimitStore() + const now = 1_000_000 + + for (let i = 0; i < 4; i++) { + const result = recordPinFailure(store, '10.0.0.1', now + i) + assert.equal(result.retryAfterMs, 0) + assert.equal(checkPinRateLimit(store, '10.0.0.1', now + i).ok, true) + } + + const locked = recordPinFailure(store, '10.0.0.1', now + 10) + assert.equal(locked.retryAfterMs, 60_000) + const blocked = checkPinRateLimit(store, '10.0.0.1', now + 11) + assert.equal(blocked.ok, false) + if (!blocked.ok) assert.ok(blocked.retryAfterMs > 0) + + assert.equal( + checkPinRateLimit(store, '10.0.0.1', now + 10 + 60_000).ok, + true, + ) + + clearPinFailures(store, '10.0.0.1') + assert.equal(checkPinRateLimit(store, '10.0.0.1', now + 20).ok, true) + }) +}) diff --git a/server/lib/pin-rate-limit.ts b/server/lib/pin-rate-limit.ts new file mode 100644 index 0000000..5fff154 --- /dev/null +++ b/server/lib/pin-rate-limit.ts @@ -0,0 +1,50 @@ +export type PinRateLimitState = { + fails: number + lockedUntil: number +} + +export type PinRateLimitStore = Map + +const MAX_ATTEMPTS_BEFORE_LOCK = 5 +const BASE_LOCK_MS = 60_000 +const MAX_LOCK_MS = 15 * 60_000 + +export function createPinRateLimitStore(): PinRateLimitStore { + return new Map() +} + +export function pinLockMsForFails(fails: number): number { + if (fails < MAX_ATTEMPTS_BEFORE_LOCK) return 0 + const extra = fails - MAX_ATTEMPTS_BEFORE_LOCK + return Math.min(MAX_LOCK_MS, BASE_LOCK_MS * 2 ** extra) +} + +export function checkPinRateLimit( + store: PinRateLimitStore, + ip: string, + now = Date.now(), +): { ok: true } | { ok: false; retryAfterMs: number } { + const entry = store.get(ip) + if (!entry || entry.lockedUntil <= now) return { ok: true } + return { ok: false, retryAfterMs: entry.lockedUntil - now } +} + +export function recordPinFailure( + store: PinRateLimitStore, + ip: string, + now = Date.now(), +): { lockedUntil: number; retryAfterMs: number } { + const prev = store.get(ip) + const fails = (prev?.fails ?? 0) + 1 + const lockMs = pinLockMsForFails(fails) + const lockedUntil = lockMs > 0 ? now + lockMs : 0 + store.set(ip, { fails, lockedUntil }) + return { + lockedUntil, + retryAfterMs: lockMs > 0 ? lockMs : 0, + } +} + +export function clearPinFailures(store: PinRateLimitStore, ip: string): void { + store.delete(ip) +} diff --git a/src/App.tsx b/src/App.tsx index 892ec30..5079387 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import { AgentPanel } from './components/AgentPanel' import { GithubPanel } from './components/GithubPanel' import { Login } from './components/Login' import { SystemPanel } from './components/SystemPanel' -import { apiFetch } from './lib/api' +import { apiFetch, clearAuthToken } from './lib/api' import type { DashboardPayload, DateRange } from './lib/types' const REFRESH_MS = 15_000 @@ -63,6 +63,7 @@ export default function App() { signal: controller.signal, }) if (res.status === 401 || res.status === 503) { + if (res.status === 401) clearAuthToken() setUser(null) setData(null) throw new Error( @@ -104,6 +105,7 @@ export default function App() { } catch { /* ignore */ } + clearAuthToken() setUser(null) setData(null) } diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 93684ed..fc6efe9 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState, type FormEvent } from 'react' -import { apiFetch } from '../lib/api' +import { apiFetch, setAuthToken } from '../lib/api' type AuthMode = 'google' | 'pin' @@ -117,12 +117,14 @@ export function Login({ onSignedIn }: Props) { }) const json = (await res.json()) as { user?: AuthUser + token?: string message?: string error?: string } - if (!res.ok || !json.user) { + if (!res.ok || !json.user || !json.token) { throw new Error(json.message ?? json.error ?? `HTTP ${res.status}`) } + setAuthToken(json.token) onSignedIn(json.user) } catch (err) { setError(err instanceof Error ? err.message : String(err)) @@ -166,12 +168,14 @@ export function Login({ onSignedIn }: Props) { }) const json = (await res.json()) as { user?: AuthUser + token?: string message?: string error?: string } - if (!res.ok || !json.user) { + if (!res.ok || !json.user || !json.token) { throw new Error(json.message ?? json.error ?? `HTTP ${res.status}`) } + setAuthToken(json.token) onSignedIn(json.user) } catch (err) { setError(err instanceof Error ? err.message : String(err)) @@ -228,7 +232,10 @@ export function Login({ onSignedIn }: Props) {
  • Add authorized JavaScript origins:{' '} - http://127.0.0.1:3847 and{' '} + http://127.0.0.1:3847,{' '} + http://localhost:3847,{' '} + http://127.0.0.1:5174,{' '} + http://localhost:5174, and{' '} {publicOrigin}
  • diff --git a/src/lib/api.ts b/src/lib/api.ts index e8f5044..1ba0914 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -4,6 +4,8 @@ declare global { } } +const TOKEN_KEY = 'agent_deck_token' + function trimSlash(value: string): string { return value.replace(/\/$/, '') } @@ -28,12 +30,39 @@ export function apiUrl(path: string): string { return `${base}${p}` } +export function getAuthToken(): string | null { + try { + return sessionStorage.getItem(TOKEN_KEY) + } catch { + return null + } +} + +export function setAuthToken(token: string | null): void { + try { + if (token) sessionStorage.setItem(TOKEN_KEY, token) + else sessionStorage.removeItem(TOKEN_KEY) + } catch { + /* ignore quota / private mode */ + } +} + +export function clearAuthToken(): void { + setAuthToken(null) +} + export function apiFetch( path: string, init?: RequestInit, ): Promise { + const headers = new Headers(init?.headers) + const token = getAuthToken() + if (token && !headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${token}`) + } return fetch(apiUrl(path), { ...init, + headers, credentials: 'include', }) } From 86dcee006c82e05388d2fbac26196bd03efe74bd Mon Sep 17 00:00:00 2001 From: ctt062 Date: Tue, 28 Jul 2026 11:38:42 +0800 Subject: [PATCH 5/6] no-mistakes(review): Harden PIN rate limit and Vite LAN proxy --- server/auth.ts | 17 ++++++----------- vite.config.ts | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/server/auth.ts b/server/auth.ts index 6d75bff..1527ab6 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -129,13 +129,8 @@ function pinMatches(input: string): boolean { return timingSafeEqual(a, b) } -function clientIp(req: Request): string { - const forwarded = req.get('x-forwarded-for') - if (forwarded) { - const first = forwarded.split(',')[0]?.trim() - if (first) return first - } - return req.ip || req.socket.remoteAddress || 'unknown' +function pinRateLimitKey(req: Request): string { + return req.socket.remoteAddress || 'unknown' } export function attachCors(app: Express): void { @@ -346,8 +341,8 @@ export function mountAuthRoutes(app: Express): void { return } - const ip = clientIp(req) - const limited = checkPinRateLimit(pinAttemptStore, ip) + const rateKey = pinRateLimitKey(req) + const limited = checkPinRateLimit(pinAttemptStore, rateKey) if (!limited.ok) { const retryAfterSec = Math.max(1, Math.ceil(limited.retryAfterMs / 1000)) res.setHeader('Retry-After', String(retryAfterSec)) @@ -361,7 +356,7 @@ export function mountAuthRoutes(app: Express): void { const pin = typeof req.body?.pin === 'string' ? req.body.pin : '' if (!pinMatches(pin)) { - const result = recordPinFailure(pinAttemptStore, ip) + const result = recordPinFailure(pinAttemptStore, rateKey) if (result.retryAfterMs > 0) { const retryAfterSec = Math.max(1, Math.ceil(result.retryAfterMs / 1000)) res.setHeader('Retry-After', String(retryAfterSec)) @@ -376,7 +371,7 @@ export function mountAuthRoutes(app: Express): void { return } - clearPinFailures(pinAttemptStore, ip) + clearPinFailures(pinAttemptStore, rateKey) const user: AuthUser = { email: 'pin@local', name: 'PIN access', diff --git a/vite.config.ts b/vite.config.ts index 799abf5..3df4cad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ proxy: { '/api': { target: apiTarget, - changeOrigin: true, + changeOrigin: false, }, }, }, From e950d89350475477382b577c61c57f69f85b152f Mon Sep 17 00:00:00 2001 From: ctt062 Date: Tue, 28 Jul 2026 11:47:27 +0800 Subject: [PATCH 6/6] no-mistakes(document): Fix README auth contracts and duplicates --- README.md | 51 +++++++++++++++------------------------------------ 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 332c181..948db19 100644 --- a/README.md +++ b/README.md @@ -78,40 +78,25 @@ Collectors and `/api/*` still run on the Mac. ## Download -### Option A - Clone with Git +Day-to-day install is [Recommended setup](#recommended-setup-auto-start--dual-auth) (`.env` auth + `npm run setup`). Auth is required for dashboard data. -```bash -git clone https://github.com/ctt062/agent-dashboard.git -cd agent-dashboard -npm install -npm run dev -``` - -### Option B - ZIP download (no Git) +### ZIP (no Git) 1. Open https://github.com/ctt062/agent-dashboard 2. Click **Code → Download ZIP** -3. Unzip, then in that folder: - -```bash -npm install -npm run dev -``` - -Open **http://127.0.0.1:5174** +3. Unzip, then continue from `cp .env.example .env` in Recommended setup. ### One-command production serve -Builds the UI and serves API + static UI from one localhost port: +After `.env` is configured (see Recommended setup), build and serve API + static UI on one port: ```bash -npm install npm run serve ``` -Open **http://127.0.0.1:3847** +Open **http://127.0.0.1:3847** (Google sign-in). -`npm start` alone also works after `npm run build` (serves `dist/` when present). +`npm start` alone also works after `npm run build` (serves `dist/` when present). Hot-reload coding: `npm run dev` → **http://127.0.0.1:5174**. ## View on your phone (same Wi-Fi) @@ -135,21 +120,13 @@ Only do this on a trusted network - LAN mode exposes local agent + Mac metrics t ## Start at login (macOS) -```bash -npm run setup -``` - -or only the LaunchAgent piece: +Use `npm run setup` from [Recommended setup](#recommended-setup-auto-start--dual-auth) (requires `.env` auth when LAN-bound). LaunchAgent only: ```bash npm run launchagent:install ``` -This keeps Agent Deck running (LAN-capable by default) and opens it once. Remove with: - -```bash -npm run launchagent:uninstall -``` +Remove with `npm run launchagent:uninstall`. ## Requirements @@ -169,7 +146,7 @@ Missing collectors degrade gracefully - each panel shows a short hint instead of - **Usage resets**: Per-provider token/limit reset times (Cursor billing cycle via local dashboard API, Codex ChatGPT wham/usage windows, Claude rolling 5h/weekly with `/usage` guidance when exact times are unavailable) - **Detailed agent stats**: period total, avg/day, active days, peak day, acceptance rate (Cursor), input/output tokens - **Dual-series charts** plus a cross-agent comparison chart -- **Dual auth**: Google on localhost / `PUBLIC_ORIGIN`; PIN on LAN IPs; Bearer token for Vercel → Mac API; `ALLOWED_EMAILS` + `DASHBOARD_PIN` required for LAN bind +- **Dual auth**: Google on localhost / `PUBLIC_ORIGIN`; PIN on LAN IPs; Bearer token for Vercel → Mac API; `GOOGLE_CLIENT_ID` + `ALLOWED_EMAILS` + `DASHBOARD_PIN` required for LAN bind - **Local web app**: `npm run setup` auto-starts at login; `serve:lan` / LaunchAgent for phone on the same Wi-Fi - **Cached collectors** (~10s TTL) with parallel collection; usage-reset lookups cache separately (~3 min). Refresh bypasses both caches @@ -193,11 +170,13 @@ Agent % is **relative share** of a local activity score across Cursor, Claude Co Default bind is `127.0.0.1`. Use `HOST=0.0.0.0` (or `npm run serve:lan`) for LAN/phone access. +`/api/dashboard` and `/api/system` require auth (session cookie and/or `Authorization: Bearer …`). `/api/health` and `/api/auth/*` are reachable without a prior session (sign-in routes issue the token). + | Endpoint | Description | |----------|-------------| -| `GET /api/dashboard?range=1d\|7d\|30d\|month` | Full payload (agents + system + GitHub). Add `refresh=1` to bypass cache. | -| `GET /api/system` | Mac snapshot only | -| `GET /api/health` | Liveness check | +| `GET /api/dashboard?range=1d\|7d\|30d\|month` | Full payload (agents + system + GitHub). Add `refresh=1` to bypass cache. Auth required. | +| `GET /api/system` | Mac snapshot only. Auth required. | +| `GET /api/health` | Liveness check (no auth) | | `GET /api/auth/config` | Auth mode for this Host/Origin (`google` or `pin`) | Override host/port if needed: @@ -211,7 +190,7 @@ HOST=0.0.0.0 PORT=4000 npm start - Dashboard stats come from files and tools already on your Mac - Google sign-in uses Google Identity Services on localhost and `PUBLIC_ORIGIN`; LAN IPs use PIN instead - Cross-origin Vercel UI uses Bearer tokens (not third-party cookies); Mac same-origin UI may still use session cookies -- When LAN bind is enabled, `ALLOWED_EMAILS` and `DASHBOARD_PIN` are required +- When LAN bind is enabled, `GOOGLE_CLIENT_ID`, `ALLOWED_EMAILS`, and `DASHBOARD_PIN` are required - Failed PIN attempts are rate-limited in memory per client IP - Usage-reset times use local Cursor / Codex / Claude credentials on this machine only to call those vendors' usage APIs; tokens are not sent to Agent Deck or any other service - Default LaunchAgent bind is LAN-capable (`HOST=0.0.0.0`) - use only on trusted Wi-Fi