# Inventory SKU Lookup — Design Spec **Date:** 2026-04-12 ## Overview When the popup opens on a Discogs release page and the feature is enabled, it SSHes (via Tailscale) to the inventory MariaDB, looks up SKUs for the current `release_id`, and auto-fills the SKU custom collection field(s) on the Discogs page. If there is a row/box count mismatch, a picker UI lets the user manually assign each SKU to the correct collection box. --- ## Database Schema Table: `wp_rmp_disc_inventory` (on MariaDB at `100.123.123.64`) | Column | Type | Notes | |---|---|---| | `sku` | varchar(255) PK | Timestamp-format integer e.g. `20250515100402` — lower = older | | `release_id` | int(11) INDEX | Discogs release ID | | `media_condition` | text nullable | | | `sleeve_condition` | text nullable | | | `price` | decimal(10,2) | May drift vs Discogs — display only, never written to Discogs | Query: ```sql SELECT sku, price, media_condition, sleeve_condition FROM wp_rmp_disc_inventory WHERE release_id = ? ORDER BY sku ASC ``` --- ## Architecture ``` Discogs page (content.js) ↕ sendMessage Popup (popup.js) ↕ fetch localhost:7790 rfid-daemon (index.js) ──ssh2──▶ mrpadmin@100.123.123.64 ──mysql2─▶ MariaDB 127.0.0.1:3306 (via tunnel) ``` The existing `updateDiscogsSku` message (`chrome.tabs.sendMessage`) already handles writing a SKU value to a specific collection box index — no changes needed to `content.js`. `state.collectionBoxCount` and `state.collectionBoxSkus` are already tracked in popup.js and populated when the popup loads on a Discogs release page. **Distinct from existing MONSTERWIKI inventory import (`handleImportInventory` / INV button):** that feature talks to the Python server on `100.91.239.7:5002` (PostgreSQL, different dataset) and is triggered manually. This new feature is auto-triggered on popup open, talks to the WordPress MariaDB at `100.123.123.64`, and only writes SKU (not price). --- ## 1. Settings UI (Connect section, popup.html) Add a new collapsible sub-section "Inventory SKU Lookup" inside the existing Connect settings block. **Fields:** | Field | Element ID | Default | |---|---|---| | Enable toggle | `inventorySkuEnabled` (checkbox) | unchecked | | DB Host | `inventory-db-host` | `100.123.123.64` | | DB Name | `inventory-db-name` | `wp_rmp_disc_` | | DB User | `inventory-db-user` | *(empty)* | | DB Password | `inventory-db-pass` (type=password) | *(empty)* | | SSH Key Path | `inventory-ssh-key` | `~/.ssh/id_rsa` | Saved and loaded by the existing "Save Connect Settings" button alongside Discogs token, Google settings, etc. Values stored in `chrome.storage.local` under key `inventorySettings`. --- ## 2. rfid-daemon: `/inventory-lookup` endpoint ### New npm dependencies - `ssh2` — SSH tunnel - `mysql2` — MariaDB client (promise API) ### Endpoint ``` GET /inventory-lookup?release_id=&dbHost=&dbName=&dbUser=&dbPass=&sshKeyPath= ``` ### Behaviour 1. Read SSH private key from `sshKeyPath` on disk (resolved from `~` to `os.homedir()`) 2. Open SSH connection to `mrpadmin@dbHost` using the private key 3. Forward a random local port → `127.0.0.1:3306` on the remote host via `ssh2` `forwardOut` 4. Connect `mysql2` to `127.0.0.1:` with `dbUser`, `dbPass`, `dbName` 5. Execute query with `release_id` as the bound parameter 6. Close tunnel and DB connection 7. Return: ```json { "rows": [ { "sku": "20250515100402", "price": "12.00", "media_condition": "VG+", "sleeve_condition": "VG" } ] } ``` Errors return `{ "error": "" }` with HTTP 500. The tunnel is opened fresh per request (no persistent connection) — requests are infrequent (one per popup open). --- ## 3. Popup logic (popup.js) ### Trigger Runs automatically when the popup finishes loading on a Discogs release page (`state.discogsTabId` is set and `state.releaseData.id` exists) and `inventorySettings.enabled` is `true`. ### Flow ``` load inventorySettings from chrome.storage.local if not enabled → stop GET /inventory-lookup?release_id=&...creds... if error → show error message in popup, stop count rows = result.rows.length count boxes = number of collection boxes on page (state.collectionBoxCount or equivalent) if rows == 0 → silent, stop if rows == 1 → writeSkuToBox(rows[0].sku, state.collectionBoxCount - 1) [silent, last box] if rows == boxes → for each i: writeSkuToBox(rows[i].sku, i) [silent, earliest SKU → first box] else → show mismatch picker UI ``` ### writeSkuToBox(sku, boxIndex) Calls `chrome.tabs.sendMessage(state.discogsTabId, { action: 'updateDiscogsSku', sku, boxIndex })` — existing content.js handler, no changes needed. --- ## 4. Mismatch Picker UI Rendered inside the popup when `rows ≠ boxes` (and not the 1-row case). **Layout:** ``` ⚠ SKU mismatch: 2 DB rows, 3 collection boxes [ 20250515100402 · $12.00 · VG+ · VG ] ← clickable card [ 20260101090000 · $18.50 · NM · NM ] ← clickable card Write selected to box: [1] [2] [3] ``` **Interaction:** - Clicking a card selects it (highlighted border/background), deselects others - The box number buttons are always visible (one per collection box, labelled 1-based) - Clicking a box number button calls `writeSkuToBox(selectedSku, boxIndex)` for the selected card - After writing, that card gets a small checkmark indicator; user can continue selecting and writing other rows - No card selected + box button click → no-op (button visually disabled until a card is selected) --- ## 5. Error / Status Display A small status line is added to the Discogs tab in the popup (below existing content) to show: - Nothing (default) - `"SKU mismatch: N rows, M boxes"` — triggers picker - `"Inventory lookup failed: "` — SSH/DB error - `"SKUs filled"` — after successful auto-fill (fades after 3s) --- ## Out of Scope - Writing price to Discogs (price may drift — display only in picker) - Caching DB results between popup opens - Handling multiple Discogs tabs simultaneously