- 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>
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.