From ad499f67825c82c04a616acf31a996a9f28956cb Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 15 Jun 2026 16:57:15 -0500 Subject: [PATCH] Assemble QMS app + SQLite refactor + Unraid single-container deploy Reconstruct the full app from init-source overlays (base + fix-1..6 + update-1..3, last-wins) at the repo root, complete the missing pieces so it builds and runs, and stage the Unraid deployment. App completion: - types/index.ts: former Prisma enums as string-literal unions + AppUser - pages/_app.tsx + styles/globals.css (mount AppProvider/ToastProvider) - API routes: auth/login, auth/me, users, submissions (+REVIEW_READY notify), forms (list/create), notifications - scripts/create-admin.js: idempotent first-admin bootstrap - 14 unbuilt nav targets stubbed via ComingSoon placeholder SQLite refactor (single-container, no external DB): - schema provider -> sqlite; enums -> String; Json -> String; FormField.options String[] -> JSON-encoded String - lib/forms.ts (de)serialises options at the DB boundary - drop mode:"insensitive" (unsupported on SQLite) - enum imports repointed from @prisma/client to @/types Deploy: - multi-stage Dockerfile (next build -> prod runner), docker-entrypoint.sh (prisma db push -> create-admin -> next start), .dockerignore - docker-compose.yml: br0 10.2.0.x, /mnt/user/appdata/qms -> /data volume - README rewritten for the Unraid/Gitea Actions flow; .env scrubbed of the live Supabase credential; vercel.json removed Verified: next build clean (41 routes); live SQLite round-trip of login/session, form options array, and submission -> REVIEW_READY. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dockerignore | 16 + .env.example | 20 + .gitea/workflows/docker-build.yml | 25 + .gitignore | 22 + .npmrc | 1 + Dockerfile | 43 + README.md | 101 + components/ComingSoon.tsx | 30 + components/forms/FieldRenderer.tsx | 96 + components/layout/Layout.tsx | 266 ++ components/ui/index.tsx | 243 ++ docker-compose.yml | 36 + docker-entrypoint.sh | 11 + lib/auth.ts | 108 + lib/context.tsx | 62 + lib/email.ts | 92 + lib/forms.ts | 30 + lib/prisma.ts | 15 + next-env.d.ts | 6 + next.config.js | 10 + package-lock.json | 3511 ++++++++++++++++++++++++++ package.json | 43 + pages/_app.tsx | 13 + pages/admin/audit-log.tsx | 5 + pages/admin/forms.tsx | 355 +++ pages/admin/index.tsx | 5 + pages/admin/settings.tsx | 5 + pages/admin/users.tsx | 5 + pages/api/auth/login.ts | 26 + pages/api/auth/me.ts | 23 + pages/api/escapes/[id].ts | 96 + pages/api/escapes/index.ts | 47 + pages/api/forms/[id].ts | 113 + pages/api/forms/[id]/clone.ts | 50 + pages/api/forms/index.ts | 71 + pages/api/ncrs/[id].ts | 78 + pages/api/ncrs/[id]/escalate.ts | 48 + pages/api/ncrs/index.ts | 41 + pages/api/notifications/index.ts | 30 + pages/api/resolutions/index.ts | 42 + pages/api/shipments/[id]/send.ts | 35 + pages/api/shipments/index.ts | 45 + pages/api/shipments/suggest.ts | 40 + pages/api/shipping-standard/index.ts | 29 + pages/api/submissions/index.ts | 39 + pages/api/users/index.ts | 19 + pages/fill/index.tsx | 253 ++ pages/fill/submissions.tsx | 5 + pages/index.tsx | 117 + pages/management/index.tsx | 5 + pages/management/reports.tsx | 5 + pages/qc/audits.tsx | 5 + pages/qc/capas.tsx | 5 + pages/qc/documents.tsx | 5 + pages/qc/escapes.tsx | 330 +++ pages/qc/index.tsx | 5 + pages/qc/ncr.tsx | 366 +++ pages/qc/risk.tsx | 5 + pages/qc/shipments.tsx | 189 ++ pages/qc/shipping-standard.tsx | 77 + pages/qc/standards.tsx | 5 + pages/qc/suppliers.tsx | 5 + postcss.config.js | 3 + prisma/schema.prisma | 431 ++++ scripts/create-admin.js | 32 + styles/globals.css | 45 + tailwind.config.js | 23 + tsconfig.json | 23 + types/cookie.d.ts | 1 + types/index.ts | 58 + 70 files changed, 8045 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/docker-build.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 components/ComingSoon.tsx create mode 100644 components/forms/FieldRenderer.tsx create mode 100644 components/layout/Layout.tsx create mode 100644 components/ui/index.tsx create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100644 lib/auth.ts create mode 100644 lib/context.tsx create mode 100644 lib/email.ts create mode 100644 lib/forms.ts create mode 100644 lib/prisma.ts create mode 100644 next-env.d.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/_app.tsx create mode 100644 pages/admin/audit-log.tsx create mode 100644 pages/admin/forms.tsx create mode 100644 pages/admin/index.tsx create mode 100644 pages/admin/settings.tsx create mode 100644 pages/admin/users.tsx create mode 100644 pages/api/auth/login.ts create mode 100644 pages/api/auth/me.ts create mode 100644 pages/api/escapes/[id].ts create mode 100644 pages/api/escapes/index.ts create mode 100644 pages/api/forms/[id].ts create mode 100644 pages/api/forms/[id]/clone.ts create mode 100644 pages/api/forms/index.ts create mode 100644 pages/api/ncrs/[id].ts create mode 100644 pages/api/ncrs/[id]/escalate.ts create mode 100644 pages/api/ncrs/index.ts create mode 100644 pages/api/notifications/index.ts create mode 100644 pages/api/resolutions/index.ts create mode 100644 pages/api/shipments/[id]/send.ts create mode 100644 pages/api/shipments/index.ts create mode 100644 pages/api/shipments/suggest.ts create mode 100644 pages/api/shipping-standard/index.ts create mode 100644 pages/api/submissions/index.ts create mode 100644 pages/api/users/index.ts create mode 100644 pages/fill/index.tsx create mode 100644 pages/fill/submissions.tsx create mode 100644 pages/index.tsx create mode 100644 pages/management/index.tsx create mode 100644 pages/management/reports.tsx create mode 100644 pages/qc/audits.tsx create mode 100644 pages/qc/capas.tsx create mode 100644 pages/qc/documents.tsx create mode 100644 pages/qc/escapes.tsx create mode 100644 pages/qc/index.tsx create mode 100644 pages/qc/ncr.tsx create mode 100644 pages/qc/risk.tsx create mode 100644 pages/qc/shipments.tsx create mode 100644 pages/qc/shipping-standard.tsx create mode 100644 pages/qc/standards.tsx create mode 100644 pages/qc/suppliers.tsx create mode 100644 postcss.config.js create mode 100644 prisma/schema.prisma create mode 100644 scripts/create-admin.js create mode 100644 styles/globals.css create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 types/cookie.d.ts create mode 100644 types/index.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d5e512 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +.next +.git +.gitea +init-source +*.db +*.db-journal +prisma/dev.db* +.env +.env.* +!.env.example +npm-debug.log* +Dockerfile +docker-compose.yml +README.md +.claude diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..34165f3 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# ─── Database (SQLite — single-file, no external DB server) ─────────────────── +# Local dev: file:./dev.db (created under prisma/) +# Container: file:/data/qms.db (set in docker-compose; /data is the mapped Unraid volume) +DATABASE_URL="file:./dev.db" + +# ─── App ────────────────────────────────────────────────────────────────────── +# Set this to the LAN URL/hostname the app is reached at on Unraid (used in notification links). +NEXT_PUBLIC_APP_URL="http://10.2.0.x:3000" + +# ─── First admin (auto-created on container start only if no admin exists) ──── +ADMIN_EMAIL="admin@yourcompany.com" +ADMIN_PASSWORD="change-me-on-first-login" +ADMIN_NAME="Administrator" + +# ─── Email (SMTP) — optional; emails are skipped entirely when SMTP_HOST unset ─ +# SMTP_HOST="smtp.resend.com" +# SMTP_PORT="587" +# SMTP_USER="resend" +# SMTP_PASS="re_your_api_key_here" +# EMAIL_FROM="qms@yourcompany.com" diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..3d499eb --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,25 @@ +name: Build and Push Docker Image +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: registry.alwisp.com + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and Push + run: | + docker build -t registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:latest . + docker push registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acef266 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# dependencies +/node_modules + +# next.js +/.next +/out + +# database (SQLite — runtime only) +*.db +*.db-journal +/prisma/dev.db* +/data + +# env / secrets +.env +.env.local +.env.*.local + +# misc +.DS_Store +*.log +.vercel diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..7f4c450 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..165e9ed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1 +# V11 Enterprise QMS — single-container image (Next.js + Prisma + SQLite) + +# ─── Builder ────────────────────────────────────────────────────────────────── +FROM node:20-alpine AS builder +WORKDIR /app + +# Install deps (legacy-peer-deps comes from .npmrc — needed for React 19) +COPY package.json package-lock.json .npmrc ./ +RUN npm ci + +# Build (prisma generate && next build). DATABASE_URL is only needed at runtime; +# a dummy value satisfies prisma generate during the image build. +COPY . . +ENV DATABASE_URL="file:/data/qms.db" +RUN npm run build + +# ─── Runner ─────────────────────────────────────────────────────────────────── +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +ENV DATABASE_URL="file:/data/qms.db" + +# Production deps only (keeps next + prisma CLI + @prisma/client; drops build tooling) +COPY package.json package-lock.json .npmrc ./ +RUN npm ci --omit=dev + +# Prisma schema + generated client for the prod node_modules +COPY prisma ./prisma +RUN npx prisma generate + +# App build output and runtime files +COPY --from=builder /app/.next ./.next +COPY next.config.js ./ +COPY scripts ./scripts +COPY docker-entrypoint.sh ./ + +# /data holds the SQLite file and is mounted from the Unraid host +RUN mkdir -p /data && chmod +x docker-entrypoint.sh + +EXPOSE 3000 +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddd6cab --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# V11 Enterprise QMS + +Full-stack quality management system — Next.js 15 (pages router) + Prisma + **SQLite**, +packaged as a **single Docker container** for self-hosting on Unraid. Starts from zero +data and learns as your team uses it. + +> **Status — partial build.** The implemented modules are: authentication, the first-build +> **Form builder** (admin), **Fill / Report an issue** (production), **Nonconformances + Resolutions +> library** (QC), **Client release / Shipments**, **Client issues / Quality escapes**, and the +> **living Shipping standard**. The remaining nav items (Admin dashboard, Users, Audit trail, +> Settings, QC dashboard, CAPA, Audits, Documents, Risk, Suppliers, Standards, My submissions, +> Management dashboard & Reports) are wired into navigation but render a "Coming soon" placeholder — +> the data model for them already exists in `prisma/schema.prisma`. + +## Roles & access + +| Role | Access | +|------|--------| +| **Admin** | Everything: form builder, users, settings, audit trail | +| **QC** | CAPA, audits, NCRs, resolutions, documents, risk, suppliers, standards, shipping standard | +| **Production** | Fill first-build forms, report issues | +| **Production lead** | Production + client release, client issues, shipping standard, NCRs | +| **Logistics lead** | Client release, client issues, shipping standard, NCRs | +| **Management** | Read-only dashboards and reports | + +Only **Admin / Production lead / Logistics lead** can email a client release package. + +--- + +## Deploy on Unraid (the intended setup) + +CI/CD: push to Gitea → Gitea Actions builds the image → pushes to `registry.alwisp.com/jason/qms:latest` +→ install/Force-update from the Unraid Docker GUI. + +### 1. Build & push (automatic) +`.gitea/workflows/docker-build.yml` builds and pushes the image to +`registry.alwisp.com//:latest` on every push to `main`. It needs two repo secrets: +`REGISTRY_USER` and `REGISTRY_TOKEN`. + +### 2. Run on Unraid +Either `docker compose up -d` with the bundled [`docker-compose.yml`](docker-compose.yml), or add a +container in the Unraid Docker GUI with the equivalent settings: + +| Setting | Value | +|---|---| +| Repository | `registry.alwisp.com/jason/qms:latest` | +| Network | **Custom: br0** — assign a free IP in `10.2.0.0/24` | +| Volume | `/mnt/user/appdata/qms` → `/data` | +| `DATABASE_URL` | `file:/data/qms.db` | +| `NEXT_PUBLIC_APP_URL` | `http://:3000` | +| `ADMIN_EMAIL` / `ADMIN_PASSWORD` / `ADMIN_NAME` | first-run admin (created only if no admin exists) | +| `SMTP_*` (optional) | enables outbound email; omit to keep notifications in-app only | + +On first start the container runs `prisma db push` (creates `/data/qms.db`), then `create-admin` +(creates the admin from `ADMIN_*` only if none exists), then serves on port `3000`. The SQLite file +lives entirely in the mapped volume, so the container is disposable — update by pulling a new image. + +The app is reached at `http://:3000`. Sign in with the admin credentials and change the +password. + +--- + +## Local development + +```bash +npm install +cp .env.example .env # DATABASE_URL defaults to file:./dev.db +npm run db:push # create the SQLite schema +npm run create-admin # seed the first admin from ADMIN_* in .env +npm run dev # http://localhost:3000 +``` + +Useful scripts: `npm run build`, `npm run start`, `npm run db:studio`. + +--- + +## Why SQLite (not Postgres) + +This deploy targets a single self-contained container, so the schema was converted from PostgreSQL to +SQLite. SQLite (via Prisma) supports neither native enums nor `Json`/scalar-list columns, so: + +- former enums are stored as `String` and validated in app code (union types in [`types/index.ts`](types/index.ts)); +- former `Json` columns (`AuditLog.before/after`, `FormSubmission.data`, `QualityStandard.specs`) and the + `FormField.options` list are stored as **JSON-encoded strings**, (de)serialised at the DB boundary + (see [`lib/forms.ts`](lib/forms.ts)). + +## Project structure + +``` +/pages + /api ← backend API routes (auth, forms, submissions, ncrs, escapes, shipments, …) + /admin /qc /fill /management ← role-scoped pages +/components ← layout shell, shared UI kit, form field renderer +/lib ← prisma client, auth/session, email, form (de)serialisation helpers +/prisma ← schema.prisma (SQLite) +/scripts ← create-admin.js (first-run bootstrap) +Dockerfile · docker-compose.yml · docker-entrypoint.sh · .gitea/workflows/build.yml +``` + +> `init-source/` holds the original delivery (base snapshot + fix/update overlays) for provenance; +> it is excluded from the build and the image. diff --git a/components/ComingSoon.tsx b/components/ComingSoon.tsx new file mode 100644 index 0000000..49c1bdd --- /dev/null +++ b/components/ComingSoon.tsx @@ -0,0 +1,30 @@ +import Layout from '@/components/layout/Layout' + +// Placeholder for QMS modules that are on the roadmap but not yet implemented. +// Keeps navigation links and role redirects valid instead of 404-ing. +export default function ComingSoon({ title, blurb }: { title: string; blurb?: string }) { + return ( + +
+
+ + + +
+

{title}

+

+ {blurb || 'This module is part of the QMS roadmap and is not built yet. The data model and navigation are already in place.'} +

+ + Coming soon + +
+
+ ) +} diff --git a/components/forms/FieldRenderer.tsx b/components/forms/FieldRenderer.tsx new file mode 100644 index 0000000..b6c1b20 --- /dev/null +++ b/components/forms/FieldRenderer.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import { Input, Textarea } from '@/components/ui' +import { FieldType } from '@/types' + +export interface FormFieldDef { + id: string + label: string + type: FieldType + hint?: string | null + options: string[] + required: boolean +} + +export function FieldRenderer({ field, value, onChange }: { + field: FormFieldDef + value: any + onChange: (v: any) => void +}) { + switch (field.type) { + case 'SHORT_TEXT': + return onChange(e.target.value)} placeholder={field.hint || ''}/> + + case 'LONG_TEXT': + return