# FoamKing Delivery Cleanup Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Prepare the FoamKing calculator for zip delivery — restructure routes under `/intern/*`, replace DB-based auth with env-based single admin user, clean up the database layer, strip debug code, harden security, write setup docs, and create a build script. **Architecture:** Next.js app with SQLite (better-sqlite3). Auth simplified from DB-backed bcrypt sessions to env-based credentials with in-memory session Map. All protected routes consolidated under `/intern/*`. Build script produces a clean zip with no git history, no docs, no secrets. **Tech Stack:** Next.js 16, TypeScript, SQLite (better-sqlite3), Tailwind CSS, shadcn/ui, nodemailer **Design doc:** `docs/plans/2026-02-22-delivery-cleanup-design.md` --- ### Task 1: Rewrite lib/auth.ts — env-based auth with swappable interface **Files:** - Rewrite: `lib/auth.ts` **Step 1: Rewrite lib/auth.ts** Replace the entire file with: ```typescript import { cookies } from "next/headers" const SESSION_COOKIE_NAME = "session" const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days // In-memory session store. Replace this with your own session/token system. const sessions = new Map() function generateSessionId(): string { const bytes = new Uint8Array(32) crypto.getRandomValues(bytes) return Array.from(bytes) .map((b) => b.toString(16).padStart(2, "0")) .join("") } // ─── AUTH INTERFACE ──────────────────────────────────────────────── // To integrate your own auth (JWT, OAuth, etc.), replace these functions. // The rest of the application only calls checkAuth(), login(), and logout(). // ─────────────────────────────────────────────────────────────────── /** * Check if the current request is authenticated. * Replace this function to integrate JWT, OAuth, or any other auth system. */ export async function checkAuth(): Promise<{ authenticated: boolean; email?: string }> { const cookieStore = await cookies() const sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value if (!sessionId) return { authenticated: false } const session = sessions.get(sessionId) if (!session) return { authenticated: false } if (session.expiresAt < new Date()) { sessions.delete(sessionId) return { authenticated: false } } return { authenticated: true, email: session.email } } /** * Authenticate with email and password. * Default: compares against ADMIN_EMAIL/ADMIN_PASSWORD env vars. * Replace this function to integrate your own auth system. */ export async function login( email: string, password: string ): Promise<{ success: true } | { success: false; error: string }> { const adminEmail = process.env.ADMIN_EMAIL const adminPassword = process.env.ADMIN_PASSWORD if (!adminEmail || !adminPassword) { return { success: false, error: "Admin-konfiguration mangler (ADMIN_EMAIL/ADMIN_PASSWORD)" } } if (email !== adminEmail || password !== adminPassword) { return { success: false, error: "Forkert email eller adgangskode" } } const sessionId = generateSessionId() const expiresAt = new Date(Date.now() + SESSION_DURATION_MS) sessions.set(sessionId, { email, expiresAt }) const cookieStore = await cookies() cookieStore.set(SESSION_COOKIE_NAME, sessionId, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", expires: expiresAt, path: "/", }) return { success: true } } /** * Log out the current session. */ export async function logout(): Promise { const cookieStore = await cookies() const sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value if (sessionId) { sessions.delete(sessionId) cookieStore.delete(SESSION_COOKIE_NAME) } } ``` **Step 2: Verify the app still compiles** Run: `npm run build` Expected: Build errors related to removed DB imports (this is expected — we fix them in later tasks) **Step 3: Commit** ```bash git add lib/auth.ts git commit -m "refactor: replace DB-based auth with env-based single admin user" ``` --- ### Task 2: Rewrite lib/db.ts — clean schema, remove users/sessions **Files:** - Rewrite: `lib/db.ts` **Step 1: Rewrite lib/db.ts** Replace the entire file. Remove users table, sessions table, all user/session functions, and migration hacks. Keep only quotes functionality: ```typescript import Database from "better-sqlite3" import path from "path" import fs from "fs" const DB_PATH = path.join(process.cwd(), "data", "quotes.db") // Ensure data directory exists const dataDir = path.dirname(DB_PATH) if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) } const db = new Database(DB_PATH) // Enable WAL mode for better concurrent read performance db.pragma("journal_mode = WAL") // Create schema if not exists db.exec(` CREATE TABLE IF NOT EXISTS quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, postal_code TEXT NOT NULL, address TEXT, area REAL NOT NULL, height REAL NOT NULL, include_floor_heating INTEGER DEFAULT 1, flooring_type TEXT DEFAULT 'STANDARD', customer_name TEXT NOT NULL, customer_email TEXT NOT NULL, customer_phone TEXT NOT NULL, remarks TEXT, total_excl_vat REAL NOT NULL, total_incl_vat REAL NOT NULL, status TEXT DEFAULT 'new', created_at TEXT DEFAULT CURRENT_TIMESTAMP, email_opened_at TEXT ) `) // Ensure quote IDs start at 1000 const countResult = db.prepare("SELECT COUNT(*) as count FROM quotes").get() as { count: number } if (countResult.count === 0) { try { const seqExists = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'quotes'").get() if (!seqExists) { db.exec( "INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)" ) db.exec("DELETE FROM quotes WHERE id = 999") } } catch { // sqlite_sequence may not exist yet on fresh DB — that's fine } } // ─── Types ───────────────────────────────────────────────────────── export interface QuoteInput { postalCode: string address?: string area: number height: number includeFloorHeating: boolean flooringType: string customerName: string customerEmail: string customerPhone: string remarks?: string totalExclVat: number totalInclVat: number } export type QuoteStatus = "new" | "contacted" | "accepted" | "rejected" export interface StoredQuote extends QuoteInput { id: number status: QuoteStatus createdAt: string emailOpenedAt: string | null } // ─── Queries ─────────────────────────────────────────────────────── export function saveQuote(quote: QuoteInput): { id: number; slug: string } { const stmt = db.prepare(` INSERT INTO quotes ( postal_code, address, area, height, include_floor_heating, flooring_type, customer_name, customer_email, customer_phone, remarks, total_excl_vat, total_incl_vat ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) const result = stmt.run( quote.postalCode, quote.address || null, quote.area, quote.height, quote.includeFloorHeating ? 1 : 0, quote.flooringType, quote.customerName, quote.customerEmail, quote.customerPhone, quote.remarks || null, quote.totalExclVat, quote.totalInclVat ) const id = result.lastInsertRowid as number const slug = `${quote.postalCode}-${id}` return { id, slug } } export function getQuoteBySlug(slug: string): StoredQuote | null { const match = slug.match(/^(\d{4})-(\d+)$/) if (!match) return null const [, postalCode, idStr] = match const id = parseInt(idStr, 10) const row = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?").get(id, postalCode) as any if (!row) return null return rowToQuote(row) } export function getQuoteById(id: number): StoredQuote | null { const row = db.prepare("SELECT * FROM quotes WHERE id = ?").get(id) as any if (!row) return null return rowToQuote(row) } export function getAllQuotes(): StoredQuote[] { const rows = db.prepare("SELECT * FROM quotes ORDER BY id DESC").all() as any[] return rows.map(rowToQuote) } export function updateQuoteStatus(id: number, status: QuoteStatus): boolean { const result = db.prepare("UPDATE quotes SET status = ? WHERE id = ?").run(status, id) return result.changes > 0 } export function markEmailOpened(id: number): boolean { const result = db.prepare( "UPDATE quotes SET email_opened_at = ? WHERE id = ? AND email_opened_at IS NULL" ).run(new Date().toISOString(), id) return result.changes > 0 } function rowToQuote(row: any): StoredQuote { return { id: row.id, postalCode: row.postal_code, address: row.address, area: row.area, height: row.height, includeFloorHeating: row.include_floor_heating === 1, flooringType: row.flooring_type, customerName: row.customer_name, customerEmail: row.customer_email, customerPhone: row.customer_phone, remarks: row.remarks, totalExclVat: row.total_excl_vat, totalInclVat: row.total_incl_vat, status: row.status || "new", createdAt: row.created_at, emailOpenedAt: row.email_opened_at || null, } } ``` **Step 2: Commit** ```bash git add lib/db.ts git commit -m "refactor: clean DB schema, remove users/sessions tables" ``` --- ### Task 3: Delete /api/auth/setup, update login and logout routes **Files:** - Delete: `app/api/auth/setup/route.ts` - Modify: `app/api/auth/login/route.ts` - Modify: `app/api/auth/logout/route.ts` **Step 1: Delete the setup endpoint** ```bash rm app/api/auth/setup/route.ts ``` **Step 2: Rewrite login route** Replace `app/api/auth/login/route.ts` with: ```typescript import { NextRequest, NextResponse } from "next/server" import { login } from "@/lib/auth" export async function POST(request: NextRequest) { try { const { email, password } = await request.json() if (!email || !password) { return NextResponse.json({ error: "Email og adgangskode er påkrævet" }, { status: 400 }) } const result = await login(email, password) if (!result.success) { return NextResponse.json({ error: result.error }, { status: 401 }) } return NextResponse.json({ success: true }) } catch { return NextResponse.json({ error: "Der opstod en fejl" }, { status: 500 }) } } ``` **Step 3: Rewrite logout route** Replace `app/api/auth/logout/route.ts` with: ```typescript import { NextResponse } from "next/server" import { logout } from "@/lib/auth" export async function POST() { try { await logout() return NextResponse.json({ success: true }) } catch { return NextResponse.json({ error: "Der opstod en fejl" }, { status: 500 }) } } ``` **Step 4: Commit** ```bash git add -A app/api/auth/ git commit -m "refactor: simplify auth routes, remove setup endpoint" ``` --- ### Task 4: Move routes — /admin → /intern/beregner, /dashboard → /intern, /historik → /intern/historik, /login → /intern/login **Files:** - Move: `app/admin/page.tsx` → `app/intern/beregner/page.tsx` - Move: `app/dashboard/page.tsx` → `app/intern/page.tsx` - Move: `app/historik/page.tsx` → `app/intern/historik/page.tsx` - Move: `app/login/page.tsx` → `app/intern/login/page.tsx` **Step 1: Create new directories and move files** ```bash mkdir -p app/intern/beregner app/intern/historik app/intern/login mv app/admin/page.tsx app/intern/beregner/page.tsx mv app/dashboard/page.tsx app/intern/page.tsx mv app/historik/page.tsx app/intern/historik/page.tsx mv app/login/page.tsx app/intern/login/page.tsx rmdir app/admin app/dashboard app/historik app/login ``` **Step 2: Update all internal links in moved files** In `app/intern/page.tsx` (was dashboard): - Change `router.push("/login")` → `router.push("/intern/login")` - Change `` → `` In `app/intern/historik/page.tsx` (was historik): - Change `router.push("/login")` → `router.push("/intern/login")` - Change `` → `` In `app/intern/login/page.tsx` (was login): - Change `fetch("/api/auth/login"` — keep as-is (API routes don't move) - Change `router.push("/dashboard")` → `router.push("/intern")` In `app/intern/beregner/page.tsx` (was admin): - No route references to update **Step 3: Update middleware.ts** Replace entire file: ```typescript import { NextResponse } from "next/server" import type { NextRequest } from "next/server" export function middleware(request: NextRequest) { const { pathname } = request.nextUrl // Don't protect the login page if (pathname === "/intern/login") { return NextResponse.next() } // Check for session cookie on all /intern/* routes const sessionCookie = request.cookies.get("session") if (!sessionCookie?.value) { const loginUrl = new URL("/intern/login", request.url) loginUrl.searchParams.set("redirect", pathname) return NextResponse.redirect(loginUrl) } return NextResponse.next() } export const config = { matcher: ["/intern/:path*"], } ``` **Step 4: Commit** ```bash git add -A git commit -m "refactor: consolidate protected routes under /intern/*" ``` --- ### Task 5: Strip debug code — console.logs, dev toggles, TODO markers **Files:** - Modify: `app/intern/beregner/page.tsx` — remove console.log and "Admin Mode" toggle reference if not needed publicly - Modify: `app/intern/page.tsx` — remove console.error - Modify: `app/intern/historik/page.tsx` — remove console.error - Modify: `app/api/quotes/route.ts` — remove console.error - Modify: `app/api/quote-request/route.ts` — remove console.error **Step 1: Remove all console.log/console.error statements** In each file listed above, find and remove all `console.log(...)` and `console.error(...)` lines. In catch blocks, keep the error handling logic but remove the console output. Known locations: - `app/intern/beregner/page.tsx:39` — `console.log("Quote request (test mode):", {...})` - `app/intern/page.tsx:49` — `console.error("Failed to fetch quotes:", error)` - `app/intern/page.tsx:90` — `console.error("Logout failed:", error)` - `app/intern/historik/page.tsx:51` — `console.error("Failed to fetch quotes:", error)` - `app/intern/historik/page.tsx:63` — `console.error("Logout failed:", error)` - `app/api/quote-request/route.ts:300` — `console.error("Quote request error:", error)` - `app/api/quotes/route.ts:15` — `console.error("Get quotes error:", error)` - `app/api/quotes/route.ts:45` — `console.error("Update quote error:", error)` **Step 2: Commit** ```bash git add -A git commit -m "chore: strip debug console.log/error statements" ``` --- ### Task 6: Add rate limiting to API routes **Files:** - Create: `lib/rate-limit.ts` - Modify: `app/api/auth/login/route.ts` - Modify: `app/api/quote-request/route.ts` **Step 1: Create lib/rate-limit.ts** ```typescript // Simple in-memory rate limiter. No external dependencies. // Tracks requests per IP with a sliding window. interface RateLimitEntry { count: number resetAt: number } const store = new Map() // Clean up expired entries periodically setInterval(() => { const now = Date.now() for (const [key, entry] of store) { if (entry.resetAt < now) store.delete(key) } }, 60_000) /** * Check if a request should be rate-limited. * @param key - Unique identifier (e.g. IP address) * @param limit - Max requests per window * @param windowMs - Time window in milliseconds * @returns true if the request is allowed, false if rate-limited */ export function rateLimit(key: string, limit: number, windowMs: number): boolean { const now = Date.now() const entry = store.get(key) if (!entry || entry.resetAt < now) { store.set(key, { count: 1, resetAt: now + windowMs }) return true } if (entry.count >= limit) { return false } entry.count++ return true } ``` **Step 2: Add rate limiting to login route** In `app/api/auth/login/route.ts`, add at the top of the POST handler: ```typescript import { rateLimit } from "@/lib/rate-limit" // Inside POST handler, before processing: const ip = request.headers.get("x-forwarded-for") || "unknown" if (!rateLimit(ip, 5, 60_000)) { return NextResponse.json({ error: "For mange forsøg. Prøv igen om lidt." }, { status: 429 }) } ``` **Step 3: Add rate limiting to quote-request route** In `app/api/quote-request/route.ts`, add at the top of the POST handler: ```typescript import { rateLimit } from "@/lib/rate-limit" // Inside POST handler, before processing: const ip = request.headers.get("x-forwarded-for") || "unknown" if (!rateLimit(ip, 10, 60_000)) { return NextResponse.json({ error: "For mange anmodninger. Prøv igen om lidt." }, { status: 429 }) } ``` **Step 4: Commit** ```bash git add lib/rate-limit.ts app/api/auth/login/route.ts app/api/quote-request/route.ts git commit -m "feat: add rate limiting to login and quote-request endpoints" ``` --- ### Task 7: Create npm run setup seed script **Files:** - Create: `scripts/setup.js` - Modify: `package.json` — add `"setup"` script **Step 1: Create scripts/setup.js** ```javascript #!/usr/bin/env node const Database = require("better-sqlite3") const path = require("path") const fs = require("fs") const DB_PATH = path.join(process.cwd(), "data", "quotes.db") const dataDir = path.dirname(DB_PATH) console.log("FoamKing Prisberegner - Database Setup") console.log("======================================\n") // Create data directory if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) console.log("Created data/ directory") } // Check if DB already exists const dbExists = fs.existsSync(DB_PATH) if (dbExists) { console.log("Database already exists at", DB_PATH) console.log("Delete data/quotes.db to start fresh.\n") process.exit(0) } const db = new Database(DB_PATH) db.pragma("journal_mode = WAL") db.exec(` CREATE TABLE quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, postal_code TEXT NOT NULL, address TEXT, area REAL NOT NULL, height REAL NOT NULL, include_floor_heating INTEGER DEFAULT 1, flooring_type TEXT DEFAULT 'STANDARD', customer_name TEXT NOT NULL, customer_email TEXT NOT NULL, customer_phone TEXT NOT NULL, remarks TEXT, total_excl_vat REAL NOT NULL, total_incl_vat REAL NOT NULL, status TEXT DEFAULT 'new', created_at TEXT DEFAULT CURRENT_TIMESTAMP, email_opened_at TEXT ) `) // Set auto-increment to start at 1000 db.exec( "INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)" ) db.exec("DELETE FROM quotes WHERE id = 999") db.close() console.log("Database created at", DB_PATH) console.log("Quote IDs will start at 1000.\n") console.log("Next steps:") console.log(" 1. Configure .env.local (see SETUP.md / OPSÆTNING.md)") console.log(" 2. npm run build") console.log(" 3. npm start") ``` **Step 2: Add setup script to package.json** Add to `"scripts"`: ```json "setup": "node scripts/setup.js" ``` **Step 3: Commit** ```bash git add scripts/setup.js package.json git commit -m "feat: add npm run setup seed script for database initialization" ``` --- ### Task 8: Update .env.example **Files:** - Rewrite: `.env.example` **Step 1: Replace .env.example** ```bash # ─── Admin Login ──────────────────────────────────────────────────── ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=changeme # ─── Email (SMTP) ────────────────────────────────────────────────── # Office 365: smtp.office365.com, port 587 # Requires SMTP AUTH enabled for the sending account in Exchange Admin Center. # See SETUP.md / OPSÆTNING.md for instructions. SMTP_HOST=smtp.office365.com SMTP_PORT=587 SMTP_USER=tilbud@foamking.dk SMTP_PASS=your_password_here EMAIL_FROM_NAME=Foam King Prisberegner EMAIL_TO=info@foamking.dk # ─── Base URL ─────────────────────────────────────────────────────── NEXT_PUBLIC_BASE_URL=https://beregner.foamking.dk # ─── Distance Calculation (Optional) ─────────────────────────────── # Without this key, distances are calculated from a built-in postal code table. # Get a free key at https://openrouteservice.org/dev/#/signup (2,000 requests/day) # OPENROUTE_API_KEY=your_api_key_here ``` **Step 2: Commit** ```bash git add .env.example git commit -m "docs: update .env.example with Office 365 SMTP and admin config" ``` --- ### Task 9: Remove bcrypt dependency from package.json **Files:** - Modify: `package.json` **Step 1: Remove bcrypt** ```bash npm uninstall bcrypt @types/bcrypt ``` **Step 2: Verify build** ```bash npm run build ``` Expected: Should compile successfully if all auth references are updated. **Step 3: Commit** ```bash git add package.json package-lock.json git commit -m "chore: remove bcrypt dependency (auth is now env-based)" ``` --- ### Task 10: Write SETUP.md and OPSÆTNING.md **Files:** - Create: `SETUP.md` - Create: `OPSÆTNING.md` **Step 1: Create SETUP.md** ```markdown # FoamKing Prisberegner — Setup Guide ## Prerequisites - Node.js 18+ (recommended: 20 LTS) - npm ## Installation 1. **Install dependencies:** ```bash npm install ``` 2. **Configure environment:** Copy the example config and edit it: ```bash cp .env.example .env.local ``` Edit `.env.local` with your values. See the configuration section below. 3. **Initialize database:** ```bash npm run setup ``` 4. **Build and start:** ```bash npm run build npm start ``` The application runs on port 3001 by default. ## Configuration (.env.local) ### Admin Login (Required) ``` ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=your-secure-password ``` These credentials are used to log in at `/intern/login`. ### Email — Office 365 SMTP (Required) ``` SMTP_HOST=smtp.office365.com SMTP_PORT=587 SMTP_USER=tilbud@foamking.dk SMTP_PASS=your-password EMAIL_FROM_NAME=Foam King Prisberegner EMAIL_TO=info@foamking.dk ``` **Important:** SMTP AUTH must be enabled for the sending account (`tilbud@foamking.dk`) in your Office 365 / Exchange Admin Center: 1. Go to [Exchange Admin Center](https://admin.exchange.microsoft.com) 2. Navigate to **Recipients** → **Mailboxes** 3. Select the `tilbud@foamking.dk` mailbox 4. Click **Manage email apps settings** (or **Mail flow** → **Email apps**) 5. Enable **Authenticated SMTP** (SMTP AUTH) 6. Save If your organization uses Security Defaults or Conditional Access policies that block legacy auth, you may need to create an exclusion for this account, or use an App Password if MFA is enabled. ### Base URL (Required) ``` NEXT_PUBLIC_BASE_URL=https://beregner.foamking.dk ``` Used for generating links in emails (e.g., quote tracking links). ### Distance Calculation (Optional) ``` OPENROUTE_API_KEY=your_api_key_here ``` Without this key, transport distances are calculated from a built-in postal code table (sufficient for price estimates). With the key, actual driving distances from OpenRouteService are used. Get a free key at [openrouteservice.org](https://openrouteservice.org/dev/#/signup) (2,000 requests/day). ## Routes | Route | Description | |-------|-------------| | `/` | Public price calculator | | `/tilbud/[id]` | Public quote view (linked from email) | | `/intern` | Admin dashboard — quote management | | `/intern/historik` | Admin — quote history | | `/intern/beregner` | Admin — detailed calculator with price breakdown | | `/intern/login` | Admin login page | ## Custom Authentication The default auth uses `ADMIN_EMAIL`/`ADMIN_PASSWORD` environment variables with in-memory sessions. To integrate your own auth system (JWT, OAuth, SSO, etc.), edit `lib/auth.ts`. The application only uses three functions: - `checkAuth()` — returns `{ authenticated: boolean; email?: string }` - `login(email, password)` — authenticates and sets session - `logout()` — clears session Replace these functions with your own implementation. The middleware in `middleware.ts` checks for a `session` cookie — update this if your auth uses a different mechanism (e.g., Bearer tokens). ## Development ```bash npm run dev # Start dev server (port 3001) npm run build # Production build npm start # Start production server npm run lint # Run ESLint npm run setup # Initialize database ``` ``` **Step 2: Create OPSÆTNING.md** Write the Danish version with the same structure and content, translated to Danish. **Step 3: Commit** ```bash git add SETUP.md OPSÆTNING.md git commit -m "docs: add setup guides in English and Danish" ``` --- ### Task 11: Create scripts/build-release.sh **Files:** - Create: `scripts/build-release.sh` **Step 1: Create the build script** ```bash #!/usr/bin/env bash set -euo pipefail TIMESTAMP=$(date +%Y%m%d-%H%M%S) DATE=$(date +%Y-%m-%d) RELEASE_DIR="/tmp/foamking-release-${TIMESTAMP}" ZIP_NAME="foamking-beregner-${DATE}.zip" PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" echo "FoamKing Release Builder" echo "========================" echo "" # Create temp directory mkdir -p "${RELEASE_DIR}" echo "Build directory: ${RELEASE_DIR}" # Copy shipping files echo "Copying files..." cp -r "${PROJECT_DIR}/app" "${RELEASE_DIR}/" cp -r "${PROJECT_DIR}/components" "${RELEASE_DIR}/" cp -r "${PROJECT_DIR}/lib" "${RELEASE_DIR}/" cp -r "${PROJECT_DIR}/public" "${RELEASE_DIR}/" mkdir -p "${RELEASE_DIR}/scripts" cp "${PROJECT_DIR}/scripts/setup.js" "${RELEASE_DIR}/scripts/" cp "${PROJECT_DIR}/middleware.ts" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/package.json" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/tsconfig.json" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/tailwind.config.ts" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/postcss.config.mjs" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/eslint.config.mjs" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/next.config.ts" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/.env.example" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/SETUP.md" "${RELEASE_DIR}/" cp "${PROJECT_DIR}/OPSÆTNING.md" "${RELEASE_DIR}/" # Copy .prettierrc if it exists [ -f "${PROJECT_DIR}/.prettierrc.json" ] && cp "${PROJECT_DIR}/.prettierrc.json" "${RELEASE_DIR}/" # Verify build echo "" echo "Installing dependencies..." cd "${RELEASE_DIR}" npm install --ignore-scripts 2>&1 | tail -1 echo "Verifying build..." npm run build 2>&1 | tail -3 # Clean up build artifacts echo "" echo "Cleaning up..." rm -rf "${RELEASE_DIR}/node_modules" rm -rf "${RELEASE_DIR}/.next" # Create zip cd /tmp zip -rq "${ZIP_NAME}" "foamking-release-${TIMESTAMP}" echo "" echo "Release built successfully!" echo "Zip: /tmp/${ZIP_NAME}" echo "Size: $(du -h "/tmp/${ZIP_NAME}" | cut -f1)" ``` **Step 2: Make executable** ```bash chmod +x scripts/build-release.sh ``` **Step 3: Test the build script** ```bash ./scripts/build-release.sh ``` Expected: Creates a zip at `/tmp/foamking-beregner-.zip` **Step 4: Commit** ```bash git add scripts/build-release.sh git commit -m "feat: add release build script for zip delivery" ``` --- ### Task 12: Final verification — build, verify no secrets, test zip **Step 1: Delete the existing data/quotes.db to test fresh setup** ```bash rm -rf data/ ``` **Step 2: Run the full flow** ```bash npm run setup npm run build ``` Expected: Both succeed. **Step 3: Verify no secrets in tracked files** Search for known credential patterns: ```bash grep -r "Foamking1066\|sk-proj-\|eyJvcm" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.md" . ``` Expected: No matches (only in .env.local which is gitignored). **Step 4: Run the build-release script** ```bash ./scripts/build-release.sh ``` Expected: Clean zip produced. **Step 5: Final commit if any fixes needed, then tag** ```bash git tag -a v1.0.0 -m "Release v1.0.0 - delivery-ready" ```