diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..fce40688 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY scanner.py cli.py dashboard.py ./ + +ENV HOST=0.0.0.0 +ENV PORT=8080 +ENV CLAUDE_USAGE_DB=/data/usage.db + +EXPOSE 8080 + +CMD ["python3", "cli.py", "dashboard", "--no-browser"] diff --git a/README.md b/README.md index e2ffe9a4..8b45ebca 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,18 @@ cd claude-usage python cli.py dashboard ``` +### Docker +``` +git clone https://github.com/phuryn/claude-usage +cd claude-usage +bash scripts/run-docker.sh +``` + +Opens the dashboard at **http://localhost:9898**. + +The script builds the image, then runs the container with: +- `~/.claude` mounted **read-only** — the container can read your transcripts but cannot modify them +- A named Docker volume (`claude-usage-data`) for the SQLite database — persisted across restarts, isolated from your home directory --- @@ -155,3 +167,5 @@ See [vscode-extension/README.md](vscode-extension/README.md) for settings, comma | `cli.py` | `scan`, `today`, `stats`, `dashboard` commands | | `Formula/claude-usage.rb` | Homebrew formula — install with `brew install --formula ` | | `vscode-extension/` | VS Code extension — embeds the dashboard inside VS Code | +| `Dockerfile` | Container image definition | +| `scripts/run-docker.sh` | Build and run the dashboard in Docker with a read-only `~/.claude` mount | diff --git a/cli.py b/cli.py index 98f3a124..b2bfbddf 100644 --- a/cli.py +++ b/cli.py @@ -16,7 +16,7 @@ from scanner import VERSION -DB_PATH = Path.home() / ".claude" / "usage.db" +DB_PATH = Path(os.environ.get("CLAUDE_USAGE_DB", Path.home() / ".claude" / "usage.db")) PRICING = { # Fable / Mythos — Anthropic's most capable class, priced at 2x Opus. diff --git a/dashboard.py b/dashboard.py index e2ac2f6f..01ad139c 100644 --- a/dashboard.py +++ b/dashboard.py @@ -12,7 +12,7 @@ from scanner import VERSION -DB_PATH = Path.home() / ".claude" / "usage.db" +DB_PATH = Path(os.environ.get("CLAUDE_USAGE_DB", Path.home() / ".claude" / "usage.db")) # Which surface is rendering the dashboard: "web" (standalone `cli.py dashboard`) # or "vscode" (embedded in the extension's sidebar webview). serve() sets this diff --git a/scanner.py b/scanner.py index 72747cda..22b0812b 100644 --- a/scanner.py +++ b/scanner.py @@ -19,7 +19,7 @@ PROJECTS_DIR = Path.home() / ".claude" / "projects" XCODE_PROJECTS_DIR = Path.home() / "Library" / "Developer" / "Xcode" / "CodingAssistant" / "ClaudeAgentConfig" / "projects" -DB_PATH = Path.home() / ".claude" / "usage.db" +DB_PATH = Path(os.environ.get("CLAUDE_USAGE_DB", Path.home() / ".claude" / "usage.db")) DEFAULT_PROJECTS_DIRS = [PROJECTS_DIR, XCODE_PROJECTS_DIR] # Higher number = higher priority when choosing a session's primary model. diff --git a/scripts/run-docker.sh b/scripts/run-docker.sh new file mode 100755 index 00000000..4dd26389 --- /dev/null +++ b/scripts/run-docker.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE="claude-usage" +CONTAINER="claude-usage" +NETWORK="claude-usage-net" +PORT=9898 + +echo "▶ Checking for running container..." +if docker ps -q --filter "name=^${CONTAINER}$" | grep -q .; then + echo "⏹ Stopping ${CONTAINER}..." + docker stop "$CONTAINER" +fi + +echo "🔗 Ensuring isolated network..." +if ! docker network inspect "$NETWORK" &>/dev/null; then + docker network create \ + --opt com.docker.network.bridge.enable_ip_masquerade=false \ + "$NETWORK" +fi + +echo "⬇ Pulling latest..." +cd "$REPO_DIR" +git pull + +echo "🔨 Building image..." +docker build -t "$IMAGE" . + +echo "🚀 Starting container..." +docker run --rm -d \ + --name "$CONTAINER" \ + --network "$NETWORK" \ + -p "$PORT:8080" \ + -v "$HOME/.claude:/root/.claude:ro" \ + -v "${CONTAINER}-data:/data" \ + -e HOST=0.0.0.0 \ + "$IMAGE" + +echo "✅ Running at http://localhost:${PORT}"