126 lines
5.9 KiB
Markdown
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`.
|