From 9457838bea40835d03734cbb33be9af27c644952 Mon Sep 17 00:00:00 2001 From: Eric Harding Date: Sat, 11 Jul 2026 18:18:10 -0400 Subject: [PATCH] Add design doc --- DESIGN.md | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..7446017 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,228 @@ +# Harding's Clocks — Repair Tracker Design Doc + +## 1. Purpose + +A dead-simple web app for a one-person clock repair shop to: + +- Track customers and the clocks they bring in +- Open service orders, log work performed, and list parts +- Generate a printable invoice matching the existing paper form + +Design principle: **KISS**. One user (Dad), one machine, one job at a time. No accounts, no multi-tenancy, no plugin system, no build ceremony beyond what Vite needs. + +## 2. Non-Goals + +- No authentication in the app (handled by Caddy — see §9) +- No customer-facing portal +- No payment processing (just record what was paid) +- No job templates — a fixed `job_type` field instead +- No inventory management — parts are free-text lines on an order +- No email/SMS, no reporting dashboards (can add later) + +## 3. Tech Stack + +| Layer | Choice | Why | +|---------------|---------------------------|--------------------------------------------------| +| Database | SQLite (`better-sqlite3`) | Single file, zero-config, synchronous API | +| Runtime | Node.js (LTS) | Familiar, boring, reliable | +| Language | TypeScript | Type safety on the data model without heaviness | +| HTTP server | Express (or Fastify) | Minimal routing; serve HTML + handle form posts | +| Frontend | HTMX + server-rendered HTML | No SPA. Server returns HTML fragments. | +| Templating | Nunjucks (or EJS) | Plain HTML templates, easy to eyeball | +| Build/dev | Vite | Fast dev server + asset bundling for the bit of JS/CSS | +| Reverse proxy | Caddy | TLS + basic-auth in front | + +**Why HTMX, not React:** the whole app is forms and lists. HTMX lets the server +return HTML fragments on `hx-post`/`hx-get`, so there's no client state to manage +and no API/JSON layer to maintain. TypeScript stays on the server where the logic is. + +## 4. Data Model + +Five tables plus one view. Money stored as **INTEGER cents** to avoid float rounding. +Dates stored as ISO-8601 TEXT (`YYYY-MM-DD` or full datetime). + +```sql +CREATE TABLE customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address_line1 TEXT, + address_line2 TEXT, + city TEXT, + state TEXT, + zip TEXT, + phone TEXT, + alt_phone TEXT, + email TEXT, + created_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE clocks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id INTEGER NOT NULL REFERENCES customers(id), + manufacturer TEXT, + model TEXT, + serial_number TEXT, + alternate_id TEXT, + reference TEXT, + description TEXT, + created_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE service_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_number INTEGER UNIQUE, + customer_id INTEGER NOT NULL REFERENCES customers(id), + clock_id INTEGER REFERENCES clocks(id), + job_type TEXT CHECK (job_type IN ( + '31 DAY','CLEANING','ESTIMATE','FIT UP/REPLACE', + 'HOUSE CALL','MOVE CLOCK','OVERHAUL','SERVICE', + 'WALK IN SERVICE')), + service_rep TEXT, + technician TEXT, + status TEXT DEFAULT 'OPEN', -- OPEN / IN PROGRESS / DONE / PAID + date_in TEXT DEFAULT (date('now')), + date_out TEXT, + due_date TEXT, + cust_po TEXT, + terms TEXT, + -- money summary (cents) + tax INTEGER DEFAULT 0, + shipping INTEGER DEFAULT 0, + pickup_delivery INTEGER DEFAULT 0, + service_call INTEGER DEFAULT 0, + disposal INTEGER DEFAULT 0, + amount_paid INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE line_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + service_order_id INTEGER NOT NULL REFERENCES service_orders(id) ON DELETE CASCADE, + kind TEXT NOT NULL DEFAULT 'labor', -- 'labor' | 'part' | 'sublet' + stock_code TEXT, + description TEXT, + quantity REAL DEFAULT 1, + unit_price INTEGER DEFAULT 0, -- cents + sort_order INTEGER DEFAULT 0 +); + +CREATE TABLE work_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + service_order_id INTEGER NOT NULL REFERENCES service_orders(id) ON DELETE CASCADE, + note TEXT NOT NULL, + logged_at TEXT DEFAULT (datetime('now')) +); + +CREATE VIEW order_totals AS +SELECT + so.id AS service_order_id, + COALESCE(SUM(CASE WHEN li.kind='part' THEN li.quantity*li.unit_price END),0) AS total_parts, + COALESCE(SUM(CASE WHEN li.kind='labor' THEN li.quantity*li.unit_price END),0) AS total_labor, + COALESCE(SUM(CASE WHEN li.kind='sublet' THEN li.quantity*li.unit_price END),0) AS total_sublet, + COALESCE(SUM(li.quantity*li.unit_price),0) + + so.tax + so.shipping + so.pickup_delivery + so.service_call + so.disposal AS grand_total +FROM service_orders so +LEFT JOIN line_items li ON li.service_order_id = so.id +GROUP BY so.id; +``` + +**Order numbers:** `order_number` starts at 8247 (next after the sample) and +increments. Simplest approach: `MAX(order_number)+1` at insert time inside a +transaction. `better-sqlite3` is synchronous so there's no race on a single-user app. + +## 5. Application Structure + +``` +clock-tracker/ +├─ src/ +│ ├─ server.ts # Express app, route wiring +│ ├─ db.ts # better-sqlite3 connection + prepared statements +│ ├─ schema.sql # the DDL above (run on first boot if db missing) +│ ├─ money.ts # cents <-> "$1.75" helpers +│ ├─ routes/ +│ │ ├─ customers.ts +│ │ ├─ clocks.ts +│ │ ├─ orders.ts # create/edit order, add line items, work log +│ │ └─ invoice.ts # printable invoice view +│ └─ views/ # Nunjucks templates + HTMX fragments +│ ├─ layout.njk +│ ├─ orders/ +│ │ ├─ list.njk +│ │ ├─ detail.njk +│ │ └─ _line_item_row.njk # fragment returned to HTMX +│ └─ invoice.njk +├─ public/ # CSS, tiny JS, htmx.min.js +├─ data/app.db # the SQLite file (gitignored) +├─ vite.config.ts +├─ tsconfig.json +└─ package.json +``` + +## 6. Routes + +| Method | Path | Returns | Purpose | +|--------|-----------------------------------|------------------|----------------------------------| +| GET | `/` | full page | Order list (open orders first) | +| GET | `/orders/new` | full page | New-order form | +| POST | `/orders` | redirect | Create order + customer/clock | +| GET | `/orders/:id` | full page | Order detail (work + parts) | +| POST | `/orders/:id/line-items` | HTML fragment | Add a labor/part line (HTMX) | +| DELETE | `/orders/:id/line-items/:lineId` | empty/200 | Remove a line (HTMX) | +| POST | `/orders/:id/work-log` | HTML fragment | Add a work-log note (HTMX) | +| POST | `/orders/:id` | HTML fragment | Update status/dates/totals | +| GET | `/orders/:id/invoice` | full page | Printable invoice | +| GET | `/customers` | full page | Customer list/search | + +HTMX pattern: adding a part line does `hx-post="/orders/42/line-items"` with +`hx-target="#lines" hx-swap="beforeend"`; the server returns just the `` for +the new line plus an out-of-band swap updating the totals box. + +## 7. The Invoice View + +`GET /orders/:id/invoice` renders `invoice.njk` — a print-oriented HTML page +mirroring the paper layout: shop header block, Ship-To, manufacturer/clock block, +the line-item table (work performed + parts), and the totals box from +`order_totals`. A `@media print` stylesheet hides nav and fits one page. +"Print" is just the browser's print dialog → save as PDF or paper. No PDF library. + +## 8. Local Dev & Build + +- `npm run dev` — Vite dev server for assets + `tsx watch src/server.ts` for the API. (Vite proxies to the Node server, or run them side by side; keep it simple with `concurrently`.) +- `npm run build` — `vite build` for `public/` assets + `tsc` for server. +- `npm start` — `node dist/server.js`, serving built assets and listening on `127.0.0.1:3000`. +- DB bootstraps itself: on start, if `data/app.db` doesn't exist, run `schema.sql`. + +## 9. Deployment & "Auth" + +The app binds to localhost only. Caddy sits in front, terminates TLS, and does +HTTP Basic Auth so only Dad gets in. Example `Caddyfile`: + +``` +clocks.example.com { + basic_auth { + dad $2a$14$...bcrypt-hash... + } + reverse_proxy 127.0.0.1:3000 +} +``` + +Generate the hash with `caddy hash-password`. That single credential is the whole +security model — acceptable because it's one private user on a small shop tool. +The app itself never sees or stores credentials. + +## 10. Backups + +The database is one file (`data/app.db`). A nightly `cron` job copies it somewhere +safe: + +``` +sqlite3 data/app.db ".backup '/backups/app-$(date +%F).db'" +``` + +Keep ~30 days. That's the entire disaster-recovery plan, and it's enough. + +## 11. Future (explicitly deferred) + +Only if actually needed: photo attachments per clock (the software's "Images" tab), +customer search-as-you-type, a simple monthly revenue total, quoted-values/estimate +tracking. None of this is built now.