diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6938d15 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.env +.env.* +!.env.example +*.log +.DS_Store +coverage +.playwright +prisma/dev.db +prisma/*.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..650223b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 +FROM node:20-bookworm-slim AS base +RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +# Dependencies +FROM base AS deps +WORKDIR /app +RUN corepack enable pnpm +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install + +# Builder +FROM base AS builder +WORKDIR /app +RUN corepack enable pnpm +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN pnpm exec prisma generate +RUN pnpm run build + +# Runner +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma +COPY --from=builder --chown=nextjs:nodejs /app/package.json ./ + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +COPY docker-entrypoint.sh /entrypoint.sh +RUN apt-get update -y && apt-get install -y gosu && rm -rf /var/lib/apt/lists/* \ + && chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["sh", "-c", "npx prisma db push && npx prisma db seed && node server.js"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..350c088 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,2 @@ +FROM node:20-bookworm-slim +RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..47ea892 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,21 @@ +# Dev avec hot reload (source montée) +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + image: iag-dev-evaluator-dev + working_dir: /app + ports: + - "3000:3000" + environment: + - DATABASE_URL=file:/data/db/dev.db + - WATCHPACK_POLLING=true + volumes: + - .:/app + - db-data:/data/db + - /app/node_modules # anonymous volume pour éviter écrasement + command: sh -c "corepack enable pnpm && pnpm install && pnpm exec prisma generate && pnpm exec prisma db push && pnpm run dev" + +volumes: + db-data: diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml new file mode 100644 index 0000000..2b56e24 --- /dev/null +++ b/docker-compose.postgres.yml @@ -0,0 +1,29 @@ +# Variante Postgres (schema.prisma doit avoir provider = "postgresql") +services: + app: + build: . + ports: + - "3000:3000" + environment: + - DATABASE_URL=postgresql://evaluator:secret@db:5432/evaluator + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: evaluator + POSTGRES_PASSWORD: secret + POSTGRES_DB: evaluator + volumes: + - pg-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U evaluator"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pg-data: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dc65027 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + app: + build: . + ports: + - "3000:3000" + environment: + - DATABASE_URL=file:/data/db/dev.db + volumes: + - db-data:/data/db + restart: unless-stopped + +volumes: + db-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..7aec3f8 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +mkdir -p /data/db +chown -R nextjs:nodejs /data/db +exec gosu nextjs "$@" diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig; diff --git a/package.json b/package.json index 38c9ec9..db6f927 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "next build", "start": "next start", "lint": "eslint", + "typecheck": "tsc --noEmit", "db:generate": "prisma generate", "db:push": "prisma db push", "db:seed": "tsx prisma/seed.ts", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2c7d121..d393632 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,7 +2,8 @@ // SQLite for local dev; switch to Postgres for production generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } datasource db { diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index ebe7fcc..659cbb5 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -13,7 +13,7 @@ const MOCK_ADMIN = { export async function POST(req: NextRequest) { try { const body = await req.json(); - const { email, password } = body; + const { email } = body; // Accept any email for MVP; in prod validate against DB if (!email) { diff --git a/src/app/evaluations/[id]/page.tsx b/src/app/evaluations/[id]/page.tsx index 776bcf6..bd1e453 100644 --- a/src/app/evaluations/[id]/page.tsx +++ b/src/app/evaluations/[id]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { CandidateForm } from "@/components/CandidateForm"; import { DimensionCard } from "@/components/DimensionCard"; @@ -70,10 +71,10 @@ export default function EvaluationDetailPage() { if (evalData?.template?.dimensions?.length > 0 && Array.isArray(templatesData)) { const tmpl = templatesData.find((t: { id: string }) => t.id === evalData.templateId); if (tmpl?.dimensions?.length) { - const dimMap = new Map(tmpl.dimensions.map((d: { id: string }) => [d.id, d])); + const dimMap = new Map(tmpl.dimensions.map((d: { id: string; suggestedQuestions?: string | null }) => [d.id, d])); evalData.template.dimensions = evalData.template.dimensions.map((d: { id: string; suggestedQuestions?: string | null }) => ({ ...d, - suggestedQuestions: d.suggestedQuestions ?? dimMap.get(d.id)?.suggestedQuestions, + suggestedQuestions: d.suggestedQuestions ?? (dimMap.get(d.id) as { suggestedQuestions?: string | null } | undefined)?.suggestedQuestions, })); } } @@ -187,9 +188,9 @@ export default function EvaluationDetailPage() { return (