Isolate parallel test runs with PID-based database filenames

When two processes run pytest simultaneously, they no longer share
test_make_post_sell_master.sqlite. Each gets its own file keyed by PID,
preventing table-exists and readonly-database errors. Also clean up
WAL/SHM journal files on exit.
This commit is contained in:
russell@unturf.com 2026-02-09 16:24:25 -05:00
parent e1a8b2f236
commit fca2f4a051
10 changed files with 1085 additions and 34 deletions

View file

@ -4,48 +4,192 @@
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.
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 simple analytics page accessible from the shop dashboard. No graphs
library — clean server-rendered HTML with CSS grid tables. Machine learning
refinements to the derived scores can enhance this page later without changing
its structure.
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.
### Content
## Access
- **Top products by views** — ranked list, last 7 / 14 / 21 days
- **Ring entry points** — which products are "front doors" (top 7 by `is_ring_entry` count)
- **Average ring depth** — how far viewers ride the ring (mean `ring_position`)
- **Daily view totals** — last 21 days, per product and shop-wide
- **Engagement leaders** — products with highest average engagement score
- **Attention holders** — products with highest average attention score
- **Study material** — products with highest learning signal (people rewind, slow down, re-read)
- **Background favorites** — products with highest passive consumption score (lean-back plays)
- **Traffic sources** — breakdown by `referrer_class` (direct / search / social / internal)
- **Device split** — mobile vs tablet vs desktop percentages
- Route: `GET /s/{shop_id}/analytics`
- Requires `shop_editor_required` (same as shop settings)
- Link from shop settings page (near existing nav)
### Access
## Page Sections
New route `/shop/analytics` — only visible to shop owner/mods. Link from shop
settings or dashboard nav.
### 1. Overview Strip (shop-wide, last 7 days)
### Privacy
A single row of key numbers at the top:
All data is anonymous aggregates computed from `mps_page_session` rows. No
individual viewer data exists to display even if someone wanted to.
| 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 |
| `templates/analytics.j2` | New template |
| `static/css/common.css` | Analytics table styles |
| `templates/snippets/shop_nav.j2` | Link to analytics (if exists) |
| `tests/test_functional.py` | Analytics page access tests |
| `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)
MPS-2 (signal gathering + `mps_page_session` table + `view_count` column)