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>
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>
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>
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>
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>
- app/vault.py: Fernet-encrypted credential store (app_secret table); key lives only in
RECORDGOD_SECRET_KEY (.env, gitignored) — refuses to store anything if the key is unset.
- app/settings_routes.py: admin-gated GET/POST /settings/secrets; values encrypted at rest,
NEVER returned. Known fields: Woo / Discogs / Cloudflare / DealGod credentials.
- site/index.html: recordgod.com landing. site/dash.html: control room (token gate +
paste-your-credentials form). main.py loads .env, mounts site at /, store at /store.
- Proven: 401 on bad token; DB holds ciphertext (gAAAA…), not plaintext.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- FastAPI twin of DealGod (async SQLAlchemy/asyncpg). wowplatter/v1: ping,
inventory/lookup, inventory/intake (enriches from discogs_full by release_id).
- schema v2: unified inventory (vinyl+general), slim crate, sales/sale_items.
Discogs catalog NOT copied — joined from discogs_full at read time.
- migrate.py: 34,543 shop rows moved (vs ~3.2M in the WowPlatter clone).
- Smoke test green; plan/readme carry the architecture + UI boundary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>