From 72897c5b2d9f3e45b70b185cb0b2c5237e79f51c Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Fri, 3 Apr 2026 13:18:12 +0000 Subject: [PATCH] =?UTF-8?q?Add=20CLAUDE.md=20=E2=80=94=20agent=20orientati?= =?UTF-8?q?on=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers: file map, KV prefix table, all data model shapes, full API route index, frontend section layout, image file lifecycle, code style preferences, git conventions, and a pointer to future-plans.md. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 392 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f886483 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,392 @@ +# 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 `