make_post_sell/CLAUDE.md
russell@unturf.com 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

18 KiB

Claude Development Notes

Production

  • Application: https://my.makepostsell.com
  • Version check: https://my.makepostsell.com/version
  • Prod shell: tmux-hosts — look for my.makepostsell.com (typically tmux window 0:3) — READ-ONLY, never deploy/fix from tmux
  • Media CDN: plan-period-files.nyc3.cdn.digitaloceanspaces.com (DigitalOcean Spaces)
  • Deploy pipeline: git push → GitLab CI (test → build → deploy) → salt-call state.highstate on prod
  • Salt states: ~/git/foxhop-states/uwsgi/ — note: MPS uses caddy_sites.sls, NOT sites.sls
  • Salt pillar: ~/git/foxhop-pillar/uwsgi/makepostsell/init.sls
  • DB path on prod: /opt/make_post_sell/make_post_sell.sqlite (owned by uwsgi, need sudo for writes)
  • Timestamps in DB are milliseconds not seconds (13 digits)
  • uWSGI: 2 processes, 8 threads, reload-on-rss 512MB (pillar-configurable), Caddy reverse proxy on :6001

Media Architecture

Files are NEVER streamed through uwsgi. The server only generates presigned URLs (15 min TTL). The client's browser/JS fetches directly from the Spaces CDN:

  • Downloads: presigned get_object URLs → client fetches from CDN
  • Uploads: presigned post → client uploads directly to Spaces
  • Thumbnails: public CDN URLs with ?ts= cache busting

