# tracking-setup — Coding Standards

Adapted from `/srv/home/scenes-v2/CODING.md`. Read this before adding new code.
The aim is consistency: every new file should look like it was written by the
same person who wrote the previous one.

---

## Code organization

### Single source of truth

Every reusable component is defined once and imported where needed.

| Component | Location | Usage |
|---|---|---|
| Jinja macros (status badge, action card, kind badge) | `app/templates/_macros.html` | `{% from "_macros.html" import status_badge, action_card %}` |
| Auth helpers (current_user, require_internal, etc.) | `app/deps.py` | `from deps import require_internal` |
| Audit log writer | `app/deps.py::audit()` | `audit(user.id, client_id, "client.create", {...})` |
| DB connection / queries | `app/db.py` | `from db import query, query_one, execute` |
| Per-feature routes | `app/routes/<feature>.py` | each module exports `router = APIRouter(...)` |

**Before creating new UI:** check if an existing macro can be extended. If
similar UI exists, extract common parts into a shared macro. **Never copy-paste
HTML between templates.**

### File structure

```
app/
├── main.py                  Slim entry: FastAPI app, static mount, router includes
├── deps.py                  Shared deps: jinja env, render(), auth, audit, config
├── version.py               VERSION + changelog
├── auth.py                  Password hashing, magic-link tokens, session cookies
├── db.py                    SQLite connection + init_db schema migrations
├── routes/                  One module per feature area
│   ├── __init__.py
│   ├── auth.py              login/logout/magic/bootstrap/account_setup
│   ├── pages.py             root (dashboards), healthz
│   ├── orgs.py              organization CRUD
│   ├── clients.py           client CRUD + reviewer invites + audit log
│   ├── crawls.py            crawl trigger + detail + background worker
│   └── reports.py           report detail/review/PDF + action edit/delete/review/comment
├── crawler/
│   └── playwright_runner.py
├── reports/
│   └── builder.py           Claude analyzer
├── templates/
│   ├── _macros.html         Shared macros (prefix _ = partial)
│   ├── base.html
│   └── ... per-page templates
└── static/
    ├── style.css            Shared base + utilities
    ├── report-review.css    Page-specific
    └── pdf.css              Print-style for WeasyPrint
```

### Naming conventions

| Type | Convention | Example |
|---|---|---|
| Templates | `lowercase.html` | `report_detail.html` |
| Partials | `_lowercase.html` | `_macros.html` |
| Python files | `snake_case.py` | `playwright_runner.py` |
| Route modules | `singular_noun.py` | `routes/orgs.py` |
| CSS classes | `kebab-case` | `.action-card` |
| JS variables | `camelCase` | `videoPlayer` |
| Data attributes | `kebab-case` | `data-action-id` |

---

## Versioning + rebuild

### When to bump `app/version.py`

**Every code change before restart/deploy.** Update `VERSION` and add a one- to
three-line changelog entry at the top of the changelog block (newest first).
The version is shown in the page footer, so you can verify a deploy hit the
container by checking the footer.

### When a `--build` is required

`./app/` is a bind mount, so most edits are picked up by `srv-gw restart`:

| Change | Action |
|---|---|
| Python under `app/*.py` | `srv-gw restart --project tracking-setup` |
| Templates in `app/templates/*.html` | `srv-gw restart --project tracking-setup` |
| CSS / JS in `app/static/*` | `srv-gw restart --project tracking-setup` (or just refresh browser — uvicorn will serve new file) |
| `requirements.txt` | `srv-gw deploy --project tracking-setup --build` |
| `Dockerfile` | `srv-gw deploy --project tracking-setup --build` |
| `.env` | `srv-gw deploy --project tracking-setup` (NOT just `restart` — env vars are only re-read on full deploy) |

---

## Templates

### Always extend a base + import macros

```html
{% extends "base.html" %}
{% from "_macros.html" import status_badge, action_card %}
{% block title %}Report — {{ report.title }}{% endblock %}
{% block head %}<link rel="stylesheet" href="/static/report-review.css">{% endblock %}
{% block content %}
  ...
{% endblock %}
```

### No inline styles

```html
<!-- Wrong -->
<div style="display: none;">

<!-- Right -->
<div class="hidden">
```

### Pass data via template vars, not hardcoded

```html
<!-- Wrong -->
<option value="master">Master</option>
<option value="single_client">Single client</option>

<!-- Right -->
{% for kind in org_kinds %}
<option value="{{ kind.value }}">{{ kind.label }}</option>
{% endfor %}
```

### Use macros for repeated UI

