Add CLAUDE.md — agent orientation file

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 13:18:12 +00:00
parent e2d0079ba0
commit 72897c5b2d

392
CLAUDE.md Normal file
View File

@@ -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 `<template>` into a live element
- `qs(el, '#id')` — scoped querySelector
- Dialog callbacks stored as module-level `let x_dialog_callback = null`,
set by the open function, called by the init-registered submit/save handler
- `render()` — top-level re-render, called after navigation or data changes
- `navigate(path)` — pushes history + calls render()
- `build_field_editor(rows_el, sel_el, new_btn_el, initial_fields)` — shared
helper that wires up field row editing; returns `{ get_fields() }`
**Section layout (by line range, approximate):**
```
190 Imports, state vars, startup data load
91250 Helper functions (field rendering, search, formatters)
251700 Components section (list, detail panel, edit dialog)
7011000 Inventory section
10011600 Grids section (list, draft editor, grid viewer)
16011900 Component templates, field definitions dialogs
19012000 Images admin section
20012450 Bins section (list, source list, types list, bin editor)
24502500 Routing (parse_url, navigate, render)
2500end init() — dialog injection, event handler registration
```
---
## Image file lifecycle
```
Upload → multer writes temp file to data/images/<generate_id()><ext>
→ settle_image_filename() renames to original filename using
rename_no_replace (atomic, no overwrite); falls back to temp name
→ source image KV record created with id = final filename
Grid processing:
source image + corners + rows/cols → sharp perspective transform
→ one cell image per panel → filenames stored in grid_image.panels[r][c]
Bin processing:
source image + corners → single de-perspectived image
→ stored as bin.image_filename (separate from bin.source_id)
→ source_id is never deleted when bin is deleted; only image_filename is
```
---
## Code style (owner preferences)
- **Indentation**: tabs, display width 4
- **Braces**: always, even single-line bodies — `if (x) { return; }`
- **Naming**: `lower_snek_case` functions/locals/singletons,
`CAPITAL_SNEK_CASE` top-level constants, `Title_Snek_Case` classes
- **Quotes**: single quotes preferred
- **Modules**: `.mjs` extension always
- **State**: stateful components must be classes; no bare module-level
variables for app state (module scope is for constants and exports only)
- **IDs**: prefer sequential integers over timestamp+random (migration pending)
- **No CDN URLs** in HTML; vendor libs via make build from node_modules
- **Error handling**: try/catch ENOENT, never existsSync-then-read (TOCTOU)
- **No over-engineering**: don't add helpers, abstractions, or error handling
for scenarios that can't happen; three similar lines beats a premature helper
---
## Git conventions (this project)
```
git config user.name 'mikael-lovqvists-claude-agent'
git config user.email 'mikaels.claude.agent@efforting.tech'
```
- Small, logical commits — one concern per commit
- Stage files explicitly by name, never `git add -A` or `git add .`
- Always `git status` and read every line before staging
- Commit message: imperative mood, explain *why* not *what*
---
## Known limitations / planned rewrite notes
See [`future-plans.md`](future-plans.md) for full detail. Key points relevant
to coding decisions:
- `app.mjs` will be split into per-section view modules
- KV store will become hierarchical (no more prefix convention)
- IDs will migrate to sequential integers
- CSS will be split per section with a build-step concatenation
- SSE for live updates across devices
- Generic fields on all entity types (already done for components, bins, bin types)
- A complete rewrite is planned; this codebase is the learning prototype