# 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.direction` ∈ `front/back/left/right` — which way the whole rack faces in the room. - `virtual_crate.direction` ∈ `forward/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.direction` ∘ `crate.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 `` + `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.