make_post_sell/docs/architecture.md
russell@unturf.com 8970e960fb
feat: MPS-24 Phase 2.6b — facet nav on shop home + mobile SERP lanes
Operator review on tablet showed two gaps in the 2.6 ship:
- Shop home (layout 2 lanes) had no facet sidebar — only tag detail did
- Mobile lanes were horizontal Netflix-style tile rows with no
  description visible at all

This batch extends the facet experience across every page where the
operator opted into categorization (home_layout >= 1):

- New templates/_facet_nav.j2 with three macros (facet_form, sidebar,
  details). One source of truth for the controls, three variants of
  the wrapper. shop_tag.j2 refactored to import the macro.
- home.j2 + shop.j2 now wrap content in .tag-detail-layout when
  home_layout >= 1, rendering both the desktop sidebar and the mobile
  <details> accordion. CSS toggles visibility per viewport.
- Each lane in layout 2 now emits BOTH horizontal tiles AND vertical
  .serp-list-row markup with 6-sentence excerpts. CSS shows tiles
  >=800px, SERP rows <800px. Tablet / phone shoppers see image +
  title + price + description excerpt under each tag heading.
- views/shop.py: facet_tags is populated for any home_layout >= 1
  (was only on ?tag= filter); sort + price now also filter the
  non-tag-filtered home grid when the shopper applies them.

Native HTML. No JS dependency. Same controls everywhere.

Test: test_shop_home_lanes_renders_facet_sidebar_and_mobile_rows.
Docs: CLAUDE.md MPS-24 section, architecture matrix, ticket Phase 2.6b.
2026-05-15 16:30:23 -04:00

16 KiB

MPS Architecture Overview

System Diagram

                                    Internet
                                       |
                              ┌────────┴────────┐
                              │  Caddy (HTTPS)   │
                              │  reverse proxy   │
                              │  :443 → :6001    │
                              └────────┬────────┘
                                       |
                              ┌────────┴────────┐
                              │   uWSGI          │
                              │   2 proc, 8 thr  │
                              │   reload@512MB   │
                              └────┬───────┬────┘
                                   |       |
                    ┌──────────────┘       └──────────────┐
                    |                                      |
           ┌────────┴────────┐                   ┌────────┴────────┐
           │  Pyramid / WSGI  │                   │   Background    │
           │  Request Cycle   │                   │   Threads       │
           │                  │                   │                 │
           │  views/          │                   │  S3 mirror      │
           │  models/         │                   │  Karaoke vocal  │
           │  templates/      │                   │  isolation      │
           │  lib/            │                   │                 │
           └───┬────┬────┬───┘                   └────────┬────────┘
               |    |    |                                 |
     ┌─────────┘    |    └─────────┐                      |
     |              |              |                      |
┌────┴────┐   ┌─────┴─────┐  ┌────┴────────┐    ┌───────┴───────┐
│ SQLite  │   │  DO Spaces │  │  Payment     │    │  DO Spaces    │
│ DB      │   │  (CDN)     │  │  Providers   │    │  + User S3    │
│         │   │            │  │              │    │  + Mirror S3  │
│ models  │   │  media     │  │  Stripe      │    │               │
│ sessions│   │  thumbs    │  │  PayPal      │    │  Presigned    │
│ signals │   │  assets    │  │  Crypto      │    │  URLs only    │
│         │   │            │  │  Gift Cards  │    │               │
└─────────┘   └────────────┘  └──────────────┘    └───────────────┘

Request Flow

Browser GET /s/{shop_id}/{slug}
  │
  ├─ Pyramid route dispatch
  │   └─ views/content.py or views/product.py
  │       ├─ Query product + shop from SQLite
  │       ├─ Compute discovery ring related products
  │       ├─ Generate presigned URLs for media (15 min TTL)
  │       └─ Render Jinja2 template
  │
  ├─ Template layers:
  │   ├─ base.j2 (theme, nav, conditional sandbox toolbar)
  │   ├─ product.j2 / content.j2 (media, metadata, CTA)
  │   ├─ snippets/related_content.j2 (ring sidebar)
  │   ├─ snippets/comments.j2 (comment form + list)
  │   └─ snippets/analytics.j2 (optional Plausible/GA)
  │
  ├─ Client JS (progressive enhancement):
  │   ├─ signals.js → anonymous beacon on unload
  │   ├─ watch.js → SPA navigation (if watch_mode_enabled)
  │   ├─ sandbox.js → filter toolbar (if sandbox_mode)
  │   └─ comments.js → AJAX comment submission
  │
  └─ Media served from CDN via presigned URLs (never through uwsgi)

Data Collection Pipeline

