Commit Graph

110 Commits

Author SHA1 Message Date
type-two
fce64c72d3 fix(cache): build_disc_cache wrote NULL artists it did not have to
The builder read release.artists_sort alone, which is NULL for roughly 9.1M of
the 10.4M releases in this dump, and cached that NULL. Not merely a blank column:
recordgod's admin inventory list runs its artist SEARCH and SORT on dc.artist
too, so an affected record becomes unfindable by artist name. Genre, style, label
and country all looked fine on the same row because those read disc_release_*;
the artist is the one catalog field where this cache is the only source.

Seen on release 447087 - cached as title "Mezcal", artist NULL, showing a dash in
the admin list, while Discogs plainly says Urban D.K.

The name was never missing. release_artist has it regardless of artists_sort, so
fall back to that. extra=0 is the RELEASE artist: extra=1 rows are credits, and
447087 also lists Carl Clarke and Jim Eliot as Producers, so folding those in
would be worse than the blank. Comma-joined rather than reconstructing Discogs
join_strings, since this only fires where artists_sort is already NULL.

Verified against the mirror: 447087 to Urban D.K. and 564101 to Idjut Boys are
recovered, while 3018249 (Skirt (7)) and 314753 (Dahlback & Dahlback) still come
from artists_sort unchanged. No credits leak in.

