make_post_sell/docs/sandbox-mode.md
russell@unturf.com 32a19a339c docs: add MPS-6 through MPS-9 tickets, architecture diagram, update JS docs
- MPS-6: referrer analytics (domain, query, trend line charts)
- MPS-7: sandbox mode creative filter system
- MPS-8: user S3 bucket + artifact storage
- MPS-9: shop S3 mirror bucket
- architecture.md: system diagram, request flow, data pipeline, S3 layout
- JAVASCRIPT.md: add sandbox.js, signals.js, MediaPipe SDK entries
- sandbox-mode.md: mark S3 upload as implemented
- mps-2.md: document referrer_domain + referrer_query columns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:19:56 -05:00

14 KiB

Sandbox Mode: Creative Filter System

Client-side creative toolkit for MPS shops. When a shop owner enables sandbox mode, all visitors see a floating filter toolbar. Users can apply real-time CSS/SVG/canvas filters to images and video, export filtered artifacts, and activate face detection overlays. All processing runs in the browser. The server stores only the boolean toggle.

Architecture

┌──────────────────────────────────────────────────────────┐
│                      Browser (client)                     │
│                                                           │
│  ┌─────────────┐   ┌──────────────┐   ┌──────────────┐  │
│  │ CSS Filters  │   │ Canvas Export │   │  MediaPipe   │  │
│  │ (live view)  │──>│ (artifacts)  │   │ Face Mesh    │  │
│  │              │   │              │   │ (on-demand)  │  │
│  │ 32 presets   │   │ ctx.filter   │   │ 468 landmarks│  │
│  │ 7 sliders    │   │ toBlob()     │   │ eye glow     │  │
│  │ SVG filters  │   │ MediaRecorder│   │ face mask    │  │
│  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘  │
│         │                  │                   │          │
│         │            ┌─────┴─────┐             │          │
│         │            │  Download  │             │          │
│         │            │  or Upload │             │          │
│         │            │  (presign) │             │          │
│         │            └───────────┘             │          │
│         │                                      │          │
│  ┌──────┴──────────────────────────────────────┴───────┐  │
│  │              sandbox.js (IIFE, ~700 lines)           │  │
│  │  localStorage persistence  │  window.sandboxReapply  │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                           │
└────────────────────────────┬──────────────────────────────┘
                             │
                    ┌────────┴────────┐
                    │  MPS Server     │
                    │                 │
                    │ shop.sandbox    │
                    │ _mode = True    │
                    │                 │
                    │ (that's it.     │
                    │  server stores  │
                    │  one boolean.)  │
                    └─────────────────┘

Filter Pipeline

User clicks preset          User adjusts slider
       │                           │
       ▼                           ▼
┌─────────────┐          ┌──────────────────┐
│ PRESETS[idx] │          │ sliderValues[id] │
│ .css string  │          │ → fn(val+unit)   │
└──────┬──────┘          └────────┬─────────┘
       │                          │
       └────────────┬─────────────┘
                    │
                    ▼
         ┌──────────────────┐
         │ getCurrentFilter  │     CSS filter string:
         │ CSS()             │──>  "sepia(40%) contrast(90%)
         └──────────────────┘      brightness(105%) saturate(80%)"
                    │
                    ▼
         ┌──────────────────┐
         │ element.style    │     Applied to: img, video
         │ .filter = css    │     (NOT <html> — stacks with
         └──────────────────┘      shop color_filter)

Export Pipeline

Image Export

