RECORDGOD/docs/RECORDGOD_NAVIGATOR_PLAN.md
type-two ca84b7f82f chore(docs): move 14 planning/handover docs to docs/ — declutter repo root
Root carried 15 *_PLAN/_DEEPDIVE/_AUDIT/bridge/handover docs (many describe shipped work).
git mv → docs/ (R100, no content change). README.md stays at root. Already *.md-dockerignored,
so no image impact — purely navigability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:45 +10:00

201 lines
15 KiB
Markdown
Raw Permalink 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.
**Props vs stock (important, every phase):** `virtual_rack_type.rack_purpose``stock` (real record racks)
or `prop` (3D scenery — COLUMNs, AIRCON, STATION, DOOR, couch, trolley…). The functional navigator must
**filter `rack_purpose <> 'prop'`** everywhere (store-layout already does). `type` ∈ rack/bench/shelf/'' is the
geometry kind, NOT the stock/prop distinction — don't filter on `type`.
---
## 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.
---
## 6b. Collections + the fallback location map (captured 2026-06-22)
**Data already migrated:** `virtual_collection` (14 rows). Model = a named **zone**:
`name, color, crate_ids[] (JSON, in click order), priority, sort_method` +
**filing rules** `genre_ids[] / style_ids[] / label_ids[] / format_descriptions[] / price_min / price_max /
year_min / year_max`, `is_active`. Comment in the source: *"grouping of crates with filing rules for smart
placement."* Real examples: `2000s HOUSE` = House style + price≥$15 + year 20002009 → 12 crates;
`OLD SCHOOL RAP` = year 19801986 → 1 crate; `BREAKS 15+` = 4 breaks styles + $15+ → 10 crates.
**Collections UI** (Collections tab): list (colour swatch + crate count) → editor where you **click crates on the
map and they're kept in click order** as removable chips, set name/colour/priority, `sort_method`
(alpha_artist/title, year, price, date_added), filing rules, and a live **match panel**
(`N matching · N located · N not located · N overlap`). Save → `virtual_collection`.
**THE fallback locator (John's headline ask).** `GET /nav/locate?release_id=` (or sku):
1. exact location first — if the record has `crate_id`+`slot_number`, return that.
2. else match the record's `genre/style/label/format/price/year` against **active collections, `priority` DESC**;
first hit → return its `crate_ids` as the **"should-be-here" zone** ("House $1215 → these 4 crates").
The match panel's four numbers fall out of this: *matching* = records a collection's rules select, *located* = of
those, how many actually sit in its crates, *not located* = match-but-misfiled, *overlap* = matched by >1 collection.
**One data dependency to decide:** collections match on discogs `genre_ids/style_ids` + `year`, which RecordGod's
slim `disc_cache` doesn't carry (only title/artist/thumb/weight). Options: **(a)** enrich `disc_cache` with
`genre/style/year` (backfill from `discogs_full` on ultra) so the matcher is self-contained — recommended; or
**(b)** denormalise genre/style/year onto `inventory` at intake. Price is already on `inventory`.
## 6c. Reorganize — the A-Z re-file planner (P4)
Server contract (thin; planning is client-side, copy WowPlatter's `class-wowplatter-ajax-store-reorg.php`):
- `POST /nav/reorg/scan {crate_ids[]}` → all in-stock items (sku, release_id, crate_id, slot, price, title, artist,
year, thumb), ordered crate→slot. (Sources can be crate ids/ranges OR a collection's crates.)
- **Client plans:** apply filters (price≤ / label / year-range) → pick a **distribution target** (start crate +
count + slots/crate) → **sort** by `sort_method` → fill target crates slot-by-slot → produce a move list
`[{sku, from_crate/slot, to_crate/slot}]` + leftovers. Card-by-card **walkthrough modal** (prev/next, MERGE, Undo).
- `POST /nav/reorg/apply {matched[], leftovers[]}` → ONE transaction: per sku `UPDATE inventory SET crate_id,
slot_number, location_updated_*`; leftovers → `crate_id/slot=NULL, location=archive`. Guard SKU-miss → roll back.
- Slot housekeeping: `compress_crate_slots` (close gaps), `insert_with_shift` (insert at slot, push others down).
Build order: **Collections + fallback locator first** (location-map foundation, data already there, every locate
benefits), **then Reorganize** (heavier planner UI). Both reuse the §4 `/nav/*` router.
## 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.