Users can configure their own S3-compatible bucket credentials in User
Settings > Artifact Storage. When configured, sandbox mode shows an
"Upload to Bucket" button that pushes exported artifacts (filtered images,
video captures) directly to the user's bucket via presigned POST.
Supports DigitalOcean Spaces, AWS S3, MinIO, Backblaze B2, and any
S3-compatible service. Files never touch the MPS server — two-step
presign pattern matches existing product upload architecture.
CDN already serves Access-Control-Allow-Origin: * but browsers only
send the Origin header when img.crossOrigin is set. sandbox.js now
sets crossOrigin="anonymous" on all images at init and after SPA
navigation so canvas.toBlob() can read pixels for export.
Architecture diagrams, export pipeline, face detection pipeline,
filter preset reference table, CORS requirements, localStorage keys,
mobile behavior, and stacking with shop color filter.
Client-side creative filter system for MPS shops. When enabled, visitors
see a floating toolbar with 32 filter presets (basic, warm, cool, dramatic,
color shifts, Instagram-style, SVG), 7 adjustment sliders, image/video
canvas export, and on-demand MediaPipe face detection (eye glow, face mask).
All processing is browser-side. Server only stores the sandbox_mode toggle.
Filters apply to individual media elements (stacks with shop color_filter).
localStorage persists filter state. Watch mode SPA re-applies on navigation.
Set PRAGMA busy_timeout=30000 on SQLite connections to prevent
"database is locked" errors under concurrent access. Fix invalid
escape sequence in email regex by using raw string.
- Add SVG line charts for session duration, engagement, and bounce rate trends (28 days)
- Track referrer_domain and referrer_query on PageSession (new migration)
- Refactor classify_referrer() to extract domain and search engine query params
- Surface internal search keywords (ShopSearchRequest) on shop analytics
- Show top referrer domains and search engine queries on both analytics pages
- Add wide bar row CSS modifier for longer domain labels
Previously every watch JSON request and product page load called
get_ring_related_products with forward=len(ring), loading ALL products
in the shop from the DB. For large rings this caused worker memory to
spike, triggering uwsgi reload-on-rss kills and producing 502s.
Cap related items to 42 forward + 3 backward (matching the sidebar
display limit). The watch JSON endpoint now accepts a dir query param
so the client can signal travel direction — when going backward the
allocation flips to 3 forward + 42 backward.
Also adds retry with backoff (up to 2 retries, 1s/2s delay) in
fetchWatchData() for transient 502/503/504 responses during worker
recycling.
Both signal and thread timeout methods break pytest-xdist: signal
corrupts workers, thread kills them mid-operation. The real fix was
timeout=10 on PayPal requests. Global test timeout is not needed.
signal-based timeouts corrupt xdist worker processes, causing mass
test failures on CI. thread method is the documented alternative
for parallel test execution.
PayPal sandbox tests used requests.post/get() with no timeout parameter.
When the CI runner at build.unturf.com cannot reach api-m.sandbox.paypal.com,
these calls block until TCP timeout (minutes), stalling all xdist workers
and causing the 1-hour CI timeout.
- Add timeout=10 to all requests calls in PayPal sandbox tests
- Add pytest-timeout with 60s default so no single test can hang the suite
The related content list only showed 42 forward items. When fresh mode
hid watched items, most were filtered out leaving a sparse list with
huge gaps. Now all ring positions are included so fresh mode always
has plenty of unwatched items to display.
- Pass forward=len(ring) to get_ring_related_products in all callers
- Cap forward in the function to prevent backward/forward overlap
- Add loading="lazy" to thumbnails beyond offset 7 (template + JS)
- Override mobile overflow hiding in fresh mode so unwatched overflow
items remain visible
All three karaoke upload paths (single product upload, async backfill,
CLI backfill script) now call update_s3_acls after writing tracks to S3,
ensuring karaoke track ACLs match the product's current visibility.
update_s3_acls silently skipped instrumentals/vocals because no s3_key
resolution existed for those file keys. Added generic fallback to
construct s3_path/file_key for any unhandled key.
Added 11 tests: unit (ACL parity across visibility levels, file_keys
membership, update_s3_acls call count), integration (visibility change
propagates to karaoke ACLs), functional (watch JSON and content page
karaoke URL embedding).
The controls bar was overflowing — "Next ▶" wrapped vertically. Switch
from single-row grid-auto-flow to a 3-column grid so items split into
two rows: toggles (Fresh/Reverse/Autoplay) on top, action buttons
(Karaoke/Random/Next) below. Reduce gap from 16px to 6×12px.
Moves vocal isolation track switching from the pop-out player into the
ring header controls so users can cycle Original/Instrumentals/Vocals
while watching. Preserves playback position on track switch, resets to
Original on SPA navigation, and adds K keyboard shortcut.
The upload endpoint streams the request body in chunks without buffering,
so raw_body stays empty on the server side. The HMAC must sign against an
empty body string to match. _sign_from_disk (which hashed file contents)
produced signatures the server could never verify — every upload got 401.
The inline path loaded entire base64 payloads into API memory via
Plug.Parsers, then decoded them again for UploadStore — 4 concurrent
45MB files consumed ~420MB on a 768MB droplet, pushing BEAM into swap
and timing out all RPCs.
Now uses POST /upload (streaming 64KB chunks, constant memory) then
references upload_ids in the execute call. The JSON body drops from
~60MB to ~500 bytes. Zero base64 encoding on the request side.
process_karaoke held 4+ copies of every media file in memory
simultaneously (raw bytes, base64, JSON serialization). With 14-16
concurrent workers on large files, memory exploded past 3.2GB RSS.
Replace all in-memory buffers with a disk-backed pipeline:
- Stream S3 download to tmpfile instead of .read()
- Build JSON payload on disk with streaming base64 encoding
- Incremental HMAC-SHA256 signing from disk
- Stream HTTP request body from file, response to file
- Decode artifacts to tmpfiles one at a time for S3 upload
Peak request-side memory drops from ~1GB/worker to ~64KB/worker.
Function signature unchanged — all callers work without modification.
The grandchild process closes all inherited fds (3..maxfd) to detach
from uWSGI. session_factory().get_bind() returned a session bound to
the now-dead SQLite fd. Capture the DB URL before forking, create a
fresh engine in the child.
4 concurrent workers was overwhelming the unsandbox API (429/502).
Cap at 1 worker for now. Also increase stderr truncation from 500
to 2000 chars to see full ffmpeg errors.
Single fork left the child in the uWSGI process group with inherited
HTTP sockets, causing 502s. Double-fork with setsid() and fd cleanup
so the grandchild is fully orphaned to init.