diff --git a/Dockerfile b/Dockerfile index 2d1f28e..820f9de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file +CMD ["node", "scripts/start-with-migrate.js"] \ No newline at end of file diff --git a/prisma/migrations/20260112150721_init/migration.sql b/prisma/migrations/20260112150721_init/migration.sql new file mode 100644 index 0000000..1733e62 --- /dev/null +++ b/prisma/migrations/20260112150721_init/migration.sql @@ -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; + diff --git a/prisma/migrations/README.md b/prisma/migrations/README.md deleted file mode 100644 index b43642a..0000000 --- a/prisma/migrations/README.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/prisma/migrations/create_activity_status.sql b/prisma/migrations/create_activity_status.sql deleted file mode 100644 index c435677..0000000 --- a/prisma/migrations/create_activity_status.sql +++ /dev/null @@ -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.)'; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2fe25d8 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/prisma/migrations/quick-fix.sh b/prisma/migrations/quick-fix.sh deleted file mode 100755 index 70d5f09..0000000 --- a/prisma/migrations/quick-fix.sh +++ /dev/null @@ -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 diff --git a/scripts/start-with-migrate.js b/scripts/start-with-migrate.js new file mode 100644 index 0000000..1a31f69 --- /dev/null +++ b/scripts/start-with-migrate.js @@ -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"]); +