Page visit (browser)
  │
  │ signals.js collects:
  │  - presence (wall_clock, visible, active ms)
  │  - scroll (depth, direction changes)
  │  - media (play, pause, seek, speed, completion)
  │  - viewport width
  │
  ▼
navigator.sendBeacon("/signals/beacon")
  │  ~300 bytes JSON, one per page visit
  │
  ▼
views/signals.py
  │
  ├─ classify_referrer(Referer header)
  │   → (class, domain, query) tuple
  │   Stores: referrer_class, referrer_domain, referrer_query
  │
  ├─ classify_device(viewport_width)
  │   → 0=mobile, 1=tablet, 2=desktop
  │
  ├─ Insert mps_page_session row
  │
  └─ If visible_ms >= 7000:
      increment product.view_count

Analytics Pipeline

mps_page_session (raw rows)
  │
  ├─ views/analytics.py
  │   │
  │   ├─ Daily bucketing functions (28-day windows):
  │   │   _daily_buckets()        → view counts (bar chart)
  │   │   _daily_avg_duration()   → avg session duration
  │   │   _daily_engagement()     → engagement ratio
  │   │   _daily_bounce_rate()    → bounce rate
  │   │   _daily_referrer_counts()→ external referrer volume
  │   │
  │   ├─ Ranked queries:
  │   │   _top_referrer_domains() → top external domains
  │   │   _top_referrer_queries() → top search engine queries
  │   │   _top_search_keywords()  → top internal search terms
  │   │
  │   └─ Aggregate queries:
  │       Overview strip (7d)
  │       Top products by views (7/14/21d)
  │       Ring entry points
  │       Engagement/attention leaders
  │       Study material / background favorites
  │       Traffic sources / device split
  │
  ├─ templates/analytics.j2 (shop-level dashboard)
  │   └─ SVG line_chart macro (server-rendered polyline)
  │
  └─ templates/analytics_product.j2 (per-product dashboard)
      └─ SVG line_chart macro

S3 Storage Architecture

┌─────────────────────────────────────────────────────────┐
│                    MPS Main Bucket                        │
│            (DigitalOcean Spaces + CDN)                   │
│                                                          │
│  {shop_id}/products/{product_id}/{filename}              │
│  {shop_id}/products/{product_id}/thumb/{filename}        │
│  {shop_id}/shop/{logo|banner}                            │
│  {shop_id}/products/{product_id}/karaoke/{filename}      │
└───────────┬─────────────────────────────────┬────────────┘
            │                                 │
     Presigned URLs                    Mirror sync
     (15 min TTL)                   (daemon thread)
            │                                 │
            ▼                                 ▼
    Browser / CDN                 ┌────────────────────┐
                                  │  Shop Mirror Bucket │
                                  │  (shop.has_s3_mirror)│
                                  │                     │
                                  │  Same key structure  │
                                  │  Passive copy        │
                                  │  Backfill on setup   │
                                  └─────────────────────┘

┌─────────────────────────────────────────────────────────┐
│              Shop Primary Bucket (BYOB)                  │
│            (shop.has_primary_s3 — MPS-16)                │
│                                                          │
│  When enabled, REPLACES MPS Main Bucket for this shop:   │
│  - All presigned URLs use shop's S3 client               │
│  - All CDN URLs use shop's cdn_endpoint                  │
│  - request.shop_uploads_client / shop_bucket_name /      │
│    shop_cdn_endpoint fall back to MPS default when off   │
│                                                          │
│  Configured via bucket-settings form section             │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                  User Artifact Bucket                     │
│               (user.has_s3_bucket)                        │
│                                                          │
│  sandbox/{user_id}/{timestamp}-{filename}                │
│                                                          │
│  Presigned POST from /u/sandbox/upload                   │
│  Browser uploads directly (never through MPS server)     │
│  50MB max per file                                       │
└─────────────────────────────────────────────────────────┘

Feature Toggle Matrix

