Commit graph

798 commits

Author SHA1 Message Date
fd39b2ae80 Fix CI build: ensure bin/python symlink exists for virtualenv-clone
Python 3.12 venv may only create bin/python3 and bin/python3.12 without
a bin/python symlink. virtualenv-clone 0.5.7 expects bin/python to exist
when introspecting the cloned venv, causing FileNotFoundError.
2026-02-12 08:03:01 -05:00
0967e4d6fb Fix mobile video overflow: constrain watch container to viewport width
Product images container now uses overflow:hidden + min-width:0 to
prevent the CSS grid child from expanding beyond the viewport. Watch
video container also gets max-width:100% + min-width:0 for the same
grid overflow fix.
2026-02-12 07:32:54 -05:00
47fb7a5440 Add Comments link below download button on mobile
Shows "Comments (N)" anchor link below the purchase/download well,
visible only on mobile where comments section is further down the page.
SPA navigation updates the count dynamically.
2026-02-12 07:32:14 -05:00
0299f85fc9 Mobile: limit related content to 7 items, add Comments anchor link
On mobile (<800px), hide related content items beyond offset 7 to
reduce scroll. Add a "Comments (N)" link at the bottom of the related
content panel that jumps to the comments section below. Hidden on
desktop where comments are already visible in the sidebar layout.
Updated all three layers: template, watch.js SPA, and watch JSON.
2026-02-12 07:27:26 -05:00
1e5fe27d38 Price history on product pages, Fresh toggle, media type filters for ring
Price history: expandable <details> section on sellable product pages,
gated to shop editors. SPA-synced via watch.py JSON + watch.js rebuild.
Ticket 09 for future membership-gated access.

Fresh toggle: hides already-watched ring items (display:none vs dimmed),
persisted in localStorage, so loopers see only unwatched content.

Media type filters: Video/Audio/Image/Docs/Other pill buttons below
ring controls. Toggle off a type to hide those rows from the ring.
All on by default, persisted in localStorage. Server-rendered via
Jinja macro + data-media-type attribute, JS-synced during SPA nav.
2026-02-11 19:22:44 -05:00
13aeab7188 Retry deploy: added reload_on_rss to caddy_sites.sls 2026-02-11 18:49:29 -05:00
69ee077cb4 Retry deploy: confirmed salt fileserver has reload_on_rss 2026-02-11 18:22:50 -05:00
f4661ad6b8 Retry deploy after salt fileserver update 2026-02-11 17:49:29 -05:00
7c79304fcb Retry deploy: salt master now has reload_on_rss in sites.sls 2026-02-11 15:45:18 -05:00
d9a565b57a Add tickets MPS-4 and MPS-5 for 502 worker recycling fix
MPS-4: Eliminate intermittent 502s via uwsgi config tuning
MPS-5: Investigate root cause of worker memory growth (~40MB/min)
2026-02-11 10:13:52 -05:00
faefd94abf Video watch metrics, per-product analytics page, SPA thumbnail sync
Analytics:
- Add video watch metrics section to shop dashboard (avg % played,
  completion rate, seeks, rewinds, pauses, speed) with per-product
  video breakdown table
- Add per-product analytics detail page at /s/{shop_id}/analytics/{product_id}
  with views over time, video watch, traffic, devices, ring entries,
  engagement, comment sentiment, and price history
- Product title links on shop analytics now go to per-product analytics
- Add get_sentiment_summary_for_product() helper

SPA fix:
- Update thumbnail image src and wrapping link href during DJ fade
- Clear stale extra thumbnails from previous product
- Add file_url to watch JSON for thumbnail-to-file links
2026-02-11 09:03:29 -05:00
2f5498a1f4 deleted: docs/tickets/an-algo/01-engagement-signal-weighting.md
deleted:    docs/tickets/an-algo/02-truth-fidelity-score.md
	deleted:    docs/tickets/an-algo/05-love-chains-referrals.md
	deleted:    docs/tickets/an-algo/06-love-letters-messaging.md
	deleted:    docs/tickets/an-algo/08-pillars-score-dashboard.md
	modified:   make_post_sell/GIT_HASH
