12-task plan covering route restructure, auth simplification, DB cleanup, security hardening, debug stripping, setup docs, and build script for zip delivery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
28 KiB
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:
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<string, { email: string; expiresAt: Date }>()
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<void> {
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
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:
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
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
rm app/api/auth/setup/route.ts
Step 2: Rewrite login route
Replace app/api/auth/login/route.ts with:
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:
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
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
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
<Link href="/historik">→<Link href="/intern/historik">
In app/intern/historik/page.tsx (was historik):
- Change
router.push("/login")→router.push("/intern/login") - Change
<Link href="/dashboard">→<Link href="/intern">
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:
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
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
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
// 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<string, RateLimitEntry>()
// 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:
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:
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
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
#!/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":
"setup": "node scripts/setup.js"
Step 3: Commit
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
# ─── 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
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
npm uninstall bcrypt @types/bcrypt
Step 2: Verify build
npm run build
Expected: Should compile successfully if all auth references are updated.
Step 3: Commit
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
# FoamKing Prisberegner — Setup Guide
## Prerequisites
- Node.js 18+ (recommended: 20 LTS)
- npm
## Installation
1. **Install dependencies:**
```bash
npm install
-
Configure environment:
Copy the example config and edit it:
cp .env.example .env.localEdit
.env.localwith your values. See the configuration section below. -
Initialize database:
npm run setup -
Build and start:
npm run build npm startThe 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:
- Go to Exchange Admin Center
- Navigate to Recipients → Mailboxes
- Select the
tilbud@foamking.dkmailbox - Click Manage email apps settings (or Mail flow → Email apps)
- Enable Authenticated SMTP (SMTP AUTH)
- 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 (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 sessionlogout()— 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
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
#!/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
chmod +x scripts/build-release.sh
Step 3: Test the build script
./scripts/build-release.sh
Expected: Creates a zip at /tmp/foamking-beregner-<date>.zip
Step 4: Commit
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
rm -rf data/
Step 2: Run the full flow
npm run setup
npm run build
Expected: Both succeed.
Step 3: Verify no secrets in tracked files
Search for known credential patterns:
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
./scripts/build-release.sh
Expected: Clean zip produced.
Step 5: Final commit if any fixes needed, then tag
git tag -a v1.0.0 -m "Release v1.0.0 - delivery-ready"