Commit graph

977 commits

Author SHA1 Message Date
d527b57b04 bump GIT_HASH to 8fcb4da 2026-04-16 15:08:41 -04:00
8fcb4da92c fix: run karaoke in detached child so upload response returns
Upload handler called process_karaoke synchronously — for audio/video
products, vocal isolation (download, ffmpeg, voxsplit, reupload) can
take several minutes. The response hung until karaoke finished, so
users saw a black screen after a successful upload while Caddy/uwsgi
timed out. The file itself was already in S3+DB, which is why hitting
Back showed the edit page with the upload present.

Fix: extract the fork+detach logic the on-demand path already used
(views/watch.py:karaoke_process) into a reusable helper,
process_karaoke_detached, in lib/karaoke.py. Both the upload path and
the on-demand path now hand off to the helper and return immediately.

The helper takes a snapshot of shop+S3+mirror config in the parent
(capture_karaoke_config) so the grandchild only touches the DB at the
end for the final metadata write — by then the parent has long since
committed. Mirror writes run synchronously in the grandchild instead
of via mirror_keys_async daemon threads that were dying at os._exit.
2026-04-16 15:05:20 -04:00
d757cbecbe fix(cart): enforce Stripe $0.50 minimum; add public sales stats
Stripe rejects charges under $0.50 — a $0.33 item was silently freed
because requires_payment threshold (64¢) skipped all card processing.

Changes:
- Cart + Invoice: add requires_stripe_payment (>= 50¢)
- cart_checkout GET + cart_complete_checkout POST: block Stripe when
  total < $0.50 and only Stripe is available; message points to DOGE/XMR
- Invoice Stripe charge loop: guard on requires_stripe_payment (safety net)
- Shop model: add public_sales_stats boolean (default False)
- shop_settings: toggle for public_sales_stats
- shop() view: include sales_count + sales_revenue when flag is on
- shop.j2: conditional stats section (sales count + revenue)
- Migration: 1a2b3c4d5e6f add_public_sales_stats_to_shop
2026-04-10 14:37:07 -04:00
8945ee106b bump GIT_HASH to 0a8400e 2026-04-07 12:54:09 -04:00
0a8400ecb2 fix: wrap script DB sessions in transaction.manager to resolve NoTransaction error
digest_sender cron was crashing with transaction.interfaces.NoTransaction.
bootstrap() doesn't start a transaction — pyramid_tm handles that for web
requests but not console scripts. Wrap DB work in transaction.manager context
instead of manual transaction.commit() calls.

Same latent defect fixed in backfill_karaoke.py.
2026-04-07 12:53:55 -04:00
0203172dcd fix test: patch _torrent_backfill_async directly, not generate_torrent_async 2026-04-07 10:47:33 -04:00
932a0dde6e fix: backfill functions return immediately, work runs in daemon thread
Both _torrent_backfill_async and _checksum_backfill_async were blocking
the request thread by iterating all products before returning — causing
the settings form to hang on "Saving...".

Fix: snapshot shop_id, bucket, S3 creds, and work list while the DB
session is live, then hand off to a single daemon thread and return.
The request redirects instantly.
2026-04-07 10:32:42 -04:00
3faf55203b torrent: index.html links thumbnails and main file with relative paths 2026-04-06 19:36:45 -04:00
ad82b812aa checksums: compute MD5+SHA-256 for every uploaded file, display on content page
- lib/checksums.py: background thread streams from S3, computes both digests
- models/product.py: checksums property + set_checksum stored in file_metadata["checksums"]
- views/product.py: trigger compute_checksums_async after every file upload
- views/shop.py: checksum backfill function (skips files already computed),
  checksum_backfill POST view, checksum_backfill_status JSON view
- routes.py: /s/{shop_id}/checksum-backfill + /s/{shop_id}/checksum-backfill-status
- templates/content.j2: <details> block near date metadata — SHA-256 then MD5
- templates/shop_settings.j2: Backfill Checksums button + progress bar with 4s polling
2026-04-06 18:18:33 -04:00
3671133b96 torrent: multi-file bundles, content/product distinction enforced
Paid products seed: preview + thumbnails + index.md + index.html
Free content seeds: content file + thumbnails + index.md + index.html
The paid product file is never included in any torrent.

- lib/torrent.py: redesign generate_torrent for directory bundles (torf multi-file)
  - description written as index.md + rendered index.html
  - torrent stored as bundle.torrent (was product.torrent)
  - build_bundle_files() encapsulates what belongs in each bundle type
- views/product.py: trigger fires on preview upload for paid products,
  product file upload for free content