2026-02-10 16:53:31 -05:00
7401b4a3c3 Fix 502s from anonymous visitors: guard request.user.can_edit_shop with null check
Bot crawlers (AhrefsBot, SemrushBot) hitting content pages caused
jinja2.UndefinedError: 'None' has no attribute 'can_edit_shop' because
request.user is None for unauthenticated visitors.
2026-02-10 08:54:51 -05:00
a950304a49 Epic DJ transitions: fade video/audio to black before static, smooth 20fps countdown fade on all content types
Video/audio → static/cross-type now fades volume + opacity over 7 seconds
instead of abrupt cut. Static content (images, PDFs) fades out smoothly
during the 7-second countdown. All transitions cancelable, no abrupt stops.
2026-02-10 07:37:41 -05:00
2c4237f136 Fix cutoffs test for dict return value 2026-02-09 19:02:01 -05:00
ed9c2a6a08 Show ring consumed across 5 time periods: today, 7d, 28d, year, lifetime 2026-02-09 18:22:28 -05:00
f2e88989a2 Fix ring consumed to count unique products viewed, not average position index 2026-02-09 18:20:07 -05:00
6c71af4017 Change avg ring depth to avg ring consumed percentage 2026-02-09 17:58:07 -05:00
fb04cf0159 Show top 42 ring entry points instead of 7 2026-02-09 17:47:30 -05:00
fca2f4a051 Isolate parallel test runs with PID-based database filenames
When two processes run pytest simultaneously, they no longer share
test_make_post_sell_master.sqlite. Each gets its own file keyed by PID,
preventing table-exists and readonly-database errors. Also clean up
WAL/SHM journal files on exit.
2026-02-09 16:24:25 -05:00
e1a8b2f236 Yield GIL in discovery ring computation to unblock uwsgi during reforge
The O(n²) greedy walk held the GIL for the entire computation, starving
uwsgi workers on the 2 vCPU box. time.sleep(0) in the inner loop
releases the GIL each iteration so request handling can interleave.
2026-02-09 15:24:15 -05:00
3e6786155a Anonymous signal gathering with view counts (MPS-2)
One beacon per page visit sent on unload (~300 bytes JSON). No cookies,
no IPs, no fingerprints. A view counts after 7 seconds of visible time.
Creators see view counts on product pages; raw data feeds future analytics.

New: PageSession model, /signals/beacon endpoint, signals.js client
collector, product view_count column with human_view_count property,
Alembic migration, SPA integration, 30 new tests (667 total, all pass).
2026-02-09 15:01:36 -05:00
dac8cc9280 Add unit, integration, and functional tests for async ring reforge
Unit (6 tests): thread spawning, dirty bit behavior, debounce
collapse, session.expire_all on re-run, exception cleanup, missing
shop handling.

Integration (3 tests): ring persisted to DB, dirty bit picks up
latest state, private/unlisted products excluded.

