site-update.sql is a full live-site export consumed locally for the 3D-store
racks/editor data work — never commit (same PII class as the purged distro dumps).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
east/west coves run only the cyclorama section (extent from the north wall) instead
of the full L-shaped depth; big static swept surface gets frustumCulled=false.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per docs/DIG_FLIP_HANDOVER.md — same createDig contract, zero index.html changes:
- real format sizes + packing by thickness (FMT map; LP-only today, CDs inevitable)
- secondhand lean: per-sleeve seeded jitter (rotation.z + position.x)
- crate drawn around the stack with the lower front lip you flip over
- covers trickle-load (fillQueue 120ms) + near-cursor priority window (-10..+15)
- pull presents at arm's length on the view axis (-0.1, 0.23-h/2, 0.12, rot -0.33)
- Add to cart -> localStorage rg_cart (SKU-keyed, matches /shop/checkout), then
the stack re-packs closed — no gap
- fix: dig camera aspect was NaN if the window loaded 0x0 and never resized;
guard + resize listener (latent in ancestor + THRIFTGOD)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The owner token is per-browser localStorage, so a sign-in here doesn't carry across domains;
the banner hands you the token to paste into the new panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each Find & Stage result card's condition dropdown now fetches a price suggestion and fills the
price input (unless the operator typed one). New GET /admin/intake/suggest?release_id=&condition=
maps the Discogs grade to a band of DealGod's composed value (M/NM→high, VG+→typ, VG→mid, G+→low).
Sourced from /api/value (AU listings) now; per-condition discogs_full.release_market suggestions
slot into the one dealgod.value() call when the fleet scrape pipeline is live — UI shape unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RareWaves/Inertia scrapes (rarw*.txt, inertia.txt, the SOH .xlsx) were tracked
in git AND baked into the Docker image (COPY . . with no exclusion). They carry a live
RareWaves buyer JWT and a supplier contact email — they don't belong in either.
- git rm --cached the 7 dumps; moved to local-only ingest_raw/
- .gitignore + .dockerignore: ingest_raw/, rarw*.txt, inertia.txt, *.xlsx
- nothing reads them by path (the xlsx ingest takes a base64 upload), so no code change
NOTE: still present in history (ef37063, 926468a) — scrub separately if the JWT is live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hip-hop room's north section is a real cyclorama (white curved infinity wall). Added a data-driven cove:
virtual_space gains cyclorama (comma list of north/south/east/west) + cyclorama_radius + cyclorama_color.
buildCyclorama() sweeps a profile per wall — [flat wall down to R] → [quarter-circle fillet radius R] →
[white floor lead-in] — so the floor curves up into the wall with no hard seam (the seamless studio look).
Cyclorama walls skip their flat wall (the cove replaces it). Set on hiphop-room (space 3): cyclorama=north,
R=0.9. v1 = single-wall cove (back curve); 3-sided + rounded vertical corners + partial-wall extent for the
L-shape = next iteration, all tunable via the columns.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend (virtual.py) now also returns object_decals: crate_type decals (apply to every crate of that
type) + rack/crate decals for this space (was space-only). Frontend: makeDecalMesh() shared by wall/
crate/rack decals; snapToFace() ports WowPlatter snapToCrate/RackFaceAndOrient (preferred_face + face_offset
→ front/back/left/right/top/bottom, eps proud of the face, yaw outward, right-face mirror, rack east/west
readability flip). buildCrate(t,front,typeDecals) stamps crate_type logos on the front; rack loop stamps
rack decals using the rack yaw. Booth now shows 5 crate-type + 4 rack logos. SLOPE CONFIRMED CORRECT: the
custom column is front_height (back=height, front=front_height); slopedSide already builds the trapezoid
right (top edge slopes back→front). Space-decal loop refactored onto makeDecalMesh (DRY).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The store renderer placed every decal at raw pos_x/y/z with raw rotation, ignoring preferred_wall/snap/
object_type — so wall art (pos with one in-plane axis, perpendicular=0, rotation 0) floated in mid-room on
the wrong axis. Ported WowPlatter public/js/virtual/decals.js snapToWallAndOrient: a space decal (rotation
all-zero or snap=wall) keeps its in-plane axis, the perpendicular axis is pushed flat to its named wall
(north z=-D/2 yaw0 · south z=D/2 yawpi · east x=W/2 yaw-pi/2 · west x=-W/2 yaw+pi/2 · floor/ceiling lie
flat), yawed to face into the room; non-zero rotation = free manual placement, respected. Wall conventions
already matched the room's walls. Fixes booth (31 decals) + entry-hall (6) automatically. Verified the snap
math against real data (all land on-axis, in-bounds, facing in). Still TODO: crate_type(5)/rack(4) logo
decals (backend only fetches object_type=space; need the face-snap port) + hiphop room has no decals authored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wishlist extraction is feasible (RareWaves wishlist has the same /products/<ean>- links as orders, colour
in slug). But the scarcity ranking (store_count + discogs_seller_count) is DealGod's data — probed the API:
/api/prices ignores ?release_id= and only has sample_size; no /api/supply. Brief requests a batch
/api/supply{ids[]} endpoint; RecordGod builds the wishlist scraper+rank once it exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(1) Inventory edit modal gains Cost + New/Used + a live Margin readout (price−cost, %); inv_edit accepts
cost_price + condition_type; inventory list returns them.
(2) Inertia/Warner SOH list = an xlsx that doubles as the order form (fill ORDER QTY, upload). New
/admin/intake/distro-sheet[/preview]: openpyxl parses header-mapped columns (ORDER QTY/UPC/ARTIST/TITLE/
FORMAT/PPD), takes rows with QTY>0 → shared _ingest_distro core (refactored out of the RareWaves path) →
staged NEW stock with cost_price=PPD, kind from FORMAT (cd/vinyl), catno kept. Upload+preview UI in New stock.
DistroItem gains kind+catno; RareWaves path now passes kind too. Verified on the real 3,368-row Warner sheet:
3 ordered titles → 4 copies, all resolved, kind=cd, cost from PPD. Added openpyxl to requirements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Buying stock from distros (RareWaves discount sales) → need per-COPY cost + a new/used flag.
- inventory.cost_price (per copy — costs vary per copy of the same release) + cost_source ('rarewaves #592619')
- inventory.condition_type 'new'|'used' (default used); backfilled 451 sealed → new
RareWaves analysed = Shopify; order pages give barcode (in /products/<ean>- handle) + title + qty + unit cost
($X.XX/ea). Wishlist = Klevu app (harder). Full plan in RAREWAVES_INGEST_PLAN.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After redirecting from /login with the token in localStorage, signin() read the (empty) #tok gate
input and overwrote TOKEN with '', so /me 401'd → removeItem + bounce back to /login → loop. Now keeps
the stored token when the box is empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier build-context trim (to drop the 916MB disc_images/) also excluded webstore/ (924K), so
/app/webstore was missing → main.py skipped the /store mount → every 'Virtual store / 3D store' link
404'd. Only disc_images/ (volume-mounted) is worth excluding. Repo .dockerignore now matches the live
/opt/recordgod one; /store serves again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Staff were stuck pasting a raw bearer token with no reset. Now: a proper /login page (email+password)
that exchanges to the staff member's existing token (SPA unchanged downstream). PBKDF2 hashing (stdlib,
no bcrypt dep). New: POST /auth/login, POST /auth/change-password (self), POST /admin/staff/{id}/password
(admin reset). staff gains email(unique)/phone/pay_rate/password_hash. Staff admin UI: add/edit details,
set-password, and timecards now show pay (hours × rate). admin.html redirects to /login when unauthed +
bounces expired tokens; nav gets Change-password + Sign-out. Owner master token still works (break-glass on
the login page). Verified e2e: login, wrong-pw 401, token auth, admin reset invalidates old pw.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Postgres answer to WowPlatter's sluggish rigid search. Was ILIKE '%..%' → Seq Scan (3038 cost),
zero typo tolerance, and multi-word phrases ('bob marley legend 2022') returned NOTHING (needs a
contiguous substring). Now: pg_trgm GIN index on disc_release.search_text (already populated) +
word_similarity operators — :q <% search_text (filter, GIN-accelerated) ranked by :q <<-> search_text.
Threshold pinned at 0.45 on the DB (recall vs speed sweet spot ~140ms; 0.3 too loose/slow, 0.6 too strict).
Added the missing disc_release_format(release_id) index (was seq-scanning per row) + inventory.title trgm
for stock-finder. All DDL idempotent in _STARTUP_DDL. Live: 'bob marly survivl' (2 typos) -> Survival in 117ms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PRICEGOD's overlay quick-add posts the DealGod target/median; store it on inventory.est_market_value
so the value lens lights up on staged stock. Verified: intake w/ est_market_value=45.53 → staged row.
Intake 'Find & stage' renders it.year (an int) through escapeH → (1977).replace is not a function →
the whole results render threw. escapeH now coerces via String() so numbers/null are safe everywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recordgod runs on the DealGod box (both on dealgod_default) → DealGod calls http://recordgod:8010
container-to-container. Minted a dedicated 'ScanGod Bridge' staff service-account token (store_id 1);
value handed to John out-of-band, suggested DealGod vault key recordgod_store_token. Proven reachable
+ authed from inside the dealgod container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implementation-grade companion to the assessment — grounded in the real schema. Tool catalog (read/draft/
propose), agent loop, ai_log + ai_proposal DDL, endpoints, UI, concrete guardrails (read-only recordgod_ai
role, statement_timeout, row caps, propose-never-commit), threat model, model routing, phase acceptance
criteria, the DealGod pgvector dependency, and open questions for a third-eye review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- settings vault gains openrouter_api_key + openrouter_model (generic Connections page renders them);
/settings/test/openrouter shows OpenRouter usage/limit.
- RECORDGOD_AI_AGENT_PLAN.md: honest assessment + phased plan for a grounded, tool-calling AI assistant.
Core stance: tools not free SQL; read-only + propose-never-commit; pgvector lives in DealGod (reached via
its API), not recordgod-db; moat = grounded data, not the model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WowPlatter self-heal idea, freed from its 3-hour import: one button scans in-stock items for
gaps (title/weight/cover) and backfills. Phase 1 = local backfill from the disc_cache mirror
(instant, unbounded UPDATE…FROM); Phase 2 = bounded Discogs re-fetch for release_ids missing from
the mirror or without cover art (rate-limited, re-run for the rest). Refactored _enrich → _fetch_release
so Heal can force-refetch (not just cache-miss). New /admin/intake/heal/{scan,run} + a 🩹 Heal admin view.
Live first run: 30,202 rows had no title/weight (migration never denormalized them → broke shipping
quotes); 30,193 healed instantly from the mirror, 239 needed Discogs (60/batch). Real win.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deeper companion to WOWPLATTER_MIGRATION_AUDIT.md — refreshes status (covered/partial/todo/dropped)
after the recent build wave, drops to tab + AJAX-action level (40 pages / ~90 tabs / 460 actions),
and proposes RecordGod's 5-group IA (STOCK/SELL/STOREFRONT/GROW/SYSTEM) to replace WP's 2-root sprawl.
Key reframe: internal mirror + local images make whole subsystems evaporate, not migrate (~1/3 of
actions drop as WP-survival/pre-data-centre enrichment).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The intake sheet is read straight from the stored Google service account (discotag@…
inventory-sheets, migrated out of WowPlatter's wp_rmp_disc_google_service into the vault) — a
signed JWT (cryptography, no new dep) → Sheets API → private sheet, no public-share needed. Uses
the saved column_mappings (timestamp/release_id/media/sleeve/price/comment, 1-indexed). SKU from
the sheet's locale timestamp '2025-01-30, 14:01:02' → 20250130140102 (matches migrated stock).
Bulk import (both connected + paste-URL paths) now SKIPS SKUs already in inventory, so importing
the 34,752-row form log only stages the genuinely-new rows (live: 2,006 new / 32,746 skipped).
Preview shows new-of-total. Endpoints: GET/POST /admin/intake/gsheet/{preview,import}.
Also: .dockerignore now excludes disc_images/ (volume-mounted, was 916MB) — build context
918MB→1MB, deploy 13min(hung)→24s. The COPY . . was baking the image store into every image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the bare release-id box with a tabbed Intake that all feeds one _stage() core:
- Find & stage: search the local Discogs mirror (instant) with a Discogs-API fallback for new
titles; pick a release → condition/price → stage. Cards show cover/artist/year/label/format.
- Discogs collection: lists the OAuth token's own folders (mrp3, 152 with stock) → page through
releases, reads media/sleeve/price from the collection's custom fields, stage-all-on-page.
- Google Sheet: paste the share URL → public CSV export (no Google creds) → auto-detect columns
(release id/media/sleeve/price/comment/timestamp) → preview → bulk import. SKU from the
Google-Form timestamp (YYYYMMDDHHMMSS), matching migrated stock.
Staging enriches title/artist/cover on demand: a disc_cache miss fetches the release from the
Discogs API and grows the local mirror (disc_cache + disc_release + artist + label), so internal
search + storefront get richer with every intake. Verified end-to-end on the VPS (incl. enrich of
a not-in-cache release) + sheet-parser self-check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thin WordPress/Woo bridge — RecordGod owns catalog/stock/pricing/shipping; the plugin is a storefront
skin + checkout adapter over /shop:
- storefront: SSR /records (browse) + /release/{id} inside the theme, with title/meta/JSON-LD MusicAlbum
- on-the-fly Woo product: /?rg_add=<sku> mints a hidden WC_Product_Simple from /shop/item (the WowPlatter
trick — 25k catalog, only sold items become Woo products)
- shipping: WC_Shipping_Method quoting /shop/shipping/quote by cart item count (AusPost flat rate)
- orders: order_status_completed -> POST /shop/woo-order (X-Bridge-Key), idempotent, marks SKUs sold
- settings page: base URL + bridge key + store id
Backend: added thumb to /shop/browse + /shop/release so storefront pages have cover art.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The thin WP plugin renders from /shop/*, mints Woo products on the fly via /shop/item, quotes
postage publicly, and posts completed orders back to /shop/woo-order (X-Bridge-Key vs vault
bridge_key) — marks SKUs sold + logs the online sale, idempotent on WC-<order#>.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/shop/audio-tracks/{id}: resolves each track's preview from its Apple song id via iTunes lookup
(au), cached in disc_release_track.apple_preview. /shop/audio validates YouTube via oembed (200=live),
caches dead in disc_release_video.dead, returns one live id. release.html + kiosk.html get a per-track
▶ on each tracklist row feeding a mini-player with a jog/seek bar (falls back to a single Listen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same audio affordance as the kiosk on release.html — Apple 30s preview (instant audio),
else YouTube embed, else Beatport/Bandcamp embed, + country-scoped Apple Music link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
site/kiosk.html: full-screen themed kiosk (idle attract cover-wall → tap → browse with
big search + genre chips + record grid → detail with tracklist, in-store locator (crate ·
rack) and a QR to buy on the customer's phone). 75s idle returns to attract. /shop/release
copies enriched with crate/rack location (public-safe). Route /kiosk. Better than WowPlatter's
kiosk: locate + phone-QR front and centre. Audio previews deferred (no internal audio yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ST.viewRot quarter-turns: drawRack remaps cells (rcell), edge labels (BACK/FRONT/L/R) and
facing-arrow glyphs per rotation while keeping all text upright; drawStore rotates rack
positions+yaw around centre with a matching unrotPt() so rack drag-to-move stays correct.
🔄 rotate button in the toolbar. Lets the diagram match where you're physically standing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/nav/insert: drop one record (release id/sku/barcode) into a crate at a chosen slot,
shifting items at>=slot down by one. Scanner: click a contents row to set the insert
anchor (before/after toggle) — drives both the bulk 'Insert at picked slot' mode and a
new Quick Insert one-record panel. Picked row highlighted; anchor is per-crate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Admin-only /admin/system; no maintenance buttons (PG autovacuums, infra owns backups).
The useful 10% of WowPlatter's System page; the WP cache/transients/WP-CLI/debug rest dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard: fast KPI tiles (instant /stats) + lazy 30-day revenue line + stock-by-format
donut. Reports rebuilt into a parameterized workbench — date presets (7d/30d/90d/12mo/all
+ custom from/to) × dimension (trend/genre/style/format/condition/payment/weekday/hour +
top releases/artists/labels/customers) × metric (revenue/count), each chart fetched ON
DEMAND. New /admin/report/{kpis,series,breakdown,top,stock} (whitelisted dims, bound dates).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app/square.py (Terminal Checkout API: device-code pairing, create/poll/cancel checkout;
creds in vault, device_id in sales_setting). /sales/terminal/{status,pair,pair/{id},
checkout,checkout/{id},checkout/{id}/cancel}. POS: 📟 Terminal payment method (shown when
paired) → pushes total to the terminal, polls, finalises the sale only on COMPLETED;
Settings → Pair a terminal (device-code flow). square_* added to admin Connections.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 1-segment parametric route was swallowing the literal GET routes (int-parse 422,
silently masked before). Moved it after all literals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
receipts.py renders store_logo (img) above the title; store_logo added to sales
settings (get/save) + a Logo URL field in POS Settings. Logo hosted at
site/receipt-logo.webp (Monster Robot, from WowPlatter receipt_templates). Print
window now waits for window.onload so the logo prints. Deploy via /opt/recordgod/deploy.sh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>