- views/shop.py: backfill eligibility mirrors same rule; backfill_status too
- tests: rewrite TestTorrentLib for new bundle interface, add bundle_files tests
2026-04-06 18:14:47 -04:00
ef372cecf3 torrent security: opt-in gate + visibility gating to prevent accidental seeding
- Add torrent_opt_in boolean to mps_product (default false)
- Gate all torrent seeding (upload trigger, backfill) on torrent_opt_in AND visibility==1
- Suppress magnet/torrent display for all users when product is private or unlisted
- Add permanence warning and explicit consent checkbox on product edit page
- backfill_status endpoint counts only opted-in public products
- Alembic migration: 0ee5654cfe7d
2026-04-06 18:07:49 -04:00
0d904e9e1c feat: torrent backfill progress bar on shop settings
Adds GET /s/{shop_id}/torrent-backfill-status JSON endpoint returning
total/done/remaining counts for products needing magnet link generation.

Shop settings page shows a progress bar + status line when torrent
distribution is enabled. Polls every 4 seconds, stops when done.
2026-04-06 17:56:02 -04:00
b72fc6bf3b feat: REST API v1 — HMAC-signed product/content creation and file upload
Adds a public/private key pair authentication system and REST API endpoints
for programmatic product and content management. Designed for CI/CD pipelines
(permacomputer.com image hosting).

Auth: HMAC-SHA256 signed requests using public/private key pairs.
The secret key never travels over the wire. Replay window: 300 seconds.

Endpoints:
  POST /api/v1/products              create product (fiat/crypto priced)
  POST /api/v1/content               create content (free)
  GET  /api/v1/products/{id}         get product
  GET  /api/v1/content/{id}          get content
  POST /api/v1/products/{id}/upload-url     presigned S3 POST for direct upload
  POST /api/v1/content/{id}/upload-url      presigned S3 POST for direct upload
  POST /api/v1/products/{id}/files/confirm  confirm upload, register metadata
  POST /api/v1/content/{id}/files/confirm   confirm upload, register metadata

Key management UI in shop settings. Secret shown once on generation.

Migration: mps_api_key table (id, shop_id, public_key, secret_key, label,
created_timestamp, last_used_timestamp, is_active)

Tests: 12 MpsApiKey unit tests, 8 REST API functional tests (269 total passing)
2026-04-06 15:23:30 -04:00
dc06551967 docs: use make migration — never hand-write revision IDs 2026-04-06 14:20:13 -04:00
c6d03bd9ae docs: clarify prod rules — restarts ok, some servers ci-only not salt 2026-04-06 13:45:29 -04:00
08509629b6 docs: never operate on production directly — always CI/CD and Salt 2026-04-06 13:44:50 -04:00
c12365143d fix: replace bad migration revision ID with proper alembic-generated ID
The torrent_distribution_support migration was manually created with a
duplicate revision ID (a1b2c3d4e5f6) already in use by an older migration,
and pointed to the wrong down_revision (fd9f7e2f2b78 root instead of
9884324a48e3 current head). This caused a cycle in the revision map and
alembic upgrade head failed silently, leaving prod with new code but old
schema → 502 on all shop endpoints.

Deleted the broken file, regenerated with alembic revision to get a proper
unique ID (c0236e351476), set correct down_revision, preserved idempotent
_column_exists guards.
2026-04-06 13:40:36 -04:00
d7e8ec5bc2 mps: torrent backfill on enable + web seed + full test coverage
- backfill: enabling torrent on a shop auto-generates .torrent for all
  existing products that have a product file (no manual trigger needed)
- web seed (BEP 19): CDN url embedded in .torrent + magnet link so
  clients bootstrap via HTTP then seed to peers (no seeder process needed)
- torrent_file_url: stored on Product, shown as download link on content
  and edit pages alongside the magnet link
- migration: idempotent _column_exists guards on all add_column calls
- template: grid layout (not flex) for magnet/torrent buttons on edit page
- tests: 13 passing tests covering all new paths (unit + functional)
  including backfill trigger, web seed construction, visibility gating
2026-04-05 22:09:20 -04:00
e06cf1c230 mps: torrent web seed + torrent_file_url for CDN-bootstrapped swarm
- embed CDN URL as BEP 19 web seed in .torrent + magnet link
- store torrent_file_url (CDN path to .torrent) on Product
- migration adds torrent_file_url column alongside magnet_link
- content page shows Magnet + .torrent download buttons
- product edit shows Copy Magnet + Open + .torrent download
- cdn_endpoint flows from request.shop_cdn_endpoint through async thread
2026-04-05 21:31:09 -04:00
7d78dff6ac mps: torrent magnet links auto-generated on file upload
lib/torrent.py: generate_torrent_async() — daemon thread that fires after
upload, downloads file from S3, builds .torrent via torf, uploads
product.torrent to S3 (public-read), saves magnet link to DB.