Functional (5 tests): edit doesn't block, product page uses existing
ring, watch JSON never reforges, new product triggers async reforge,
no trigger when watch mode off.
2026-02-09 14:08:11 -05:00
bda4d39858 Use dirty bit instead of drop for async ring reforge debounce
When a reforge is already running, set a dirty flag so the thread
loops once more after finishing — picks up latest product state
from rapid saves without queueing multiple reforges.
2026-02-09 13:55:00 -05:00
3ba8802d9a Fix SQLAlchemy deprecation in async reforge, add playback progress restore, add signal gathering tickets 2026-02-09 13:52:34 -05:00
7b7a2d1edb Async ring reforge, update og:image during SPA navigation, remove inline reforge
Ring reforge now runs in a background thread to avoid blocking requests.
Product/content views no longer reforge inline — they use the existing ring
or fall back to related products. og:image meta tag updated during SPA
navigation so the now-playing row shows the correct thumbnail.
2026-02-09 13:45:42 -05:00
6eb4b3ca95 Refresh presigned media URL every 7 minutes to prevent TTL expiry
Long videos (>15 min) would die mid-playback when the 15-minute presigned
URL expired. Now a timer fetches a fresh URL from the watch JSON endpoint
every 7 minutes, swaps the src, and seeks back to the saved position.
Timer resets on every product transition.
2026-02-09 13:27:22 -05:00
204bca813d Skip DJ crossfade for short clips, hard-cut to next on ended
Videos shorter than 7 seconds would trigger DJ crossfade immediately on
first timeupdate, fading out before they even played. Now short clips
play fully and transition instantly to the next preloaded item.
2026-02-09 13:04:16 -05:00
c5c0cb1b00 Ensure sticky video stays on screen for entire page scroll in watch mode
Desktop: override align-items to stretch on watch-mode grid so watch-left
fills the full row height, giving the sticky video room to stick as long
as any content is visible in either column.
2026-02-09 13:04:10 -05:00
df1cd64851 Make /random redirect to any public product or content page
Removed media-only filter and popout_player_enabled gate. Now picks
from all public items and redirects to the correct /p/ or /c/ URL.
2026-02-09 12:54:03 -05:00
a3725e8070 Add container fullscreen for crossfade, mobile watch layout fix, sitemap link on subscribe page
Promote video fullscreen to .watch-video-container so DJ crossfade
transitions remain seamless. Use display:contents on mobile watch-left
so sticky video sticks properly. Add z-index layering for desktop
watch mode scrolling. Add sitemap.xml link to subscribe page feeds.
2026-02-09 12:36:29 -05:00
86a05a8b88 Fix sticky video z-index so comments and ring buttons scroll under it
Desktop: added position:relative + z-index:5 to description and comments
in watch mode so they stack below the sticky video (z-index 10).

Mobile: changed watch-left to display:contents so sticky video sticks
against the full grid container instead of just the watch-left box.
Previously product-right (ring/download) was outside watch-left, ending
the sticky context and pushing the video offscreen.
2026-02-09 12:36:21 -05:00
98202683d6 Fix subscribe page always highlighting None instead of user's actual frequency
The view returned empty context on GET, so the template hardcoded checked
on the None radio. Now the view looks up the logged-in user's existing
subscription and passes current_frequency to the template, which sets
checked on the correct radio button dynamically.
2026-02-09 11:54:51 -05:00
433f4e25ce Show 42 forward items in ring sidebar instead of 21 2026-02-09 11:54:20 -05:00
f780f0dab6 Redesign ring header into structured 2-row grid, show full now-playing title and thumbnail
Ring header restructured from flat 1fr/auto grid into two semantic rows:
- Info row: title + badges left, progress counter right
- Controls row: Reverse and Autoplay toggles right-aligned

Now-playing row shows full title (no line-clamp truncation) and product
thumbnail. JS reads og:image meta tag for current thumbnail during SPA
navigation. SPA updates for edit button, download button, file info,
comments, and canonical link. Footer and docs updates.
2026-02-09 11:46:54 -05:00
3ade78d512 Fix ring double-advance defect in DJ crossfade, use defect not bug in CLAUDE.md
completeDjCrossfade was advancing ringPosition after syncRingPosition
already set it correctly at the halfway mark, causing autoplay to skip
one song ahead of what the sidebar displayed. Removed the redundant
advance — syncRingPosition handles positioning via updatePageContent.
2026-02-09 11:31:46 -05:00
7f8a5e631a Show 3 previous + current (highlighted) + 21 next items in ring sidebar
Ring sidebar now displays a window around the current position: 3 items
behind (dimmed), the current item highlighted in blue with a play marker,
and 21 items ahead. Each item shows its signed ring offset (-3 to +21).

get_ring_related_products returns dicts with product and offset. Views
pass offset through to JS. Server-rendered template handles both formats.
2026-02-09 11:02:16 -05:00
98efbfe566 Track ring loop completions with +N badge and show mod badge for shop owners
When a viewer watches every item in the ring, the loop counter increments,
history resets (un-dims all items), and a +1/+2/+3 badge appears next to
the progress display. Shop owners/editors get a green "mod" badge.

