# tracking-setup

AI-assisted Google Analytics / Tag Manager / Ads tracking-setup tool. Crawls a
client's site, proposes a tracking spec via Claude, routes it through internal
review and client approval. v2+ will add the Google API write layer.

For the full plan, see **[PLANNING.md](./PLANNING.md)**.

## Project info
- Type: custom (FastAPI in Docker)
- Internal port: 8000 (mapped to host port `${PORT}` in `.env`)
- Domain: tracking-setup.demoing.info (placeholder — rebrand before going live)
- Created by: rian
- Created: 2026-05-07

## Bootstrap (first-time setup)

On first boot, the app creates a placeholder admin user and prints a one-time
magic link to the container logs. Visit that URL to claim the admin account
and set your real email + password.

```bash
srv-gw logs --project tracking-setup | grep -A1 "BOOTSTRAP ADMIN"
```

If you miss the printout, you can rotate it: stop the container, delete
`/srv/apps/tracking-setup/data/app.db`, restart. (Only safe before any real
data is in the system.)

## Required environment

Edit `/srv/apps/tracking-setup/.env` (chmod 660, group `tracking-setup-dev`):

| Var | Status | Notes |
|---|---|---|
| `SECRET_KEY` | set | Signs sessions + magic-link tokens. Rotating invalidates all sessions. |
| `ANTHROPIC_API_KEY` | **REQUIRED — empty in scaffold** | Crawl/analyzer fails loud without this. |
| `RESEND_API_KEY` | unset (v2) | Magic links currently print to stdout (`DEV_MAGIC_LINK_PRINT=1`). |
| `EMAIL_FROM` | placeholder | Set to your real sender domain when wiring Resend. |
| `CRAWL_MAX_PAGES` | 25 | Per-CrawlRun page cap. |
| `CRAWL_MAX_SPEND_USD` | 2.00 | Per-CrawlRun Anthropic spend cap. Crawl aborts if exceeded. |
| `APP_BASE_URL` | https://tracking-setup.demoing.info | Used in magic-link URLs. |

After `.env` changes, run `srv-gw deploy --project tracking-setup` (NOT just
`restart` — env_file is only re-read on deploy).

## Managing the container
Always go through `srv-gw` (NOT `docker` directly):
```bash
srv-gw deploy --project tracking-setup     # Full recreate (re-reads .env + docker-compose.yml)
srv-gw deploy --project tracking-setup --build  # Rebuild image (after Dockerfile changes)
srv-gw restart --project tracking-setup    # Quick restart (does NOT reload .env)
srv-gw status --project tracking-setup
srv-gw logs --project tracking-setup
srv-gw stop --project tracking-setup
srv-gw start --project tracking-setup
```

The `app/` directory is bind-mounted, so route/template/CSS changes only need a
`restart`. Dockerfile or requirements.txt changes need `--build`.

## Where things live

```
/srv/apps/tracking-setup/
├── docker-compose.yml      FastAPI + Playwright (single image)
├── Dockerfile              mcr.microsoft.com/playwright/python base
├── requirements.txt
├── .env                    Secrets (chmod 660 rian:tracking-setup-dev)
├── CLAUDE.md               This file
├── PLANNING.md             Full architecture plan + status
├── app/
│   ├── main.py             FastAPI entry — all routes, auth helpers
│   ├── auth.py             Session cookies, password hashing, magic-link tokens
│   ├── db.py               SQLite connection + init_db() schema
│   ├── crawler/
│   │   └── playwright_runner.py   Headless crawl → screenshots/DOM/interactive.json
│   ├── reports/
│   │   └── builder.py             Claude analyzer → Report + Action rows
│   ├── templates/          Jinja2
│   └── static/style.css
└── data/                   (bind mount, persists across deploys)
    ├── app.db              SQLite
    ├── crawls/<run-id>/    Per-page artifacts + analyzer_raw.json
    └── reports/            (reserved for v2 PDF caching)
```

## Auth model

- **Internal users** (admin, internal): email + bcrypt password. Full access to
  their organizations.
- **Client users** (role=client): magic-link only. Scoped to specific clients
  via `user_client_access`.

Magic links are 48-hour single-use tokens (`auth.issue_magic_link()` /
`auth.consume_magic_link()`). When `DEV_MAGIC_LINK_PRINT=1` they're echoed to
stdout instead of being emailed — handy for development; flip off when wiring
Resend in v1.1.

## v1 workflow (analysis-only MVP)

1. Internal user creates an Organization (master account or single-client).
2. Adds a Client under it with primary URL — checkbox confirms authorization to crawl.
3. Invites client reviewers (creates user + magic-link, scoped to that client).
4. Clicks **Start crawl** → background thread runs Playwright → analyzer → Report.
5. Reviews/edits the draft report internally; can edit titles, notes, spec JSON,
   delete actions.
6. Clicks **Send to client** → status flips to `sent` and a fresh magic link is
   issued for each reviewer.
7. Reviewer logs in via magic link → walks through actions on `/reports/{id}/review`
   → approves / rejects each with optional comments.
8. When every action is decided, report flips to `approved` automatically.
9. Internal user (or reviewer) can download a **PDF** of the plan at any time.

What's NOT in v1 (deferred to v2+): Google OAuth, GA4/GTM/Ads/Search Console
APIs, "Apply" step that pushes the spec into the actual accounts, brand-new
account creation flows.

## For Claude (AI Assistant)

### Common tasks

**Add a new route:** edit `app/main.py`, add a Jinja template under
`app/templates/`, restart the container. Mirror existing patterns —
`require_internal()` for staff-only, `require_user()` for both.

**Schema changes:** add the migration to `app/db.py::init_db()`. Use
`CREATE TABLE IF NOT EXISTS` and `ALTER TABLE` guarded by checking
`_table_columns()` so it's safe to run repeatedly.

**Iterating on the analyzer prompt:** edit `SYSTEM_PROMPT` in
`app/reports/builder.py`. Each prompt change invalidates the cache, so first
call after a change costs full input tokens. Watch the `ai_spend_cents` on
each crawl as you iterate.

**Re-running analyzer on an existing crawl:** there's no UI for this in v1.
For now, run another crawl. v1.1 should add "regenerate report from this
crawl" to avoid burning Playwright time on already-captured pages.

### Don'ts

- **Never commit `.env` or `data/`** — both contain secrets / customer data.
- **Don't bypass `require_internal` / `require_user`** — every protected route
  uses one of these. RBAC checks live in `_user_can_access_client()`.
- **Don't email-bypass `DEV_MAGIC_LINK_PRINT`** until you've actually wired
  Resend; otherwise reviewer invites silently never arrive.
- **Don't expose port 8000 on `0.0.0.0`** — see /srv/CLAUDE.md "Home Service
  Security Rules". Stay on `172.17.0.1:${PORT}`.

## Known issues / next steps

See PLANNING.md "Phased rollout" + "Open questions" sections.
