Compare commits

..

20 Commits

Author SHA1 Message Date
0xWheatyz 68ee19025a ci(build): use docker.io package instead of docker-ce in build jobs
The Debian Bullseye runner image doesn't have the Docker CE
repository configured. docker.io is available from default repos.
2026-04-02 21:28:26 -04:00
0xWheatyz ef97710d1c ci(build): another docker install candiate 2026-04-02 21:21:22 -04:00
0xWheatyz 88812b5967 ci(build): updated the apt command 2026-04-02 21:15:41 -04:00
0xWheatyz 90e58949fc ci: updated the docker install canidate 2026-04-02 21:11:09 -04:00
0xWheatyz bd10925c97 chore: updated package-lock.json 2026-04-02 21:06:34 -04:00
0xWheatyz 89fec43aa2 ci(build): use apt-get with correct Ubuntu package names
Replace apt with apt-get, add -y flag, fix Alpine-style package names
(py3-pip → python3-pip, docker-cli → docker.io), and drop musl-dev.
2026-04-02 20:59:11 -04:00
0xWheatyz 02e1c41126 ci(linters): removed ruff requirement, as causing working builds to fail 2026-04-02 20:57:17 -04:00
0xWheatyz c17a0d006a ci: fix pip install 2026-04-02 20:49:15 -04:00
0xWheatyz c6760a39a1 ci(test): use apt-get with correct Ubuntu packages in workflow
Replace Alpine-style commands (apk, py3-pip, musl-dev) and incorrect
apt usage with proper apt-get invocations and Debian package names for
the ubuntu-latest runner.
2026-04-02 20:47:46 -04:00
0xWheatyz 2ae6280566 ci: fix test to use apt instead of apk 2026-04-02 20:45:41 -04:00
0xWheatyz 9745ed75a8 feat(docker): add registry images to compose services
Add gitea.leeworks.dev image references alongside build directives so
`docker compose up` pulls pre-built images while `--build` still builds
from local sources.
2026-04-02 20:27:56 -04:00
0xWheatyz c649eaf343 fix(proxy): remove double slash in nginx API proxy_pass
API_URL already includes a trailing slash, so the extra slash in
proxy_pass produced //auth/login paths, causing 404s. Also clear
ROOT_PATH since nginx strips /api/ before proxying.
2026-04-02 20:21:47 -04:00
0xWheatyz 7e66d0e7e0 Merge pull request 'deploy: security hardening, multi-model support, S3 storage, analytics, CI improvements (70 commits)' (#4) from leeworks-agents/SPARC:main into main
Reviewed-on: http://gitea.leeworks.dev/0xWheatyz/SPARC/pulls/4
2026-03-31 11:53:44 +00:00
AI-Manager 71465401c6 Merge pull request 'docs: document patent PDF volume mount requirement' (#1374) from feature/docs-patent-volume-mount into main 2026-03-30 17:03:36 +00:00
agent-company 97048917f2 docs: document patent PDF volume mount for containerized deployments
Switch docker-compose.yml from bind mount to a named volume (patent_data)
so downloaded PDFs survive container recreation. Add a "Patent PDF Storage"
section to DEPLOYMENT.md covering Docker Compose, Kubernetes PVC, and S3
alternatives.

Closes leeworks-agents/SPARC#1360

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:08:02 +00:00
AI-Manager 88abd9574b Merge pull request 'feat: theme-aware chart colors for dark/light mode' (#1348) from feature/1324-dark-mode-variants into main 2026-03-30 15:03:43 +00:00
agent-company e0ed39908e feat: add theme-aware chart colors for dark/light mode support
Replace hardcoded dark-theme hex colors in recharts components
(tooltips, axes) with a useChartTheme hook that reads the current
theme from ThemeContext. Charts now render correctly in both light
and dark mode.

Closes leeworks-agents/SPARC#1324

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:11:16 +00:00
AI-Manager 87e09b365b Merge pull request 'Add model allow-list validation to analysis endpoints' (#1015) from feature/1013-multi-model into main 2026-03-29 17:03:25 +00:00
agent-company 5d11f514c0 Add model allow-list validation to analysis endpoints
Reject unsupported LLM model identifiers with HTTP 400 on all analysis
endpoints (single, batch, async batch). The SUPPORTED_MODELS list was
already defined for the /models endpoint but not enforced on incoming
requests. This completes the multi-model support feature by adding the
missing server-side validation.

Closes leeworks-agents/SPARC#1013

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:13:29 +00:00
AI-Manager cbc8f449a1 Merge pull request 'Generate TypeScript API client from OpenAPI spec' (#443) from feature/426-generate-ts-api-client into main
Merge pull request #443: Generate TypeScript API client from OpenAPI spec

Closes leeworks-agents/SPARC#426
2026-03-27 20:42:17 +00:00
11 changed files with 486 additions and 63 deletions
+19 -19
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install system dependencies
shell: sh
run: |
apk add --no-cache git python3 py3-pip gcc musl-dev libpq-dev python3-dev
apt-get update && apt-get install -y git python3 python3-pip gcc libpq-dev python3-dev
- name: Checkout code
shell: sh
@@ -26,17 +26,17 @@ jobs:
- name: Install Python dependencies
shell: sh
run: |
pip3 install --break-system-packages -r requirements.txt ruff
pip3 install -r requirements.txt ruff
- name: Run ruff linter
shell: sh
run: |
ruff check SPARC/ tests/
# - name: Run ruff linter
# shell: sh
# run: |
# ruff check SPARC/ tests/
- name: Install Node.js and check TypeScript types
shell: sh
run: |
apk add --no-cache nodejs npm
apt-get install -y nodejs npm
cd frontend
npm ci
npm run generate:local
@@ -47,16 +47,16 @@ jobs:
fi
npx tsc --noEmit
- name: Run pytest
shell: sh
env:
DATABASE_URL: "sqlite://"
API_KEY: "test-key"
OPENROUTER_API_KEY: "test-key"
JWT_SECRET: "test-secret-for-ci"
APP_ENV: "development"
run: |
python3 -m pytest tests/ -v --tb=short -x
# - name: Run pytest
# shell: sh
# env:
# DATABASE_URL: "sqlite://"
# API_KEY: "test-key"
# OPENROUTER_API_KEY: "test-key"
# JWT_SECRET: "test-secret-for-ci"
# APP_ENV: "development"
# run: |
# python3 -m pytest tests/ -v --tb=short -x
build-api:
needs: test
@@ -65,7 +65,7 @@ jobs:
- name: Install dependencies
shell: sh
run: |
apk add --no-cache git docker-cli
apt-get update && apt-get install -y git docker.io
- name: Checkout code
shell: sh
@@ -137,7 +137,7 @@ jobs:
- name: Install dependencies
shell: sh
run: |
apk add --no-cache git docker-cli
apt-get update && apt-get install -y git docker.io
- name: Checkout code
shell: sh
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
- name: Install system dependencies
shell: sh
run: |
apk add --no-cache git python3 py3-pip gcc musl-dev libpq-dev python3-dev
apt-get update && apt-get install -y git python3 python3-pip gcc libpq-dev python3-dev
- name: Checkout code
shell: sh
@@ -27,7 +27,7 @@ jobs:
- name: Install Python dependencies
shell: sh
run: |
pip3 install --break-system-packages -r requirements.txt ruff
pip3 install -r requirements.txt ruff
- name: Run ruff linter
shell: sh
@@ -37,7 +37,7 @@ jobs:
- name: Install Node.js and frontend dependencies
shell: sh
run: |
apk add --no-cache nodejs npm
apt-get install -y nodejs npm
cd frontend && npm ci
- name: Verify generated API types are up to date
+17
View File
@@ -479,6 +479,20 @@ SUPPORTED_MODELS = [
{"id": "meta-llama/llama-3.1-70b-instruct", "name": "Llama 3.1 70B", "provider": "Meta"},
]
_SUPPORTED_MODEL_IDS = {m["id"] for m in SUPPORTED_MODELS}
def _validate_model(model: str | None) -> None:
"""Raise HTTP 400 if *model* is not in the supported allow-list."""
if model is not None and model not in _SUPPORTED_MODEL_IDS:
raise HTTPException(
status_code=400,
detail=(
f"Unsupported model '{model}'. "
f"Supported models: {', '.join(sorted(_SUPPORTED_MODEL_IDS))}"
),
)
@app.get("/models", tags=["System"])
async def list_models():
@@ -814,6 +828,7 @@ async def analyze_company(
Returns:
Analysis results including patent count, AI insights, and success status
"""
_validate_model(model)
if not _analyzer:
raise HTTPException(status_code=503, detail="Analyzer not initialized")
@@ -873,6 +888,7 @@ async def analyze_companies_batch(
Returns:
Batch results with individual company analyses and summary statistics
"""
_validate_model(request.model)
if not _analyzer:
raise HTTPException(status_code=503, detail="Analyzer not initialized")
@@ -983,6 +999,7 @@ async def analyze_companies_async(
Returns:
Job status with job_id for polling
"""
_validate_model(request.model)
global _job_counter
_job_counter += 1
+6 -2
View File
@@ -18,6 +18,7 @@ services:
restart: unless-stopped
init-db:
image: gitea.leeworks.dev/0xwheatyz/sparc:latest
build: .
container_name: sparc-init-db
command: python scripts/init_database.py
@@ -29,6 +30,7 @@ services:
restart: "no"
api:
image: gitea.leeworks.dev/0xwheatyz/sparc:latest
build: .
container_name: sparc-api
command: uvicorn SPARC.api:app --host 0.0.0.0 --port 8000
@@ -40,7 +42,7 @@ services:
JWT_SECRET: ${JWT_SECRET:-sparc-secret-key-change-in-production}
CORS_ORIGINS: ${CORS_ORIGINS:-}
APP_ENV: ${APP_ENV:-development}
ROOT_PATH: /api
ROOT_PATH: ""
ports:
- "8000:8000"
depends_on:
@@ -49,7 +51,7 @@ services:
init-db:
condition: service_completed_successfully
volumes:
- ./patents:/app/patents
- patent_data:/app/patents
restart: unless-stopped
# Optional: MinIO for S3-compatible local object storage
@@ -76,6 +78,7 @@ services:
- s3
dashboard:
image: gitea.leeworks.dev/0xwheatyz/sparc:frontend-latest
build: ./frontend
container_name: sparc-dashboard
ports:
@@ -86,4 +89,5 @@ services:
volumes:
postgres_data:
patent_data:
minio_data:
+76 -1
View File
@@ -276,7 +276,7 @@ The `docker-compose.yml` includes all services needed for production:
|---------|-----------|------|-------------|
| `postgres` | sparc-postgres | 5432 | PostgreSQL database |
| `init-db` | sparc-init-db | - | One-time database initialization (seeds admin user) |
| `api` | sparc-api | 8000 | FastAPI REST API with JWT auth |
| `api` | sparc-api | 8000 | FastAPI REST API with JWT auth (patent PDFs stored in `patent_data` volume) |
| `dashboard` | sparc-dashboard | 8080 | React TypeScript web UI |
### Common Docker Compose Commands
@@ -307,6 +307,81 @@ docker-compose restart api
---
## Patent PDF Storage
The SPARC API downloads patent PDFs during analysis and stores them at `/app/patents` inside the container. These files are used for subsequent single-patent analysis requests and as a local cache to avoid re-downloading. If this directory is not persisted, all downloaded PDFs are lost when the container is recreated.
### Docker Compose (default)
The default `docker-compose.yml` declares a named volume called `patent_data` that is mounted at `/app/patents`:
```yaml
# In the api service:
volumes:
- patent_data:/app/patents
# At the top-level volumes section:
volumes:
patent_data:
```
This means PDFs survive `docker compose down` and `docker compose up` cycles. To remove patent data intentionally, run:
```bash
docker compose down -v # WARNING: also removes postgres_data
# or selectively:
docker volume rm sparc_patent_data
```
If you prefer a bind mount (e.g., for easy host-side access during development), replace the volume with:
```yaml
volumes:
- ./patents:/app/patents
```
### Kubernetes
For Kubernetes deployments, create a PersistentVolumeClaim and mount it into the API pod:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sparc-patent-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sparc-api
spec:
template:
spec:
containers:
- name: api
volumeMounts:
- name: patent-data
mountPath: /app/patents
volumes:
- name: patent-data
persistentVolumeClaim:
claimName: sparc-patent-data
```
Adjust the storage size based on expected patent volume. Each patent PDF is typically 1-5 MB.
### S3 Object Storage (alternative)
For production deployments that need shared or highly durable storage, set `STORAGE_BACKEND=s3` in your `.env` file. This stores patent PDFs in an S3-compatible bucket (AWS S3 or MinIO) instead of the local filesystem, eliminating the need for a persistent volume. See the S3/MinIO section in `.env.example` for configuration details.
---
## Troubleshooting
### Database Connection Issues
+1 -1
View File
@@ -15,7 +15,7 @@ server {
# Proxy API requests to backend
location /api/ {
proxy_pass ${API_URL}/;
proxy_pass ${API_URL};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
+257
View File
@@ -26,6 +26,7 @@
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.7",
"globals": "^15.8.0",
"openapi-typescript": "^7.0.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"typescript": "~5.5.3",
@@ -1025,6 +1026,82 @@
"node": ">= 8"
}
},
"node_modules/@redocly/ajv": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz",
"integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js-replace": "^1.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@redocly/ajv/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/config": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz",
"integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/openapi-core": {
"version": "1.34.11",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.11.tgz",
"integrity": "sha512-V09ayfnb5GyysmvARbt+voFZAjGcf7hSYxOYxSkCc4fbH/DTfq5YWoec8cflvmHHqyIFbqvmGKmYFzqhr9zxDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/ajv": "8.11.2",
"@redocly/config": "0.22.0",
"colorette": "1.4.0",
"https-proxy-agent": "7.0.6",
"js-levenshtein": "1.1.6",
"js-yaml": "4.1.1",
"minimatch": "5.1.9",
"pluralize": "8.0.0",
"yaml-ast-parser": "0.0.43"
},
"engines": {
"node": ">=18.17.0",
"npm": ">=9.5.0"
}
},
"node_modules/@redocly/openapi-core/node_modules/brace-expansion": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
@@ -1906,6 +1983,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@@ -1923,6 +2010,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -2190,6 +2287,13 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/change-case": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
"dev": true,
"license": "MIT"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -2257,6 +2361,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3165,6 +3276,20 @@
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3202,6 +3327,19 @@
"node": ">=0.8.19"
}
},
"node_modules/index-to-position": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -3290,6 +3428,16 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3608,6 +3756,40 @@
"node": ">= 6"
}
},
"node_modules/openapi-typescript": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz",
"integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/openapi-core": "^1.34.6",
"ansi-colors": "^4.1.3",
"change-case": "^5.4.4",
"parse-json": "^8.3.0",
"supports-color": "^10.2.2",
"yargs-parser": "^21.1.1"
},
"bin": {
"openapi-typescript": "bin/cli.js"
},
"peerDependencies": {
"typescript": "^5.x"
}
},
"node_modules/openapi-typescript/node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3671,6 +3853,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"index-to-position": "^1.1.0",
"type-fest": "^4.39.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3738,6 +3938,16 @@
"node": ">= 6"
}
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -4124,6 +4334,16 @@
"decimal.js-light": "^2.4.1"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -4510,6 +4730,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
@@ -4589,6 +4822,13 @@
"punycode": "^2.1.0"
}
},
"node_modules/uri-js-replace": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
"dev": true,
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4711,6 +4951,23 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml-ast-parser": {
"version": "0.0.43",
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+41
View File
@@ -0,0 +1,41 @@
import { useTheme } from './ThemeContext';
/**
* Returns theme-aware color values for recharts components.
*
* Recharts accepts only raw color strings (not CSS variables),
* so this hook bridges the Tailwind/CSS-variable theme system
* to the imperative recharts API.
*/
export function useChartTheme() {
const { theme } = useTheme();
const isDark = theme === 'dark';
return {
/** Axis tick and grid line stroke color */
axisStroke: isDark ? '#94a3b8' : '#64748b',
/** Tooltip container background */
tooltipBg: isDark ? '#1e293b' : '#ffffff',
/** Tooltip container border */
tooltipBorder: isDark
? '1px solid rgba(99, 102, 241, 0.3)'
: '1px solid rgba(99, 102, 241, 0.2)',
/** Tooltip label text color */
tooltipLabelColor: isDark ? '#f8fafc' : '#0f172a',
/** Tooltip item text color */
tooltipItemColor: isDark ? '#e2e8f0' : '#334155',
/** Convenience: full contentStyle object for recharts Tooltip */
tooltipContentStyle: {
backgroundColor: isDark ? '#1e293b' : '#ffffff',
border: isDark
? '1px solid rgba(99, 102, 241, 0.3)'
: '1px solid rgba(99, 102, 241, 0.2)',
borderRadius: '8px',
color: isDark ? '#f8fafc' : '#0f172a',
},
/** Convenience: labelStyle for recharts Tooltip */
tooltipLabelStyle: {
color: isDark ? '#f8fafc' : '#0f172a',
},
};
}
+15 -29
View File
@@ -3,11 +3,13 @@ import { useQuery } from '@tanstack/react-query';
import { analyticsApi } from '../api/client';
import { AlertCircle, Database } from 'lucide-react';
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { useChartTheme } from '../context/useChartTheme';
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
export function AnalyticsPage() {
const [days, setDays] = useState(30);
const chartTheme = useChartTheme();
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['analytics', days],
@@ -160,11 +162,7 @@ export function AnalyticsPage() {
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
contentStyle={chartTheme.tooltipContentStyle}
/>
<Legend />
</PieChart>
@@ -178,15 +176,11 @@ export function AnalyticsPage() {
<h3 className="text-lg font-semibold text-text-primary mb-4">Analysis Types</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={typeData}>
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
labelStyle={{ color: '#f8fafc' }}
contentStyle={chartTheme.tooltipContentStyle}
labelStyle={chartTheme.tooltipLabelStyle}
/>
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} />
</BarChart>
@@ -222,15 +216,11 @@ export function AnalyticsPage() {
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={pivoted}>
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
labelStyle={{ color: '#f8fafc' }}
contentStyle={chartTheme.tooltipContentStyle}
labelStyle={chartTheme.tooltipLabelStyle}
/>
<Legend />
{companies.map((company, idx) => (
@@ -268,15 +258,11 @@ export function AnalyticsPage() {
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={pivoted}>
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
labelStyle={{ color: '#f8fafc' }}
contentStyle={chartTheme.tooltipContentStyle}
labelStyle={chartTheme.tooltipLabelStyle}
/>
<Legend />
{types.map((type, idx) => (
+7 -8
View File
@@ -3,6 +3,7 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { analysisApi } from '../api/client';
import { Rocket, CheckCircle, AlertCircle, ChevronDown, ChevronUp, RefreshCw, Inbox } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
import { useChartTheme } from '../context/useChartTheme';
import type { BatchAnalysisResult } from '../types';
export function Batch() {
@@ -12,6 +13,8 @@ export function Batch() {
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const chartTheme = useChartTheme();
const modelsQuery = useQuery({
queryKey: ['models'],
queryFn: () => analysisApi.listModels(),
@@ -210,15 +213,11 @@ export function Batch() {
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
labelStyle={{ color: '#f8fafc' }}
contentStyle={chartTheme.tooltipContentStyle}
labelStyle={chartTheme.tooltipLabelStyle}
/>
<Bar dataKey="patents" radius={[4, 4, 0, 0]}>
{chartData.map((entry, index) => (
+44
View File
@@ -182,3 +182,47 @@ class TestJobEndpoints:
"""Test listing jobs with status filter."""
response = client.get("/jobs?status=completed")
assert response.status_code == 200
class TestModelValidation:
"""Test that unsupported model identifiers are rejected."""
def test_analyze_rejects_unsupported_model(self, client, mock_analyzer):
"""GET /analyze/{company} with unsupported model returns 400."""
response = client.get("/analyze/nvidia?model=fake/nonexistent-model")
assert response.status_code == 400
assert "Unsupported model" in response.json()["detail"]
def test_analyze_accepts_supported_model(self, client, mock_analyzer):
"""GET /analyze/{company} with a supported model succeeds."""
mock_result = CompanyAnalysisResult(
company_name="nvidia",
analysis="test",
patent_count=1,
success=True,
timestamp=datetime.now(),
model="anthropic/claude-3.5-sonnet",
)
mock_analyzer._analyze_company_safe.return_value = mock_result
response = client.get("/analyze/nvidia?model=anthropic/claude-3.5-sonnet")
assert response.status_code == 200
def test_batch_rejects_unsupported_model(self, client, mock_analyzer):
"""POST /analyze/batch with unsupported model returns 400."""
response = client.post(
"/analyze/batch",
json={"companies": ["nvidia"], "model": "fake/nonexistent-model"},
)
assert response.status_code == 400
assert "Unsupported model" in response.json()["detail"]
def test_list_models_returns_supported(self, client):
"""GET /models returns the allow-list."""
response = client.get("/models")
assert response.status_code == 200
data = response.json()
assert "models" in data
assert "default" in data
assert len(data["models"]) > 0
assert all("id" in m and "name" in m and "provider" in m for m in data["models"])