This commit is contained in:
jason
2026-04-20 15:49:01 -05:00
parent 381a31d607
commit b98837a72c
46 changed files with 8883 additions and 37 deletions
+103
View File
@@ -0,0 +1,103 @@
# Architecture
## Runtime
- **Single container.** Node 20 (alpine) running Next.js in production mode. One process serves both the UI and the REST API.
- **Single volume (`/data`).** SQLite database (`app.db`), user uploads (`uploads/`), and backups (`backups/`) live side-by-side so the whole application state is one directory to back up.
- **Stateless app, stateful volume.** Nothing in the image needs to survive a rebuild except the schema, which is applied to `/data/app.db` at container start via `prisma migrate deploy`.
- **No background workers yet.** All work happens inline on a request. PDF generation and STEP thumbnailing will stay request-scoped until throughput forces otherwise.
## Request flow
```
Browser ──TLS── Reverse proxy ── HTTP ── Next.js (app+api) ── Prisma ── SQLite
└─► /data/uploads
```
- Reverse proxy terminates TLS (handled by Unraid SWAG / NPM / Caddy / Traefik etc.).
- The container listens on `:3000` with `HOSTNAME=0.0.0.0`.
- Health probe: `GET /api/health` runs `SELECT 1` against SQLite.
## Authentication
Two roles, no external identity providers.
### Admin
- Email + password. Password hashed with bcrypt (cost 12).
- Longer session default (`ADMIN_SESSION_HOURS`, default 8h).
### Operator
- Name + 4-digit PIN. PIN hashed with bcrypt (cost 12).
- Device session default 12h (`OPERATOR_SESSION_HOURS`).
- 5 failed attempts lock the account for 15 minutes (configurable).
- The operator login UI is a tile grid of active operator names, followed by a numeric keypad; no email/username typing on a phone.
### Sessions
- Server-side, backed by the `Session` table.
- Cookie `mrp_session` holds a random 32-byte token (base64url). Only the SHA-256 of the token is stored server-side, so a DB leak cannot hand out live sessions.
- `httpOnly`, `sameSite=lax`, `secure` in production, path `/`, `expires` matched to DB TTL.
- `lastSeenAt` is touched at most once per minute to avoid write amplification.
- `purgeExpiredSessions()` is exposed for a future cron.
## Data model
See [`prisma/schema.prisma`](../prisma/schema.prisma) for the canonical definition. Highlights:
- **Project → Assembly → Part → Operation.** `@@unique([assemblyId, code])` and `@@unique([partId, sequence])` keep the hierarchy tidy.
- **Operation.claimedByUserId** enforces single-claim at the app layer; the status/claim fields are indexed for dashboard queries.
- **TimeLog** is separate from Operation so a step can have multiple start/stop sessions over time.
- **QCRecord** supports both `inline` (checkbox on the step card) and `dedicated` (a QC operation type) via its `kind` field.
- **FileAsset** is a single table for all uploads; parts and purchase orders reference by nullable foreign key.
- **AuditLog** captures mutations with before/after snapshots as JSON strings (SQLite does not have a native Json type).
Enums are stored as strings rather than Prisma enums because SQLite has no enum support; validation is done at the Zod layer.
## API
- Planned surface: `/api/v1/*` (REST + JSON). The auth, operator list, and health endpoints in the current commit are the internal surface and will move under `/api/v1` when step 10 formalizes the public API.
- Every mutation writes an `AuditLog` row with actor, IP, and before/after payloads.
- Zod schemas validate request bodies; they will also drive OpenAPI generation in step 10.
## Front-end structure
- `app/login/*` — public login pages (chooser, admin form, operator tile grid + keypad).
- `app/admin/*` — admin shell (requires admin session, top-nav layout).
- `app/op/*` — operator shell (requires operator session, mobile-friendly layout).
- `app/api/*` — route handlers.
- `components/` — shared client components (initially just `LogoutButton`).
Every protected layout calls `requireAdmin()` or `requireOperator()` in its server component, which redirects unauthenticated requests to `/login`. There is intentionally no edge middleware because Prisma does not run on the Edge runtime.
## Files on disk
```
/data
├── app.db SQLite database
├── app.db-wal, app.db-shm SQLite WAL sidecars (when enabled)
├── backups/ point-in-time SQLite snapshots (future)
└── uploads/ user uploads, organized by content hash
├── step/
├── pdf/
├── dxf/
├── svg/
└── image/
```
Upload handling (step 2) will write content-addressed files: hash the incoming bytes, store at `uploads/<kind>/<sha256>.<ext>`, and reference via `FileAsset.path`. Duplicate uploads collapse to one file on disk.
## Build-time vs. run-time
- `npm run build` runs `prisma generate` and then `next build`. No database required.
- `prisma migrate deploy` runs **at container start**, not at build time, so the image is portable across environments.
- The bootstrap admin seed runs at start too, idempotently (it does nothing if an admin already exists).
## Security posture
- Passwords and PINs: bcrypt with cost 12.
- Session tokens: cryptographically random, never stored plaintext.
- Cookies: `httpOnly`, `sameSite=lax`, `secure` in prod.
- Input validation: Zod on every route.
- Audit log for every mutation and every login attempt (success or failure).
- PIN lockout: 5 attempts / 15 min (configurable). Addresses the small 4-digit search space.
- `APP_SECRET` is not yet used to sign QR tokens — that arrives in step 3. It is validated at boot so deploy-time misconfiguration fails loudly.
+28
View File
@@ -0,0 +1,28 @@
# Build Plan
The roadmap agreed at project kickoff. Each step is committed separately so the app can be deployed and used at any point in the sequence.
| # | Step | Status |
| - | ---- | ------ |
| 1 | Repo scaffold, Dockerfile, Prisma schema, auth (admin email/password, operator PIN, 12h session) | **In progress** |
| 2 | Admin CRUD: Machines, Operation Templates, Projects → Assemblies → Parts, file uploads | planned |
| 3 | Operation authoring (template or ad-hoc) + QR token generation | planned |
| 4 | Operator scan flow: claim → start → units/notes → QC prompt → close | planned |
| 5 | PDF generation: per-operation card + per-part cover sheet | planned |
| 6 | Fasteners + PO (PDF + lifecycle: draft → sent → partial → received) | planned |
| 7 | Admin dashboard (WIP, overdue, plan-vs-actual) + audit log viewer | planned |
| 8 | In-browser STEP viewer + server-side thumbnails | planned |
| 9 | QC records (inline checkboxes + dedicated QC operation type) | planned |
| 10 | OpenAPI docs at `/api/docs`, nightly SQLite backup script | planned |
## Locked design decisions
- **Hierarchy:** Project → Assembly → Part → Operation.
- **QR granularity:** one QR per Operation; each step prints its own card.
- **Claim model:** an Operation locks to one operator on Start; other scans of an in-progress operation show a read-only view noting who holds it.
- **Operators can hold multiple operations at once** (across different parts).
- **Purchase orders:** PDF generation + lifecycle states (`draft → sent → partial → received → cancelled`).
- **No offline mode.** The app assumes the shop LAN is up.
- **Language:** English only in the UI.
- **Persistence:** SQLite in a single mounted `/data` volume; migrations via Prisma.
- **Integrations:** none for now; REST API is versioned (`/api/v1/*`) and fully documented for future use.
+98
View File
@@ -0,0 +1,98 @@
# Deploying on Unraid
The MRP app is a single Docker container that stores everything (SQLite database + uploaded files + backups) under a single `/data` volume.
## 1. Prepare environment
Pick a host directory on your Unraid array (example: `/mnt/user/appdata/mrp-qrcode/`). This will hold the database and uploaded files, and will survive container upgrades.
Generate a strong app secret:
```bash
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
```
## 2. docker-compose (recommended)
Create `.env` next to `docker-compose.yml`:
```env
APP_URL=https://mrp.yourdomain.tld
APP_SECRET=<paste-the-secret-from-step-1>
BOOTSTRAP_ADMIN_EMAIL=you@yourdomain.tld
BOOTSTRAP_ADMIN_PASSWORD=<a-strong-password>
BOOTSTRAP_ADMIN_NAME=Your Name
```
Then:
```bash
docker compose up -d --build
```
The container will:
1. Create `/data/uploads` and `/data/backups` inside the volume.
2. Run `prisma migrate deploy`.
3. Create the bootstrap admin if no admin exists.
4. Start the web server on port 3000.
## 3. Bind the `/data` volume to host storage (Unraid)
If you prefer a host bind mount over the named volume, replace the `volumes:` block in `docker-compose.yml` with:
```yaml
volumes:
- /mnt/user/appdata/mrp-qrcode:/data
```
Make sure the host directory is owned by UID 1001 (the `nextjs` user inside the container):
```bash
mkdir -p /mnt/user/appdata/mrp-qrcode
chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode
```
## 4. Reverse proxy / subdomain
Point your reverse proxy (SWAG, Nginx Proxy Manager, Caddy, Traefik — whatever is already on your Unraid) at `http://<container-ip>:3000` and terminate TLS there.
`APP_URL` must match the externally reachable URL — it is embedded in QR code payloads and used for absolute links. If operators scan a card and land on `http://10.x.x.x:3000`, their phone probably cannot reach that IP; always set `APP_URL` to the public subdomain.
## 5. Backups
The container does not yet run automatic backups. Until step 9 of the build plan ships, back up `/data` with your Unraid backup strategy:
- `/data/app.db` (SQLite file)
- `/data/app.db-wal` and `/data/app.db-shm` if present (SQLite WAL sidecars)
- `/data/uploads/`
A safe way to snapshot a live SQLite DB is:
```bash
docker exec mrp-qrcode sqlite3 /data/app.db ".backup '/data/backups/app-$(date +%F).db'"
```
## 6. Upgrades
```bash
git pull
docker compose up -d --build
```
Migrations run automatically on start. Before major upgrades, snapshot the DB as above.
## 7. First-login checklist
1. Sign in at `/login/admin` with the bootstrap credentials.
2. Change your password (admin settings — shipping in a later step).
3. Create your operators (each gets a name and a 4-digit PIN).
4. Add your machines.
5. Create operation templates for repetitive steps.
6. Create your first project.
## Troubleshooting
- **`APP_SECRET must be at least 32 chars`** — the container refuses to start without one. Regenerate as shown in step 1.
- **`migrations/` is empty** — run `npx prisma migrate dev --name init` locally once, commit the generated `prisma/migrations/` directory, rebuild the image.
- **Healthcheck failing** — `docker logs mrp-qrcode` and check DB permissions on `/data`.