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>
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>
(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>
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>
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>
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>
- 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>
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>
/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>
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>
staff_shift table (one open shift per staff via partial unique index). /admin/clock
(status+today total), /clock/in (idempotent), /clock/out; /admin/timecards (admin)
= per-staff hours + recent shifts over a window. search.html top bar gets a live
clock widget (🟢 On 2h14m · Clock off · Log out; Clock on when off) ticking every 30s;
Log out warns if still clocked on. admin Staff tab gains a Timecards report.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Token resolver now checks the env owner-token (admin) then a staff table (name/token/
role). New require_admin gates /settings/* (API keys/connections) + staff CRUD; everything
operational (search/scan/inventory/labels/intake) stays on require_token so staff can do it.
admin.html: /admin/me drives ROLE; data-admin nav (Staff + Connections) hidden for staff +
go() blocks the views; new Staff tab to add operators, set role, reset/show token once,
deactivate. Tokens shown once on create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/nav/rack returns each crate's virtual_crate_type.material_color + type name;
drawRack fills/strokes/arrows each crate in its real colour (Blue Crate #0000FF,
Wooden Rack Bin #BA906A, White Tub #FFFFFF, Black Tub #191970, Shelf #C0C0C0…)
instead of all-blue. Legend lists the crate types present on the rack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-crate panel under Crate Contents: rename (label_text) + a Facing segment
(↑back/↓front/←left/→right) that rotates the crate live — the map arrow flips on
click. CrateEditIn gained 'direction'. Restores the crate editing my tabbed rewrite
dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sources stay the pick-ordered crates; targets default to those same real crate IDs
sorted ascending (4,3,2,1 → pour A–Z into 1,2,3,4), shown in an editable field so you
can consolidate/spread. Plan now shows where items land per crate. Drops the Start+Count
inputs (which could target non-existent crate IDs and orphan items).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One persistent central store/rack map with a top tab strip (Scanner / Returns /
Reorganize / Collections / Stock Finder) all driving the shared map, + Store/Rack/
Crate/Item view buttons and an omni scan bar (release/SKU/c12/r3). Reorganize: click
crates IN ORDER (numbered pick badges feed the A–Z distribution planner) + drag racks
to move them on the store map (persists pos_x/pos_z; RackEditIn gained position fields).
Collections edit highlights picked crates on the map. Returns = stub (manual path via
Scanner + Release Info for now).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Storefront /wantlist form (themed, email-keyed, guest-friendly) posts to
/shop/wantlist → wantlist table (lazy DDL). Release pages deep-link to it
(prefilled; 'not in stock — request this record' when no copies). Admin gets
a Wantlist tab: pending/found/fulfilled queue with one-click status actions.
Default storefront menu → Records + Wanted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
records.html reads /artist|label|genre|style/{val} from the path, sets the
filter + an entity header (artist/label name via /shop/{kind}/{id}). /shop/browse
gains artist/label EXISTS filters + primary artist_id; release page links
artist→/artist/{id} and genre/style pills→/{kind}/{name}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>