Files

108 lines
6.8 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project overview
SmartUp is a web dashboard for monitoring API upstream rate groups and sending change notifications via webhooks (generic JSON / DingTalk bot). It supports multiple upstream auth types, periodic rate checking with APScheduler, snapshot diffing, and balance tracking with cost analysis.
**Sibling directories** (`new-api/`, `nox-api/`, `sub2api/`) are the Go-based AI API gateways that SmartUp monitors (upstreams) and syncs to (downstreams/websites). They are independent Go projects, not part of the SmartUp Python app. `browser-extension/` is a Chrome Manifest V3 extension for capturing auth credentials from real browser sessions.
When the Python app's interaction with upstream/downstream APIs is unclear — e.g. auth flows, response shapes, endpoint behavior — read the relevant Go source in `new-api/`, `nox-api/`, or `sub2api/` to understand what the upstream/downstream expects.
## Tech stack
- **Backend**: FastAPI (Python 3.12) + SQLAlchemy 2.0 (ORM, SQLite) + APScheduler
- **Frontend**: Vue 3 + Element Plus + Vite + Pinia + ECharts
- **Deploy**: Docker Compose single-container (multi-stage build: Node → Python)
## Commands
```bash
# Backend dev
cd backend && source ../.venv/bin/activate
uvicorn app.main:app --reload --port 8000
# Run all backend tests
.venv/bin/pytest backend/ -q
# Run a single test file
.venv/bin/pytest backend/test_upstream_token_refresh.py -q
# Run a specific test
.venv/bin/pytest backend/test_upstream_token_refresh.py::test_bearer_proactive_refresh_when_expiry_approaching -q
# Frontend dev
cd frontend && npm run dev # proxies to localhost:8000
# Docker (from repo root)
make up # start existing container
make up-build # rebuild image then start
make down log # stop / view logs
```
The `.venv` at repo root is the backend venv. Frontend uses its own `node_modules`.
## Architecture
### Backend (`backend/app/`)
**Entry point**: `main.py` — FastAPI app with CORS, lifespan (init DB, create admin, start scheduler), mount SPA static files at `/`, API docs at `/api/docs`.
**Layers**:
- `routers/` — FastAPI route handlers (thin, call services). Prefixes: `/api/upstreams`, `/api/websites`, `/api/webhooks`, `/api/logs`, `/api/auth`, `/api/custom-pages`, `/api/external-api-logs`, `/api/finance`, `/api/auth-capture`
- `services/` — Business logic. Key services:
- `upstream_client.py` (~1150 lines) — HTTP client for all upstream API calls. Auth types: `none`, `bearer`, `api_key`, `login_password`, `new_api_token`, `nox_token`, `cookie`. Handles rate/balance fetching, token refresh, proactive bearer refresh
- `scheduler.py` — APScheduler background checks. Calls `_check_upstream()` which fetches rates, diffs snapshots, triggers webhooks
- `snapshot_service.py` — Rate snapshot diffing and pruning
- `webhook_service.py` — Send notifications (generic JSON / DingTalk with HMAC signature)
- `auth_config.py` — Auth config masking, key whitelist per auth_type
- `finance_service.py` — Daily cost tracking (usage_stats or balance_delta mode)
- `website_client.py` / `website_sync.py` — Website group management and sync
- `models/` — SQLAlchemy 2.0 ORM (`mapped_column` style). Key models: `Upstream`, `UpstreamRateSnapshot`, `WebhookConfig`, `NotificationLog`, `Website`, `CustomPage`, `UpstreamGeneratedKey`, `UpstreamRechargeEvent`, `FinanceDailySummary`
- `schemas/` — Pydantic request/response schemas
- `utils/` — JWT auth (`auth.py`), DingTalk HMAC signature (`dingtalk.py`), number formatting
**Database**: SQLite with WAL mode, 64MB mmap, 20MB page cache. Schema migrations are handled inline in `database.py` (`_migrate_*` functions) — no Alembic. New columns/indexes are added via `ALTER TABLE IF NOT EXISTS` patterns.
**Auth**: JWT-based (python-jose). Admin user created on first startup from `ADMIN_EMAIL`/`ADMIN_PASSWORD` env vars. Each startup syncs the admin password hash from `.env` — changing `.env` admin password takes effect on restart.
**Upstream auth types** (in `upstream_client.py`):
| Type | Config fields | Refresh behavior |
|------|--------------|-----------------|
| `bearer` | token, refresh_token, expires_in, token_expires_at, refresh_path | Proactive 1h before expiry + 401 fallback |
| `login_password` | email, password | Refresh via refresh_token, fallback to re-login |
| `api_key` | key, header | None |
| `new_api_token` / `nox_token` | token, user_id, provider | None |
| `cookie` | cookie_string | None |
**Scheduler**: Single-threaded `BackgroundScheduler`. Jobs are added/removed dynamically when upstreams change. Each upstream has its own `check_interval_seconds`.
### Frontend (`frontend/src/`)
- **Router**: Vue Router with auth guard (`requiresAuth` meta). Routes: `/login`, `/upstreams`, `/websites`, `/webhooks`, `/logs`, `/external-api-logs`, `/custom-pages`, `/finance`
- **State**: Pinia `authStore` — token + email in localStorage
- **API**: Axios with retry (3x exponential backoff, GET/HEAD only). 401 interceptor clears auth + redirects to login. Base URL `/` (same-origin in Docker)
- **UI**: Element Plus component library, ECharts for charts, Lucide icons
- **Views**: Each major feature has its own view component (Upstreams, Websites, Webhooks, Finance, etc.)
### Deployment
- **Dockerfile**: Two-stage — Node 20 builds Vue app, Python 3.12-slim runs uvicorn. Frontend dist copied to `/app/static`. tini as PID 1.
- **docker-compose.yml**: Single `smartup` service, port `SERVER_PORT` (default 8899), volume mounts `./data` for SQLite persistence, healthcheck on `/healthz`
## Key patterns
- **Auth config persistence**: `UpstreamClient` takes `on_auth_config_update` callback. The scheduler's callback persists refreshed tokens to DB via `normalize_auth_config()`.
- **401 retry in client**: `_send_request()` catches 401 and calls the appropriate refresh method, then retries once. For bearer, this is `_refresh_bearer_token()`; for login_password, it tries refresh then re-login.
- **Snapshot diffing**: Rates fetched from upstream → `build_snapshot()` → compare to latest snapshot via `diff_snapshots()` → fire webhooks on changes.
- **Secret masking**: `auth_config.py` defines `MASK` sentinel. Sensitive fields are replaced with `***` in API responses. `normalize_auth_config()` strips masks before saving.
## Testing
Tests live alongside backend code as `backend/test_*.py`. They use pytest with `monkeypatch` for dependency injection. Test HTTP clients use a `FakeHttpClient` pattern (see `test_upstream_token_refresh.py`) that records calls and returns canned responses. No test database — tests mock at the HTTP layer.
## Environment variables
See `.env.example`. Required: `ADMIN_PASSWORD`, `JWT_SECRET`. Optional: `SERVER_PORT` (8899), `UNHEALTHY_THRESHOLD` (3), `TZ` (Asia/Shanghai), `CORS_ORIGINS`.