```html
<!-- Wrong: 4 templates each render their own status pill -->
<span class="status status-{{ r.status }}">{{ r.status }}</span>

<!-- Right -->
{{ status_badge(r.status) }}
```

---

## CSS

### Page-specific files vs shared

- **`style.css`** — base styles, utilities, components used in 2+ pages.
- **`<page>.css`** — styles unique to one page. Loaded via `{% block head %}<link rel="stylesheet" href="/static/<page>.css">{% endblock %}`.

If a style starts being shared, hoist it into `style.css`. If a section of
`style.css` is only used by one page, extract it.

### Section comments in style.css

Major sections use this header style:

```css
/* ==========================================================================
   Section Name
   ========================================================================== */
```

### CSS variables

All colors and shared spacing live in `:root`. **Only use variables that are
defined** — undefined variables silently fail and produce transparent elements.

### Status / kind badges

Driven by the macro pattern. Add a new status? Add the class hook to
`style.css` section "Status badges":

```css
.status-my_new_state { background: ...; color: ...; }
```

The macro will pick it up automatically — no template change needed.

---

## FastAPI routes

### One module per feature area

A new feature gets its own file in `app/routes/`. Module exports a `router =
APIRouter(...)`. `app/main.py` includes it via `app.include_router(...)`.

```python
# app/routes/widgets.py
from fastapi import APIRouter, Request
from deps import render, require_internal

router = APIRouter(prefix="/widgets")

@router.get("")
def widgets_list(request: Request):
    user = require_internal(request)
    ...
```

### Auth on every protected route

- Internal-only: `user = require_internal(request)`
- Either: `user = require_user(request)`
- Public: nothing (and document it).

For RBAC on per-client resources: `if not user_can_access_client(user, cid): raise HTTPException(403)`.

### Audit on every state change

```python
audit(user.id, client_id, "report.send", {"report_id": report_id, "n_reviewers": n})
```

Convention: `kind = "<entity>.<verb>"`, lowercase, snake_case. The audit table
becomes the per-client timeline shown at `/clients/{id}/audit`.

### Schema migrations

All in `app/db.py::init_db()`. Use `CREATE TABLE IF NOT EXISTS` and
`ALTER TABLE` guarded by `_table_columns()` — the function must be safe to run
on every startup.

---

## Background work

Long-running tasks (Playwright crawls, future GA4 API writes) run in
`threading.Thread(target=..., daemon=True)`. Don't block the FastAPI event
loop with sync I/O.

Any background worker that touches the DB opens its own connection via
`db.execute()` / `db.query()` — connections aren't shared across threads.

For v1 this is enough. If we ever run >5 concurrent crawls or need
restart-resilient jobs, swap to RQ/Celery/Arq.

---

## Anthropic / Claude integration

`app/reports/builder.py` is the only place we call the Anthropic API. Keep it
that way:
- One system prompt, cached via `cache_control: ephemeral`.
- Per-CrawlRun spend cap enforced via `_calc_spend_cents(usage)` then check
  against `CRAWL_MAX_SPEND_USD` env.
- Raw Claude response saved to `<crawl_dir>/analyzer_raw.json` for debugging
  prompt regressions.

When tuning the prompt, bump `version.py` (so you can correlate a behavior
change to a deployed version) and watch `ai_spend_cents` on the next few crawls.

---

## Component checklist

When adding new UI:

- [ ] Can I extend an existing macro?
- [ ] Are styles in CSS, not inline?
- [ ] Is it a shared style (→ `style.css`) or page-specific (→ `<page>.css`)?
- [ ] Did I update CSS variables instead of hardcoding colors?
- [ ] Does it work on mobile (≤600px)?
- [ ] If it's a status/state, did I add the class hook to the badge section?
- [ ] If it's a new route, is it in `app/routes/<feature>.py` (not main.py)?
- [ ] Does it call `audit()` on state changes?
- [ ] Did I bump `app/version.py`?

---

## Don'ts

- **No inline styles, no inline JS** (no `onclick="..."`).
- **No selectors via `:nth-child(...)`** — use IDs or `data-*` attributes.
- **No copying HTML between templates** — extract a macro.
- **No hardcoded enum values in templates** — pass via context.
- **No undefined CSS variables** — they fail silently.
- **No bypassing `require_user` / `require_internal`** — every protected route
  uses one of them.
- **No DB writes outside `audit()`-tagged paths** — if it changes state worth
  showing on the audit log, it should call `audit()`.
- **No emoji in code or files** unless rian explicitly asks for them.
