Refactor Docker entrypoint to run Prisma migrations; update schema

Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
Cursor Agent
2026-01-12 15:08:23 +00:00
parent 80f57184c7
commit f1cc398248
7 changed files with 291 additions and 250 deletions

View File

@@ -66,6 +66,7 @@ RUN adduser --system --uid 1001 nextjs
# Copy the built application
COPY --from=builder /app/public ./public
COPY --from=builder /app/scripts ./scripts
# Set the correct permission for prerender cache
RUN mkdir .next
@@ -82,6 +83,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy Prisma files
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/prisma ./node_modules/prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
# Note: Environment variables should be passed via docker-compose or runtime environment
# DO NOT copy .env files into the image for security reasons
@@ -97,4 +100,4 @@ ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
CMD ["node", "scripts/start-with-migrate.js"]

View File

@@ -0,0 +1,246 @@
-- CreateEnum
CREATE TYPE "ContentStatus" AS ENUM ('DRAFT', 'PUBLISHED');
-- CreateEnum
CREATE TYPE "Difficulty" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT');
-- CreateEnum
CREATE TYPE "InteractionType" AS ENUM ('LIKE', 'SHARE', 'BOOKMARK', 'COMMENT');
-- CreateTable
CREATE TABLE "Project" (
"id" SERIAL NOT NULL,
"slug" VARCHAR(255) NOT NULL,
"defaultLocale" VARCHAR(10) NOT NULL DEFAULT 'en',
"title" VARCHAR(255) NOT NULL,
"description" TEXT NOT NULL,
"content" TEXT NOT NULL,
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
"featured" BOOLEAN NOT NULL DEFAULT false,
"category" VARCHAR(100) NOT NULL,
"date" VARCHAR(10) NOT NULL,
"github" VARCHAR(500),
"live" VARCHAR(500),
"published" BOOLEAN NOT NULL DEFAULT true,
"imageUrl" VARCHAR(500),
"metaDescription" TEXT,
"keywords" TEXT,
"ogImage" VARCHAR(500),
"schema" JSONB,
"difficulty" "Difficulty" NOT NULL DEFAULT 'INTERMEDIATE',
"timeToComplete" VARCHAR(100),
"technologies" TEXT[] DEFAULT ARRAY[]::TEXT[],
"challenges" TEXT[] DEFAULT ARRAY[]::TEXT[],
"lessonsLearned" TEXT[] DEFAULT ARRAY[]::TEXT[],
"futureImprovements" TEXT[] DEFAULT ARRAY[]::TEXT[],
"demoVideo" VARCHAR(500),
"screenshots" TEXT[] DEFAULT ARRAY[]::TEXT[],
"colorScheme" VARCHAR(100) NOT NULL DEFAULT 'Dark',
"accessibility" BOOLEAN NOT NULL DEFAULT true,
"performance" JSONB NOT NULL DEFAULT '{"loadTime": "1.5s", "bundleSize": "50KB", "lighthouse": 90}',
"analytics" JSONB NOT NULL DEFAULT '{"likes": 0, "views": 0, "shares": 0}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "project_translations" (
"id" SERIAL NOT NULL,
"project_id" INTEGER NOT NULL,
"locale" VARCHAR(10) NOT NULL,
"title" VARCHAR(255) NOT NULL,
"description" TEXT NOT NULL,
"content" JSONB,
"metaDescription" TEXT,
"keywords" TEXT,
"ogImage" VARCHAR(500),
"schema" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "project_translations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "content_pages" (
"id" SERIAL NOT NULL,
"key" VARCHAR(100) NOT NULL,
"status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "content_pages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "content_page_translations" (
"id" SERIAL NOT NULL,
"page_id" INTEGER NOT NULL,
"locale" VARCHAR(10) NOT NULL,
"title" TEXT,
"slug" VARCHAR(255),
"content" JSONB NOT NULL,
"metaDescription" TEXT,
"keywords" TEXT,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "content_page_translations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "site_settings" (
"id" INTEGER NOT NULL DEFAULT 1,
"defaultLocale" VARCHAR(10) NOT NULL DEFAULT 'en',
"locales" TEXT[] DEFAULT ARRAY['en', 'de']::TEXT[],
"theme" JSONB,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "site_settings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PageView" (
"id" SERIAL NOT NULL,
"project_id" INTEGER,
"page" VARCHAR(100) NOT NULL,
"ip" VARCHAR(45),
"user_agent" TEXT,
"referrer" VARCHAR(500),
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PageView_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserInteraction" (
"id" SERIAL NOT NULL,
"project_id" INTEGER NOT NULL,
"type" "InteractionType" NOT NULL,
"ip" VARCHAR(45),
"user_agent" TEXT,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserInteraction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Contact" (
"id" SERIAL NOT NULL,
"name" VARCHAR(255) NOT NULL,
"email" VARCHAR(255) NOT NULL,
"subject" VARCHAR(500) NOT NULL,
"message" TEXT NOT NULL,
"responded" BOOLEAN NOT NULL DEFAULT false,
"response_template" VARCHAR(50),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Contact_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "activity_status" (
"id" INTEGER NOT NULL DEFAULT 1,
"activity_type" VARCHAR(50),
"activity_details" VARCHAR(255),
"activity_project" VARCHAR(255),
"activity_language" VARCHAR(50),
"activity_repo" VARCHAR(500),
"music_playing" BOOLEAN NOT NULL DEFAULT false,
"music_track" VARCHAR(255),
"music_artist" VARCHAR(255),
"music_album" VARCHAR(255),
"music_platform" VARCHAR(50),
"music_progress" INTEGER,
"music_album_art" VARCHAR(500),
"watching_title" VARCHAR(255),
"watching_platform" VARCHAR(50),
"watching_type" VARCHAR(50),
"gaming_game" VARCHAR(255),
"gaming_platform" VARCHAR(50),
"gaming_status" VARCHAR(50),
"status_mood" VARCHAR(50),
"status_message" VARCHAR(500),
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "activity_status_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug");
-- CreateIndex
CREATE INDEX "Project_category_idx" ON "Project"("category");
-- CreateIndex
CREATE INDEX "Project_featured_idx" ON "Project"("featured");
-- CreateIndex
CREATE INDEX "Project_published_idx" ON "Project"("published");
-- CreateIndex
CREATE INDEX "Project_difficulty_idx" ON "Project"("difficulty");
-- CreateIndex
CREATE INDEX "Project_created_at_idx" ON "Project"("created_at");
-- CreateIndex
CREATE INDEX "Project_tags_idx" ON "Project"("tags");
-- CreateIndex
CREATE INDEX "project_translations_locale_idx" ON "project_translations"("locale");
-- CreateIndex
CREATE INDEX "project_translations_project_id_idx" ON "project_translations"("project_id");
-- CreateIndex
CREATE UNIQUE INDEX "project_translations_project_id_locale_key" ON "project_translations"("project_id", "locale");
-- CreateIndex
CREATE UNIQUE INDEX "content_pages_key_key" ON "content_pages"("key");
-- CreateIndex
CREATE INDEX "content_page_translations_locale_idx" ON "content_page_translations"("locale");
-- CreateIndex
CREATE INDEX "content_page_translations_slug_idx" ON "content_page_translations"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "content_page_translations_page_id_locale_key" ON "content_page_translations"("page_id", "locale");
-- CreateIndex
CREATE INDEX "PageView_project_id_idx" ON "PageView"("project_id");
-- CreateIndex
CREATE INDEX "PageView_timestamp_idx" ON "PageView"("timestamp");
-- CreateIndex
CREATE INDEX "PageView_page_idx" ON "PageView"("page");
-- CreateIndex
CREATE INDEX "UserInteraction_project_id_idx" ON "UserInteraction"("project_id");
-- CreateIndex
CREATE INDEX "UserInteraction_type_idx" ON "UserInteraction"("type");
-- CreateIndex
CREATE INDEX "UserInteraction_timestamp_idx" ON "UserInteraction"("timestamp");
-- CreateIndex
CREATE INDEX "Contact_email_idx" ON "Contact"("email");
-- CreateIndex
CREATE INDEX "Contact_responded_idx" ON "Contact"("responded");
-- CreateIndex
CREATE INDEX "Contact_created_at_idx" ON "Contact"("created_at");
-- AddForeignKey
ALTER TABLE "project_translations" ADD CONSTRAINT "project_translations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "content_page_translations" ADD CONSTRAINT "content_page_translations_page_id_fkey" FOREIGN KEY ("page_id") REFERENCES "content_pages"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,127 +0,0 @@
# Database Migrations
This directory contains SQL migration scripts for manual database updates.
## Running Migrations
### Method 1: Using psql (Recommended)
```bash
# Connect to your database
psql -d portfolio -f prisma/migrations/create_activity_status.sql
# Or with connection string
psql "postgresql://user:password@localhost:5432/portfolio" -f prisma/migrations/create_activity_status.sql
```
### Method 2: Using Docker
```bash
# If your database is in Docker
docker exec -i postgres_container psql -U username -d portfolio < prisma/migrations/create_activity_status.sql
```
### Method 3: Using pgAdmin or Database GUI
1. Open pgAdmin or your database GUI
2. Connect to your `portfolio` database
3. Open Query Tool
4. Copy and paste the contents of `create_activity_status.sql`
5. Execute the query
## Verifying Migration
After running the migration, verify it was successful:
```bash
# Check if table exists
psql -d portfolio -c "\dt activity_status"
# View table structure
psql -d portfolio -c "\d activity_status"
# Check if default row was inserted
psql -d portfolio -c "SELECT * FROM activity_status;"
```
Expected output:
```
id | activity_type | ... | updated_at
----+---------------+-----+---------------------------
1 | | ... | 2024-01-15 10:30:00+00
```
## Migration: create_activity_status.sql
**Purpose**: Creates the `activity_status` table for n8n activity feed integration.
**What it does**:
- Creates `activity_status` table with all necessary columns
- Inserts a default row with `id = 1`
- Sets up automatic `updated_at` timestamp trigger
- Adds table comment for documentation
**Required by**:
- `/api/n8n/status` endpoint
- `ActivityFeed` component
- n8n workflows for status updates
**Safe to run multiple times**: Yes (uses `IF NOT EXISTS` and `ON CONFLICT`)
## Troubleshooting
### "relation already exists"
Table already exists - migration is already applied. Safe to ignore.
### "permission denied"
Your database user needs CREATE TABLE permissions:
```sql
GRANT CREATE ON DATABASE portfolio TO your_user;
```
### "database does not exist"
Create the database first:
```bash
createdb portfolio
# Or
psql -c "CREATE DATABASE portfolio;"
```
### "connection refused"
Ensure PostgreSQL is running:
```bash
# Check status
pg_isready
# Start PostgreSQL (macOS)
brew services start postgresql
# Start PostgreSQL (Linux)
sudo systemctl start postgresql
```
## Rolling Back
To remove the activity_status table:
```sql
DROP TRIGGER IF EXISTS activity_status_updated_at ON activity_status;
DROP FUNCTION IF EXISTS update_activity_status_updated_at();
DROP TABLE IF EXISTS activity_status;
```
Save this as `rollback_activity_status.sql` and run if needed.
## Future Migrations
When adding new migrations:
1. Create a new `.sql` file with descriptive name
2. Use timestamps in filename: `YYYYMMDD_description.sql`
3. Document what it does in this README
4. Test on local database first
5. Mark as safe/unsafe for production
---
**Last Updated**: 2024-01-15
**Status**: Required for n8n integration

View File

@@ -1,49 +0,0 @@
-- Create activity_status table for n8n integration
CREATE TABLE IF NOT EXISTS activity_status (
id INTEGER PRIMARY KEY DEFAULT 1,
activity_type VARCHAR(50),
activity_details VARCHAR(255),
activity_project VARCHAR(255),
activity_language VARCHAR(50),
activity_repo VARCHAR(500),
music_playing BOOLEAN DEFAULT FALSE,
music_track VARCHAR(255),
music_artist VARCHAR(255),
music_album VARCHAR(255),
music_platform VARCHAR(50),
music_progress INTEGER,
music_album_art VARCHAR(500),
watching_title VARCHAR(255),
watching_platform VARCHAR(50),
watching_type VARCHAR(50),
gaming_game VARCHAR(255),
gaming_platform VARCHAR(50),
gaming_status VARCHAR(50),
status_mood VARCHAR(50),
status_message VARCHAR(500),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Insert default row
INSERT INTO activity_status (id, updated_at)
VALUES (1, NOW())
ON CONFLICT (id) DO NOTHING;
-- Create function to automatically update updated_at
CREATE OR REPLACE FUNCTION update_activity_status_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger for automatic timestamp updates
DROP TRIGGER IF EXISTS activity_status_updated_at ON activity_status;
CREATE TRIGGER activity_status_updated_at
BEFORE UPDATE ON activity_status
FOR EACH ROW
EXECUTE FUNCTION update_activity_status_updated_at();
-- Add helpful comment
COMMENT ON TABLE activity_status IS 'Stores real-time activity status from n8n workflows (coding, music, gaming, etc.)';

View File

@@ -0,0 +1 @@
provider = "postgresql"

View File

@@ -1,73 +0,0 @@
#!/bin/bash
# Quick Fix Script for Portfolio Database
# This script creates the activity_status table needed for n8n integration
set -e
echo "🔧 Portfolio Database Quick Fix"
echo "================================"
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if .env.local exists
if [ ! -f .env.local ]; then
echo -e "${RED}❌ Error: .env.local not found${NC}"
echo "Please create .env.local with DATABASE_URL"
exit 1
fi
# Load DATABASE_URL from .env.local
export $(grep -v '^#' .env.local | xargs)
if [ -z "$DATABASE_URL" ]; then
echo -e "${RED}❌ Error: DATABASE_URL not found in .env.local${NC}"
exit 1
fi
echo -e "${GREEN}✓ Found DATABASE_URL${NC}"
echo ""
# Extract database name from DATABASE_URL
DB_NAME=$(echo $DATABASE_URL | sed -n 's/.*\/\([^?]*\).*/\1/p')
echo "📦 Database: $DB_NAME"
echo ""
# Run the migration
echo "🚀 Creating activity_status table..."
echo ""
psql "$DATABASE_URL" -f prisma/migrations/create_activity_status.sql
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}✅ SUCCESS! Migration completed${NC}"
echo ""
echo "Verifying table..."
psql "$DATABASE_URL" -c "\d activity_status" | head -20
echo ""
echo "Checking default row..."
psql "$DATABASE_URL" -c "SELECT id, updated_at FROM activity_status LIMIT 1;"
echo ""
echo -e "${GREEN}🎉 All done! Your database is ready.${NC}"
echo ""
echo "Next steps:"
echo " 1. Restart your Next.js dev server: npm run dev"
echo " 2. Visit http://localhost:3000"
echo " 3. The activity feed should now work without errors"
else
echo ""
echo -e "${RED}❌ Migration failed${NC}"
echo ""
echo "Troubleshooting:"
echo " 1. Ensure PostgreSQL is running: pg_isready"
echo " 2. Check your DATABASE_URL in .env.local"
echo " 3. Verify database exists: psql -l | grep $DB_NAME"
echo " 4. Try manual migration: psql $DB_NAME -f prisma/migrations/create_activity_status.sql"
exit 1
fi

View File

@@ -0,0 +1,40 @@
/**
* Container entrypoint: apply Prisma migrations, then start Next server.
*
* Why:
* - In real deployments you want schema changes applied automatically per deploy.
* - `prisma migrate deploy` is safe to run multiple times (idempotent).
*
* Controls:
* - Set `SKIP_PRISMA_MIGRATE=true` to skip migrations (emergency / debugging).
*/
const { spawnSync } = require("node:child_process");
function run(cmd, args, opts = {}) {
const res = spawnSync(cmd, args, {
stdio: "inherit",
env: process.env,
...opts,
});
if (res.error) {
throw res.error;
}
if (typeof res.status === "number" && res.status !== 0) {
// propagate exit code
process.exit(res.status);
}
}
const skip = String(process.env.SKIP_PRISMA_MIGRATE || "").toLowerCase() === "true";
if (!skip) {
// Avoid relying on `npx` resolution in minimal runtimes.
// We copy `node_modules/prisma` into the runtime image.
run("node", ["node_modules/prisma/build/index.js", "migrate", "deploy"]);
} else {
// eslint-disable-next-line no-console
console.log("SKIP_PRISMA_MIGRATE=true -> skipping prisma migrate deploy");
}
run("node", ["server.js"]);