# Planetterrian Portfolio Analytics — the One-Place Contract

Every portfolio metric for Patrick Novak’s five companies lands in
**`api/dashboard.json`**, rolled up by `scripts/generate_dashboard.py` from
honest fixtures under `api/fixtures/`. That file is the **universal source of
truth** for the portfolio.

**Nerra pattern (UX / JSON vibes, not a clone):** this hub mirrors the Mission
Control shell used at [nerranetwork.com/management.html](https://nerranetwork.com/management.html)
— status chips (OK/warn/fail), alert band, glance tiles, catalog-style table,
landmine/guard cards, side-by-side labeled sources, and `fetch("api/dashboard.json")`.
It does **not** port Nerra show/RSS/OP3/Spotify/Apple/YouTube pipelines.

| Audience | Entry point |
|----------|-------------|
| Humans | [`management.html`](../management.html) (Portfolio Mission Control) |
| Machines / AI tools | [`api/dashboard.json`](../api/dashboard.json) |
| This contract | `docs/analytics.md` (you are here) |

**Nerra’s deep show/ops Mission Control** remains at
[nerranetwork.com/management.html](https://nerranetwork.com/management.html)
(`Planetterrian/nerranetwork`: `management.html`, `scripts/generate_dashboard.py`,
`docs/analytics.md`, `api/dashboard.json`). This hub is **portfolio SoT only** —
it does not replace Nerra’s network dashboard.

## Non-negotiable locks

1. **Never one blended portfolio score.** Five brands, five north stars —
   shown side by side. Inventory counts of “measured vs not measured” are
   wiring status, not performance.
2. **Never invent metrics.** Unknown → `null` / `status: "not_measured"` /
   explicit note. Absence is not zero. Soft’s legitimate `[GA4] 0` is measured.
3. **Sources labeled side by side.** Every number carries `[GA4]` or `[GSC]`
   (or says not measured). Never one fake reach number across brands or
   platforms.
4. **Soft = tips / no charge.** Nerra Soft Personal north star is
   `soft_personal_interest_submit` only. `newsletter_signup` ≠ Soft. Paid
   `/join.html` ≠ Soft.
5. **Rel drip = NO SEND.** Relationship Copilot activation drip E1–E3B is
   drafts only — no conversion row, do not count unsent Resend drafts.

## Brands + GA4 IDs + north stars

| Brand | Domain | GA4 | North-star event | Soft notes |
|-------|--------|-----|------------------|------------|
| Nerra Network | nerranetwork.com | `G-6PWJCVQQ7B` | `soft_personal_interest_submit` | Tips / no charge; paid join separate; newsletter secondary |
| Lil Words | lilwords.com | `G-8CZK8K52W2` | `web_play_start` | FR/ES GSC page rows; Play Store secondary; App Store gated |
| Avvizo | avvizo.com | `G-QZ5ZKQ0PPZ` | `families_trial_signup_start` | Caregiver + pricing GSC rows |
| Bill Saved | billsaved.com | `G-CH6QNJ9827` | `first_assist_start` | Hold-less Assist only |
| Relationship Copilot | relationshipcopilot.com | `G-3HE7V5FTSR` | `first_mutual_reveal_completed` | Premium soft secondary; drip **NO SEND** |

## Product-within-brand rows

Digests and the Mission Control page keep **distinct funnels** per brand.
Do not collapse Soft tips into paid join, Play into Store clicks, trial into
page_view, Assist into Coached/Handled, or Reveal into Premium/drip.

See brand fixtures under `api/fixtures/brands/` and the Dashboard v1.1
improvement brief for the full product map.

## Source files (v0)

| File | Role |
|------|------|
| `api/fixtures/portfolio.json` | Portfolio meta, windows, contract flags, brand order |
| `api/fixtures/brands/*.json` | Per-brand traffic + north star + product rows (honest labeled fills) |
| `api/dashboard.json` | Rollup SoT written by the generator |
| `scripts/generate_dashboard.py` | Offline-capable rollup (`--offline` default, `--dry-run`, `--out`) |
| `management.html` | Human Mission Control — `fetch("api/dashboard.json")` |

### `api/dashboard.json` sections (v0.2)

| Key | Role (Nerra analog) |
|-----|---------------------|
| `portfolio.landmines_counts` | Header OK/warn/fail chips |
| `glance.tiles` | Hero tiles — **one per brand north star**, never a portfolio total |
| `catalog.rows` | Catalog table (brands, not shows) |
| `brands[]` | Per-brand deep sections + product rows |
| `landmines[]` | Guard / landmine cards |
| `data_gaps[]` | Honest unknowns |
| `alerts[]` | Alert band (warn/fail landmines surfaced) |

v0 is **fixture-mode**: numbers come from the last labeled GA4+GSC fill
(see `portfolio.windows.as_of_label`), not live API connectors. When
connectors exist later, fetchers should follow Nerra’s convention:
**missing secret → clean no-op** (leave previous JSON untouched; stale
`fetched_at` means degraded, not deleted).

## Reading guide for AI tools

1. Start at `api/dashboard.json`. Check `generated_at`, each brand’s
   `traffic.*.configured` / `fetched_at`, and `north_star.status`.
2. Treat `status: "not_measured"` or `configured: false` as **unknown** —
   never coerce to `0`. Soft’s `0` is allowed only because
   `status: "measured"` and `key_event_on: true`.
3. Report brands separately. Do not average users, sessions, or north-star
   events across the portfolio.
4. Product rows under each brand are the digest depth: Soft vs join vs
   newsletter; Play vs FR/ES vs Store; Trial vs caregiver vs pricing;
   Assist vs blog; Reveal vs Premium; drip **NO SEND**.
5. For Nerra show-level ops (RSS, OP3, Spotify, voice drift, landmines),
   leave this hub and read
   [nerranetwork.com/api/dashboard.json](https://nerranetwork.com/api/dashboard.json)
   + that repo’s `docs/analytics.md`.

## Windows (fixture fill)

As recorded in fixtures (do not invent newer windows):

- **GA4 Yesterday** = 2026-09-22
- **GA4 Last 7d** = 2026-09-16 → 2026-09-22
- **GSC UI 7d** = 2026-09-15 → 2026-09-21 (lags GA4)
- **Fill label** = 2026-09-23 ~17:31–17:44 PT (Soft re-fill ~17:44 PT)

## Regenerating

```bash
python scripts/generate_dashboard.py           # write api/dashboard.json
python scripts/generate_dashboard.py --dry-run # stdout only
python scripts/generate_dashboard.py --offline # explicit; default anyway
```

No heavy build step — Cloudflare Pages serves the static files as-is.
