Files
2026-07-13 07:50:57 -04:00

126 lines
5.9 KiB
Markdown

# 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` bind to `127.0.0.1:3000` by default;
override with the `HOST`/`PORT` env vars (the `Containerfile` sets
`HOST=0.0.0.0` so container port mapping works). The SQLite file lives at
`data/app.db` (relative to `process.cwd()`, gitignored) and bootstraps itself
from `schema.sql` on first run if missing.
## Container deployment
`Containerfile` is a two-stage build (compile w/ native toolchain → slim
alpine runtime). It runs as a non-root `app` user and expects `data/app.db`
to persist via a bind-mounted host folder at `/app/data`:
```
docker build -t hardingsclocks .
docker run -d -p 3000:3000 -v /path/on/host/data:/app/data hardingsclocks
```
Caddy (or whatever's doing auth/TLS per DESIGN.md §9) should reverse-proxy to
the mapped `3000` port on the host.
## 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`.