Files
nike/CLAUDE.md
Robert Helewka 9f1d85f04b
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 33s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m36s
docs: add Claude rules and workspace configuration
2026-07-14 13:34:05 -04:00

9.7 KiB

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. 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.

  • 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.

  • 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 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.