stage 1
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user