Files
Hardings-service/CLAUDE.md
T

5.0 KiB

Harding's Clocks — Repair Tracker

A repair-order tracker for a one-person clock repair shop. Full spec lives in DESIGN.md — read it for the data model, route table, and design rationale. This file covers what's useful for day-to-day development that isn't already in the design doc.

Goals & constraints

  • One user (Dad), one machine, one job at a time. Optimize for simplicity over flexibility — this is explicitly not meant to grow into a multi-tenant or general-purpose product.
  • No client-side app framework: the server renders HTML, HTMX does partial updates. Don't introduce React/Vue/a JSON API layer for this app.
  • No auth in the app itself — that's Caddy's job in front of it (see DESIGN.md §9). Don't add login/session code here.
  • Money is always integer cents in the DB; format at the view layer via the money Nunjucks filter (src/money.ts). Never store floats for currency.

Tech stack

Express + TypeScript (ESM/NodeNext) + better-sqlite3 + Nunjucks + HTMX. Package manager is pnpm (not npm — a package-lock.json should not reappear; pnpm-lock.yaml is the one committed).

Deviation from DESIGN.md §3/§8: the doc lists Vite for asset bundling. There's no client JS/TS to bundle — just vendored htmx.min.js and one plain CSS file — so Vite was dropped as ceremony with no payoff. Express serves public/ and src/views/*.njk directly. tsc compiles the server; the build script then copies schema.sql and views/ into dist/ since tsc only touches .ts files.

pnpm + better-sqlite3 gotcha

better-sqlite3 needs a native addon compiled on install. pnpm blocks postinstall/install scripts by default for security, which leaves better_sqlite3.node missing and the app fails at runtime with a bindings error (not at pnpm install time — it only surfaces when new Database() runs). This is handled by pnpm.onlyBuiltDependencies: ["better-sqlite3"] in package.json, which allows pnpm to run its build script non-interactively. If native-binding errors show up after adding new deps, check whether they need the same treatment (pnpm approve-builds for one-off interactive approval, or add to onlyBuiltDependencies for it to be automatic).

Commands

pnpm install   # first-time setup (compiles better-sqlite3's native binding)
pnpm dev       # tsx watch src/server.ts — reads views/schema straight from src/
pnpm build     # tsc + copy schema.sql/views into dist/
pnpm start     # node dist/server.js — run this after pnpm build
pnpm backup    # scripts/backup.ts — see DESIGN.md §10

Nunjucks template changes are not hot-reloaded (chokidar isn't a dependency, to avoid the extra install); restart pnpm dev after editing .njk files.

The dev server and dist/server.js both bind to 127.0.0.1:3000. The SQLite file lives at data/app.db (gitignored) and bootstraps itself from schema.sql on first run if missing.

Structure

src/
├─ server.ts        # Express app, Nunjucks config, route mounting
├─ db.ts             # DB connection, schema bootstrap, order-number allocation
├─ money.ts          # cents <-> string helpers ("money", "lineTotal" filters)
├─ schema.sql        # DDL — source of truth for the data model
├─ routes/
│  ├─ orders.ts       # order list/detail/create/update/delete, line items, work log
│  ├─ customers.ts    # customer list/search
│  ├─ clocks.ts        # HTMX fragment: clock <option> list for a given customer
│  └─ invoice.ts       # printable invoice page
└─ views/             # Nunjucks templates; `_`-prefixed files are HTMX fragments
public/css/style.css  # all styling — design tokens live in :root, see DESIGN.md §12
scripts/backup.ts     # nightly SQLite backup via better-sqlite3's backup API

HTMX patterns used here

  • Adding/removing a line item returns the changed <tr> (or nothing, for a delete) plus an out-of-band swap of #totals-summary so the totals box stays in sync without a full page reload. See src/views/orders/_line_item_added.njk and _totals.njk (the oob flag controls whether hx-swap-oob="true" is emitted).
  • The order-summary edit form (status, dates, tax/shipping/etc.) posts to POST /orders/:id and swaps the whole #order-summary card via hx-swap="outerHTML", which naturally refreshes its embedded totals too.
  • Deleting an order uses hx-delete and an HX-Redirect response header rather than a plain 3xx, since HTMX needs that header to force a full-page navigation after an XHR delete.

Data model notes worth remembering

  • order_number starts at 8247, is allocated via MAX(order_number) + 1 in nextOrderNumber() (src/db.ts), and is never reused — gaps from soft-deleted orders are expected and fine.
  • Orders are soft-deleted (deleted_at); every query that lists/reads orders filters on deleted_at IS NULL (baked into the order_totals view too).
  • order_totals is a SQL view, not computed in application code — if you add a new money field to service_orders, update the view in schema.sql.