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:
392
CLAUDE.md
Normal file
392
CLAUDE.md
Normal 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):**
|
||||
```
|
||||
1–90 Imports, state vars, startup data load
|
||||
91–250 Helper functions (field rendering, search, formatters)
|
||||
251–700 Components section (list, detail panel, edit dialog)
|
||||
701–1000 Inventory section
|
||||
1001–1600 Grids section (list, draft editor, grid viewer)
|
||||
1601–1900 Component templates, field definitions dialogs
|
||||
1901–2000 Images admin section
|
||||
2001–2450 Bins section (list, source list, types list, bin editor)
|
||||
2450–2500 Routing (parse_url, navigate, render)
|
||||
2500–end 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
|
||||
Reference in New Issue
Block a user