Assemble QMS app + SQLite refactor + Unraid single-container deploy
Build and Push Docker Image / build (push) Successful in 1m12s

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) <noreply@anthropic.com>
This commit is contained in:
jason
2026-06-15 16:57:15 -05:00
parent 631890c5bd
commit ad499f6782
70 changed files with 8045 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
import { useState, FormEvent } from 'react'
import { useRouter } from 'next/router'
import { useApp } from '@/lib/context'
import Head from 'next/head'
export default function LoginPage() {
const { login, user, loading } = useApp()
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
if (!loading && user) {
const dest = { ADMIN: '/admin', QC: '/qc', PRODUCTION: '/fill', PRODUCTION_LEAD: '/fill', LOGISTICS_LEAD: '/qc/shipments', MANAGEMENT: '/management' }
router.replace(dest[user.role] || '/qc')
return null
}
const submit = async (e: FormEvent) => {
e.preventDefault()
setBusy(true)
setError('')
const err = await login(email, password)
if (err) { setError(err); setBusy(false) }
}
return (
<>
<Head><title>Sign in V11 QMS</title></Head>
<div style={{
minHeight: '100vh', background: '#F8F7FD',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px'
}}>
<div style={{ width: '100%', maxWidth: '380px' }}>
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{
width: '44px', height: '44px', borderRadius: '12px',
background: '#534AB7', display: 'inline-flex', alignItems: 'center',
justifyContent: 'center', marginBottom: '12px'
}}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2">
<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
</div>
<h1 style={{ fontSize: '20px', fontWeight: '600', color: '#1a1a1a', margin: '0 0 4px' }}>
V11 Enterprise QMS
</h1>
<p style={{ fontSize: '13px', color: '#888', margin: 0 }}>Sign in to your account</p>
</div>
<form onSubmit={submit} style={{
background: 'white', borderRadius: '12px',
border: '0.5px solid #e8e8e8', padding: '24px'
}}>
{error && (
<div style={{
background: '#FCEBEB', color: '#791F1F', border: '0.5px solid #F09595',
borderRadius: '8px', padding: '9px 12px', fontSize: '12px', marginBottom: '16px'
}}>
{error}
</div>
)}
<div style={{ marginBottom: '14px' }}>
<label style={{ display: 'block', fontSize: '11px', color: '#666', marginBottom: '4px', fontWeight: '500' }}>
Email address
</label>
<input
type="email" required value={email}
onChange={e => setEmail(e.target.value)}
placeholder="you@company.com"
style={{
width: '100%', padding: '9px 11px', fontSize: '13px',
border: '0.5px solid #ddd', borderRadius: '8px',
outline: 'none', fontFamily: 'inherit', background: 'transparent'
}}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', fontSize: '11px', color: '#666', marginBottom: '4px', fontWeight: '500' }}>
Password
</label>
<input
type="password" required value={password}
onChange={e => setPassword(e.target.value)}
placeholder="••••••••"
style={{
width: '100%', padding: '9px 11px', fontSize: '13px',
border: '0.5px solid #ddd', borderRadius: '8px',
outline: 'none', fontFamily: 'inherit', background: 'transparent'
}}
/>
</div>
<button
type="submit" disabled={busy}
style={{
width: '100%', padding: '10px', fontSize: '13px', fontWeight: '500',
background: busy ? '#AFA9EC' : '#534AB7', color: 'white',
border: 'none', borderRadius: '8px', cursor: busy ? 'not-allowed' : 'pointer',
fontFamily: 'inherit', transition: 'background 0.1s'
}}
>
{busy ? 'Signing in…' : 'Sign in'}
</button>
</form>
<p style={{ textAlign: 'center', fontSize: '11px', color: '#aaa', marginTop: '16px' }}>
No account? Ask your system administrator.
</p>
</div>
</div>
</>
)
}