RECORDGOD/RECORDGOD_NAVIGATOR_PLAN.md
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

11 KiB
Raw Blame History

RecordGod — Search & Inventory Management (Navigator) port

Faithful port of WowPlatter's Search & Inventory Management workstation onto RecordGod's Postgres + the already-migrated virtual_* geometry. This is the real thing — Store→Rack→Crate→Item drill-down, per-level slot layouts with orientation, scan-to-slot, reorganize, returns, stock finder — NOT the toy /search that ships today (that becomes Phase 0's store view).

Source studied: wowplatter/admin/partials/search.php (844 lines), admin/js/search/main.js (8,482 lines, the live renderer), admin/js/src/navigator/{diagram,scanner,views,state}.js, includes/repositories/class-wowplatter-virtual-store-repository.php, schema-manager virtual tables.

UI rule: light/clean shop-admin theme (see memory recordgod-admin-light-ui), not the dark dash.


1. The workstation shape

  • Top bar: Space selector (booth-room (default) …), view zoom = Store · Rack · Crate · Item + Auto, level controls ◀ Level 1 of 2 (BOTTOM) ▶, rack-rotate, refresh.
  • 5 tabs: Scanner · Returns · Reorganize · Collections · Stock Finder.
  • Center: the canvas diagram (changes with the zoom level).
  • Right: Crate Contents list (items in the active crate) + collapsible side panels: Quick Insert · Release Info · Crate Info · Rack Info.

The 4 zoom levels are ONE canvas that re-renders:

  • Store — top-down room: racks at their positions, doors/portals, compass. Click a rack → Rack.
  • Rack — one level of the selected rack (prev/next level), crates laid in slots with an orientation arrow + genre label + slot number, BACK/FRONT/L/R around the rack. Click a crate → Crate.
  • Crate — the crate's slot grid (rows×cols from crate_type) + Crate Contents (items by slot_number).
  • Item — individual slot/record detail.

2. Data model — ALREADY in RecordGod (migrated verbatim by migrate_virtual.py)

Everything below is already in recordgod-db; the work is API + front-end, not schema.

table the bits that matter
virtual_space room_width, room_depth, is_default, visible — the room
virtual_rack_type width, height, depth, panel flags, material; levels via virtual_rack_type_level
virtual_rack_type_level per-type level rows (level index, shelf height/thickness) — defines how many levels a rack has
virtual_rack pos_x/pos_y/pos_z, rotation_x/y/z, space_id, rack_type_id, direction (front/back/left/right), genre_ids, style_ids, attach_wall, name
virtual_crate_type width/height/depth, record_rows, record_columns (the slot grid), material
virtual_crate rack_id, rack_level_index, crate_type_id, pos_x/y/z (relative to rack origin, m), direction (forward/back/left/right) = the crate's facing, rotation_x/y/z (all 0 in data — IGNORE for facing), layer (upper/lower/floor), name
inventory crate_id + slot_number — each record's home crate + its slot

Hierarchy: Space → Racks(positioned) → Levels(rack_level_index) → Crates(in slots, oriented) → Items(slot_number).

Genre/style: virtual_rack.genre_ids/style_ids are comma-sep id lists; crate-level tags via a tag table (WowPlatter wowplatter_get_crate_tags). Confirm the migrated tag table name in P2.


3. The geometry, decoded (the tricky x/y/z)

Units. Rack width/depth are cm (/100 = metres, /200 = half). Crate dims + crate pos_* are metres. Rack pos_x/pos_z are room metres.

Facing composes — the slippery bit (confirmed in data 2026-06-22). There are TWO independent direction columns and they STACK:

  • virtual_rack.directionfront/back/left/right — which way the whole rack faces in the room.
  • virtual_crate.directionforward/back/left/right — which way THAT crate faces (data: forward 59 / back 57 / left 114 / right 122). This is the facing field — NOT rotation_y (it's 0.0 on every row).
  • A crate's world facing = rack.directioncrate.direction. In the rack-level (front-on) view the orientation arrow is the crate's OWN direction in the rack frame; in the store/3D world view you must compose it with the rack's direction or the arrows point the wrong way. Never read one without the other.
  • Vocab differs on purpose: crate says forward, rack says front — don't treat the strings as interchangeable.

Render convention (copied from production main.js, do not reinvent):

  • directionYaw(dir) → radians: forward/front=0, back=π, left=π/2, right=−π/2. (north/east/south/west alias n=0/e=−π/2-ish via the same table.)
  • Crate placement in the rack-level view is by slot_number, not pos_x. Grid: cell=0.34m, cols=floor(rackW/0.34), rows=round(rackD/0.34), slot is 1-indexed row-major; posX=-rackW/2 + cellX/2 + col*cellX, posZ=-rackD/2 + cellZ/2 + row*cellZ. Fall back to pos_x/pos_z only if no slot_number.
  • Crate rect yaw = directionYaw(crate.direction) + crate.rotation_y (rotation_y≈0, so direction drives it).
  • View-rotation (the rack-rotate button, 0/90/180/270) composes on top: rotatePoint = 90:(-z,x) · 180:(-x,-z) · 270:(z,-x), effective W/D swap at 90/270.
  • Facing arrow = rotatedArrows[viewRot][crate.direction], where rotatedArrows[0] = {forward:↓, back:↑, left:←, right:→} and 90/180/270 cycle it. The arrow (not the square) is what the eye reads.
  • Edge labels at viewRot 0: BACK top, FRONT bottom, L left, R right — rotate with the view.
  • World facing (store/3D view) = directionYaw(rack.direction) + directionYaw(crate.direction) + crate.rotation_y. In the drilldown you're head-on to the rack, so only crate.direction + view-rotation apply.

Store view (renderStoreView, diagram.js):

  1. bounds over all racks in the active space (pos ± half), createWorldToCanvas(min,max,pad) → fit+centre.
  2. each rack = 4 corners {±halfW,±halfD}rotatePoint(yaw)+pos → toCanvas → filled rotated rect + name.
  3. portals = door-span line + facing triangle at (x/roomW·W, z/roomD·H), facing_deg; orange=paired, red=unpaired.
  4. compass top-right. Hit-test by polygon (pointInPolygon) — crates first, then racks.

Rack view (renderShelfFrontView / renderRackDrilldown): show one rack_level_index at a time. Crates on that level are ordered into slots along the rack width (virtual_crate.pos_x ascending); each slot draws the crate square with: crate id, slot number (sequential), genre label, and an orientation arrow derived from the crate's direction/rotation_y (← → ↑ ↓ = which way the spine faces). BACK/FRONT and L/R labels come from the rack's direction. Level prev/next steps rack_level_index.

Crate view (renderCrateLookupDiagram): a record_rows × record_columns grid (from crate_type); items placed by slot_number; empties dimmed. Crate Contents list = inventory WHERE crate_id=? ordered by slot_number.


4. API surface (RecordGod routes ← WowPlatter ajax actions)

All admin-gated, JSON, under /nav/* (new navigator_routes.py). Read side first, then writes.

RecordGod route replaces ajax returns / does
GET /nav/spaces get spaces the room list for the selector
GET /nav/store-layout?space_id= get_store_layout racks (+pos/rot/type/dims) + portals for the store view
GET /nav/rack/{id} get_rack_details rack + its levels + crates-per-level (+pos/slot/orientation)
GET /nav/crate/{id} get_crate_details + get_crate_contents crate info (name/label/rack/level/last-scanned) + items by slot
GET /nav/crate/{id}/breakdown get_crate_content_breakdown/analytics genre/style mix, counts
GET /nav/genres GET /nav/styles get_all_genres/styles tag pickers
POST /nav/crate/{id} save crate rename / relabel / set genres+styles
POST /nav/rack/{id} save rack rename / set genres+styles
POST /nav/scan scan-to-slot assign Release IDs/SKUs to a crate's slots sequentially (mode: replace/append)
POST /nav/crate/{id}/compress compress_crate_slots close gaps in slot numbering
POST /nav/compress-all compress_all_crate_slots compress every crate
POST /nav/reorg apply_crate_reorganization move/merge crates between racks/levels; distribute items across slots/crates
GET /nav/collections get_collections collection boxes (ties into the Discogs collection-box pricing flow)
GET /nav/stock-finder?q= stock finder locate any item → its crate/slot (this is today's /search logic, kept)

inventory writes (crate_id/slot_number) reuse the non-destructive UPSERT rule — never clobber sold/hand-edited rows.


5. Front-end

New site/navigator.html (light theme) — replaces /search, keeps the same nav.js top bar. One <canvas> + state.js-style module: { view, spaceId, rackId, levelIndex, crateId }. Port the four renderers from main.js/diagram.js (store / rack-level-slots / crate-grid / item), the polygon hit-test, level prev/next, rack-rotate, and the side panels (Crate Info / Rack Info editors, Release Info, Quick Insert). Workflows: scanner scan-to-slot, returns, reorganize (move/merge/distribute), compress, stock finder. release_id stays the universal key.


6. Phases (ship-thin, each usable)

  • P0 — store view. /nav/store-layout + the top-down room render (racks at real pos/rot, click a rack). Upgrades today's flat /search map. Zero risk, immediate value.
  • P1 — rack drill-down + levels + slots. /nav/rack/{id}; render one level, prev/next, slots with orientation arrows + genre labels. This is the tricky-geometry phase — do it carefully.
  • P2 — crate view + info editors. contents by slot, Crate/Rack Info panels (name/label/genre/style/last-scanned), compress slots.
  • P3 — Scanner. active crate → scan Release IDs/SKUs → sequential slot assign (replace/append), Test/Process/Clear.
  • P4 — Reorganize + Returns. move/merge crates, distribute items across slots; returns back to home crate.
  • P5 — Collections + Stock Finder + polish. collection boxes, item view, rack-rotate, the "dope" pass.

Start P0→P1: that already gives a clickable store → rack → level → crate locate, which is the daily driver.


7. Open checks before building

  1. Confirm virtual_rack_type_level + the crate-tag table actually came across in the migration (column names).
  2. Confirm the slot-ordering rule WowPlatter uses (pos_x ascending vs an explicit slot_number on the crate).
  3. Crate facing field / composition RESOLVED 2026-06-22 — full render convention captured in §3 ("Render convention"): directionYaw table, slot→grid placement, view-rotation matrix, the rotatedArrows facing table, and the world-facing composition. P1 copies it verbatim.