Commit Graph

63 Commits

Author SHA1 Message Date
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
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
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
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
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
type-two
16c85184cd feat(shop): wire audio/video previews — kiosk Listen (Apple 30s / YouTube / Beatport)
audio_sync.py mirrors WowPlatter disc_release_audio (22.5k; apple_preview_url, beatport_embed,
bandcamp…) + disc_release_video (231k YouTube) into Postgres. /shop/audio/{id} returns playable
sources. Kiosk detail gets a ▶ Listen: Apple 30s preview (instant <audio>), else YouTube embed,
else Beatport/Bandcamp embed; stops on close/idle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:10:02 +10:00
type-two
bab0826fea feat(shop): in-store kiosk page — touch browse, locate-in-store, scan-to-buy QR
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>
2026-06-23 23:01:54 +10:00
type-two
e20be06bf0 feat(nav): Quick Insert + click-to-pick insert slot (insert-with-shift)
/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>
2026-06-23 19:45:42 +10:00
type-two
a57b74700c feat(admin): System tab — read-only Postgres health (db size/version/uptime/conns/cache-hit, largest tables, data freshness)
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>
2026-06-23 18:44:44 +10:00
type-two
6a95efb9bb fix(report): count paid+completed sales (migrated=paid), fix hour :00 bind-param, add format breakdown
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:26:57 +10:00
type-two
336d525585 feat(admin): comprehensive lazy dashboard + reports workbench (Chart.js)
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>
2026-06-23 18:22:17 +10:00
type-two
4ab9c09899 feat(pos): Square Terminal payments — push checkout to a paired terminal
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>
2026-06-23 17:57:26 +10:00
type-two
5673ee125d fix(sales): register GET /{sale_id} last so /settings /customers /past /discounts resolve
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>
2026-06-23 17:38:16 +10:00
type-two
bf08cd9b35 feat(pos): receipt logo + Monster Robot template (store name/address/footer)
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>
2026-06-23 17:33:08 +10:00
type-two
5351cfb9c2 feat(pos): rebuild Sales — 3 tabs, smooth checkout + tender/change, print+email receipts
7 tabs → Register / History / Settings. Register: search/scan + cart + checkout flow
(cash tender→change, split tenders, layby deposit, notes) → completes → receipt auto-pops.
Receipts now server-rendered (app/receipts.py, one template) with per-item discounts, GST
breakdown, split lines, tendered/change, store header/footer; Print opens a clean window
(no visibility hack); Email via app/mailer.py (stdlib SMTP, creds in vault, admin-only) with
a Settings 'send test' button. History merges past sales + laybys (reprint/collect). Settings
holds currency/GST/discount + receipt header/footer + promotions. sales gains amount_tendered
+ notes; SaleIn carries tendered/notes; /sales/{id}/receipt + /{id}/email + /email-test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:29:42 +10:00
type-two
b46930d468 feat(staff): time clock — clock on/off = shift = timecard
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>
2026-06-23 15:40:02 +10:00
type-two
5e2a9e7ab1 feat(auth): staff accounts + roles — operators run the store, admins keep the keys
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>
2026-06-23 15:33:19 +10:00
type-two
13e5200b8d feat(nav): colour crates by type in the rack diagram (material_color)
/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>
2026-06-23 11:26:34 +10:00
type-two
cba339151f feat(nav): Crate Info panel — edit label_text + rotate crate.direction in /search
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>
2026-06-23 10:51:41 +10:00
type-two
c4b544373d feat(nav): unified tabbed Search & Inventory workstation (WowPlatter parity)
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>
2026-06-23 09:58:18 +10:00
type-two
48f8579bff feat(shop): P4 wantlist — public 'request a record' page + admin queue
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>
2026-06-23 01:29:00 +10:00
type-two
28a46615a5 feat(shop): entity browse pages — artist/label/genre/style storefront views
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>
2026-06-23 01:18:15 +10:00
type-two
fcc3d7b2ff feat(storefront): P0 — public Browse (faceted release catalog) + Release detail pages + /shop/{browse,facets,release} API; Woo-transacted
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:37:04 +10:00
type-two
0269decb87 feat(search): editable Rack Info (name) + Reorganize planner UI (scan source crates → filter → A-Z distribute into target crates → review → apply)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:58:10 +10:00
type-two
83466389af feat(scanner): scan-to-slot assigner (replace/append/prepend/insert + Test preview + recover-last) + editable crate info (name/label); hdr Content-Type fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:46:14 +10:00
type-two
19c6e03c70 feat(search): clickable crate contents → Release Info panel (cover/label/format/country/year/genre/style + all copies + locate); crate genre/style + last-scanned + compress
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:40:06 +10:00
type-two
d6e75667bf feat(customers): CRM — list with spend/orders/last + record page (overview stats, editable info, mailing opt-in, sales history)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:17:11 +10:00
type-two
a817b9d163 feat(import): bulk stock-in — resolve Release ID/barcode/catno against disc_release → create inventory (UI + endpoint)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:48:43 +10:00
type-two
4ece321b76 feat(inventory): CRUD backend (add/edit/delete/bulk) — first cream from the WowPlatter audit; + migration audit doc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:39:17 +10:00
type-two
0b679ba079 fix(images): /img/r serves local copy else 302 to discogs webp (server-side fetch is CF-blocked); drop unused pillow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:48:08 +10:00
type-two
ddaa6ecedf feat(images): RecordGod WebP cover cache (/img/r/{release_id}) — same-VPS, configurable DISC_IMAGE_DIR, no DealGod dep
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:45:31 +10:00
type-two
af3a611667 feat(collections): interactive editor — click-map crate select, genre/style/price/year rules, live match stats, priority
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:33:15 +10:00
type-two
10af3464ac feat(disc): mirror WowPlatter Discogs metadata into recordgod (disc_sync.py) + locator matches on collection genre/style ids
- disc_sync.py: MariaDB clone -> recordgod disc_* (30.8k releases, genre/style/label/format/identifier/artist/track/master), keeping WowPlatter's own genre/style ids
- locator reads disc_release.genre_ids/style_ids + parses collection 'ID,0' id format
- images stay with DealGod; masters sparse (backfill from Ultra later)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:19:47 +10:00
type-two
a1b4538954 fix(collections): is_active is int not bool — cast in queries
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:58:50 +10:00
type-two
879a58992f feat(collections): Collections CRUD + fallback locator + Reorganize scan/apply/compress API + Collections UI
- collections_routes.py: /nav/collections CRUD over virtual_collection + /nav/locate (exact-then-rules)
- navigator_routes.py: /nav/reorg/{scan,apply,compress} (atomic re-file)
- disc_cache gets year/genre_ids/style_ids cols for rule matching (backfill TODO)
- /search: collections list + locator fallback shows the should-be-here zone for unfiled hits

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:56:34 +10:00
type-two
fb533a7701 fix(nav): store view shows functional racks only (rack_purpose<>prop), truncate labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:38:29 +10:00
type-two
53b1a64a48 feat(nav): light/dark switch every page + store→rack→crate navigator
- nav.js owns a light/dark toggle (persisted, default light) across all owner pages
- /nav API: spaces, store-layout, rack/{id}, crate/{id} over migrated virtual_* geometry
- /search rebuilt as Store→Rack→Crate drill-down (true rack rotation, per-level slot grid,
  crate facing arrows + BACK/FRONT/L/R, search→locate)
- facing convention (directionYaw, slot-grid, rotatedArrows, world composition) in RECORDGOD_NAVIGATOR_PLAN.md
- no-cache HTML routes so deploys show instantly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:33:24 +10:00
type-two
eb502efaf4 feat(pos): full Sales console + migrate customers/discounts/settings 2026-06-22 02:40:35 +10:00
type-two
584fd140ad feat: Search/locate workstation + Search & Sales in nav (cache-bust nav.js) 2026-06-22 01:57:54 +10:00