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

372 lines
14 KiB
Markdown

# 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:
```json
{
"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.