From b391fd017bc3b77ca40c12e7b744aeddbda54b66 Mon Sep 17 00:00:00 2001 From: Lee Chapman Date: Sun, 11 Jan 2026 12:47:02 -0800 Subject: [PATCH 1/3] fix: add build target to Makefile The CI pipeline uses 'make build' but the Makefile didn't have a build target for Python projects. Error: make: *** No rule to make target 'build'. Stop. Changes: - Added build target that runs 'pip install --upgrade pip build' and 'python -m build' - Build depends on clean target to ensure fresh builds - Updated .PHONY declaration to include build - Updated help text to include build target --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cfd1d07..b58bede 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install dev test lint format clean docker-build docker-login docker-push docker-pull docker-run run check check-all commit-changes release venv +.PHONY: help install dev test lint format clean build docker-build docker-login docker-push docker-pull docker-run run check check-all commit-changes release venv # Force use of bash shell (required for make to work properly with line continuations) SHELL := /bin/bash @@ -67,6 +67,10 @@ format: $(BLACK) src tests $(RUFF) check --fix src tests +build: clean + pip install --upgrade pip build + python -m build + clean: find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find . -type f -name "*.pyc" -delete From 9b3535c3bc0639df0e150c73f885fd2f2472c363 Mon Sep 17 00:00:00 2001 From: Lee Chapman Date: Sun, 1 Feb 2026 23:35:46 -0800 Subject: [PATCH 2/3] feat: add Claude API proxy with key from k8s secret - Add /claude/* proxy to Anthropic API - CLAUDE_API_KEY from env (k8s secret) - GET /api/config returns claudeEnabled for UI - Add httpx dependency for async proxy --- pyproject.toml | 1 + src/app_readonly.py | 12 ++++++++++ src/claude_proxy.py | 58 +++++++++++++++++++++++++++++++++++++++++++++ src/config.py | 3 +++ 4 files changed, 74 insertions(+) create mode 100644 src/claude_proxy.py diff --git a/pyproject.toml b/pyproject.toml index fe8669a..abdd8eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "fastapi>=0.104.1", "uvicorn>=0.24.0", "requests>=2.31.0", + "httpx>=0.24.0", "python-dotenv>=1.0.0", "APScheduler>=3.10.4", "psycopg2-binary>=2.9.9", diff --git a/src/app_readonly.py b/src/app_readonly.py index 8c382f7..10fb031 100644 --- a/src/app_readonly.py +++ b/src/app_readonly.py @@ -5,6 +5,7 @@ from src.config import Config from src.database import Database from src.scraper import HellDivers2Scraper +from src.claude_proxy import router as claude_router # Configure logging logging.basicConfig( @@ -39,6 +40,9 @@ async def lifespan(app: FastAPI): lifespan=lifespan, ) +# Claude proxy (when CLAUDE_API_KEY is set) +app.include_router(claude_router) + # Add CORS middleware app.add_middleware( CORSMiddleware, @@ -382,6 +386,14 @@ async def get_biomes(): # ======================== +@app.get("/api/config", tags=["Config"]) +async def get_config(): + """Public config for UI (e.g. whether Claude is available via backend).""" + return { + "claudeEnabled": bool(Config.CLAUDE_API_KEY), + } + + @app.get("/api/health", tags=["Health"]) async def health_check(): """Health check endpoint""" diff --git a/src/claude_proxy.py b/src/claude_proxy.py new file mode 100644 index 0000000..85a798d --- /dev/null +++ b/src/claude_proxy.py @@ -0,0 +1,58 @@ +""" +Claude API proxy - forwards /claude/* to Anthropic API with API key from config. +The key is read from CLAUDE_API_KEY env (typically from a k8s secret). +""" +import logging +from fastapi import APIRouter, Request, HTTPException +from fastapi.responses import Response +import httpx +from src.config import Config + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/claude", tags=["Claude"]) +ANTHROPIC_BASE = "https://api.anthropic.com" + + +@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) +async def proxy_to_anthropic(request: Request, path: str): + """Proxy requests to Anthropic API, adding API key from server config.""" + api_key = Config.CLAUDE_API_KEY + if not api_key: + raise HTTPException( + status_code=503, + detail="Claude API key not configured. Set CLAUDE_API_KEY in environment.", + ) + + # Map /claude/messages -> /v1/messages + anthropic_path = f"/v1/{path}" if path else "/v1" + url = f"{ANTHROPIC_BASE}{anthropic_path}" + + # Forward headers, ensure API key is set (server key takes precedence) + headers = dict(request.headers) + headers.pop("host", None) + headers["x-api-key"] = api_key + headers["anthropic-dangerous-direct-browser-access"] = "true" + + try: + body = await request.body() + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.request( + method=request.method, + url=url, + headers=headers, + content=body, + ) + # Forward only safe headers (exclude transfer-encoding, connection, etc.) + forward_headers = { + k: v for k, v in response.headers.items() + if k.lower() not in ("transfer-encoding", "connection", "content-encoding") + } + return Response( + content=response.content, + status_code=response.status_code, + headers=forward_headers, + ) + except httpx.HTTPError as e: + logger.error(f"Claude proxy error: {e}") + raise HTTPException(status_code=502, detail=f"Upstream error: {str(e)}") diff --git a/src/config.py b/src/config.py index 72619db..818fc3e 100644 --- a/src/config.py +++ b/src/config.py @@ -25,6 +25,9 @@ class Config: # Logging LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + # Claude API (optional - for UI Claude integration via backend proxy) + CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY", "") + class DevelopmentConfig(Config): """Development configuration""" From fd41c761256a78d1f71c0b3a6d0d6745869fb767 Mon Sep 17 00:00:00 2001 From: Lee Chapman Date: Sun, 1 Feb 2026 23:47:04 -0800 Subject: [PATCH 3/3] chore: remove trailing whitespace in test_database.py --- tests/test_database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_database.py b/tests/test_database.py index 7bb7a14..ff42820 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -48,7 +48,7 @@ def test_save_war_status(self, temp_db, mock_psycopg2): data = {"war_id": 1, "status": "active"} result = temp_db.save_war_status(data) - + # Verify save was called assert result is True mock_cursor.execute.assert_called() @@ -74,7 +74,7 @@ def test_save_statistics(self, temp_db, mock_psycopg2): data = {"total_players": 1000, "total_kills": 50000, "missions_won": 2000} result = temp_db.save_statistics(data) - + # Verify save was called assert result is True mock_cursor.execute.assert_called()