BYOB (Bring Your Own Bucket): Shops can configure their own S3-compatible bucket (bucket-settings form section). When enabled, all presigned URLs and CDN references use the shop's bucket. Always use shop-aware request methods in views and templates:

  • request.shop_uploads_client — S3 client (shop's or MPS default)
  • request.shop_bucket_name — bucket name (shop's or MPS default)
  • request.shop_cdn_endpoint — CDN URL (shop's or MPS default)

NEVER use request.app["bucket.secure_uploads"], request.app["bucket.secure_uploads.get_endpoint"], or request.secure_uploads_client directly in views or templates. These are only used internally by request_methods.py as fallbacks.

Karaoke Pipeline (lib/karaoke.py)

Disk-backed vocal isolation pipeline using spectral mid-side Wiener masking (voxsplit.c, zero ML deps). Streams media to unsandbox via POST /upload (64KB chunks, constant memory), executes in zerotrust container, streams response back, uploads instrumentals + vocals to S3.

Full architecture doc: docs/karaoke-pipeline.md (with dot diagrams).

  • Concurrency: ThreadPoolExecutor sized to account's unsandbox concurrency limit
  • Memory: ~64KB per worker at every stage (disk-backed, not in-memory)
  • Upstream limit: 3.698GB / 3,698,742,051 bytes per file (unsandbox @max_upload_bytes)
  • Retries: 3 attempts with exponential backoff (5s, 10s)
  • Callers: views/product.py (upload), views/watch.py (on-demand), views/shop.py (backfill), scripts/backfill_karaoke.py
  • On-demand: POST /karaoke/{product_id} — forks detached child, watch.js 10s refresh detects completion, auto-switches to instrumentals
  • Streaming path: MPS → POST /upload → API encrypts to disk → pool pulls via GET /internal/upload/{id} → pipes into container /root/input/ — zero bytes cross Erlang distribution

Project Setup

This project uses a Makefile for most development operations. Use make commands instead of running tools directly.

Common Development Tasks

Testing

  • Run tests: make test
    • This installs development dependencies and runs the test suite with py.test
    • Tests are located in make_post_sell/tests/

Installation & Setup

  • Install from source for development: make install-from-source
  • Install from PyPI: make install-from-pypi
  • Initialize database: make init-db

Development Server

  • Start development server: make serve
    • Runs with auto-reload enabled
    • Uses data/development.ini configuration

Environment Management

  • Create virtual environment: make venv
  • Clean up environment: make clean
  • Activate environment: source env/bin/activate

Code Structure

Key Directories

  • make_post_sell/views/ - View controllers
  • make_post_sell/models/ - Database models
  • make_post_sell/tests/ - Test suite

Important Files

  • make_post_sell/views/cart.py - Cart and checkout logic
  • development.ini - Configuration file

Design System Files

  • static/css/tokens.css — Design tokens (colors, typography, spacing, shape, elevation, motion, z-index), base resets, utility classes, animations. Single source of truth. Light mode :root, dark mode [data-theme="dark"].
  • static/css/common.css — Component styles consuming tokens via var(--token, fallback).
  • templates/styleguide.j2 — Live component reference at /styleguide (view: views/misc.py:23).
  • docs/design-system.md — Full design system reference doc (token tables, architecture diagram, conventions).

Testing Notes

The project uses pytest with unittest framework. There are three types of tests:

Test Types

  • Unit tests (test_models.py) - Test individual model methods and properties in isolation
  • Integration tests (test_integration.py) - Test interactions between models and business logic
  • Functional tests (test_functional.py) - End-to-end tests through the web interface

Running Tests

Before running tests: Source environment variables with source vars.sh to set required Stripe API keys and other configuration.

# Run all tests
make test

# Run specific test types
env/bin/py.test make_post_sell/tests/test_models.py     # Unit tests
env/bin/py.test make_post_sell/tests/test_integration.py # Integration tests  
env/bin/py.test make_post_sell/tests/test_functional.py  # Functional tests

# Run with coverage
env/bin/py.test --cov=make_post_sell.models.cart --cov-report=term-missing make_post_sell/tests/test_models.py::TestCart

Current Coverage (712 tests)

  • Cart model unit tests cover critical business logic like requires_payment threshold (64 cents)
  • Shop environment, trial, and BYOB model properties (TestShopEnvironment, TestShopTrial, TestShopBYOB)
  • Gift card model unit tests (generation, validation, transactions)
  • Integration tests verify free coupon checkout, gift card flows, and multi-model interactions
  • Functional tests cover cart/checkout/payment, gift card settings, environment settings, bucket settings

Database Location

The SQLite database is located at: data/make_post_sell.sqlite

CRITICAL WARNING: NEVER delete or remove database files without explicit user permission. The database contains production data and cannot be easily recovered. Always ask before any destructive operations.

MANDATORY: ALWAYS create a backup of the database before any database operations (migrations, schema changes, etc.):

cp data/make_post_sell.sqlite data/make_post_sell.sqlite.backup-$(date +%Y%m%d-%H%M%S)

Query crypto payments:

-- Note: Remove dashes from UUIDs when querying
SELECT * FROM mps_crypto_payment WHERE id = 'paymentuuidherewithoutdashes';

Database Migrations

When making changes to database models, always create Alembic migrations:

Creating Migrations

CRITICAL: ALWAYS use alembic revision to generate migration files. NEVER manually create migration files or make up revision IDs. Alembic generates unique revision IDs that are required for proper migration tracking.

# Activate environment first
source env/bin/activate

# Create a new migration (manual) - ALWAYS use this command
alembic -c data/development.ini revision -m "description of change"

# OR: Create autogenerated migration (compares DB with models)
alembic -c data/development.ini revision --autogenerate -m "description of change"

# Edit the generated migration file in make_post_sell/scripts/alembic/versions/
# The file will have a proper unique ID like: 05be3044c2d2_description_of_change.py

Running Migrations

# Apply all pending migrations
alembic -c data/development.ini upgrade head

# Check current migration status
alembic -c data/development.ini current

# View migration history
alembic -c data/development.ini history

IMPORTANT: Always backup the database before running migrations!

Important Migration Notes

Idempotent Migrations: make init-db creates all tables from models, so migrations that run afterward must not fail if tables/columns already exist. Always guard create_table with _table_exists and add_column with _column_exists:

def _table_exists(name):
    conn = op.get_bind()
    result = conn.execute(
        sa.text("SELECT name FROM sqlite_master WHERE type='table' AND name=:name"),
        {"name": name},
    )
    return result.fetchone() is not None


def _column_exists(table, column):
    conn = op.get_bind()
    result = conn.execute(sa.text(f"PRAGMA table_info({table})"))
    return any(row[1] == column for row in result.fetchall())


def upgrade():
    if not _table_exists("mps_new_table"):
        op.create_table(...)

    if not _column_exists("mps_shop", "new_column"):
        op.add_column(...)

SQLite Column Defaults: When adding NOT NULL columns with defaults to existing tables in SQLite, use server_default with raw SQL values:

# Correct - uses server_default for raw SQL
op.add_column(
    "mps_shop",
    sa.Column("stripe_enabled", sa.Boolean(), nullable=False, server_default="1"),
)

# Wrong - default won't work with existing data
op.add_column(
    "mps_shop", 
    sa.Column("stripe_enabled", sa.Boolean(), nullable=False, default=True),
)

Cryptocurrency RPC Access

Monero Wallet RPC

When investigating or manually testing Monero RPC calls, use digest authentication with these credentials (from Makefile):

  • Username: test_user
  • Password: test_pass
  • URL: http://127.0.0.1:18083/json_rpc

Example curl command with digest auth:

curl --digest -u "test_user:test_pass" -X POST http://127.0.0.1:18083/json_rpc \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":"0","method":"get_transfer_by_txid","params":{"txid":"transaction_hash_here"}}'

Dogecoin Core RPC

Dogecoin uses basic authentication (from dogecoin.conf):

  • Username: mps_doge_user
  • Password: change_this_password_in_production
  • URL: http://127.0.0.1:22555

Common Issues and Solutions

UUID Objects

Always use uuid_str when you need a string copy of the identifier. Models inherit uuid_str property from RBase.

IMPORTANT: UUIDs are stored in the database WITHOUT dashes. When querying by ID, remove dashes from the UUID:

  • Correct: WHERE id = '0f92cd2a86f54dc1b98ef5c8b37bc7f8'
  • Wrong: WHERE id = '0f92cd2a-86f5-4dc1-b98e-f5c8b37bc7f8'

Development Standards and Expectations

CRITICAL WORK ETHIC: The user pays significant money for development work and expects thorough, complete solutions. NEVER try to do the minimum or cut corners. When asked to implement features, provide comprehensive, production-ready implementations that consider all aspects of the request.

CSS LAYOUT REQUIREMENTS: This project uses CSS Grid exclusively for layout. NEVER use Flexbox (flex) for layout. Always use CSS Grid properties for positioning and alignment.

DESIGN TOKENS: All new styles must consume tokens from tokens.css — never hardcode colors, spacing, radii, shadows, or font sizes. Use var(--token-name) or var(--token-name, fallback). The token scale uses a 4px spacing base and major third (1.250) type scale.

STYLEGUIDE: When creating new UI components (buttons, wells, alerts, layout patterns, etc.), add a live example to /styleguide (make_post_sell/templates/styleguide.j2). The styleguide is the single source of truth for the component library. If it's not in the styleguide, it doesn't exist as a pattern.

CSS MEDIA SIZING: Never combine width: 100% with max-height on media elements (img, video). width: 100% forces the element to span the full container even when max-height constrains the rendered content, creating dead whitespace. Use width: auto + max-width: 100% + max-height instead — the element shrinks to match the actual content aspect ratio within both constraints.

MOBILE USABILITY: Never use hover-only interactions (:hover to reveal controls, opacity: 0 with hover reveal, etc.). Mobile/touch devices have no hover state — controls hidden behind hover are invisible and unreachable. All interactive elements (buttons, toggles, links) must be always visible and tappable. Design touch-first, then optionally enhance for desktop hover.

SPA + NORMAL MODE: Watch mode uses SPA navigation (watch.js) that swaps content without a full page reload. When adding or modifying links, buttons, forms, or any product-specific content on pages that participate in watch mode (content.j2, product.j2), you MUST ensure:

  1. Server-rendered HTML works for the initial page load (normal mode, no-JS, crawlers)
  2. updatePageContent() in watch.js updates the same element during SPA navigation
  3. The watch JSON endpoint (watch.py) returns any new data the JS needs

Elements that must stay in sync: CTA edit button, download button, comment form product_id, file type/size, description, title, canonical link, related items, comments link count. If you add a new product-specific element, add it to all three layers.

TESTING INTEGRITY: NEVER skip, delete, or disable unit tests or integration tests when they break. When tests fail:

  1. FIX THE TESTS - Update them to work with new functionality
  2. FIX THE CODE - If the tests reveal actual defects, fix the underlying issue
  3. ADD MORE TESTS - Ensure new functionality is properly covered

Disabling or removing tests weakens the codebase and is unacceptable. Tests are critical safety nets that prevent regressions.

MANDATORY TEST COVERAGE: Every new feature, model property, view handler, or form section MUST have tests across all three layers:

  • Unit tests (test_models.py) — Test new model properties, methods, and business logic in isolation using mock.patch. No DB required.
  • Integration tests (test_integration.py) — Test interactions between models, especially multi-model workflows (e.g., cart + coupon + gift card).
  • Functional tests (test_functional.py) — Test through the web interface using webtest.TestApp. Cover settings form POSTs, page loads, flash messages, and DB state changes.

If a feature touches all three layers (model + view + template), it needs tests in all three files. No exceptions. Untested code is incomplete code.

AUTO-PUSH: When you write new tests to cover new code paths and the full test suite passes, commit and push without asking. Bump GIT_HASH after pushing.

Post-Work Chores

After completing a feature or significant change, always perform these chores before considering the work done:

  1. Tests — Write unit tests (test_models.py), integration tests (test_integration.py), and functional tests (test_functional.py) covering the new code paths. All three layers are required for new features.
  2. Docs — Update docs/architecture.md (feature toggle matrix, ticket index, diagrams) and docs/design-system.md (new components/sections) to reflect the change.
  3. Portal — Update the marketing site at ~/git/www.makepostsell.com (feature cards in index.html, includes list in pricing.html) when a user-facing feature is added.
  4. CLAUDE.md — Update this file if the change introduces new patterns, form sections, model columns, or conventions that future work needs to know about.
  5. Commit & push — Per AUTO-PUSH, commit and push when tests pass. Bump GIT_HASH.

Commit Message Guidelines

CRITICAL: Do not include Claude Code attribution in commit messages. Attributing human work to Claude is inappropriate and misrepresents the actual authorship of the code. All code changes should be attributed to the human developer who reviewed, approved, and committed the work.

Capability-Driven Presentation

Follow Russell Ballestrini's capability-driven presentation practice (russell.ballestrini.net/capability-driven-presentation/). A page need not look identical across all browsers. Accommodate what the user's browser can do:

  1. Single canonical URI — one URL serves the content.
  2. Consistent content — regardless of viewer capabilities.
  3. Graceful enhancement/degradation — use available capabilities to enhance presentation.

The js-only / <noscript> pattern

<noscript>
  <style>.js-only {display: none;}</style>
</noscript>

Apply the js-only class to any element that requires JavaScript to function. When JS is unavailable, these elements hide automatically — the user never sees a broken control.

AJAX form submission

Comment forms use progressive enhancement: the form works as a normal POST + redirect without JS. When JS is available, comments.js intercepts the submit, sends via fetch() with X-Requested-With: XMLHttpRequest, and inserts the new comment into the DOM without a page reload (preserving media playback). The server returns JSON (HTTP 201) for AJAX requests and falls back to the normal redirect flow on any error.

CI/CD Notes

  • Build uses virtualenv-clone which requires bin/python symlink (Python 3.12 venv may only create python3)
  • The CI creates a symlink before cloning: test -f env/bin/python || ln -sf python3 env/bin/python
  • When updating Salt states (foxhop-states), always run salt-run fileserver.update on the salt master before triggering a deploy — gitfs cache can serve stale files
  • MPS uses caddy_sites.sls (NOT sites.sls) — changes to the uwsgi service template context must be added to both files

Mobile Layout

On mobile (max-width: 800px), the product page reorders to single column:

  1. product-images (order 1) — sticky in watch mode
  2. product-right (order 2) — price, download, related content
  3. product-description (order 3)
  4. product-comments (order 4)

Related content on mobile shows only 7 next items (vs 42 on desktop) via .related-content-overflow class. A "Comments (N)" anchor link appears on mobile to jump to the comments section below.

Style

  • Never use "AI" — always say "machine learning." We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.