Feature Model Column Form Section Default
Watch mode shop.watch_mode_enabled ribbon-settings Off
Sandbox mode shop.sandbox_mode ribbon-settings Off
Show price history (MPS-24 Phase 2.5) shop.show_price_history ribbon-settings Off
Show dates shop.show_dates ribbon-settings On
Grid lanes shop.grid_lanes_enabled ribbon-settings Off
Color filter shop.color_filter ribbon-settings 0 (none)
Comments shop.comments_enabled comment-settings On
Stripe shop.stripe_enabled stripe-settings On
PayPal shop.paypal_* paypal-settings Off
Crypto shop.monero_* / shop.dogecoin_* crypto-settings Off
Gift cards shop.gift_card_enabled gift-card-settings Off
S3 mirror shop.mirror_s3_* mirror-settings Off
BYOB (primary S3) shop.primary_s3_* bucket-settings Off
Environment shop.environment environment-settings 0 (production)
Trial shop.trial_started_timestamp Auto on creation 21 days
Discovery ring shop.discovery_ring Automatic Auto-computed
Subscriptions shop.subscription_* ribbon-settings Off
Karaoke (vocal isolation) shop.unsandbox_*_key unsandbox-settings Off
Make-an-offer shop.offer_enabled + product.allow_offers offer-settings Off
Offer expiration (hours) shop.offer_expiration_hours offer-settings 48
Offer pay window post-accept (hours) shop.offer_acceptance_payment_hours offer-settings 24
In-app notifications (always on, no toggle — every txn email also drops a row) n/a On
Home page layout (MPS-24) shop.home_layout (0=flat / 1=chips / 2=lanes) home-layout-settings 0 (flat)
Tag chip / lane caps (MPS-24) shop.home_layout_tag_limit / shop.home_layout_per_lane_limit home-layout-settings 8 / 10
Featured products strip (MPS-24) shop.featured_product_ids_json home-layout-settings empty
Product tags (MPS-24) Tag + ProductTag association product edit + /s/{id}/tags bulk editor
Tag auto-suggest (MPS-24 Phase 2) lib/tag_suggest.py over Product.title + Product.description ?show_suggestions=1 on /s/{id}/tags + scripts/backfill_tags.py Never auto-applies
Tag-detail facet sidebar (MPS-24 Phase 2.6) shop_tag.j2 + views/shop.py _price_range_from_request / _filter_by_price_range Sort + price range + categories list, GET form, sidebar ≥800px, top chip strip <800px Always on for tag detail pages
6-sentence SERP excerpt (MPS-24 Phase 2.6) Product.excerpt_sentences(n=6, max_chars=1500) via shared _strip_markdown() helper shop_tag.j2 row description Always on
Facet nav on shop home (MPS-24 Phase 2.6b) _facet_nav.j2 macros + home.j2 / shop.j2 wrappers Desktop sidebar + mobile <details> accordion when shop.home_layout >= 1 Opt-in via home_layout
Mobile SERP rows under each lane (MPS-24 Phase 2.6b) home.j2 / shop.j2 .tag-lane-rows markup + CSS visibility swap Horizontal tiles ≥800px; vertical SERP rows with 6-sentence excerpts <800px On for layout 2

Ticket Index

Ticket Title Status
MPS-0 AJAX Comment Submission Complete
MPS-1 YouTube-Style Watch Experience Complete
MPS-2 Anonymous Signal Gathering & View Count Complete
MPS-3 Creator Analytics Dashboard Complete
MPS-4 Eliminate 502s from uWSGI Worker Recycling Complete
MPS-5 Investigate uWSGI Worker Memory Growth Open
MPS-6 Referrer Analytics — Domain, Query, Trend Lines Complete
MPS-7 Sandbox Mode — Creative Filter System Complete
MPS-8 User S3 Bucket + Artifact Storage Complete
MPS-9 Shop S3 Mirror Bucket Complete
MPS-10 Gift Card — Models & Migration Complete
MPS-11 Gift Card — Purchase Flow Complete
MPS-12 Gift Card — Redemption at Checkout Complete
MPS-13 Gift Card — Shop Admin & Settings Complete
MPS-14 Shop Environment — Dev & Stage Shops Complete
MPS-15 21-Day Free Trial Complete
MPS-16 Bring Your Own Bucket (BYOB) Complete
MPS-17 REST API v1 — HMAC-signed product/content + upload Open
MPS-18 Karaoke Mode — Diagnose & Fix Vocal Isolation Open (Broken in prod)
MPS-19 BitTorrent / Magnet Link — Diagnose & Fix Distribution Open (Broken in prod)
MPS-20 Auction House Mode (eBay-style Bidding) Complete
MPS-21 Make-an-Offer Mode Complete
MPS-22 Kill-Switch Feature Flags — Karaoke + Torrent Off by Default Complete
MPS-23 Consolidated Transactional Sender Identity + Shop Contact Email Open
MPS-24 Shop home page overhaul + product categorization (tags + chips + lanes + auto-suggest) In progress (Phases 1 + 2 landed)
Doc Purpose
Design System Design tokens, CSS architecture, component library
Notifications In-app notification table, kinds, and per-transition wiring
Auction House MPS-20 — bidding, soft-close, payment deadline
Make Offer MPS-21 — offer state machine, counter rounds, expiry
JavaScript Client-side JS architecture
Karaoke Pipeline Vocal isolation: pipeline, on-demand, streaming architecture
Sandbox Mode Creative filter system
Auction House MPS-20: state machine, bidding logic, soft-close, proxy
Make-an-Offer MPS-21: state machine, counter rounds, auto-accept/decline
Testing Performance Test suite optimization