views/product.py: trigger generate_torrent_async when file_key=='product'
and shop.torrent_enabled. Removed manual magnet link save — it's automatic.

templates/product_edit.j2: magnet link is read-only + copy button + Open
button when generated; 'upload the product file' hint when not yet generated.

tests: updated to reflect auto-generation model, added content page test.
2026-04-05 20:03:47 -04:00
d5f55f8131 mps: torrent distribution — shop toggle + per-product magnet link
Shop settings: new 'Torrent Distribution' section with single checkbox
(torrent_enabled, default False). When on, product edit shows a magnet
link field; content page shows a Torrent button alongside Download.

- models/shop.py: torrent_enabled (Boolean, default False)
- models/product.py: torrent_magnet_link (UnicodeText, nullable)
- views/shop.py: torrent-settings form section handler
- views/product.py: save/validate magnet link on product edit
- views/content.py: pass torrent_magnet_link to content template
- templates/shop_settings.j2: Torrent Distribution section
- templates/product_edit.j2: magnet link field (shown when torrent enabled)
- templates/content.j2: Torrent button beside Download
- migration: a1b2c3d4e5f6 adds both columns
- tests: 6 functional tests covering enable/disable, magnet save,
  invalid scheme rejection, and no-leak when disabled
2026-04-05 19:57:44 -04:00
41a8525953 feat: add Previous button left of Random in watch controls 2026-04-03 10:00:10 -04:00
6ec646a075 fix: restore logo full width on mobile — max-height 100px, max-width 100% 2026-04-03 09:58:34 -04:00
f77a8029c1 bump GIT_HASH 2026-03-31 20:10:39 -04:00
5ceeaf8a69 feat: share watch queue across tabs via localStorage + storage events
Queue was sessionStorage (per-tab only). Switch to localStorage so any
tab can add to our shared queue. Storage events fire in all other same-origin
tabs on mutation — renderQueue() wired to keep UI in sync automatically.
2026-03-31 20:10:19 -04:00
f12fa036a6 style: avoid "the", use "our" — writing style rule + sweep 2026-03-31 13:20:21 -04:00
422d462b46 bump GIT_HASH 2026-03-30 09:26:44 -04:00
1883329af1 docs: mark bleach CWE-407 as fixed in security section 2026-03-30 09:26:42 -04:00
79ecef66ef bump GIT_HASH 2026-03-30 09:26:17 -04:00
c71fd328c3 fix: CWE-407 — limit HTML nesting depth before bleach to prevent O(2^N)
Add limit_html_nesting() to sanitize_html.py. Flattens any HTML element
nested beyond depth 20 using html.parser (O(N)) before the content reaches
bleach/html5lib. N=35 attack drops from 12.8s to 0.04s.

Wire into markdown_to_html() in render.py — single enforcement point for
all callers: product descriptions, shop descriptions, privacy policy,
terms of service, markup preview.

No byte cap — books, long-form content, and deep table-of-contents
structures are fully supported. The depth limit (20 levels) prevents the
exponential zone while accommodating any legitimate nesting depth.
2026-03-30 09:26:07 -04:00
4ef3059c8d bump GIT_HASH 2026-03-29 21:48:57 -04:00
72626bf65a fix: CWE-407 — cap markup preview input + harden color regex
markup_editor_preview: reject requests where data exceeds 100KB
before passing to markdown/bleach pipeline (same class as
CWE-407 bleach O(2^N) finding — no input cap on AJAX preview).

add_shop_theme_classes: replace single compound regex with four
anchored non-backtracking patterns to eliminate ReDoS risk on
color validation. Practical risk was low (theme_link_color is
always derived from color_scale()), but the pattern was unsafe.
2026-03-29 21:48:52 -04:00
8b3c9a1456 bump GIT_HASH 2026-03-29 21:20:20 -04:00
389f80a730 docs: CWE-407 security section — bleach O(2^N) exposure and PoC
Add Security section to CLAUDE.md documenting both CWE-407 surfaces:
- Search/feed endpoints (fixed, commit f9cbebb)
- Bleach HTML sanitization: O(2^N) on crafted HTML, no input cap in MPS

