# MPS-3: Creator Analytics Dashboard

## Problem

MPS-2 collects anonymous signals and derives Engagement, Attention, Learning,
and Passive Consumption scores. Creators need somewhere to see this data and
act on it — know which content pulls people in, which holds them, and where
traffic comes from so they know where to spend time and energy.

## Solution

Add a server-rendered analytics page at `/s/{shop_id}/analytics`. No JS graph
libraries — CSS grid tables, server-computed aggregates, progressive
enhancement. Machine learning can refine the derived scores later without
changing the page structure.

## Access

- Route: `GET /s/{shop_id}/analytics`
- Requires `shop_editor_required` (same as shop settings)
- Link from shop settings page (near existing nav)

## Page Sections

### 1. Overview Strip (shop-wide, last 7 days)

A single row of key numbers at the top:

| Metric | Query |
|--------|-------|
| Total views | `COUNT(*) WHERE visible_ms >= 7000 AND created_timestamp > 7d ago` |
| Unique products viewed | `COUNT(DISTINCT product_id) WHERE visible_ms >= 7000` |
| Avg session duration | `AVG(wall_clock_ms)` |
| Avg ring depth | `AVG(ring_position) WHERE ring_position IS NOT NULL` |
| Top traffic source | `MODE(referrer_class)` (show label: direct/search/social/internal) |
| Device split | `% per device_class` shown as "42% mobile · 7% tablet · 51% desktop" |

### 2. Top Products by Views (last 7 / 14 / 21 days)

Ranked table, top 21 products:

| # | Title | Views (7d) | Views (14d) | Views (21d) | Trend |
|---|-------|-----------|------------|------------|-------|

"Trend" = simple arrow: views(7d) > views(14d)/2 → rising, else falling.
Each title links to the product page.

Query: `GROUP BY product_id`, `COUNT(*) WHERE visible_ms >= 7000`, partitioned
by time windows using `created_timestamp`.

### 3. Ring Entry Points (top 7 front doors)

Which products do people land on first?

| # | Title | Ring Entries (21d) | % of All Entries |
|---|-------|--------------------|-----------------|

Query: `COUNT(*) WHERE is_ring_entry = true GROUP BY product_id ORDER BY
count DESC LIMIT 7`.

This tells the creator: "People find your shop through these 7 products — make
sure they're polished."

### 4. Engagement & Attention Leaders (top 7 each)

Two side-by-side tables:

**Engagement Leaders** (highest lean-in):

| # | Title | Avg Engagement | Sessions |
|---|-------|---------------|----------|

`engagement = active_ms / wall_clock_ms` — computed per session, averaged per
product. Only include sessions with `wall_clock_ms >= 7000` (real visits).

**Attention Holders** (highest focused presence):

| # | Title | Avg Attention | Sessions |
|---|-------|--------------|----------|

`attention = visible_ms / wall_clock_ms` — same filtering.

### 5. Study Material vs Background Favorites (top 7 each)

Two side-by-side tables:

**Study Material** (people rewind, slow down, re-read):

| # | Title | Learning Score | Sessions |
|---|-------|---------------|----------|

Learning score per session = count of true indicators:
- `media_seek_back_count > 0`
- `media_speed < 1.0` (and not NULL)
- `scroll_direction_changes > 3`
- `media_pause_count > 2`
- `active_ms / wall_clock_ms > 0.7`

Average per product, ranked. Minimum 7 sessions to qualify.

**Background Favorites** (lean-back plays):

| # | Title | Passive Score | Sessions |
|---|-------|--------------|----------|

Passive score per session = count of true indicators:
- `visible_ms / wall_clock_ms > 0.7`
- `active_ms / wall_clock_ms < 0.3`
- `media_percent_played > 0.69`
- `media_play_count = 1`
- `media_pause_count = 0`

Average per product, ranked. Minimum 7 sessions to qualify.

### 6. Traffic Sources (last 21 days)

Simple breakdown table:

| Source | Sessions | % |
|--------|----------|---|
| Direct | 142 | 42% |
| Search | 69 | 21% |
| Social | 47 | 14% |
| Internal | 70 | 21% |
| Unknown | 7 | 2% |

Query: `COUNT(*) GROUP BY referrer_class`.

### 7. Device Split (last 21 days)

| Device | Sessions | % |
|--------|----------|---|
| Mobile | 210 | 42% |
| Tablet | 42 | 8% |
| Desktop | 252 | 50% |

Query: `COUNT(*) GROUP BY device_class`.

## Query Strategy

All queries run against `mps_page_session` with time filters using
`created_timestamp`. Since timestamps are milliseconds:

```python
cutoff_7d = now_timestamp() - (7 * 24 * 60 * 60 * 1000)
cutoff_14d = now_timestamp() - (14 * 24 * 60 * 60 * 1000)
cutoff_21d = now_timestamp() - (21 * 24 * 60 * 60 * 1000)
```

For the engagement/attention/learning/passive scores, compute per-session in
the SQL query using CASE expressions, then AVG per product. SQLite handles
this fine for shops with < 100K sessions.

For larger shops (future), the 90-day retention + daily rollup from MPS-2
provides pre-aggregated data.

## Template Layout

CSS grid, two-column on desktop, single-column on mobile. No flexbox per
project rules.

```
[Overview Strip — full width]
[Top Products — full width]
[Ring Entry Points — full width]
[Engagement Leaders | Attention Holders — side by side]
[Study Material | Background Favorites — side by side]
[Traffic Sources | Device Split — side by side]
```

Tables use `<table>` with `class="analytics-table"`. No zebra striping —
keep it clean. Product titles are links. Numbers right-aligned.

## Privacy Note

Displayed in a small footer on the page:

> All data is anonymous. No individual viewer can be identified. Counts
> represent aggregate sessions, not people.

## Files Changed

| File | Change |
|------|--------|
| `views/shop.py` | New `analytics` view with aggregate queries |
| `routes.py` | New route `shop_analytics` |
| `templates/analytics.j2` | New template with 7 sections |
| `static/css/common.css` | `.analytics-table`, `.analytics-overview` styles |
| `templates/shop_settings.j2` | Link to analytics page |
| `tests/test_functional.py` | Analytics page access + permission tests |
| `tests/test_models.py` | Score computation unit tests |

## Depends On

MPS-2 (signal gathering + `mps_page_session` table + `view_count` column)
