167 lines
9.7 KiB
Markdown
167 lines
9.7 KiB
Markdown
# CLAUDE.md — Nike
|
|
|
|
🐾 A Heluca repo (project #205). Orientation before action; verify before
|
|
asserting. The universal standards in `~/.claude/CLAUDE.md` apply; this file
|
|
records what is **specific to Nike** and where it diverges from the other
|
|
FastAPI/MCP apps in the estate (Hecate, Periplus, Argos).
|
|
|
|
## What Nike is
|
|
|
|
Nike is a **football (soccer) data platform** — a read-through cache and MCP
|
|
front-end over the external **TheSportsDB** API, with a SvelteKit dashboard. One
|
|
FastAPI process on `0.0.0.0:{PORT}` hosts three surfaces:
|
|
|
|
- **`/`** — the SvelteKit dashboard (`dashboard/build`, static SPA).
|
|
- **`/api/*`** — JSON API the dashboard consumes (status, tools, logs, tool-runner, telemetry).
|
|
- **`/mcp`** — FastMCP streamable-HTTP endpoint; the eight football tools.
|
|
|
|
Data flow: an MCP tool calls `sportsdb.py` (the TheSportsDB client), which is
|
|
fronted by a **two-layer cache** — a short-lived in-memory TTL cache in
|
|
`sportsdb.py` and a **PostgreSQL cache** in `db.py` for permanent data (teams,
|
|
players, leagues, events). The tool formats the result as text for the LLM.
|
|
|
|
Shape-wise Nike is a cousin of Hecate/Periplus (FastAPI + FastMCP + SvelteKit +
|
|
PostgreSQL + HAProxy + systemd), but three things set it apart and drive most of
|
|
the rules below: it is **a cache in front of a third-party API**, its **tools are
|
|
synchronous and return plain strings**, and its **config/DB/HTTP stack is the
|
|
sync `os.getenv` + `psycopg2` + `requests` generation**, not
|
|
pydantic-settings/SQLAlchemy/httpx.
|
|
|
|
Modules: `config.py` (settings), `sportsdb.py` (external client + TTL cache),
|
|
`db.py` (PG pool + cache helpers), `logging_config.py`, `server.py` (FastAPI +
|
|
all MCP tools + routes). `schema.sql` is the cache DDL. Keep it this flat.
|
|
|
|
## Red Panda Approval™
|
|
|
|
Canonical rubric: [docs/red_panda_standards.md](docs/red_panda_standards.md).
|
|
The README's emoji "under 500 lines 🚨" box is not the standard. Nike is already
|
|
one of the more estate-conformant apps — hold that bar:
|
|
|
|
1. **Fresh Environment Test** — from a clean checkout: `pip install -e .`,
|
|
`scripts/apply_schema.py` provisions the cache DB, `cd dashboard && npm run
|
|
build`, then `python run.py` serves dashboard + MCP. The dashboard **must be
|
|
built** before FastAPI can serve it (unbuilt → the index route returns a 503
|
|
with build instructions, by design). A schema change must apply cleanly on a
|
|
fresh DB.
|
|
2. **Elegant Simplicity** — one client function per TheSportsDB endpoint; tools
|
|
compose them. Don't inline a second `requests` call or a second cache in a tool.
|
|
3. **Observable & Debuggable** — structured JSON logging (`logging_config.py`),
|
|
`nike_*` Prometheus metrics, an in-memory request log (`/api/logs`), and the
|
|
dashboard status cards. A tool logs its name + args + duration via `_log(...)`.
|
|
4. **Consistent Patterns** — new tools look like the eight existing: `@mcp.tool(
|
|
annotations=ToolAnnotations(readOnlyHint=True))`, synchronous `def`, resolve
|
|
team → call `sportsdb` → format a text table, `_log(...)` before returning.
|
|
5. **Actually Works** — against the real TheSportsDB (free key `3` for basic,
|
|
premium for the ★ tools). The free key returns limited data; verify premium-gated
|
|
tools with an actual premium key or acknowledge you couldn't.
|
|
|
|
## The landmines (what breaks if you don't know it)
|
|
|
|
- **Tools return plain formatted `str`, not JSON and not Pydantic models.** Nike's
|
|
tools build human-readable ASCII tables/blocks for the LLM
|
|
(`"=== {team} ==="`, aligned standings columns). This is a *third* return
|
|
convention in the estate — don't "fix" a tool to return JSON or a model. Match
|
|
the existing formatting style. See [.claude/rules/mcp-tools.md](.claude/rules/mcp-tools.md).
|
|
|
|
- **Tools are synchronous `def`** even though the app is async — `sportsdb`
|
|
(`requests`) and `db` (`psycopg2`) are blocking. Don't convert tools to `async`.
|
|
(FastMCP runs them in a threadpool.) The `/api/run` tool-runner calls
|
|
`tool.fn(**args)` directly — keep tools plain callables.
|
|
|
|
- **Read-through cache: permanent vs volatile.** Permanent data (teams, players,
|
|
leagues, events) is cached in PostgreSQL — `_resolve_team` checks the DB first,
|
|
hits the API on a miss, then caches. **Volatile data is always fetched live and
|
|
never cached to PG**: standings, livescores, current fixtures. Don't add
|
|
standings/livescores to the PG cache, and don't skip the cache for team/player
|
|
lookups. See [.claude/rules/cache-api.md](.claude/rules/cache-api.md).
|
|
|
|
- **Rows come in two shapes.** A team/player dict may be a **raw TheSportsDB dict**
|
|
(`strTeam`, `idTeam`, `strLeague`) or a **cached DB row** (`name`, `id`,
|
|
`league_name`). The tools read both with `x.get("strTeam") or x.get("name")`.
|
|
Preserve that dual-shape handling when you touch formatting — a cache hit and a
|
|
cache miss must render identically.
|
|
|
|
- **Free vs premium key gate.** `config.SPORTSDB_KEY in ('3', '')` means "free
|
|
key." Premium-only tools (`get_match_detail`, `get_livescores`, V2 squad lists)
|
|
check this and return a clear "requires a premium key" message, or degrade to
|
|
cached data (`get_roster` falls back to the DB roster). Premium tools are also
|
|
`tags={"premium"}` so the dashboard can flag them. Keep both the runtime check
|
|
and the tag in sync when adding a premium tool.
|
|
|
|
- **Per-API-call `try/except … pass` is deliberate.** Each external call inside a
|
|
tool is individually guarded so a partial failure yields partial results, not a
|
|
dead tool. Don't "clean this up" into one big try or into hard failures — a
|
|
missing fixtures endpoint shouldn't blank the whole team page. (It's the one
|
|
place broad `except` is correct; elsewhere, catch narrowly and log.)
|
|
|
|
- **The tool catalogue is derived, single-source.** `/api/tools` and the MCP
|
|
`instructions`/`football_analyst` prompt all enumerate the tools; the dashboard
|
|
reads `/api/tools` (from `mcp.list_tools()`). When you add/rename a tool, the
|
|
catalogue updates itself — but **update the human-written `instructions` string
|
|
and the `football_analyst` prompt by hand**, they don't.
|
|
|
|
## Observability & edge (the estate-conformant parts — keep them)
|
|
|
|
- **`/metrics` is Prometheus exposition, IP-restricted in Python.**
|
|
`_METRICS_ALLOWED_NETS` = `10.10.0.0/24`, `172.16.0.0/12`, `127.0.0.0/8`,
|
|
`::1/128` (the standard four ranges) — no auth. The middleware **skips**
|
|
`/live`, `/ready`, `/metrics`, `/mcp` from request metrics (health probes must
|
|
not inflate counters — `red_panda_standards.md`). Keep both.
|
|
- **Health routes register both slash forms** (`/live` + `/live/`, `/ready` +
|
|
`/ready/`). `/ready` checks the DB and returns 503 if it's down.
|
|
- **Structured logging is re-applied inside the lifespan** (`configure_logging`
|
|
again after `db.create_pool`) because uvicorn's `config.load()` overwrites
|
|
handlers — the estate FastAPI logging landmine. Don't remove the second call.
|
|
- **HAProxy is the edge.** `proxy_headers=True` + `forwarded_allow_ips=
|
|
TRUSTED_PROXY_IPS`. The `/api/*` write routes are **not individually
|
|
authenticated** — access control is at HAProxy. If Nike is ever exposed directly,
|
|
those write endpoints (`/api/run`, `/api/cache/invalidate`) need protection. The
|
|
telemetry endpoint (`/api/v1/telemetry`) is intentionally unprotected
|
|
(`sendBeacon` can't set headers).
|
|
|
|
## Config & deploy
|
|
|
|
- **Config is `os.getenv` + `python-dotenv`, NOT pydantic-settings.** `config.py`
|
|
loads `.env` from the repo root and reads `NIKE_*` vars into module constants.
|
|
A new knob = a new `os.getenv("NIKE_...", default)` **plus** a line in
|
|
`.env.example`. Do **not** introduce a `Settings` model. `.env` is gitignored
|
|
(correctly — only `.env.example` is tracked); never commit real secrets.
|
|
- **DB access is `psycopg2` with a `ThreadedConnectionPool`**, checked out via the
|
|
`get_conn()` contextmanager (commit-on-success / rollback-on-exception). No
|
|
SQLAlchemy, no async DB. New queries use `get_conn()` and parameterised SQL.
|
|
- **Ships as systemd** (`nike.service`) and also has a `Dockerfile` +
|
|
`docker-compose.yml`. See [.claude/rules/config-deploy.md](.claude/rules/config-deploy.md)
|
|
for the drift in the checked-in unit.
|
|
|
|
## README & code drift — trust the code
|
|
|
|
- The health-check prose is Django/K8s boilerplate: `/ready/` does not "validate
|
|
cache connectivity" and `/live/` has nothing to do with "Django responding."
|
|
`/ready` checks the PG cache DB; `/live` is a bare 200.
|
|
- The emoji file-size rubric is not the real standard.
|
|
|
|
When README and code disagree, the code wins. Fixing the README is worthwhile —
|
|
flag it, don't fold it in.
|
|
|
|
## Known liabilities (flag, don't silently fix)
|
|
|
|
Pre-existing; surface them, let Robert decide:
|
|
|
|
- **`db.check_connection()` opens a fresh `psycopg2.connect` outside the pool** on
|
|
every call — and `/ready`, `/metrics`, and `/api/status` all call it. The pool's
|
|
hottest paths bypass the pool. A pooled `SELECT 1` would be cheaper.
|
|
- **`nike.service` has a stale `WorkingDirectory`/`ExecStart`** pointing at
|
|
`/home/robert/gitea/nike` (the repo lives at `~/git/nike`) — the checked-in unit
|
|
wouldn't start as-is.
|
|
- **`.DS_Store` is tracked** at the repo root and in `nike/`. macOS cruft — should
|
|
be gitignored and removed.
|
|
|
|
## Path-scoped rules (`.claude/rules/`)
|
|
|
|
- `mcp-tools.md` — sync tools, string-table returns, premium gating, `_log`, dual dict shapes.
|
|
- `cache-api.md` — read-through pattern, permanent-vs-volatile, TTL + PG layers, per-call try/except.
|
|
- `db.md` — psycopg2 pool, get_conn contextmanager, cache-table upserts, schema.sql.
|
|
- `fastapi-dashboard.md` — /api routes, tool-runner, metrics middleware, health routes, HAProxy edge, mount order.
|
|
- `svelte.md` — static SPA, derived tool list, telemetry, DaisyUI, no server side.
|
|
- `config-deploy.md` — os.getenv config, NIKE_ prefix, systemd + docker, fresh-env build order.
|