diff --git a/.gitignore b/.gitignore index fdba633..7803749 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,6 @@ vars.fish vars.sh -# GIT_HASH is rewritten by setup.py at install time via -# `git rev-parse --short HEAD`. Tracking it just produced churn: -# every feature commit was followed by a "bump GIT_HASH to X" commit -# whose value was always one commit behind HEAD (because the new -# bump commit itself shifted HEAD again). setup.py owns this file. -make_post_sell/GIT_HASH - *.egg *.egg-info *.pyc @@ -20,7 +13,6 @@ coverage.xml build/ dist/ data/ -data*/ src/ .tox/ nosetests.xml @@ -38,10 +30,8 @@ test *.dkim.key caddy -.claude monero-wallet-cli.log monero-wallet-rpc.log monero-wallet-rpc.log* - diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99546c7..9e13111 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,10 +24,7 @@ build: - cp -pr env/static static - tar -zcf static.tar.gz static # Clean up the directory outside of the gitlab-runner filesystem. - # rm -rf can race with background processes on build runners — verify removal. - - rm -rf /opt/make_post_sell/env; for i in 1 2 3; do [ ! -d /opt/make_post_sell/env ] && break; sleep 2; rm -rf /opt/make_post_sell/env; done; [ ! -d /opt/make_post_sell/env ] - # Ensure bin/python symlink exists (Python 3.12 venv may only create python3). - - test -f env/bin/python || ln -sf python3 env/bin/python + - rm -rf /opt/make_post_sell/env # Clone the virtualenv with virtualenv-clone into the desired location. - virtualenv-clone -vvv $PWD/env /opt/make_post_sell/env # Create a tarball of the virtualenv. @@ -65,11 +62,6 @@ pypi-twine: - python3 -m venv twine_env - source twine_env/bin/activate - pip install --upgrade pip - # Pin twine <6 — newer twine auto-detects GitLab CI and refuses to - # fall back to ~/.pypirc on the runner, requiring PYPI_ID_TOKEN - # (Trusted Publishing OIDC). Until we migrate to Trusted Publishing, - # stick with the classic ~/.pypirc auth on the build runner. - - pip install "twine<6" build + - pip install twine build - python3 -m build - - twine check dist/* - - twine upload --non-interactive dist/* + - twine upload dist/* diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index ee3919e..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,154 +0,0 @@ -Changelog -========= - -All notable changes to this project will be documented in this file. - -2026-02-09 ----------- - -DJ Crossfade -~~~~~~~~~~~~ - -* 7-second audio + visual crossfade starts before current video ends -* Volume ramps down on outgoing, ramps up on incoming simultaneously -* Opacity crossfade dissolves between videos during transition -* Countdown overlay shows during crossfade with Play Now / Cancel controls -* User-initiated transitions (click related item, queue) also get visual crossfade -* Falls back to standard countdown when preloaded data isn't ready or media types differ - -Footer & Links -~~~~~~~~~~~~~~ - -* Fixed apex domain link to use ``www.makepostsell.com`` in shop footer -* Added Source Code (GitLab) and PyPI links to shop footer - -2026-02-08 ----------- - -Watch Mode v2 -~~~~~~~~~~~~~ - -* Continuous playback with crossfade transitions between media -* Discovery ring: deterministic content traversal precomputed per shop -* Up Next sidebar with compact rows, numbered index, and queue management -* Countdown overlay (7s) as frosted-glass bottom bar between items -* Queue system: add/remove items, reorder playback -* Autoplay toggle switch inline with Up Next heading -* Two-phase preload: fast JSON fetch, then buffer media 30s before end -* Recently-played tracking: filter items from Up Next for 4 hours -* Support for video, audio, and static content (PDFs, images) with auto-advance -* Sticky video on desktop scroll (offset below ribbon) -* ``watch.js`` — new standalone JS module for all watch mode logic - -Pop-out Media Player -~~~~~~~~~~~~~~~~~~~~ - -* Draggable pop-out window with prev/next navigation -* Keyboard shortcuts and responsive controls -* Auto-advance for images/PDFs (60s timer, reset on scroll) -* ``player.js`` — new standalone JS module -* ``/random`` and ``/tv`` endpoints for media playback - -Lazy Cart Creation -~~~~~~~~~~~~~~~~~~ - -* Anonymous session carts are now in-memory until a product is added -* Prevents bots and crawlers from creating empty cart rows in the database -* UUID stays stable across requests via session cookie -* Transient cart guard in authentication merge flow - -AJAX Comments -~~~~~~~~~~~~~ - -* Comment form submits via fetch when JS is available -* Preserves media playback — no page reload interrupts -* Falls back to normal POST + redirect without JS -* ``comments.js`` — new standalone JS module - -Email Subscriptions -~~~~~~~~~~~~~~~~~~~ - -* Email digest subscriptions with configurable frequency -* @mentions in comments notify mentioned users -* RSS and Atom feed autodiscovery in ```` -* Subscribe link in shop nav bar - -Related Content -~~~~~~~~~~~~~~~ - -* Jaccard similarity for related content ranking -* Related content thumbnails in sidebar - -Feeds & SEO -~~~~~~~~~~~~ - -* Sitemap generation (``/sitemap.xml``) -* RSS (``/rss.xml``) and Atom (``/atom.xml``) feeds -* Google site verification meta tag support -* Feeds served as ``application/xml`` for browser rendering -* Feed links open in new window - -Adyen Integration -~~~~~~~~~~~~~~~~~ - -* Added Adyen as a payment processor -* Per-shop enable/disable toggle -* See ``docs/ADYEN.md`` for details - -Stripe Improvements -~~~~~~~~~~~~~~~~~~~ - -* Payment tracking and webhook resilience -* Per-shop Stripe enable/disable - -UI & Layout -~~~~~~~~~~~ - -* Sticky ribbon + logo/nav on desktop scroll -* Footer pushed to bottom of viewport on short pages -* Product media uses viewport-relative sizing (``max-height: 42vh``) -* Reduced heading font sizes for tighter layout -* Product/content dates toggle (``show_dates`` shop setting) -* Inline video playback on thumbnail click (replaced pop-up) - -Infrastructure -~~~~~~~~~~~~~~ - -* ``/version`` endpoint with baked git hash for deploy verification -* ``GIT_HASH`` tracked in repo, baked at install time -* Removed jQuery — all vanilla JavaScript -* Replaced JS toggles with pure CSS ``
`` elements -* Pinned ``setuptools<81`` for Python 3.12+ compatibility - -2025-12-22 (2:30 PM) --------------------- - -PayPal Integration -~~~~~~~~~~~~~~~~~~ - -* Added PayPal as a payment processor alongside Stripe and crypto payments -* New ``PayPalUserShop`` model for saved payment methods -* Invoice model extended with ``paypal_order_id`` and ``paypal_capture_id`` columns -* Shop settings now include PayPal client ID and secret configuration -* Checkout page supports PayPal payment option when enabled -* Added PayPal saved payment methods (vault) support -* Added ``/billing/disconnect-paypal`` route for users to manage saved PayPal -* See ``docs/PAYPAL.md`` for details - -CSS Grid Lanes -~~~~~~~~~~~~~~ - -* Added toggleable CSS Grid Lanes (masonry layout) setting per shop -* New ``grid_lanes_enabled`` column on Shop model - -Video Thumbnails -~~~~~~~~~~~~~~~~ - -* Added play button overlay on video thumbnails for unlocked content -* Styled video play overlay with red tint and click-to-play text - -Meta Tags -~~~~~~~~~ - -* Added Twitter card meta tags for proper link unfurling on Matrix/Discord -* Increased meta description truncation to 500 chars diff --git a/CLAUDE.md b/CLAUDE.md index ad80f28..b53d383 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,66 +1,5 @@ # 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 - -### 🚨 NEVER Operate on Production Directly - -**ABSOLUTE RULE**: ALL fixes go through CI/CD and Salt. No exceptions. - -- **NEVER** SSH into prod and run `ALTER TABLE`, `sqlite3`, or any direct DB command -- **NEVER** bypass the migration system — if `alembic upgrade head` fails, fix the migration and push -- **The fix is always in the code.** Push to master → CI tests → deploy (Salt highstate or CI-direct) → Alembic runs on every instance -- Restarting services via SSH is fine for recovery, but the underlying fix must still go through code + deploy - -**Why this matters**: MPS runs open source on multiple servers (makepostsell.com, memopoly.com, and any operator instance). A manual fix on one server leaves every other instance broken. The migration system exists to apply changes everywhere consistently. - -**When prod has a 502**: diagnose via logs (read-only SSH is fine), fix the code, push. The deploy pipeline reaches all instances. A manual fix reaches one. - -### Media Architecture - -Files are NEVER streamed through uwsgi. Our server only generates presigned URLs (15 min TTL). Our client's browser/JS fetches directly from our 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 our 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. - -### Transactional Email (lib/mail.py) - -All transactional mail (OTP login codes, receipts, sale/offer notifications, gift cards, invites) sends from **one warm sending identity**: `app.email.sender` (default `no-reply@origin.makepostsell.com`, overridable via `MPS_EMAIL_SENDER`) — *not* per-shop `no-reply@`. The recipient-facing name is the **shop name** when in shop context, else `app.email.from_name` (default `Make Post Sell`); `lib/mail.py:format_from_header()` builds the `From:` header. Why a single identity: operator custom-domain shops (e.g. `shop.unturf.com`) have no DKIM key MPS controls and don't authorize MPS's sending IPs in SPF, so per-domain `From:` lands in spam. `origin.makepostsell.com` is DKIM-signed by opendkim on the origin box (`d=makepostsell.com`, selector `20190727`) and SPF-authorized (`v=spf1 a a:mx1.foxhop.net -all`), and outbound is relayed through `mx1.foxhop.net` (warm IP, see `foxhop-pillar/postfix/makepostsell.sls` → `postfix_relayhost`). Reply-To / per-shop contact email is still TODO — see `docs/tickets/mps-23.md`. - -### 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. @@ -69,7 +8,7 @@ This project uses a Makefile for most development operations. Use `make` command ### Testing - Run tests: `make test` - - This installs development dependencies and runs our test suite with py.test + - This installs development dependencies and runs the test suite with py.test - Tests are located in `make_post_sell/tests/` ### Installation & Setup @@ -87,42 +26,6 @@ This project uses a Makefile for most development operations. Use `make` command - Clean up environment: `make clean` - Activate environment: `source env/bin/activate` -### Dependencies — two-file source of truth - -Runtime deps live in **two** files; they are NOT redundant. - -- `requirements.py3.txt` — what `setup.py` reads for `install_requires`. - Used by `pip install .` / editable dev installs. Source-of-truth for - what `make_post_sell` declares as its deps. -- `requirements-prod.lock` — hash-pinned, full transitive closure, - generated by `make pins-lock` (uv pip compile). What CI's - `install-source-prod` actually installs into `env.tar.gz` via - `pip install --require-hashes -r requirements-prod.lock`. - -**Whenever you edit `requirements.py3.txt`, you MUST run `make pins-lock` -and commit the regenerated `requirements-prod.lock` in the same PR.** - -If you forget: CI tests will still pass (test stage uses the unpinned -files), and the build artifact will still publish — but the artifact's -venv will be missing whatever dep you added. The deploy to prod will -silently roll out an env without the new dep. The first runtime import -of it is where the world finds out. - -Pattern that has bitten us: -1. Add `erldistpy>=0.1.6` to `requirements.py3.txt`. Tests pass. -2. Forget `make pins-lock`. CI builds env.tar.gz from the stale lock. -3. Highstate ships → `pip show erldistpy` returns empty on prod → - `ModuleNotFoundError` the moment crypto_watcher reaches for it. - -Same shape for any other dep — make a habit of running `make pins-lock` -after every requirements edit, before commit. - -Same trap on the CLI side: if your new module imports a stdlib-adjacent -library that's not yet pinned (e.g. someone reaches for `click` -instead of `argparse`), it triggers `ModuleNotFoundError` in CI tests -because the CI runner's env doesn't carry the transitive. Match repo -convention (stdlib `argparse` for CLIs) before adding deps. - ## Code Structure ### Key Directories @@ -134,20 +37,14 @@ convention (stdlib `argparse` for CLIs) before adding deps. - `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 -Our project uses pytest with unittest framework. There are three types of tests: +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 our web interface +- **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. @@ -165,20 +62,18 @@ env/bin/py.test make_post_sell/tests/test_functional.py # Functional tests 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) +### Current Coverage - 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 +- Integration tests verify the original AttributeError bug fix for free coupon checkout +- Functional tests provide end-to-end coverage of cart/checkout/payment flows ## Database Location -Our SQLite database is located at: `data/make_post_sell.sqlite` +The SQLite database is located at: `data/make_post_sell.sqlite` -**CRITICAL WARNING**: NEVER delete or remove database files without explicit user permission. Our database contains production data and cannot be easily recovered. Always ask before any destructive operations. +**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 our database before any database operations (migrations, schema changes, etc.): +**MANDATORY**: ALWAYS create a backup of the database before any database operations (migrations, schema changes, etc.): ```bash cp data/make_post_sell.sqlite data/make_post_sell.sqlite.backup-$(date +%Y%m%d-%H%M%S) ``` @@ -189,96 +84,10 @@ Query crypto payments: 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 `make migration` to generate migration files. NEVER manually create migration files. NEVER hand-write or invent revision IDs. Alembic generates cryptographically unique revision IDs — a made-up ID like `a1b2c3d4e5f6` will corrupt the migration chain and break production deploys. - -```bash -# The ONLY correct way to create a migration: -make migration m="description of change" -# → writes make_post_sell/scripts/alembic/versions/05be3044c2d2_description_of_change.py -# → revision ID is auto-generated (e.g. 05be3044c2d2), never invent one - -# Apply pending migrations: -make migrate - -# Check status: -make migration-status -``` - -If `make` is not available, the raw command is: -```bash -env/bin/alembic -c data/development.ini revision --autogenerate -m "description of change" -``` - -The generated file lives in `make_post_sell/scripts/alembic/versions/`. Edit it to add `_column_exists` / `_table_exists` guards (see idempotent pattern below), then commit it. - -### Running Migrations -```bash -# 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 our 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`: - -```python -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: - -```python -# 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): +When debugging 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` @@ -299,545 +108,23 @@ Dogecoin uses basic authentication (from dogecoin.conf): ## Common Issues and Solutions ### UUID Objects -Always use `uuid_str` when you need a string copy of our identifier. Models inherit `uuid_str` property from `RBase`. +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 our database WITHOUT dashes. When querying by ID, remove dashes from our UUID: +**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**: Our user pays significant money for development work and expects thorough, complete solutions. NEVER try to do our minimum or cut corners. When asked to implement features, provide comprehensive, production-ready implementations that consider all aspects of our request. - -**CSS LAYOUT REQUIREMENTS — GRID ONLY, NO FLEXBOX, NO EXCEPTIONS**: -This project uses CSS Grid exclusively for layout. **NEVER** write `display: flex`, `display: inline-flex`, `flex:`, `flex-direction`, `flex-wrap`, `justify-content: flex-*`, `align-items: flex-*`, or `flex-grow/shrink/basis`. There is no situation where flexbox is acceptable. - -Grid equivalents for the patterns you'd reach for flex: -- **Centering content** (one item dead-center): `display: grid; place-items: center;` (or `display: inline-grid; place-items: center;` for inline-level buttons/badges). -- **Two items, one left one right** (`justify-content: space-between`): `display: grid; grid-template-columns: 1fr auto;` (left item in the `1fr` column, right item in `auto`). -- **Row of items, right-aligned**: don't make the container a grid — set `text-align: right` (or `text-align: end`) and let inline-level children flow/wrap naturally. Or `display: grid; grid-auto-flow: column; grid-auto-columns: max-content; justify-content: end;` if you don't need wrapping. -- **Vertical stack with last item pushed to bottom** (`margin-top: auto` in flex): `display: grid; align-content: space-between;` on the container (works when the container is taller than its content, e.g. inside a `align-items: stretch` parent grid). -- **Equal-height cells in a row**: parent `display: grid; grid-template-columns: repeat(auto-fit, minmax(Npx, 1fr)); align-items: stretch;`. -- **Icon + label header**: `display: grid; grid-template-columns: auto 1fr; align-items: center; gap: var(--space-N);`. - -Note: `align-items`, `justify-items`, `align-content`, `justify-content`, `place-items`, `place-content`, `gap` are all **valid in grid context** — only the `flex-*` keyword values (`flex-start`, `flex-end`) and the `flex` shorthand / `flex-direction` / `flex-wrap` / `display: flex` are forbidden. Use `start`/`end`/`center`/`stretch`/`space-between` etc. as the values. - -**SHAME LOG — 2026-05-11**: agent blackops shipped `display: inline-flex` on `.mps-button` and several edit-page components (`.edit-status-bar`, `.edit-save-bar`, `.upload-thumbnails-header`, `.edit-card-icon`, h3 headers, `.mps-button-primary`) across multiple commits before fox caught it. All converted to Grid. This rule is non-negotiable; re-read it before touching any CSS. - -**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)`. Our token scale uses a 4px spacing base and major third (1.250) type scale. - -**DARK-MODE TRAP — only reference vars that are REAL tokens.** A `var(--name, fallback)` where `--name` is **not** defined in `tokens.css` silently uses the light `fallback` in BOTH themes → looks fine in light, broken (light card / invisible text) in dark. This bit us repeatedly (wells, suggest cards, checksum table). The offenders were ad-hoc names like `--color-surface*`, `--color-border*`, `--color-text*`, `--text-color`, `--surface*` — none are tokens. **Use the real theme-aware tokens**: surfaces → `--surface-base` / `--surface-dim` / `--surface-container`; borders → `--border-light` / `--border-default` / `--border-color`; text → `--text-primary` / `--text-body` / `--text-muted` (all carry `:root` + `[data-theme="dark"]` values). Pre-commit grep gate (must be empty): -``` -grep -oE 'var\(\s*--(color-(surface|border|text)[a-z0-9-]*|surface(-[a-z]+)?|text-color)\s*,' make_post_sell/static/css/common.css -``` - -**STYLEGUIDE**: When creating new UI components (buttons, wells, alerts, layout patterns, etc.), add a live example to `/styleguide` (`make_post_sell/templates/styleguide.j2`). Our styleguide is our single source of truth for our component library. If it's not in our 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 our element to span our full container even when `max-height` constrains our rendered content, creating dead whitespace. Use `width: auto` + `max-width: 100%` + `max-height` instead — our element shrinks to match our 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 our initial page load (normal mode, no-JS, crawlers) -2. **`updatePageContent()` in watch.js** updates our same element during SPA navigation -3. **Our watch JSON endpoint** (`watch.py`) returns any new data our 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. +**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. **TESTING INTEGRITY**: NEVER skip, delete, or disable unit tests or integration tests when they break. When tests fail: -1. **FIX OUR TESTS** - Update them to work with new functionality -2. **FIX OUR CODE** - If our tests reveal actual defects, fix our underlying issue +1. **FIX THE TESTS** - Update them to work with new functionality +2. **FIX THE CODE** - If the tests reveal actual bugs, fix the underlying issue 3. **ADD MORE TESTS** - Ensure new functionality is properly covered -Disabling or removing tests weakens our 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 our 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**: Commit and push when our work is done — no need to ask fox. If tests were written, they must pass first. If no tests are required (defect fix, config, docs), push immediately after committing. 🔥 == 🔥 — remove all friction. - -**DO NOT bump `make_post_sell/GIT_HASH`.** The file is gitignored. `setup.py` rewrites it at install time via `git rev-parse --short HEAD`, so the deployed `/version` hash is always the real HEAD. Manually committing a bumped value just produced churn — each bump's recorded value was one commit behind the actual HEAD it was bumping toward. - -## Post-Work Chores - -After completing a feature or significant change, always perform these chores before considering our work done: - -1. **Tests** — Write unit tests (`test_models.py`), integration tests (`test_integration.py`), and functional tests (`test_functional.py`) covering our 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 our change. -3. **Portal** — Update our 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 our 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 done. No friction. (Do **not** bump GIT_HASH — setup.py handles it at install time.) +Disabling or removing tests weakens the codebase and is unacceptable. Tests are critical safety nets that prevent regressions. ## Commit Message Guidelines -**CRITICAL**: Do not include Claude Code attribution in commit messages. Attributing human work to Claude is inappropriate and misrepresents our actual authorship of our code. All code changes should be attributed to our human developer who reviewed, approved, and committed our work. - -## Ticket Scoping — One Feature, One Ticket - -**Prefer one ticket that fully scopes a feature** over splitting it into `MPS-N`, `MPS-N+1`, `MPS-N+2` phase tickets. Phases inside a single ticket are fine — they let us land work incrementally — but they live in **one document** with **one ticket number**. - -Spawn a new ticket only when: -- A "next phase" is a genuinely separate feature with different goals or stakeholders -- A "next phase" is gated on something external (a vendor decision, another team's work) -- A "next phase" has uncertain priority and may never ship - -**Why**: splitting one feature across `MPS-24` / `MPS-25` / `MPS-26` fragments commit history, multiplies status pages, and forces a future reader to re-stitch three documents to understand one change. One ticket, with `### Phase 1` / `### Phase 2` headings inside it, is our default. Multiple tickets is our exception, not our pattern. - -When in doubt: write it as one ticket. If it grows past ~400 lines or the phases truly drift in goal, split it then — not pre-emptively. - -## 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 our user's browser can do: - -1. **Single canonical URI** — one URL serves our content. -2. **Consistent content** — regardless of viewer capabilities. -3. **Graceful enhancement/degradation** — use available capabilities to enhance presentation. - -### Our `js-only` / `