img.product-main
       │
       ▼
  new Image()
  crossOrigin = "anonymous"    ← CORS required from CDN
       │
       ▼
  canvas.getContext("2d")
  ctx.filter = getCurrentFilterCSS()
  ctx.drawImage(img, 0, 0)
       │
       ▼
  canvas.toBlob("image/png")
       │
       ├──> Download (a.download = "sandbox-export.png")
       │
       └──> Upload (presigned POST to user's S3 bucket)

Video Export

video element
       │
       ▼
  canvas.captureStream(30fps)
       │
       ▼
  MediaRecorder(stream, "video/webm")
       │
       ▼
  requestAnimationFrame loop:
    ctx.filter = getCurrentFilterCSS()
    ctx.drawImage(video, 0, 0)
       │
       ▼
  recorder.stop() → Blob
       │
       └──> Download ("sandbox-export.webm")

Face Detection Pipeline (MediaPipe)

User clicks "Load Face Detection"
       │
       ▼
  Load face_mesh.js from CDN (~4MB WASM)
  Load camera_utils.js from CDN
       │
       ▼
  FaceMesh({ maxNumFaces: 4, refineLandmarks: true })
       │
       │
  ┌────┴────────────────────┐
  │                         │
  ▼                         ▼
Image path              Video path
  │                         │
  draw to canvas            Camera utility
  faceMesh.send()           onFrame → send()
  │                         │
  ▼                         ▼
  onResults(landmarks)      onResults(landmarks)
  │                         │
  └────────┬────────────────┘
           │
           ▼
    468 face landmarks per face
           │
    ┌──────┴──────┐
    │             │
    ▼             ▼
  Eye Glow     Face Mask
    │             │
    │  Iris       │  Face outline
    │  landmarks  │  (landmarks
    │  468-477    │  10,338,297...)
    │             │
    │  Radial     │  Fill face oval
    │  gradient   │  Draw eye shapes
    │  overlay    │  Draw eyebrows
    │             │  Draw mustache
    │             │  Draw goatee
    ▼             ▼
  Canvas overlay composited
  on top of media element

Filter Preset Reference

Basic (4)

Name CSS Filter Chain
None none
Grayscale grayscale(100%)
Sepia sepia(100%)
Invert invert(100%)

Warm (4)

Name CSS Filter Chain
Warm sepia(30%) saturate(140%) brightness(105%)
Sunset sepia(50%) hue-rotate(-15deg) saturate(150%)
Golden sepia(60%) brightness(110%) contrast(90%)
Amber sepia(40%) saturate(160%) hue-rotate(-10deg) brightness(108%)

Cool (4)

Name CSS Filter Chain
Cool sepia(20%) hue-rotate(180deg) saturate(120%)
Arctic sepia(30%) hue-rotate(200deg) brightness(110%)
Moonlight sepia(40%) hue-rotate(220deg) saturate(80%) brightness(105%)
Frost sepia(25%) hue-rotate(190deg) brightness(115%) contrast(95%)

Dramatic (4)

Name CSS Filter Chain
Hi-Contrast contrast(180%) saturate(120%)
Noir grayscale(100%) contrast(150%) brightness(90%)
Faded contrast(80%) saturate(60%) brightness(110%)
Vintage sepia(40%) contrast(90%) brightness(105%) saturate(80%)

Color (5)

Name CSS Filter Chain
Red Shift sepia(100%) hue-rotate(-30deg) saturate(300%)
Green Shift sepia(100%) hue-rotate(85deg) saturate(300%)
Blue Shift sepia(100%) hue-rotate(200deg) saturate(300%)
Purple Haze sepia(100%) hue-rotate(265deg) saturate(200%)
Cyan sepia(100%) hue-rotate(145deg) saturate(300%)

Instagram-Style (7)

Name CSS Filter Chain
Clarendon contrast(120%) saturate(125%)
Juno sepia(10%) contrast(110%) saturate(150%) brightness(105%)
Lark contrast(90%) brightness(115%) saturate(85%)
Gingham brightness(105%) hue-rotate(350deg) saturate(80%)
Nashville sepia(25%) contrast(120%) brightness(105%) saturate(120%) hue-rotate(-15deg)
Valencia sepia(15%) contrast(110%) brightness(108%) saturate(120%)
Walden brightness(110%) saturate(160%) sepia(30%) hue-rotate(350deg)

SVG Filters (4)

Name Applied As
Duo Blue url(#sandbox-duotone-blue) — SVG feColorMatrix
Duo Green url(#sandbox-duotone-green) — SVG feColorMatrix
Film Grain url(#sandbox-grain) — SVG feTurbulence + feBlend
Vignette url(#sandbox-vignette) — SVG feFlood + feGaussianBlur

Adjustment Sliders

Slider Range Default CSS Function
Brightness 0-200% 100% brightness()
Contrast 0-200% 100% contrast()
Saturation 0-300% 100% saturate()
Hue 0-360deg 0deg hue-rotate()
Blur 0-20px 0px blur()
Sepia 0-100% 0% sepia()
Grayscale 0-100% 0% grayscale()

Shop Owner Configuration

Enable in Shop Settings > Announcement Ribbon Settings (the form section that contains display toggles):

Settings > ribbon-settings form section > Sandbox Mode > Enable/Disable

The toggle adds sandbox_mode = True to the shop model. The base template conditionally renders the toolbar HTML, SVG filter definitions, and loads sandbox.js only when enabled.

Watch Mode SPA Integration

When watch mode is enabled, SPA navigation replaces media elements without a full page reload. The updatePageContent() function in watch.js calls window.sandboxReapply() after swapping DOM elements. This re-applies the active sandbox filter to newly loaded images and video with a 50ms delay to let the DOM settle.

CORS Requirements for Canvas Export

CSS filter preview works without CORS — filters are applied as rendering hints on the element. Canvas export (toBlob) requires pixel access, which requires CORS headers from the CDN.

DigitalOcean Spaces CORS configuration needed:

{
    "CORSRules": [{
        "AllowedOrigins": ["*"],
        "AllowedMethods": ["GET"],
        "AllowedHeaders": ["*"],
        "MaxAgeSeconds": 3600
    }]
}

Without CORS, the export button shows a user-friendly error message. Filter preview continues to work normally.

localStorage Keys

Key Value Purpose
mps_sandbox_preset Integer (0-31) Active preset index
mps_sandbox_sliders JSON object Slider values
mps_sandbox_panel "0" or "1" Panel open/closed
mps_sandbox_mode "preset" or "custom" Filter mode

Artifact Storage (User S3 Bucket)

Users can configure their own S3-compatible bucket to upload sandbox artifacts.

User Settings (/u/settings)
  │
  │  Artifact Storage form:
  │  - S3 Endpoint URL
  │  - Region (optional)
  │  - Bucket Name
  │  - Access Key
  │  - Secret Key
  │
  ▼
POST /u/settings/storage
  │
  │  Saves credentials on User model
  │  (s3_endpoint, s3_region, s3_bucket,
  │   s3_access_key, s3_secret_key)
  │
  ▼
Sandbox toolbar shows "Upload to Bucket" button
  │  (only when data-has-bucket="1")
  │
  │  1. User exports image/video (download)
  │  2. User clicks "Upload to Bucket"
  │  3. JS POSTs to /u/sandbox/upload
  │  4. Server generates presigned POST with user's credentials
  │  5. JS uploads blob directly to user's S3 bucket
  │
  ▼
User's S3 Bucket
  sandbox/{user_id}/{timestamp}-{filename}

Supported services: DigitalOcean Spaces, AWS S3, MinIO, Backblaze B2, any S3-compatible endpoint.

Security: Credentials stored as plain Unicode columns (same pattern as Stripe/PayPal/Adyen keys on Shop model). Secret key field uses type="password" in the form.

Files

File Role
models/shop.py sandbox_mode Boolean column
models/user.py S3 credential columns + has_s3_bucket property
views/shop.py Toggle handler in ribbon-settings
views/user.py Storage settings POST handler + S3 fields in settings dict
views/user_sandbox.py Presigned upload endpoint (/u/sandbox/upload)
templates/shop_settings.j2 Radio buttons for enable/disable
templates/user_settings.j2 Artifact Storage form
templates/base.j2 Conditional toolbar HTML + SVG defs + data-has-bucket
static/js/sandbox.js Client-side filter engine + upload-to-bucket
static/css/common.css Toolbar layout (CSS Grid)
static/js/watch.js sandboxReapply() hook in SPA navigation
routes.py user_storage_settings, user_sandbox_upload routes
scripts/alembic/versions/a2c13d3117f2_*.py Shop sandbox_mode migration
scripts/alembic/versions/f898ba460612_*.py User S3 credentials migration

Mobile Behavior

On screens max-width: 800px:

  • Toggle button: bottom-right corner (10px offset)
  • Panel: full-width (10px margin left + right)
  • Max height: 50vh (scrollable)
  • All controls always visible (no hover-only interactions)
  • Touch targets adequate for finger tapping

Stacking with Shop Color Filter

The shop-level color_filter (grayscale, red, green, blue, etc.) applies to <html> via data-color-filter attribute. Sandbox filters apply to individual img and video elements via element.style.filter. These stack: the shop filter colors the entire page, and the sandbox filter adds per-element effects on top. This is intentional — the two systems are independent and composable.