Ring state: ringLoops persisted in localStorage alongside existing ring state.
Server: watch_json now returns is_mod flag. Templates pass data-watch-is-mod.
2026-02-09 10:57:56 -05:00
740001f09b Fix radio highlight not updating on subscribe page, default to None
Replace display:none on radio inputs with visually-hidden technique
(position:absolute, opacity:0) so :checked pseudo-class updates
reliably when clicking labels. Default frequency changed to None.
2026-02-09 10:52:45 -05:00
50bfb13309 Stack all watch-video-container children in same grid cell for DJ crossfade
grid-area: 1/1 was only on video elements, so non-video children (countdown
overlay, unmute button) could push videos into separate grid rows. Apply
grid-area: 1/1 to all children so both active and standby videos always
occupy the same x/y position with z-index layering.
2026-02-09 10:30:14 -05:00
df9270b3aa Add Immediate frequency option, reorder to None/Immediate/Daily/Weekly, default Daily
Subscription frequency was missing Immediate and defaulted to Weekly.
Now offers four options in logical order with Daily as the default digest.
2026-02-09 10:22:16 -05:00
7f3b3e2fee Add comment moderation dashboard at /s/{shop_id}/comments 2026-02-09 09:37:32 -05:00
5b39ab273a Anonymous commenting with email verification via OTP
Show comment form to all visitors instead of requiring sign-in first.
Anonymous users enter email + comment, verify via 6-digit OTP, then
their comment is created. Uses existing OTP infrastructure and honeypot
spam protection.
2026-02-09 09:11:13 -05:00
7257a588a0 Fix DJ crossfade event listener accumulation and non-idempotent preload
Named event handlers (onMediaEnded, onMediaLoadedMetadata, onMediaTimeUpdate)
replace anonymous functions so setupMediaEvents() can removeEventListener
on both active and standby elements before adding fresh listeners. This
prevents duplicate handlers from accumulating after each crossfade swap.

preloadNext() now tracks preloadingId to skip redundant fetches — prevents
presigned URL regeneration from invalidating already-buffered standby media.
2026-02-09 09:10:39 -05:00
08aceed3b8 Fix mobile watch mode: content scrolls under sticky video, add horizontal padding
- Give product-images, description, comments, and product-right opaque
  backgrounds so they slide beneath the sticky video instead of through it
- Add 4px horizontal padding to mobile two-column layout matching nav inset
2026-02-09 07:47:42 -05:00
836d78d310 Move ring.json route before shop_slug catch-all so it matches 2026-02-09 07:44:07 -05:00
b933daffd9 Forge ring on first access, add /s/{shop_id}/ring.json endpoint
Existing shops had no ring until a product edit triggered it.
Now watch_json and server-rendered views forge the ring on first
access if empty. Added ring.json diagnostic endpoint to inspect
a shop's discovery ring state.
2026-02-09 07:19:11 -05:00
bc41c3481b Precompute discovery ring per shop for deterministic content traversal
Replace per-request Jaccard similarity with a precomputed circular
ordering of all public products. Greedy nearest-neighbor walk starts
from the newest product, accumulating stems to cluster similar content.
Ring reforges on product create/edit/upload when watch mode is enabled.

Frontend stores ring position, direction, and watch history in
localStorage. Queue items splice into playback without moving ring
position. Watched items dim in sidebar with progress indicator.
2026-02-08 16:07:25 -05:00
4c1cd11050 Lazy cart creation: don't persist session carts until product added
Bots and crawlers were creating empty cart rows on every page visit,
bloating the database (531k empty rows, 80% of all carts). Session
carts are now kept in-memory until a product is actually added, at
which point the view persists them via dbsession.add + flush.

- add_session_cart returns in-memory Cart, reuses UUID across requests
- authentication.py guards dbsession.delete for transient carts
- 17 new tests: unit, integration, and functional coverage
2026-02-08 15:26:35 -05:00
5ee2175a3f Serve feeds as application/xml so browsers render instead of download
Also add target=_blank to subscribe page feed links.
2026-02-08 14:24:45 -05:00