API Doctor & Activity

The /smallstack/api/ admin pages give you a one-pane sanity check for the REST surface, plus an operational view of /api/* traffic and the threat signals derived from it.

Two pages, both staff-gated:

  • /smallstack/api/ — Health diagnostics
  • /smallstack/api/activity/ — per-endpoint operational view + threat panel + filterable request log

The CLI equivalent is python manage.py api_doctor. The web UI re-binds the same _check_* methods to HTML, so the two stay in lockstep.

Health page

Renders nine read-only checks as colored cards:

Check What's verified
openapi-spec-validator The validator dependency is importable
Installed apps apps.smallstack is registered; warns on missing apps.activity or axes (the threat panel relies on both)
API registry At least one CRUDView has enable_api = True (warns at 0 — Swagger would be empty)
URL conf /api/schema/, /api/schema/openapi.json, /api/docs/, /api/redoc/, /api/auth/token/, /api/auth/me/ all resolve
Swagger / ReDoc shells Both UI shells return 200 and contain their CDN script tag
OpenAPI validity The generated spec passes OpenAPI 3.0.3 validation
Endpoint consistency Every entry in _api_registry resolves to a live list URL
Orphan files Files declaring enable_api = True that aren't reachable through INSTALLED_APPS (typically a missing from . import views in AppConfig.ready())
APIToken inventory Count of active vs revoked tokens

The Run Self-Test button (top right) does what make api-test does, except in-process: mints a temp readonly APIToken, hits /api/schema/ + /api/schema/openapi.json + the first list endpoint, then revokes the token in a finally block. The result swaps into the page via htmx — no token is ever left active, even if the test fails mid-stream.

Activity page

Three regions stacked top to bottom.

Top endpoints

Aggregates RequestLog rows under /api/* across the selected since window. Columns: Path · Hits · Avg ms · Errors · Error rate. The error-rate column is colored red ≥10% and orange ≥1% so anomalies pop without you having to sort.

Threat signals

The heuristics in apps/api/threats.py run on every page load. Each detector is a pure function over RequestLog and axes.models.AccessAttempt, returning a list of ThreatSignal dataclasses; the view aggregates and orders by severity.

Detector Severity Threshold
Active axes lockouts HIGH failures_since_start >= AXES_FAILURE_LIMIT (5) within the cooloff window
Auth-failure burst HIGH ≥ 10 × 401/403 on /api/* from one IP in the last hour
Path scanning MEDIUM ≥ 10 distinct paths and ≥ 20 × 404s from one IP in the last hour
Request burst MEDIUM ≥ 200 requests from one IP in the last minute
Scanner user-agent MEDIUM UA matches sqlmap, nikto, nmap, masscan, zgrab, dirbuster, gobuster, ffuf, wpscan, acunetix, nessus, burpsuite, metasploit, openvas (substring, case-insensitive)
Revoked-token use LOW Any /api/* request authenticated with an is_active=False APIToken in the last 24h

What this is: observation. Each row links the offending IP back into the filtered request log so you can investigate. What it isn't: a WAF or active response. There is no "block this IP" button — that's the operator's call. Use axes for active lockouts, your server firewall for IP blocks, or the token manager to revoke compromised keys.

False-positive shape worth knowing:

  • A single staff user mistyping their password 10 times in an hour will fire the auth-failure burst (HIGH). Pair it with the user/IP in the log table before reacting.
  • A legitimate test script hitting many 404s during development will fire path scanning. Filter by ?since=1h and check the request log to confirm it's an internal IP.
  • A monitoring tool that crawls /api/missing-endpoint/ repeatedly will look like reconnaissance. Add the monitor's UA to an allowlist if it becomes noisy (no current allowlist UI; edit SCANNER_UA_PATTERNS in apps/api/threats.py).

Recent /api requests

The full request log filtered to /api/*. Filters: since window, method, status class, IP, user, scanner-UA toggle. Paginated 50 per page. The filter form only applies to this region — the top-endpoints summary and threat panel always reflect the current since window.

Dashboard widget

/smallstack/ shows an API card next to the MCP card. Cheap to render — no HTTP, no DB writes. It shows endpoint count + the highest-priority signal:

Condition Status Detail
1+ HIGH-severity threat in last 24h degraded N high-severity threats
Orphan files detected degraded N unregistered files
No CRUDView with enable_api=True operational Awaiting enable_api
Clean operational All checks passing

What we can't see

Worth being honest about so you don't misinterpret an empty threat panel as "we're safe":

  • No geoip — we don't ship a geo database. An IP from a high-risk country isn't a signal here; it's a signal in your edge layer.
  • No TLS handshake info — if there's a cipher-downgrade attack, this page can't tell you.
  • No request-body sampling — payload-based attacks (SSRF, SQLi via body, command injection) aren't visible unless they produce a downstream 401/403/404 that the heuristics already catch.
  • No per-key cross-tenant analytics — the request log shows which token authed a request, but doesn't profile usage shape across tenants.

Each of those is a scoped feature; raise an issue if you need one.

Theme Settings

Choose the color mode for your app.

The accent color for your app.

Choose the font family that fits your app.

Choose the gray shade for your app.

Choose the border radius factor for your app.

Choose the page layout for your app.