Files
Hardings-service/CLAUDE.md
T

5.3 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.

Verifying UI changes

Don't spin up Playwright/chromium-cli/headless-browser automation to verify frontend changes in this repo. Make the code change, explain what you did and why it should work, and ask the user (Dad's the only one who runs this app) to check it in their own browser.

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.