# CLAUDE.md — Electronics Inventory Agent orientation file. Read this before touching code. --- ## What this project is A self-hosted electronics inventory web app. Tracks components, PDFs/datasheets, physical storage grids (photographed and de-perspectived), and bins. Single-user, no auth. Node.js + Express 5 backend, vanilla JS SPA frontend, flat NDJSON key-value store for persistence. --- ## File map ``` server.mjs Entry point. All Express routes. ~940 lines. lib/ storage.mjs KV store CRUD wrappers (one section per entity type). Read this to understand what data exists and its prefix. kv-store.mjs Flat NDJSON key-value store (Simple_KeyValue_Store class). Auto-loads on construct, auto-flushes with debounce. grid-image.mjs Image processing: de-perspective a photo into a grid of cell images using sharp. compute_bin_size() lives here. ids.mjs generate_id() — timestamp-base36 + random suffix. (Planned: migrate to sequential integers.) public/ app.mjs SPA. All rendering, routing, dialog logic. ~2600 lines. Sections separated by // --- comments. lib/api.mjs All fetch wrappers. Read this for the full API surface. lib/dom.mjs Tiny DOM helpers: qs(), clone(), show(), hide(). views/grid-setup.mjs Grid_Setup class — canvas corner editor with pan/zoom. Used for both grid source images and bin corner editing. templates.html All HTML templates (id="t-*"). Injected into body at init. style.css All styles. Single file (planned: split per section). index.html Shell. Loads app.mjs as module. Nav buttons hardcoded. tools/ mv-sync.c / mv-sync renameat2(RENAME_NOREPLACE) binary for atomic rename without overwrite. Used by settle_image_filename(). Makefile Builds mv-sync. data/ inventory.ndjson The database. All entities in one flat KV file. images/ All uploaded and processed images. pdfs/ Uploaded PDF files. thumbs/ PDF thumbnails (generated by pdftoppm). ``` --- ## KV store — key prefixes | Prefix | Entity | Storage function family | |---------|---------------------|---------------------------| | `f:` | Field definitions | get_field / set_field | | `c:` | Components | get_component / set_component | | `i:` | Inventory entries | get_inventory_entry / set_inventory_entry | | `d:` | Grid drafts | get_grid_draft / set_grid_draft | | `s:` | Source images | get_source_image / add_source_image | | `g:` | Grid images | get_grid_image / set_grid_image | | `ct:` | Component templates | get_component_template / set_component_template | | `pdf:` | PDF files | get_pdf / set_pdf | | `bt:` | Bin types | get_bin_type / set_bin_type | | `bin:` | Bins | get_bin / set_bin | All `list_*()` functions do a full-scan `startsWith(prefix)` over the store. --- ## Data model shapes ### Field definition (`f:`) ```js { id: string, // generate_id() name: string, // e.g. 'resistance' unit: string, // e.g. 'Ω' — optional description: string, created_at: number, // ms timestamp } ``` ### Component (`c:`) ```js { id: string, name: string, description: string, fields: { [field_id]: string }, // values keyed by field definition id images: string[], // filenames in data/images/ file_ids: string[], // linked PDF ids created_at: number, updated_at: number, } ``` ### Inventory entry (`i:`) ```js { id: string, component_id: string, location_type: 'physical' | 'bom' | 'digital' | 'grid', location_ref: string, // free text for physical/bom/digital quantity: string, notes: string, grid_id: string | null, // set when location_type === 'grid' grid_row: number | null, grid_col: number | null, images: string[], created_at: number, updated_at: number, } ``` ### Source image (`s:`) ```js { id: string, // filename in data/images/ (used as key too) original_name: string, width: number, height: number, uses: ('grid' | 'bin')[], // which features reference this image created_at: number, } ``` ### Grid draft (`d:`) ```js { id: string, source_id: string, // source image filename rows: number, cols: number, corners: [{x,y}, {x,y}, {x,y}, {x,y}], // TL, TR, BR, BL in image coords created_at: number, updated_at: number, } ``` ### Grid image (`g:`) — result of processing a grid draft ```js { id: string, source_id: string, rows: number, cols: number, corners: [{x,y}, ...], panels: [[{ filename, component_id?, notes? }, ...], ...], // [row][col] created_at: number, updated_at: number, } ``` ### Component template (`ct:`) ```js { id: string, name: string, formatter: string, // JS function body string, compiled at runtime created_at: number, updated_at: number, } ``` ### PDF (`pdf:`) ```js { id: string, display_name: string, filename: string, // in data/pdfs/ thumb_prefix: string, // in data/thumbs/ — pdftoppm output prefix created_at: number, } ``` ### Bin type (`bt:`) ```js { id: string, name: string, phys_w: number, // mm phys_h: number, // mm description: string, fields: { [field_id]: string }, created_at: number, updated_at: number, } ``` ### Bin (`bin:`) ```js { id: string, name: string, type_id: string | null, // ref to bin type source_id: string, // source image filename (always kept) source_w: number, source_h: number, corners: [{x,y}, {x,y}, {x,y}, {x,y}], // TL, TR, BR, BL in image coords phys_w: number | null, // mm — null means infer from corners phys_h: number | null, image_filename: string | null, // processed output in data/images/; null if not yet processed bin_w: number | null, // px dimensions of processed output bin_h: number | null, fields: { [field_id]: string }, contents: [ // embedded content items { id: string, type: 'component' | 'item', component_id: string | null, // set when type === 'component' name: string | null, // set when type === 'item' quantity: string, notes: string, created_at: number, } ], created_at: number, updated_at: number, } ``` --- ## API routes (server.mjs) All responses: `{ ok: true, ...data }` or `{ ok: false, error: string }`. ``` GET /api/fields POST /api/fields body: { name, unit?, description? } PUT /api/fields/:id DELETE /api/fields/:id GET /api/components POST /api/components body: { name, description?, fields? } GET /api/components/:id PUT /api/components/:id body: { name?, description?, fields?, file_ids? } DELETE /api/components/:id POST /api/components/:id/images multipart: images[] DELETE /api/components/:id/images/:img_id GET /api/inventory POST /api/inventory body: { component_id, location_type, ... } PUT /api/inventory/:id DELETE /api/inventory/:id POST /api/inventory/:id/images multipart: images[] DELETE /api/inventory/:id/images/:img_id GET /api/grid-drafts POST /api/grid-drafts body: { source_id, rows, cols } PUT /api/grid-drafts/:id DELETE /api/grid-drafts/:id GET /api/source-images POST /api/source-images multipart: images[] — creates source records (uses: ['grid']) PUT /api/source-images/:id body: { uses } DELETE /api/source-images/:id guarded: refused if any grid/bin still references it GET /api/grid-images GET /api/grid-images/:id POST /api/grid-images body: { draft_id } — processes draft → grid image PUT /api/grid-images/:id/panels/:pi body: { component_id?, notes? } DELETE /api/grid-images/:id GET /api/component-templates POST /api/component-templates body: { name, formatter } PUT /api/component-templates/:id DELETE /api/component-templates/:id GET /api/pdfs POST /api/pdfs multipart: file, display_name, filename PUT /api/pdfs/:id body: { display_name, filename } DELETE /api/pdfs/:id guarded: refused if any component references it GET /api/bin-types POST /api/bin-types body: { name, phys_w, phys_h, description?, fields? } PUT /api/bin-types/:id DELETE /api/bin-types/:id guarded: refused if any bin references it GET /api/bins GET /api/bins/:id POST /api/bins multipart: image, name?, type_id? — upload + create POST /api/bins/from-source body: { source_id, name?, type_id? } PUT /api/bins/:id body: { name?, type_id?, fields? } PUT /api/bins/:id/corners body: { corners, phys_w?, phys_h? } — triggers reprocess DELETE /api/bins/:id only deletes processed image_filename, not source POST /api/bins/:id/contents body: { type, component_id?, name?, quantity, notes } PUT /api/bins/:id/contents/:cid body: { quantity?, notes?, name? } DELETE /api/bins/:id/contents/:cid POST /api/maintenance/purge-missing-sources removes source KV entries whose files are gone POST /api/maintenance/pdf-thumbs regenerates missing PDF thumbnails ``` --- ## Frontend (app.mjs) structure Module-level state variables at the top (`all_components`, `all_fields`, etc.). All loaded once at startup via parallel API calls, mutated in place on changes. **Key patterns:** - `clone('t-template-id')` — clones a `