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

155 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `<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.