Add docs/poc-cwe407.py: proof-of-concept timing harness covering
rbox-search, rbox-page, rbox-dump, mps-search, mps-sitemap vectors.
Authorized use only — run against own staging/dev instance.
2026-03-29 21:19:59 -04:00
7aaf189cbd ops: operation voyeur — credential opsec protocol 2026-03-29 15:46:08 -04:00
9131f5900e bump GIT_HASH 2026-03-28 18:25:43 -04:00
6c89b31830 fix: add X-Requested-With header to markup editor preview fetch — Pyramid xhr=True predicate was rejecting requests 2026-03-28 18:25:21 -04:00
84a21e1e7a bump GIT_HASH 2026-03-28 15:13:05 -04:00
f9cbebb6b5 fix: cap search keywords and feed queries to prevent CWE-407 amplification
- shop.py: strip empty tokens, cap keywords to 10 before passing to model
- product.py: add .limit(200) per keyword query — was unbounded .all()
- feeds.py: add .limit(1000) on product/content feed queries — was unbounded
2026-03-28 15:12:41 -04:00
eec598604b docs: 🔥 == 🔥 in auto-push rule 2026-03-28 11:46:08 -04:00
ba3a6e66b2 docs: auto-push without asking — remove human friction 2026-03-28 11:45:34 -04:00
2bd25672f9 fix: cap ring related items at 42 — was rendering entire ring with forward=len(ring) 2026-03-28 11:44:23 -04:00
01bd9086fe docs: wordpress import pipeline architecture with dot diagrams
Design doc for ingesting WordPress sites into MPS shops. Covers two
input modes (REST API + WXR XML), 4-phase HTML conversion pipeline,
content/media/comment mapping, CLI interface, competitive analysis,
and future enhancements. Includes rendered dot diagrams for the
architecture overview and HTML conversion detail flow.
2026-03-12 11:32:47 -04:00
8649e6aaae docs: karaoke pipeline architecture with dot diagrams
Add docs/karaoke-pipeline.md covering the full streaming pipeline from
MPS through unsandbox API to zerotrust container and back. Includes two
Graphviz dot diagrams (rendered to SVG):

- karaoke-pipeline.dot: full system flow across MPS, API, pool, container
- karaoke-ondemand.dot: watch mode on-demand user flow

Update architecture.md feature toggle matrix and related docs table.
Update CLAUDE.md karaoke section with streaming path and on-demand info.
2026-03-11 17:49:57 -04:00
6fd6bfd756 bump GIT_HASH to 4a12722 2026-03-11 17:20:51 -04:00
4a12722b4d feat: on-demand karaoke processing — button next to random triggers vocal isolation
Karaoke button (🎤) now always visible for audio/video products when the
shop has unsandbox API keys configured. Clicking it triggers on-demand
processing via POST /karaoke/{product_id} if tracks don't exist yet.

Server forks a detached child process (survives uWSGI recycling) to run
process_karaoke, then the existing 10s watch_json refresh picks up the
new URLs when processing completes. Button shows hourglass during
processing and auto-switches to instrumentals on completion.

Changes:
- New route + view: POST /karaoke/{product_id} (on-demand processing)
- watch_json + content.py: add karaoke_eligible flag
- content.j2: data-karaoke-eligible attribute on media container
- watch.js: show button when eligible, trigger processing, detect
  completion via URL refresh, auto-switch to instrumentals
- related_content.j2: mic emoji button, JS controls visibility
- CSS: disabled state for processing button
- 8 new functional tests covering eligibility, processing, edge cases
2026-03-11 17:20:26 -04:00
924921e232 bump GIT_HASH to 5e0d263 2026-03-11 13:03:35 -04:00
5e0d263b7c fix: 3 watch mode defects — crossfade race, filter-aware next, countdown position
1. Crossfade race condition: when a DJ crossfade is active and the user
   clicks a new song, cancelDjCrossfade resets djCrossfadeActive but the
   old song is still near its end — timeupdate immediately re-triggers
   startDjCrossfade, racing with the in-flight fetch. Added
   navigationPending flag to block DJ crossfade and ended handler while
   a user-initiated navigation is in progress.

2. getNextItem/getNextUnwatchedItem now respect media type filters.
   Previously, filtering to "video only" still showed an image in the
   countdown because the ring walked by position without checking
   data-media-type. Now skips filtered items.

3. Countdown overlay moved from position:absolute inside the video/audio
   container (covering native controls) to a flow-positioned element
   between the media and title. Removed watch-countdown-static class
   since all countdown instances now use the same in-flow layout.
2026-03-11 13:03:18 -04:00
a71a3d4c6e bump GIT_HASH to 92dc61f 2026-03-08 00:45:15 -05:00
92dc61fc50 fix: address 4 defects from CodeRabbit review
- reforge_discovery_ring_async: add non-production guard matching sync version
- is_trial_active/is_trial_expired: check trial_ended flag
- has_primary_s3: include primary_s3_region in validation
- gift card migration downgrade: add missing json_gift_cards drop + existence guards
2026-03-08 00:45:04 -05:00