diff --git a/README.md b/README.md index 30c5da3..836ad6b 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ A single-container, self-hosted Manufacturing Resource Planning (MRP) app built ## Status -**Step 1 of the build plan** is in this commit: repo scaffold, Prisma schema, Docker build, and authentication (admin email + password; operator name + 4-digit PIN with 12h device session). Everything downstream — project / assembly / part CRUD, QR generation, operator scan flow, PDF travelers, fasteners & POs, dashboards, STEP viewer, QC — is planned but not yet implemented. +Steps 1 – 3 of the build plan are in this repo: -See [`docs/BUILD-PLAN.md`](docs/BUILD-PLAN.md) for the sequenced roadmap. +- **1.** Scaffold + auth (admin email/password, operator name/4-digit PIN with 12 h device session, PIN lockout, audited sessions). +- **2.** Admin CRUD (users, machines, operation templates, projects / assemblies / parts) with content-addressed STEP / PDF / DXF / SVG file uploads. +- **3.** Operation authoring with per-operation QR tokens (192-bit, base64url). + +Planned (not yet shipped): operator scan flow → PDF traveler print → fasteners & POs → dashboard → STEP viewer → QC records → OpenAPI docs + backups. See [`docs/BUILD-PLAN.md`](docs/BUILD-PLAN.md). ## Core concepts @@ -25,30 +29,61 @@ See [`docs/BUILD-PLAN.md`](docs/BUILD-PLAN.md) for the sequenced roadmap. - bcryptjs for password / PIN hashing - Zod for input validation and environment parsing +## Deployment + +The primary deployment target is **Unraid**, using an image built by a **Gitea Actions** runner on every push to `main` and pulled from the private registry at **`registry.alwisp.com`** into Unraid's Docker GUI. + +``` + push to main ─► Gitea Actions (docker-build.yml) ─► registry.alwisp.com + (docker build + push) /:latest + │ + ▼ + Unraid Docker tab ─► pull / force-update +``` + +Two deploy paths are supported; pick one: + +- **`docker pull`** — Unraid pulls a prebuilt image the Gitea runner already tagged. Fastest, this is what the runner is for. +- **`docker build`** — Unraid clones this repo and builds the image locally, no registry required. + +See [`docs/DEPLOY.md`](docs/DEPLOY.md) for the full, click-by-click Unraid GUI walkthrough (template fields, volume mapping, env vars, update flow, backups). + +### TL;DR Unraid install + +1. **Docker tab → Add Container**. +2. **Repository**: `registry.alwisp.com//:latest` (the owner/repo path matches `${{ gitea.repository }}` from the workflow). +3. **Network Type**: Bridge. **Port**: host `3000` → container `3000`. +4. **Path**: host `/mnt/user/appdata/mrp-qrcode` → container `/data`. +5. **Variables** (required): + - `APP_URL` = the public HTTPS URL your reverse proxy serves (`https://mrp.yourdomain.tld`) + - `APP_SECRET` = a ≥32-char secret (`openssl rand -base64 48`) + - `BOOTSTRAP_ADMIN_EMAIL`, `BOOTSTRAP_ADMIN_PASSWORD`, `BOOTSTRAP_ADMIN_NAME` +6. Apply. The container runs migrations, creates the bootstrap admin on first boot, and comes up on `:3000`. + ## Local development Prerequisites: Node 20+, npm. ```bash cp .env.example .env -# edit .env and set APP_SECRET to at least 32 random chars +# edit .env: set APP_SECRET to >=32 random chars npm install -npx prisma migrate dev --name init +npx prisma migrate deploy npm run db:seed # creates the bootstrap admin from .env npm run dev ``` -Visit http://localhost:3000 and sign in as the bootstrap admin. +Visit and sign in as the bootstrap admin. -## Docker / Unraid deployment +## CI: Gitea Actions -See [`docs/DEPLOY.md`](docs/DEPLOY.md). In short: +Image builds are driven by `.gitea/workflows/docker-build.yml`. On every push to `main` the runner: -```bash -docker compose up -d --build -``` +1. Logs into `registry.alwisp.com` with the `REGISTRY_USER` / `REGISTRY_TOKEN` repo secrets. +2. Runs `docker build -t registry.alwisp.com/${{ gitea.repository }}:latest .` +3. Pushes the `:latest` tag. -The container runs `prisma migrate deploy` on every start and creates a bootstrap admin on first boot if none exists. All persistent state lives in the `/data` volume (`app.db` + `uploads/` + `backups/`). +Point Unraid at `registry.alwisp.com//:latest` and use **Check for Updates / Force Update** (or the *CA Auto Update Applications* plugin) to roll new builds. ## Environment @@ -57,15 +92,16 @@ All env vars are documented in [`.env.example`](.env.example). `APP_SECRET` must ## Project layout ``` -app/ Next.js routes (UI + /api/*) +app/ Next.js routes (UI + /api/v1/*) components/ Shared React components -lib/ env, prisma, auth, session, password, audit, request helpers +lib/ env, prisma, auth, session, password, qr, audit, request helpers prisma/ schema.prisma + migrations/ scripts/ seed.ts and future ops scripts docker/ entrypoint.sh docs/ Project docs (DEPLOY, BUILD-PLAN, ARCHITECTURE) +.gitea/workflows/ Gitea Actions (docker-build.yml → registry.alwisp.com) ``` ## Not in this repo -The top-level `AGENTS.md`, `SKILLS.md`, `hubs/`, and `skills/` directories are the coding-agent instruction suite this project was started from. They are reference material for AI assistants and are not shipped in the Docker image (they are listed in `.dockerignore`). +The top-level `AGENTS.md`, `SKILLS.md`, `hubs/`, and `skills/` directories are the coding-agent instruction suite this project was started from. They are reference material for AI assistants, are listed in `.dockerignore`, and are not shipped in the Docker image. diff --git a/docker/rebuild.sh b/docker/rebuild.sh new file mode 100644 index 0000000..c4bc683 --- /dev/null +++ b/docker/rebuild.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env sh +# Pull latest source from Gitea, rebuild the local image, and restart the +# container. Intended for the "Unraid builds locally" deploy path documented +# in docs/DEPLOY.md §5. Run it on the Unraid host, not inside the container. +# +# Usage: +# SRC_DIR=/mnt/user/appdata/mrp-qrcode-src \ +# IMAGE=mrp-qrcode:local \ +# CONTAINER=mrp-qrcode \ +# docker/rebuild.sh +# +# Wire it up as a User Scripts plugin cron job to get poor-man's CI. + +set -eu + +SRC_DIR="${SRC_DIR:-/mnt/user/appdata/mrp-qrcode-src}" +IMAGE="${IMAGE:-mrp-qrcode:local}" +CONTAINER="${CONTAINER:-mrp-qrcode}" +BRANCH="${BRANCH:-main}" + +cd "$SRC_DIR" + +echo "[rebuild] fetching $BRANCH" +git fetch --prune origin "$BRANCH" +git checkout "$BRANCH" +git reset --hard "origin/$BRANCH" + +echo "[rebuild] building $IMAGE" +docker build -t "$IMAGE" . + +echo "[rebuild] restarting $CONTAINER" +if docker inspect "$CONTAINER" >/dev/null 2>&1; then + docker restart "$CONTAINER" +else + echo "[rebuild] container $CONTAINER not found — create it from the Unraid GUI first." + exit 1 +fi + +echo "[rebuild] done" diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 780590f..5b5a901 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,98 +1,262 @@ # Deploying on Unraid -The MRP app is a single Docker container that stores everything (SQLite database + uploaded files + backups) under a single `/data` volume. +The MRP app is a single Docker container that stores everything (SQLite database + uploaded files + backups) under a single `/data` volume. This guide walks through installing it on an Unraid server using the **Docker GUI**, with images coming from a **Gitea Actions** runner that rebuilds on every push and publishes to `registry.alwisp.com`. -## 1. Prepare environment +You can skip the registry and have Unraid build locally — both paths are documented below. -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: +## Architecture at a glance -```bash -node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" +``` + git push main ─► Gitea Actions (.gitea/workflows/docker-build.yml) + │ + ├── docker login registry.alwisp.com + ├── docker build -f Dockerfile . + └── docker push registry.alwisp.com//:latest + │ + ▼ + Unraid Docker tab ──► pull image + │ + ▼ + Container: /data volume, :3000, reverse-proxied ``` -## 2. docker-compose (recommended) +--- -Create `.env` next to `docker-compose.yml`: +## 1. Prerequisites -```env -APP_URL=https://mrp.yourdomain.tld -APP_SECRET= -BOOTSTRAP_ADMIN_EMAIL=you@yourdomain.tld -BOOTSTRAP_ADMIN_PASSWORD= -BOOTSTRAP_ADMIN_NAME=Your Name -``` +- **Unraid** 6.11+ with the Docker service enabled. +- A **subdomain** (e.g. `mrp.yourdomain.tld`) pointed at Unraid through your existing reverse proxy (SWAG, Nginx Proxy Manager, Caddy, Traefik, Zoraxy — whichever you already use). The subdomain is what operators will scan into from their phones. +- A Gitea server hosting this repo with: + - Actions enabled, runner online + - Two repo secrets: `REGISTRY_USER` and `REGISTRY_TOKEN` with push rights to `registry.alwisp.com` + - The workflow at `.gitea/workflows/docker-build.yml` (already committed) -Then: +If you don't have the CI path, skip to [§5: Unraid builds locally](#5-alternative-unraid-builds-locally). -```bash -docker compose up -d --build -``` +--- -The container will: +## 2. Prepare the data directory on Unraid -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): +Create the appdata folder. Unraid conventionally uses `/mnt/user/appdata/`: ```bash mkdir -p /mnt/user/appdata/mrp-qrcode chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode ``` -## 4. Reverse proxy / subdomain +UID 1001 is the `nextjs` user inside the container (`Dockerfile` creates it). Getting this wrong is the #1 cause of "can't write to /data" errors at first boot. -Point your reverse proxy (SWAG, Nginx Proxy Manager, Caddy, Traefik — whatever is already on your Unraid) at `http://: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: +Generate a secret you will paste into the `APP_SECRET` env var: ```bash -docker exec mrp-qrcode sqlite3 /data/app.db ".backup '/data/backups/app-$(date +%F).db'" +openssl rand -base64 48 ``` -## 6. Upgrades +Keep it somewhere safe. Changing it logs every existing session out. + +--- + +## 3. Gitea Actions: auto-build the image + +The workflow at [`.gitea/workflows/docker-build.yml`](../.gitea/workflows/docker-build.yml) triggers on every push to `main`. It runs in `catthehacker/ubuntu:act-latest` and does exactly: + +```yaml +- docker/login-action@v3 # registry.alwisp.com, REGISTRY_USER / REGISTRY_TOKEN +- docker build -t registry.alwisp.com/${{ gitea.repository }}:latest . +- docker push registry.alwisp.com/${{ gitea.repository }}:latest +``` + +`${{ gitea.repository }}` expands to `/`, so the published image path is: + +``` +registry.alwisp.com//:latest +``` + +**One-time repo setup**: + +1. Gitea repo → **Settings → Actions → Runners** — confirm your runner is online and labelled `ubuntu-latest`. +2. Gitea repo → **Settings → Secrets** — add: + - `REGISTRY_USER` — a user that can push to `registry.alwisp.com` + - `REGISTRY_TOKEN` — that user's password or personal access token + +**Pull credentials on Unraid** (required if the registry is private): ```bash -git pull +# As root on Unraid +docker login registry.alwisp.com -u -p +``` + +Credentials are stored in `/root/.docker/config.json`; Unraid reuses them automatically on pull. + +--- + +## 4. Install on Unraid (Docker GUI, `docker pull` path) + +1. Open the Unraid web UI → **Docker** tab → **Add Container**. +2. Fill the template. Bold fields matter; the rest can stay default: + +| Field | Value | +|---|---| +| **Name** | `mrp-qrcode` | +| **Repository** | `registry.alwisp.com//:latest` | +| **Network Type** | `Bridge` | +| **Console shell command** | `sh` | +| **Privileged** | Off | +| **Icon URL** (optional) | point at any PNG you host; public/icon.png in the repo is a reasonable pick | + +3. **Port mapping** — *Add another Path, Port, Variable, Label or Device* → *Port*: + +| Name | Container Port | Host Port | Protocol | +|---|---|---|---| +| Web UI | `3000` | `3000` (or any free port) | TCP | + +4. **Volume mapping** — *Add* → *Path*: + +| Name | Container Path | Host Path | Access | +|---|---|---|---| +| Data | `/data` | `/mnt/user/appdata/mrp-qrcode` | Read/Write | + +5. **Environment variables** — *Add* → *Variable* for each: + +| Key | Required | Example | Notes | +|---|---|---|---| +| `APP_URL` | ✅ | `https://mrp.yourdomain.tld` | Public URL; baked into QR payloads. | +| `APP_SECRET` | ✅ | *(paste from §2)* | ≥32 chars. Signs sessions + QR tokens. | +| `BOOTSTRAP_ADMIN_EMAIL` | ✅ on first boot | `you@yourdomain.tld` | Only used if no admin exists. | +| `BOOTSTRAP_ADMIN_PASSWORD` | ✅ on first boot | *(strong password)* | Change in the UI after login. | +| `BOOTSTRAP_ADMIN_NAME` | | `Plant Manager` | Display name. | +| `ADMIN_SESSION_HOURS` | | `8` | Admin cookie TTL. | +| `OPERATOR_SESSION_HOURS` | | `12` | Operator device TTL. | +| `PIN_MAX_ATTEMPTS` | | `5` | Lockout threshold. | +| `PIN_LOCKOUT_MINUTES` | | `15` | Lockout window. | + +`DATABASE_URL` and `UPLOAD_DIR` are pre-set inside the image (`file:/data/app.db` and `/data/uploads`). Do not override them unless you really mean it. + +6. **Apply**. Unraid pulls the image, starts the container, and the entrypoint: + 1. Creates `/data/uploads` and `/data/backups` if missing. + 2. Runs `prisma migrate deploy`. + 3. Creates the bootstrap admin if no admin exists yet. + 4. Starts Next.js on `:3000`. + +7. Check the log (click the container icon → **Logs**). You should see `✓ Ready on http://0.0.0.0:3000`. + +8. Point your reverse proxy at `http://:3000` with your TLS cert, and browse to `https://mrp.yourdomain.tld/login/admin`. + +### Updates + +Every push to `main` produces a new `:latest` image at `registry.alwisp.com//`. + +- **Manual pull**: Docker tab → container row → **Force update** (Unraid does `docker pull` then recreates the container). +- **Automated**: install the *CA Auto Update Applications* plugin from Community Applications and let it pull fresh `:latest` on a schedule. + +The `/data` volume is preserved across recreations. Migrations run automatically on start; if a release adds a new Prisma migration, it will apply once on first boot of the new image. + +### Pinning (optional) + +The current `docker-build.yml` only publishes `:latest`. If you want immutable tags for rollback, extend the workflow to also push `${{ gitea.sha }}` or a release tag, then point the Unraid **Repository** field at the pinned tag (e.g. `registry.alwisp.com//:v0.4.0`). Rollback becomes a one-field change + **Apply**. + +--- + +## 5. Alternative: Unraid builds locally (`docker build` path) + +If you don't want a registry at all, Unraid can build from a local clone of the repo. + +1. On Unraid: + + ```bash + mkdir -p /mnt/user/appdata/mrp-qrcode-src + cd /mnt/user/appdata/mrp-qrcode-src + git clone https:////.git . + docker build -t mrp-qrcode:local . + ``` + +2. In the Docker GUI template (§4), set: + - **Repository**: `mrp-qrcode:local` + - Everything else identical. + +3. To update: `git pull && docker build -t mrp-qrcode:local . && docker restart mrp-qrcode`. + +A tiny convenience script lives at `docker/rebuild.sh` if you want to cron it. + +--- + +## 6. Alternative: `docker compose` on the Unraid CLI + +The repo also ships a `docker-compose.yml` for users who prefer CLI. This path bypasses the Unraid GUI entirely — state goes to a Docker-managed volume instead of `/mnt/user/appdata/…`, unless you override the mount. + +```bash +cp .env.example .env +# edit .env with real APP_URL, APP_SECRET, bootstrap admin creds docker compose up -d --build ``` -Migrations run automatically on start. Before major upgrades, snapshot the DB as above. +To mount `/data` under `/mnt/user/appdata/mrp-qrcode` instead of the named volume, change `docker-compose.yml`: -## 7. First-login checklist +```yaml +volumes: + - /mnt/user/appdata/mrp-qrcode:/data +``` -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. +and `chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode` first. -## 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`. +## 7. Reverse proxy notes + +`APP_URL` must match the externally reachable URL — it is embedded in QR code payloads and used for absolute links on traveler cards. 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. + +Minimal Nginx Proxy Manager config: + +- Domain: `mrp.yourdomain.tld` +- Scheme: `http`, Forward host: ``, Forward port: `3000` +- Block Common Exploits: on +- Websockets: on (Next.js hot reload uses them in dev; in prod they're unused but harmless) +- SSL: request a Let's Encrypt cert; Force SSL + HTTP/2 on. + +--- + +## 8. Backups + +The container does not yet run automatic backups (shipping in step 10 of the build plan). Until then, your Unraid backup strategy should cover: + +- `/mnt/user/appdata/mrp-qrcode/app.db` (SQLite file) +- `/mnt/user/appdata/mrp-qrcode/app.db-wal` and `app.db-shm` if present +- `/mnt/user/appdata/mrp-qrcode/uploads/` (STEP / PDF / DXF / SVG assets) + +A safe live snapshot of SQLite (works while the app is running): + +```bash +docker exec mrp-qrcode sqlite3 /data/app.db \ + ".backup '/data/backups/app-$(date +%F).db'" +``` + +Cron it nightly via **User Scripts** plugin. + +--- + +## 9. First-login checklist + +1. Sign in at `https://mrp.yourdomain.tld/login/admin` with the bootstrap email + password you set in step 4. +2. **Users** → change your own password; create operators (each gets a name + 4-digit PIN). +3. **Machines** → add your shop equipment (NCT punch, press brake, rivet, weld, …). +4. **Operation templates** → pre-author your common recipes so they appear in the operation picker later. +5. **Projects** → create your first project, add assemblies, add parts, upload STEP / drawing / cut files. +6. **Operations** on each part → author steps, reorder, verify QR tokens appear in the table. + +Step 4 of the build plan (the operator scan flow) lands next; until then, the QR tokens are visible and tested but not yet scannable to an operator view. + +--- + +## 10. Troubleshooting + +| Symptom | Fix | +|---|---| +| `APP_SECRET must be at least 32 chars` on start | Regenerate via `openssl rand -base64 48` and paste into the env var. | +| `EACCES` / permission errors writing to `/data` | `chown -R 1001:1001 /mnt/user/appdata/mrp-qrcode` on the host. | +| Healthcheck failing | `docker logs mrp-qrcode` — usually a missing env var or a broken reverse proxy. | +| Unraid won't pull the image | Run `docker login registry.alwisp.com` on the host with the same user/token the runner uses. Confirm the repo name matches `${{ gitea.repository }}` exactly (case-sensitive). | +| Migrations didn't run | The entrypoint calls `prisma migrate deploy`. Logs will show which migration failed. Don't manually touch `_prisma_migrations`. | +| QR codes link to `localhost` | `APP_URL` was left at default. Update the env var and restart the container. | +| Can't log in after redeploy | `APP_SECRET` changed → every session cookie is invalid. Log in again. |