initialising for ci-cd pipeline
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
This commit is contained in:
@@ -345,3 +345,10 @@ Spec: `docs/21-FRONTEND-PHASE2.md` (flows/screens) · contract: `docs/30-BACKEND
|
||||
5. **Inline "add variant category":** a "+" icon button next to the checkboxes opens an inline name field that calls `variantCategoriesApi.create()` directly from `/new`, appends the result to the in-memory list, and auto-checks it — so a brand-new dimension (e.g. "Material") can be added without leaving the Item form, and it also then appears on `/dashboard/products/variants`.
|
||||
6. **Color gets a real color picker:** for whichever checked category is literally named "Color" (case-insensitive), the free-text input is replaced with a native `<input type="color">` swatch picker *plus* a required "Color name" text field — picking red alone isn't enough, a name is mandatory too. The pair is encoded as a single string `"<name>|<hex>"` in `valuesByCategory` (helpers `encodeColorValue`/`decodeColorValue`/`partLabel` in `new/page.tsx`) so the existing generic value-list plumbing didn't need a parallel data shape; every place that displays or SKU-generates from a color value decodes it back to just the name (the hex only ever drives the swatch dot next to chips and table cells) — so SKUs read `HAR-CRI` (from "Crimson"), never `HAR-EF4`.
|
||||
- Verified: `tsc --noEmit` clean after every step (same pre-existing `login/page.tsx` error only, confirmed unchanged throughout). Each change was driven end-to-end through a headless Playwright session against the dev server and screenshotted — field removal, subcategory always-visible + disabled state, checkbox show/hide of category builders, cartesian flat table with 2+ active categories, inline category creation followed by its builder appearing immediately, and the color picker + name → chip swatch → table swatch → final SKU/item name chain — with `console --errors` clean at every step and at least one full create-and-redirect-to-Items-list confirmed per major change.
|
||||
|
||||
## Deployment readiness (2026-09-14) — `docs/40-DEPLOYMENT.md`
|
||||
- [x] **Standalone build + runtime image.** `next.config.ts` → `output: "standalone"`; runtime-only `Dockerfile` (`node:24-alpine`, non-root, copies `.next/standalone` + `.next/static` + `public`) and `.dockerignore` (keeps `.next`). The `/api` rewrite is resolved at build time and only matters locally — Apache sends `/api/` straight to ERPCore in deployed environments, so one image serves both test and release. **Deviation from the plan:** Node 24 instead of 20 (Node 20 is past end-of-life). Verified: `npm run build` produced `.next/standalone/server.js`; started on :3999 → `GET /login` 200, `GET /dashboard` without a session cookie → 307 to `/login`.
|
||||
- [x] **Lint can gate CI.** `react-hooks/set-state-in-effect` temporarily `"warn"` (React Compiler rule from eslint-config-next 16, ~80 existing hits). Fixed the other 12 errors: 7 unescaped apostrophes (`sales/day-end`, `settings/invoice-template`, `login/forgot`, `login/forgot/otp`, `login/forgot/reset`); `PasswordRequirement` hoisted to module scope in `login/forgot/reset` (a component created during render remounts on every keystroke); the `visibleGrns` memo moved above `toggleAll` in `procurement/purchase-invoices/new` (it was read before its declaration, so the compiler could not preserve the memo). Now `eslint .` → 0 errors / 114 warnings; `tsc --noEmit` clean.
|
||||
- [ ] Clear the `set-state-in-effect` backlog, then set the rule back to `"error"`.
|
||||
- [x] **2026-09-15 — production-only deploy (`docs/40-DEPLOYMENT.md` "Active").** Supersedes the runtime-only image above: `Dockerfile` is now multi-stage (deps → builder → runner on `node:24-alpine`, the Client_New_Fe pattern), so `next build` runs inside `docker build` on the server, and `.dockerignore` excludes `.next` and `node_modules`. Still `output: "standalone"` and no build args. The deploy job replaces the `erp-web` container on `127.0.0.1:3020` only when `Frontend/erp-system` changed and fails the job if `/login` doesn't answer within 30 s. PR checks now run `tsc` + `eslint` + `next build` in a capped container without producing an image. The multi-stage `docker build` has not been run yet (no Docker locally).
|
||||
- [x] **2026-09-15 — one workflow file.** `.gitea/workflows/ci.yml` and `deploy.yml` merged into `.gitea/workflows/ci-cd.yml`: the frontend PR check (`tsc` + `eslint` + `next build`) runs while a PR is open, and the `Build & Deploy` job (`docker build` → replace `erp-web` → `/login` check) runs only when a PR is merged into `production` or on a manual run. No change to the Dockerfile or the app.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# `next build` runs inside the image (see Dockerfile), so local build output and dependencies must
|
||||
# not be sent as build context: they are large, and a Windows node_modules would break the Linux build.
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
*.tsbuildinfo
|
||||
npm-debug.log*
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
@@ -0,0 +1,38 @@
|
||||
# ERP frontend image — built on the server by .gitea/workflows/ci-cd.yml (`docker build`), using the
|
||||
# same pattern as Client_New_Fe. There are no build args: the app only calls same-origin /api/v1 and
|
||||
# Apache routes /api/ straight to ERPCore (docs/40-DEPLOYMENT.md, "Active").
|
||||
|
||||
FROM node:24-alpine AS deps
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache libc6-compat
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
# Shared 8 GB VPS: keep the build's heap bounded (docker build has no --memory under BuildKit).
|
||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||
NODE_ENV=production \
|
||||
NODE_OPTIONS=--max-old-space-size=2048
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
PORT=3000 \
|
||||
HOSTNAME=0.0.0.0
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -112,6 +112,17 @@ function NewPurchaseInvoiceContent() {
|
||||
[vendors]
|
||||
)
|
||||
|
||||
// Deep-linked from a GRN detail page's "Create Purchase Invoice" prompt — that GRN is the
|
||||
// whole reason this page was opened, so show only it rather than the vendor's full eligible
|
||||
// list (which could pull in unrelated GRNs the user never meant to consolidate here).
|
||||
// Declared before toggleAll, which reads it: referencing it ahead of its declaration stopped
|
||||
// the React Compiler from preserving this memo (react-hooks/preserve-manual-memoization).
|
||||
const deepLinkedGrnId = grnIdFromQuery ? Number(grnIdFromQuery) : null
|
||||
const visibleGrns = useMemo(
|
||||
() => (deepLinkedGrnId === null ? (eligibleGrns ?? []) : (eligibleGrns ?? []).filter((g) => g.grnId === deepLinkedGrnId)),
|
||||
[eligibleGrns, deepLinkedGrnId]
|
||||
)
|
||||
|
||||
function toggleGrn(grnId: number, checked: boolean) {
|
||||
setSelectedGrnIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
@@ -125,15 +136,6 @@ function NewPurchaseInvoiceContent() {
|
||||
setSelectedGrnIds(checked ? new Set(visibleGrns.map((g) => g.grnId)) : new Set())
|
||||
}
|
||||
|
||||
// Deep-linked from a GRN detail page's "Create Purchase Invoice" prompt — that GRN is the
|
||||
// whole reason this page was opened, so show only it rather than the vendor's full eligible
|
||||
// list (which could pull in unrelated GRNs the user never meant to consolidate here).
|
||||
const deepLinkedGrnId = grnIdFromQuery ? Number(grnIdFromQuery) : null
|
||||
const visibleGrns = useMemo(
|
||||
() => (deepLinkedGrnId === null ? (eligibleGrns ?? []) : (eligibleGrns ?? []).filter((g) => g.grnId === deepLinkedGrnId)),
|
||||
[eligibleGrns, deepLinkedGrnId]
|
||||
)
|
||||
|
||||
const selectedGrns = useMemo(
|
||||
() => (eligibleGrns ?? []).filter((g) => selectedGrnIds.has(g.grnId)),
|
||||
[eligibleGrns, selectedGrnIds]
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function SalesDayEndPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Day End</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Close a cashier's business day — locks their posted sales slips and bundle sales, and posts one consolidated journal entry to the ledger.
|
||||
Close a cashier's business day — locks their posted sales slips and bundle sales, and posts one consolidated journal entry to the ledger.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,14 +149,14 @@ export default function InvoiceTemplatePage() {
|
||||
<Switch checked={showBankDetails} onCheckedChange={setShowBankDetails} className="mt-1 shrink-0" aria-label="Show bank details" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-base font-medium text-foreground">Show bank details</span>
|
||||
<span className="text-sm text-muted-foreground">Prints the company's bank name, account and SWIFT code on invoices.</span>
|
||||
<span className="text-sm text-muted-foreground">Prints the company's bank name, account and SWIFT code on invoices.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 py-4">
|
||||
<Switch checked={showTaxRegNo} onCheckedChange={setShowTaxRegNo} className="mt-1 shrink-0" aria-label="Show tax registration no." />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-base font-medium text-foreground">Show tax registration no.</span>
|
||||
<span className="text-sm text-muted-foreground">Prints the company's tax registration number in the invoice header.</span>
|
||||
<span className="text-sm text-muted-foreground">Prints the company's tax registration number in the invoice header.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function ForgotOtpPage() {
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground text-center mb-8">We've sent a verification code to your email address. Enter it below to continue.</p>
|
||||
<p className="text-sm text-muted-foreground text-center mb-8">We've sent a verification code to your email address. Enter it below to continue.</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-center">
|
||||
@@ -48,7 +48,7 @@ export default function ForgotOtpPage() {
|
||||
}}
|
||||
className="text-sm font-medium text-foreground hover:text-foreground/80 transition-colors"
|
||||
>
|
||||
Didn't receive the code? Resend
|
||||
Didn't receive the code? Resend
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Resend code available in <span className="font-semibold text-foreground">{Math.max(0, secondsLeft)}s</span></p>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function ForgotEmailPage() {
|
||||
<div className="w-full max-w-2xl rounded-xl bg-card p-8 pl-10 shadow-lg border border-border">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-2xl font-bold text-foreground">Reset your password</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -13,6 +13,19 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Check, Eye, EyeOff } from "lucide-react"
|
||||
|
||||
// Declared at module scope: a component defined inside the page body is a new type on every
|
||||
// render, so React would remount (and reset) each requirement row on each keystroke.
|
||||
function PasswordRequirement({ met, label }: { met: boolean; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex items-center justify-center w-5 h-5 rounded-full shrink-0 text-sm font-semibold ${met ? 'bg-success/10 text-success' : 'bg-muted text-muted-foreground'}`}>
|
||||
{met ? <Check className="size-4" /> : <span>○</span>}
|
||||
</div>
|
||||
<span className={`text-sm ${met ? 'text-foreground font-medium' : 'text-muted-foreground'}`}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ForgotResetPage() {
|
||||
const router = useRouter()
|
||||
const [password, setPassword] = useState("")
|
||||
@@ -28,15 +41,6 @@ export default function ForgotResetPage() {
|
||||
const passwordsMatch = password === confirm && password.length > 0
|
||||
const allValid = meetsLength && hasUpper && hasNumber && hasSpecial && passwordsMatch
|
||||
|
||||
const PasswordRequirement = ({ met, label }: { met: boolean; label: string }) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex items-center justify-center w-5 h-5 rounded-full shrink-0 text-sm font-semibold ${met ? 'bg-success/10 text-success' : 'bg-muted text-muted-foreground'}`}>
|
||||
{met ? <Check className="size-4" /> : <span>○</span>}
|
||||
</div>
|
||||
<span className={`text-sm ${met ? 'text-foreground font-medium' : 'text-muted-foreground'}`}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
|
||||
<div className="w-full max-w-xl rounded-xl bg-card p-8 shadow-lg border border-border">
|
||||
@@ -97,7 +101,7 @@ export default function ForgotResetPage() {
|
||||
</div>
|
||||
|
||||
{!passwordsMatch && confirm.length > 0 && (
|
||||
<p className="text-sm text-error font-medium">Passwords don't match.</p>
|
||||
<p className="text-sm text-error font-medium">Passwords don't match.</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
|
||||
@@ -5,6 +5,14 @@ import nextTs from "eslint-config-next/typescript";
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
{
|
||||
rules: {
|
||||
// Temporarily a warning: ~80 pre-existing violations from the React Compiler rules that
|
||||
// eslint-config-next 16 enabled. Everything else stays an error so CI can gate on lint
|
||||
// now; flip this back to "error" once the backlog is cleared (docs/40-DEPLOYMENT.md §7).
|
||||
"react-hooks/set-state-in-effect": "warn",
|
||||
},
|
||||
},
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
|
||||
@@ -8,9 +8,16 @@ import path from "path"
|
||||
//
|
||||
// Defaults to the backend's HTTP port: the HTTPS port uses a self-signed dev cert that
|
||||
// this proxy would reject.
|
||||
//
|
||||
// Rewrites are resolved at `next build`, so this is effectively a local-dev setting. In the
|
||||
// deployed environments Apache sends /api/ straight to ERPCore before Next ever sees it, which
|
||||
// keeps one built image valid for both test and release (docs/40-DEPLOYMENT.md).
|
||||
const BACKEND_ORIGIN = process.env.BACKEND_ORIGIN ?? "http://localhost:5224"
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Self-contained server bundle (.next/standalone) for the runtime-only Docker image
|
||||
// (Dockerfile in this folder). Has no effect on `next dev`.
|
||||
output: "standalone",
|
||||
turbopack: {
|
||||
root: path.join(__dirname),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user