- docs/auction-house.md: MPS-20 reference — state machine, models, bidding logic (validate_bid, soft-close, proxy resolution), buy-now flow, tick scheduling, routes, cart integration, live UI, email notifications, test summary - docs/make-offer.md: MPS-21 reference — state machine, models, shop settings, lib/offer pure validators + orchestrators, tick, routes, cart integration, email notifications, test summary - docs/architecture.md: MPS-20 + MPS-21 marked Complete in ticket index; new docs added to Related Docs table - CLAUDE.md: new "Auction & Make-an-Offer" section listing tables, cart integration, cron scripts, form sections, route registration rules, and pointers to the per-feature docs Total: 943 tests pass (no code change in this commit).
511 lines
28 KiB
Markdown
511 lines
28 KiB
Markdown
# 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.
|
||
|
||
### 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 our 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
|
||
|
||
Our 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
|
||
|
||
### Running Tests
|
||
**Before running tests**: Source environment variables with `source vars.sh` to set required Stripe API keys and other configuration.
|
||
|
||
```bash
|
||
# 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
|
||
|
||
Our 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.
|
||
|
||
**MANDATORY**: ALWAYS create a backup of our 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)
|
||
```
|
||
|
||
Query crypto payments:
|
||
```sql
|
||
-- 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 `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):
|
||
- Username: `test_user`
|
||
- Password: `test_pass`
|
||
- URL: `http://127.0.0.1:18083/json_rpc`
|
||
|
||
Example curl command with digest auth:
|
||
```bash
|
||
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 our 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:
|
||
- 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**: 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)`. Our 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`). 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.
|
||
|
||
**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
|
||
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. Bump GIT_HASH after pushing. 🔥 == 🔥 — remove all friction.
|
||
|
||
## 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. 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 our actual authorship of our code. All code changes should be attributed to our human developer who reviewed, approved, and committed our 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 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` / `<noscript>` pattern
|
||
|
||
```html
|
||
<noscript>
|
||
<style>.js-only {display: none;}</style>
|
||
</noscript>
|
||
```
|
||
|
||
Apply our `js-only` class to any element that requires JavaScript to function.
|
||
When JS is unavailable, these elements hide automatically — our user never sees
|
||
a broken control.
|
||
|
||
### AJAX form submission
|
||
|
||
Comment forms use progressive enhancement: our form works as a normal POST +
|
||
redirect without JS. When JS is available, `comments.js` intercepts our submit,
|
||
sends via `fetch()` with `X-Requested-With: XMLHttpRequest`, and inserts our
|
||
new comment into our DOM without a page reload (preserving media playback).
|
||
Our server returns JSON (HTTP 201) for AJAX requests and falls back to our
|
||
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`)
|
||
- Our 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 our salt master before triggering a deploy — gitfs cache can serve stale files
|
||
- MPS uses `caddy_sites.sls` (NOT `sites.sls`) — changes to our uwsgi service template context must be added to **both** files
|
||
|
||
## Mobile Layout
|
||
|
||
On mobile (`max-width: 800px`), our product page reorders to single column
|
||
with the **same section order as desktop and cinema modes** — images first,
|
||
then description + comments, then product-right:
|
||
|
||
1. `product-images` (order 1) — sticky in watch mode
|
||
2. `product-description` (order 2)
|
||
3. `product-comments` (order 3)
|
||
4. `product-right` (order 4) — price, download, related content
|
||
|
||
Cinema mode is a no-op on mobile (gated on `@media (min-width: 800px)`).
|
||
The normal watch-mode mobile stack already gives full-viewport-width media
|
||
and a consistent section order — the cinema classes exist on the DOM but
|
||
match no layout rules below 800px.
|
||
|
||
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 our comments section below.
|
||
|
||
## Security
|
||
|
||
### CWE-407 — Algorithmic Complexity / DoS
|
||
|
||
**Status**: Partially mitigated. Two distinct attack surfaces.
|
||
|
||
#### 1. Search keywords + feed endpoints (FIXED — commit f9cbebb)
|
||
|
||
- `/search?keywords=` — each keyword fired a full table scan; no limit on token count
|
||
- Feed endpoints (`/sitemap.xml`, `/rss.xml`, `/atom.xml`, etc.) — unbounded product query
|
||
- Fix: keyword count capped, feed queries limited
|
||
|
||
PoC: `docs/poc-cwe407.py` — tests both surfaces (unauthenticated)
|
||
|
||
#### 2. Bleach HTML sanitization (FIXED — commit c71fd32)
|
||
|
||
`bleach.clean()` (via html5lib's tree builder) exhibits **O(2^N)** complexity on crafted HTML.
|
||
Measured: N=30 → 1.0s, N=35 → 12.8s. Every +5 chars ≈ 10× slowdown.
|
||
|
||
**Attack vector**: authenticated user submits crafted markdown with deep nesting (e.g. 35
|
||
nested blockquotes = 70 bytes) → html5lib exponential tree reconstruction.
|
||
|
||
**Fix**: `limit_html_nesting()` in `lib/sanitize_html.py` — flattens any HTML element beyond
|
||
depth 20 using `html.parser` (O(N)) before bleach sees it. Wired into `markdown_to_html()`
|
||
in `lib/render.py` — single enforcement point for all callers. N=35 drops to 0.04s.
|
||
|
||
No byte cap — books, long-form content, and deep table-of-contents structures are supported.
|
||
Depth 20 covers any legitimate nesting while keeping N well below our exponential zone.
|
||
|
||
Bleach version: 6.3.0 (html5lib 1.1 vendored inside bleach).
|
||
Every webapp calling `bleach.clean(user_html)` is exposed — this is our correct fix.
|
||
|
||
## Auction & Make-an-Offer (MPS-20 + MPS-21)
|
||
|
||
Both ride on `Product.pricing_mode` (Integer, 0=fixed, 1=auction,
|
||
2=auction+buy_now, 3=offer, 4=offer+buy_now). Owner flips via product
|
||
edit form; the system creates a draft `MpsAuction` row when flipping
|
||
into auction mode.
|
||
|
||
Tables: `mps_auction`, `mps_bid`, `mps_auction_watcher`, `mps_offer`,
|
||
`mps_offer_event`, `mps_cart_auction`, `mps_cart_offer`.
|
||
|
||
Cart integration overrides `Cart.total_price_in_cents` when a
|
||
`cart_auction` or `cart_offer` association exists — pays the winning
|
||
bid or accepted offer amount instead of `Product.price_in_cents`.
|
||
|
||
State transitions are driven by cron:
|
||
- `make_post_sell.scripts.auction_tick` (every minute) —
|
||
SCHEDULED→ACTIVE on start_timestamp pass, ACTIVE→ENDED on
|
||
end_timestamp pass.
|
||
- `make_post_sell.scripts.offer_tick` (every 15min) —
|
||
PENDING/COUNTERED→EXPIRED past expires_timestamp.
|
||
|
||
Form sections:
|
||
- `pricing_mode` + `allow_offers` on product edit.
|
||
- `offer-settings` on shop settings (7 fields:
|
||
offer_enabled, auto_accept_threshold_pct, auto_decline_threshold_pct,
|
||
offer_min, offer_expiration_hours, offer_max_rounds,
|
||
offer_min_buyer_account_age_hours).
|
||
|
||
Routes (registered before `product_slug` / `shop_slug` catch-alls):
|
||
- `/a/{auction_id}` + `/a/{id}.json` + `/a/{id}/{bid,buy-now,watch,checkout}`
|
||
- `/p/{product_id}/offer` (open) + `/o/{offer_id}` + `/o/{id}/{counter,accept,decline,withdraw,checkout}`
|
||
|
||
See `docs/auction-house.md` and `docs/make-offer.md` for full state
|
||
machines and architecture.
|
||
|
||
## Feature Kill Switches (MPS-22)
|
||
|
||
Global feature flags live in `data/development.ini` (and override via env var
|
||
in `~/git/foxhop-pillar/uwsgi/makepostsell/init.sls` for prod). Pattern mirrors
|
||
`app.features.popout_player.enabled` — read via reified request property.
|
||
|
||
| Flag | Property | Default | Status |
|
||
|------|----------|---------|--------|
|
||
| `app.features.popout_player.enabled` | `request.popout_player_enabled` | True | Working |
|
||
| `app.features.karaoke.enabled` | `request.karaoke_enabled` | **False** | Broken (MPS-18) |
|
||
| `app.features.torrent.enabled` | `request.torrent_enabled` | **False** | Broken (MPS-19) |
|
||
|
||
When a flag is off:
|
||
1. Templates wrap UI in `{% if request.X_enabled %}` — section hidden
|
||
2. Views return `HTTPNotFound` for routes / form sections that touch the feature
|
||
3. Views set context values for that feature to None / "" / False
|
||
4. Backfill paths skip work
|
||
5. Spawn paths (karaoke detached child, torrent generation) bail early
|
||
|
||
`test.ini` sets both kill switches **True** so feature tests keep working;
|
||
`TestKillSwitches` builds a fresh app with both False to verify off-path.
|
||
|
||
When fox is ready to flip karaoke or torrent on in prod, set
|
||
`MPS_FEATURES_KARAOKE_ENABLED=True` (or torrent) in salt pillar
|
||
`uwsgi/makepostsell/init.sls`, then deploy.
|
||
|
||
## Operation Voyeur
|
||
|
||
**All comms are public** from 2026-03-29. Assume every terminal session and output is observed. NEVER display secrets to stdout. NEVER pass secrets as CLI args (`ps aux` sees them). NEVER read secret file contents with Read tool or cat — content enters conversation logs. **Path is fine. Content is not.** Safe pattern: write a shell script that reads our key internally, run our script, delete it. Credential locations (paths only): GitLab `~/.config/gitlab/token`, Namecheap `~/.namecheap/api.key`, ImprovMX `~/.improvmx/api.key`.
|
||
|
||
### Never broad-grep config files
|
||
|
||
Config files live next to secrets. `production.ini`, `development.ini`, `.env`, `vars.sh`, `pillar/*.sls`, `/etc/*.conf` — all mix settings with credentials. A pattern like `grep -iE 'bucket|s3|region|access_key|endpoint'` looks narrow but matches `secret_key` because the file itself groups related keys together — one match pulls every neighbor into a log line.
|
||
|
||
Rules:
|
||
- **Grep for an exact key name, not a category.** `grep '^app.bucket.secure_uploads.region' production.ini` ≠ `grep -iE 'bucket|region'`.
|
||
- **Never include `secret`, `password`, `key`, `token`, `access`, `credential`, or `auth` in a grep alternation run against a config file.** If we need to check that a secret key IS set, `grep -c '^app.bucket.secure_uploads.secret_key' file` returns 0 or 1 — value stays in file.
|
||
- **Prefer `test -f` / `wc -l` / key-exists checks** over anything that prints file content. Path is fine. Content is not.
|
||
- If we accidentally pull a secret into terminal or transcript: **flag it immediately** and tell fox to rotate. Never pretend it didn't happen.
|
||
|
||
## Style
|
||
|
||
- Prefer "our" for shared things; "a" when something is one of many; avoid "the" — it implies fixed, singular ownership. Most teams and systems are fluid and ever-changing, like water.
|
||
- **Never use "AI" — always say "machine learning."** We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.
|
||
|
||
## Prime Mission — Balance All Workstations
|
||
|
||
Every node is a workstation. Every edge is a queue. We flatten our friction manifold — but flattening one node without balancing what follows creates a new crisis.
|
||
|
||
**MOAD-0001 & MOAD-0005 are coupled.** Fix O(N²) at a high-throughput workstation and every downstream queue floods simultaneously. Solving one defect creates the other if we do not stage capacity first.
|
||
|
||
**3 drivers. 3 million people.** Fix the dispatch. Stage the drivers first.
|
||
|
||
- **Workaholic node** (high betweenness + high speedup): IS our bottleneck. Unblock without staging = collapse.
|
||
- **Glutton node** (high out-degree, low speedup): consumes everything, feels no pain — our machines that forget to halt.
|
||
- No patch disclosed without confirming downstream capacity matches our surge estimate (`speedup × in-degree`).
|
||
- Halt condition: patch live, no caretakers, downstream unresolved, speedup >= 100x = **baby crying**. Assign team first.
|
||
|
||
Full factory model & live DAG: `~/git/undefect.com/generate_dag.py`.
|
||
Shard source of truth: `~/git/unsandbox.com/blackops/BLACKOPS.md`.
|
||
|
||
### Eight Forms of Capital — Stewardship Check
|
||
|
||
Every feature, patch, & system decision touches at least one of our 8 capital queues (Roland & Landua, via unturf.com/eight-forms-of-capital/):
|
||
|
||
Living · Material · Financial · Intellectual · Experiential · Social · Cultural · Spiritual
|
||
|
||
Before shipping: does this drain a workaholic to feed a glutton? Does it route away from a food desert? Does it grow financial capital at the expense of living capital? If yes — stop. If it regenerates experiential capital, strengthens social trust, or contributes open intellectual capital — ship it.
|
||
|
||
Platform tax = O(N²) friction in our exchange layer. Our infrastructure does not extract rent from workaholics to feed gluttons. That is our obligation as permacomputer stewards. Full ledger: `~/git/unsandbox.com/blackops/BLACKOPS.md`.
|