This mattered urgently because the builder TRUNCATEs disc_cache and rebuilds, so
the next run would have silently undone the prod backfill of those two rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:50:04 +10:00
type-two
dfd3221617 docs: point the PLATFORM.md link at ../dealgod/PLATFORM.md (now version-controlled)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:03:37 +10:00
type-two
79b0f7e439 docs: README — RecordGod is a StoreGod module; live code = STOREGOD/plugins/recordgod
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:14:58 +10:00
type-two
720afcaee4 feat(storegod_v1): ping returns name+role (parity with STOREGOD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:23:29 +10:00
type-two
840e1c4359 refactor: wowplatter_v1 → storegod_v1; mount /storegod/v1 canonical + /wowplatter/v1 legacy alias
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:50:46 +10:00
type-two
e8013a4633 feat(shop): DECKS — two-channel preview mixer on release pages (Serato internal-mode vibe)
Pure Web Audio, zero deps (site/decks.js, self-contained UI):
- deck A/B with waveform (client-computed peaks), click-to-seek, play/pause, CUE
- ±8% varispeed pitch (playbackRate — turntable behaviour, deliberately no keylock)
- channel gains + equal-power crossfader, DynamicsCompressor master limiter
- AudioContext lazily created on first load (autoplay policy safe)

Sources:
- local preview MP3s: PREVIEWS_DIR (default ./previews, gitignored) served at
  /previews, discovered via GET /shop/local-previews/{release_id}
  ({release_id}.mp3 / {release_id}-*.mp3|.m4a); on prod bind-mount the collection
- Apple 30s previews: GET /shop/preview-proxy?u= (STRICT whitelist
  *.itunes.apple.com / *.mzstatic.com) — Apple CDN sends no CORS, Web Audio needs it
- YouTube/Beatport/Bandcamp embeds untouched: iframes can never enter a mixer

release.html: A/B load buttons per track preview + local-cut rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:57:47 +10:00
type-two
00e08a86dd feat(sku): canonical 14-digit timestamp SKUs so RFID/EPC tagging works (new items)
RecordGod generated 'RG'+random-hex SKUs, which the SCANGOD EPC codec rejects
(it requires 14 digits: SKU × 10^9 + release_id packed onto the UHF chip). New
inventory now gets a 14-digit product-added timestamp (YYYYMMDDHHMMSS) via
_fresh_sku(), which bumps a second on collision to stay unique against the sku
PRIMARY KEY. release_id stays its own column (the sku+space+release_id string is
only the physical chip encoding, which the codec already builds).

New items only — existing 'RG' SKUs are untouched (they keep working; they just
aren't RFID-taggable until re-SKU'd, a separate migration if ever wanted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 03:34:27 +10:00
type-two
61601f1067 money: atomic stock claims, amount validation, discount gate (converge with StoreGod twin)
R1 checkout qty clamp to one-row-per-copy; R2 claim-then-charge in
online checkout with release-on-decline and post-charge write guard;
R3 POS create_sale per-sku atomic claim (matches held copies so
hold->counter completion still works) before booking the sale;
R4 take_payment positive-amount check + atomic amount_paid increment;
R5 POS discount gate (admin only, RecordGod role idiom); R6 log +
re-raise if the order write fails after a successful charge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 03:02:30 +10:00
type-two
8535389cf1 security(layout): SSRF guard on /layout/import-brand (reject internal hosts, no redirects)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 02:49:09 +10:00
type-two
5ebb997978 security(pii): keep customer SQL dumps out of git and the Docker image
customers.sql (610KB phpMyAdmin dump: wp_rmp_users password hashes + real
customer names/emails/phones/addresses) sat untracked but matched NEITHER
.gitignore NOR .dockerignore, and Dockerfile does COPY . . — so it was one
'git add .' from history and already bakeable into the public image.

- .gitignore:  customers*.sql  (schema.sql stays tracked)
- .dockerignore: *.sql  (startup DDL is inline in app/main.py; no .sql needed at runtime)

Working-tree customers.sql shredded. site-update.sql was already gitignored;
now dockerignored too.

NOTE: rebuild+redeploy the image to purge copies already baked into layers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 02:04:45 +10:00
type-two
cfd6b54731 chore(security): gitignore live-site MariaDB dumps (customer/order PII)
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>
2026-07-03 14:16:09 +10:00
type-two
db1788bfe5 feat(app): payments/packs/discogs-mp/dealgod modules + admin, shop, sales route work
Batch-committing the in-flight app work (all modules compile clean):
- app/payments.py, app/packs.py, app/discogs_mp.py, app/dealgod.py (new)
- admin_routes, main, navigator/sales/shop routes, site/search.html
- customer_woo_sync + disc_sync tweaks; test_dealgod_supply, test_startup_ddl

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:07:28 +10:00
type-two
d46025e887 fix(virtual): cyclorama extent for east/west coves + never frustum-cull the cove
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>
2026-07-02 23:07:28 +10:00
type-two
d0c91faf79 feat(dig): port THRIFTGOD's evolved crate-flip back into /store
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>
2026-07-02 23:07:28 +10:00
type-two
916b26102c docs: DIG_FLIP_HANDOVER — port THRIFTGOD's evolved crate-riffle into /store
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:52:26 +10:00
type-two
b6994a96e3 feat(admin): legacy-panel banner — new admin lives at robotmonster.party/admin + copy-my-token
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>
2026-07-02 17:40:40 +10:00
type-two
c44e6643db feat(intake): condition dropdown auto-fills suggested price
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>
2026-06-30 18:40:06 +10:00
type-two
ca84b7f82f chore(docs): move 14 planning/handover docs to docs/ — declutter repo root
Root carried 15 *_PLAN/_DEEPDIVE/_AUDIT/bridge/handover docs (many describe shipped work).
git mv → docs/ (R100, no content change). README.md stays at root. Already *.md-dockerignored,
so no image impact — purely navigability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:45 +10:00
type-two
9e3c132a11 chore(security): purge distro raw dumps (supplier PII + account JWT) from git + image
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>
2026-06-30 15:15:43 +10:00
type-two
5551963ef4 feat(3d): procedural cyclorama / infinity cove (new — not in WowPlatter)
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>
2026-06-27 12:47:11 +10:00
type-two
205967d506 feat(3d): crate-type + rack logo decals on object faces; verified bin slope
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>
2026-06-27 12:33:30 +10:00
type-two
44517d4930 fix(3d): decals snap to their preferred_wall (ported WowPlatter snapToWallAndOrient)
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>
2026-06-27 12:23:18 +10:00
type-two
d489e8807d feat(buylist): wishlist scarcity ranking via DealGod /api/supply
DealGod shipped POST /api/supply (store_count + discogs_seller_count per release). RecordGod half:
PRICEGOD scrapes the RareWaves wishlist (www.rarewaves.com/pages/wishlist) → /products/<ean>- barcodes +
colour-from-slug → POST /admin/wishlist/scarcity → resolve barcodes → batch DealGod /api/supply → upsert
buylist (store_count/au_copies/lowest_au/discogs_seller_count/discogs_lowest + already_stocked). New 🎯 Buy
list admin view ranks rarest-first (store_count asc, sellers asc), flags colour vinyl + already-stocked.
Endpoints /admin/{wishlist/scarcity, buylist, buylist/clear}; _dealgod_supply batches ≤200. Verified e2e on
6 real wishlist items: ranked 0-store rarest first, AU prices live, sellers backfill per DealGod's pin sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:07:48 +10:00
type-two
379a53bc01 docs(wishlist): scarcity needs a DealGod per-release supply endpoint (request brief)
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>
2026-06-27 11:42:31 +10:00
type-two
ef37063b2e feat(distro): Inertia xlsx ingest + cost/new-used/margin in inventory editor
(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>
2026-06-27 11:39:39 +10:00
type-two
926468a8b5 feat(distro): RareWaves order ingest → cost-tracked NEW stock + approval landing
End-to-end: PRICEGOD scrapes a RareWaves order (account.rarewaves.com/orders/<id>) → POST /admin/intake/distro
→ resolve barcode→release_id (normalised: DB barcodes stored inconsistently, functional index on digits-only +
EAN/UPC leading-zero variants; Discogs barcode-search fallback) → stage one NEW copy per qty with cost_price +
cost_source, in_stock=false. Idempotent per order. New '/admin/newstock' approval queue: staff confirm/re-pick the
release (trgm search), set retail, flip new/used → publish (staged=f, in_stock=t). Bulk publish-all-matched-&-priced.
Backend verified e2e (ingest→queue→approve→live, unapproved stay staged). PRICEGOD: rarewaves.js content script +
account.rarewaves.com host perm + wowplatter.distro + bg rwDistro handler (needs extension reload to test live).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:13:48 +10:00
type-two
4c462861f5 feat(inventory): cost_price + cost_source + new/used split (distro-ingest foundations)
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>
2026-06-27 10:49:51 +10:00
type-two
1c8f15514c fix(auth): login loop — signin() wiped the stored token with the empty token-box
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>
2026-06-26 17:25:21 +10:00
type-two
bc59cc1c4c fix(deploy): stop excluding webstore/ from the image — it broke the /store 3D mount
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>
2026-06-26 17:10:05 +10:00
type-two
9a66f59bfe feat(auth): staff email+password login + password reset (login stays token-based under the hood)
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>
2026-06-26 16:24:34 +10:00
type-two
f9ab9527f3 perf(search): pg_trgm fuzzy staff search — typo-tolerant, multi-word, ~100ms (was ILIKE seq scan)
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>
2026-06-25 16:10:36 +10:00
type-two
56eab72c9c feat(intake): wowplatter/v1 intake carries est_market_value (DealGod target from PRICEGOD)
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.
2026-06-25 02:00:40 +10:00
type-two
03be6bb67f fix(admin): escapeH crashed on numbers (intake search cards)
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>
2026-06-25 01:11:38 +10:00
type-two
1695b2dfbb docs(scangod): connection details for the DealGod bridge (network + auth, no secret)
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>
2026-06-24 17:48:44 +10:00
type-two
2a41731fd3 feat(intake): ScanGod bridge — POST /admin/intake/scan (photo→staged product) + reply
DealGod's ScanGod POSTs camera-captured + bench-measured items; RecordGod resolves barcode→release_id
(local disc_release_identifier 'Barcode' → Discogs fallback), enriches, stores MEASURED weight_g
(overrides the 280g default → real AusPost quotes), dims_mm + scan_id → inventory.attributes,
est_market_value, base64 condition photos → DISC_IMAGE_DIR/items/<sku>/ served at /img/item/<sku>/<n>.
Lands staged (non-destructive review queue). Per-store-token auth (require_token). Smoke-tested live
(barcode→release 782254, 312g measured, photo served). Contract answered in SCANGOD_BRIDGE_REPLY.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:16:18 +10:00
type-two
e7335d77ab docs(ai): detailed build plan for the RecordGod AI assistant (for review)
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>
2026-06-24 16:22:04 +10:00
type-two
9e028db735 feat(connect): OpenRouter key + test in vault; AI agent integration plan
- 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>
2026-06-24 16:12:19 +10:00
type-two
9a0bf41fbe feat(intake): Heal sweep — backfill missing catalog metadata (salvaged self-healing pass)
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>
2026-06-24 11:57:36 +10:00
type-two
beabfe1297 docs(audit): WowPlatter mega deep-dive + reorganise
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>
2026-06-24 11:07:38 +10:00
type-two
25127b1b72 feat(intake): connected Google sheet via service account + skip-existing bulk stage
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>
2026-06-24 10:32:31 +10:00
type-two
88172924eb feat(intake): three-source intake — internal lookup + Discogs collection + Google Sheet
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>
2026-06-24 10:05:46 +10:00
type-two
5ad4de1d2c feat(bridge): RecordGod WP bridge plugin (WowPlatter successor) + thumb on browse/release
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>
2026-06-24 01:43:30 +10:00
type-two
8a3d833986 feat(shop): WP/Woo bridge endpoints — /shop/item, /shop/shipping/quote (public), /shop/woo-order (key-gated)
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>
2026-06-24 01:28:34 +10:00
type-two
bfe6a08dfb feat(shipping): use current AusPost own-packaging rates (RecordGod not live pre-1-Jul, no dating needed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:05:12 +10:00
type-two
ef4769784f feat: Woo customer sync (non-destructive, in refresh.sh) + AusPost rates mirror & quote calc
customer_woo_sync.py: WooCommerce online customers (wc_customer_lookup+billing) → RecordGod
customer CRM, links-by-email/refresh-by-wp_user_id/insert-new, never clobbers POS rows; added
as refresh.sh step 5 (after the --clean push). shipping_sync.py mirrors disc_post_flat_rates
(+zones) → post_flat_rate/post_zone. /sales/shipping/quote (units|weight_g → 280g/record →
≤5kg parcels → flat rate × parcels, Parcel Post + Express). POS Register '📦 post' quotes the
cart + adds a postage line. AusPost +5% lands 2026-07-01 — re-pull rates then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:55:56 +10:00
type-two
4c152954b5 fix(shop): key YouTube dead-cache by (release_id, uri) — mirrored video table has no id col
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:47:42 +10:00
type-two
988e6437cb feat(shop): per-track Apple previews (lazy iTunes) + jog mini-player + dead-YouTube check
/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>
2026-06-23 23:44:17 +10:00
type-two
d6e1e15982 feat(shop): storefront release page gets ▶ Listen too (Apple preview / YouTube / Beatport)
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>
2026-06-23 23:22:46 +10:00
type-two
aa56e874ad feat(shop): country-scoped Apple ids in audio — AU-preferred music.apple.com link + clean preview
audio_sync mirrors apple_id ({cc:album_id}) + apple_song_id (kept raw, country preserved).
/shop/audio parses it: AU>US>first → music.apple.com/{cc}/album/{id}, per-track preview array
→ first clean 30s url. Kiosk Listen uses apple_preview + an Apple Music ↗ link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:17:06 +10:00
type-two
7e9d5028b0 fix(shop): apple_preview_url is a JSON array — return the first clean preview URL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:12:39 +10:00