diff --git a/.gitea/workflows/ci-cd-fast.yml.disabled b/.gitea/workflows/ci-cd-fast.yml.disabled deleted file mode 100644 index fda4d17..0000000 --- a/.gitea/workflows/ci-cd-fast.yml.disabled +++ /dev/null @@ -1,318 +0,0 @@ -name: CI/CD Pipeline (Fast) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app - -jobs: - production: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js (Fast) - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - # Disable cache to avoid slow validation - cache: '' - - - name: Cache npm dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install dependencies - run: npm ci --prefer-offline --no-audit - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Build Docker image - run: | - docker build -t ${{ env.DOCKER_IMAGE }}:latest . - docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - - - name: Prepare for zero-downtime deployment - run: | - echo "๐Ÿš€ Preparing zero-downtime deployment..." - - # Check if current container is running - if docker ps -q -f name=portfolio-app | grep -q .; then - echo "๐Ÿ“Š Current container is running, proceeding with zero-downtime update" - CURRENT_CONTAINER_RUNNING=true - else - echo "๐Ÿ“Š No current container running, doing fresh deployment" - CURRENT_CONTAINER_RUNNING=false - fi - - # Ensure database and redis are running - echo "๐Ÿ”ง Ensuring database and redis are running..." - docker compose up -d postgres redis - - # Wait for services to be ready - sleep 10 - - - name: Verify secrets and variables before deployment - run: | - echo "๐Ÿ” Verifying secrets and variables..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "โŒ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "โŒ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "โŒ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "โŒ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "โŒ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - - echo "โœ… All required secrets and variables are present" - - - name: Deploy with zero downtime - run: | - echo "๐Ÿš€ Deploying with zero downtime..." - - if [ "$CURRENT_CONTAINER_RUNNING" = "true" ]; then - echo "๐Ÿ”„ Performing rolling update..." - - # Generate unique container name - TIMESTAMP=$(date +%s) - TEMP_CONTAINER_NAME="portfolio-app-temp-$TIMESTAMP" - echo "๐Ÿ”ง Using temporary container name: $TEMP_CONTAINER_NAME" - - # Clean up any existing temporary containers - echo "๐Ÿงน Cleaning up any existing temporary containers..." - - # Remove specific known problematic containers - docker rm -f portfolio-app-new portfolio-app-temp-* portfolio-app-backup || true - - # Find and remove any containers with portfolio-app in the name (except the main one) - EXISTING_CONTAINERS=$(docker ps -a --format "table {{.Names}}" | grep "portfolio-app" | grep -v "^portfolio-app$" || true) - if [ -n "$EXISTING_CONTAINERS" ]; then - echo "๐Ÿ—‘๏ธ Removing existing portfolio-app containers:" - echo "$EXISTING_CONTAINERS" - echo "$EXISTING_CONTAINERS" | xargs -r docker rm -f || true - fi - - # Also clean up any stopped containers - docker container prune -f || true - - # Start new container with unique temporary name (no port mapping needed for health check) - docker run -d \ - --name $TEMP_CONTAINER_NAME \ - --restart unless-stopped \ - --network portfolio_net \ - -e NODE_ENV=${{ vars.NODE_ENV }} \ - -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ - -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ - -e REDIS_URL=redis://redis:6379 \ - -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ - -e NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ - -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ - -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ - -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ - ${{ env.DOCKER_IMAGE }}:latest - - # Wait for new container to be ready - echo "โณ Waiting for new container to be ready..." - sleep 15 - - # Health check new container using docker exec - for i in {1..20}; do - if docker exec $TEMP_CONTAINER_NAME curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… New container is healthy!" - break - fi - echo "โณ Health check attempt $i/20..." - sleep 3 - done - - # Stop old container - echo "๐Ÿ›‘ Stopping old container..." - docker stop portfolio-app || true - - # Remove old container - docker rm portfolio-app || true - - # Rename new container - docker rename $TEMP_CONTAINER_NAME portfolio-app - - # Update port mapping - docker stop portfolio-app - docker rm portfolio-app - - # Start with correct port - docker run -d \ - --name portfolio-app \ - --restart unless-stopped \ - --network portfolio_net \ - -p 3000:3000 \ - -e NODE_ENV=${{ vars.NODE_ENV }} \ - -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ - -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ - -e REDIS_URL=redis://redis:6379 \ - -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ - -e NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ - -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ - -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ - -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ - ${{ env.DOCKER_IMAGE }}:latest - - echo "โœ… Rolling update completed!" - else - echo "๐Ÿ†• Fresh deployment..." - docker compose up -d - fi - env: - NODE_ENV: ${{ vars.NODE_ENV }} - LOG_LEVEL: ${{ vars.LOG_LEVEL }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - - - name: Wait for container to be ready - run: | - echo "โณ Waiting for container to be ready..." - sleep 15 - - # Check if container is actually running - if ! docker ps --filter "name=portfolio-app" --format "{{.Names}}" | grep -q "portfolio-app"; then - echo "โŒ Container failed to start" - echo "Container logs:" - docker logs portfolio-app --tail=50 - exit 1 - fi - - # Wait for health check with better error handling - echo "๐Ÿฅ Performing health check..." - for i in {1..40}; do - # First try direct access to port 3000 - if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Application is healthy (direct access)!" - break - fi - - # If direct access fails, try through docker exec (internal container check) - if docker exec portfolio-app curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Application is healthy (internal check)!" - # Check if port is properly exposed - if ! curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โš ๏ธ Application is running but port 3000 is not exposed to host" - echo "This might be expected in some deployment configurations" - break - fi - fi - - # Check if container is still running - if ! docker ps --filter "name=portfolio-app" --format "{{.Names}}" | grep -q "portfolio-app"; then - echo "โŒ Container stopped during health check" - echo "Container logs:" - docker logs portfolio-app --tail=50 - exit 1 - fi - - echo "โณ Health check attempt $i/40..." - sleep 3 - done - - # Final health check - try both methods - if docker exec portfolio-app curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Final health check passed (internal)" - # Try external access if possible - if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… External access also working" - else - echo "โš ๏ธ External access not available (port not exposed)" - fi - else - echo "โŒ Health check timeout - application not responding" - echo "Container logs:" - docker logs portfolio-app --tail=100 - exit 1 - fi - - - name: Health check - run: | - echo "๐Ÿ” Final health verification..." - - # Check container status - docker ps --filter "name=portfolio-app" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - - # Test health endpoint - try both methods - echo "๐Ÿฅ Testing health endpoint..." - if curl -f http://localhost:3000/api/health; then - echo "โœ… Health endpoint accessible externally" - elif docker exec portfolio-app curl -f http://localhost:3000/api/health; then - echo "โœ… Health endpoint accessible internally (external port not exposed)" - else - echo "โŒ Health endpoint not accessible" - exit 1 - fi - - # Test main page - try both methods - echo "๐ŸŒ Testing main page..." - if curl -f http://localhost:3000/ > /dev/null; then - echo "โœ… Main page is accessible externally" - elif docker exec portfolio-app curl -f http://localhost:3000/ > /dev/null; then - echo "โœ… Main page is accessible internally (external port not exposed)" - else - echo "โŒ Main page is not accessible" - exit 1 - fi - - echo "โœ… Deployment successful!" - - - name: Cleanup old images - run: | - docker image prune -f - docker system prune -f \ No newline at end of file diff --git a/.gitea/workflows/ci-cd-fixed.yml.disabled b/.gitea/workflows/ci-cd-fixed.yml.disabled deleted file mode 100644 index 7ad8231..0000000 --- a/.gitea/workflows/ci-cd-fixed.yml.disabled +++ /dev/null @@ -1,153 +0,0 @@ -name: CI/CD Pipeline (Fixed & Reliable) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app - -jobs: - production: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Build Docker image - run: | - echo "๐Ÿ—๏ธ Building Docker image..." - docker build -t ${{ env.DOCKER_IMAGE }}:latest . - docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - echo "โœ… Docker image built successfully" - - - name: Deploy with fixed configuration - run: | - echo "๐Ÿš€ Deploying with fixed configuration..." - - # Export environment variables with defaults - export NODE_ENV="${NODE_ENV:-production}" - export LOG_LEVEL="${LOG_LEVEL:-info}" - export NEXT_PUBLIC_BASE_URL="${NEXT_PUBLIC_BASE_URL:-https://dk0.dev}" - export NEXT_PUBLIC_UMAMI_URL="${NEXT_PUBLIC_UMAMI_URL:-https://analytics.dk0.dev}" - export NEXT_PUBLIC_UMAMI_WEBSITE_ID="${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-b3665829-927a-4ada-b9bb-fcf24171061e}" - export MY_EMAIL="${MY_EMAIL:-contact@dk0.dev}" - export MY_INFO_EMAIL="${MY_INFO_EMAIL:-info@dk0.dev}" - export MY_PASSWORD="${MY_PASSWORD:-your-email-password}" - export MY_INFO_PASSWORD="${MY_INFO_PASSWORD:-your-info-email-password}" - export ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH:-admin:your_secure_password_here}" - - echo "๐Ÿ“ Environment variables configured:" - echo " - NODE_ENV: ${NODE_ENV}" - echo " - NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}" - echo " - MY_EMAIL: ${MY_EMAIL}" - echo " - MY_INFO_EMAIL: ${MY_INFO_EMAIL}" - echo " - MY_PASSWORD: [SET]" - echo " - MY_INFO_PASSWORD: [SET]" - echo " - ADMIN_BASIC_AUTH: [SET]" - echo " - LOG_LEVEL: ${LOG_LEVEL}" - - # Stop old containers - echo "๐Ÿ›‘ Stopping old containers..." - docker compose down || true - - # Clean up orphaned containers - echo "๐Ÿงน Cleaning up orphaned containers..." - docker compose down --remove-orphans || true - - # Start new containers - echo "๐Ÿš€ Starting new containers..." - docker compose up -d - - echo "โœ… Deployment completed!" - env: - NODE_ENV: ${{ vars.NODE_ENV || 'production' }} - LOG_LEVEL: ${{ vars.LOG_LEVEL || 'info' }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL || 'https://dk0.dev' }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL || 'https://analytics.dk0.dev' }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID || 'b3665829-927a-4ada-b9bb-fcf24171061e' }} - MY_EMAIL: ${{ vars.MY_EMAIL || 'contact@dk0.dev' }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL || 'info@dk0.dev' }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD || 'your-email-password' }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD || 'your-info-email-password' }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH || 'admin:your_secure_password_here' }} - - - name: Wait for containers to be ready - run: | - echo "โณ Waiting for containers to be ready..." - sleep 30 - - # Check if all containers are running - echo "๐Ÿ“Š Checking container status..." - docker compose ps - - # Wait for application container to be healthy - echo "๐Ÿฅ Waiting for application container to be healthy..." - for i in {1..30}; do - if docker exec portfolio-app curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Application container is healthy!" - break - fi - echo "โณ Waiting for application container... ($i/30)" - sleep 3 - done - - - name: Health check - run: | - echo "๐Ÿ” Running comprehensive health checks..." - - # Check container status - echo "๐Ÿ“Š Container status:" - docker compose ps - - # Check application container - echo "๐Ÿฅ Checking application container..." - if docker exec portfolio-app curl -f http://localhost:3000/api/health; then - echo "โœ… Application health check passed!" - else - echo "โŒ Application health check failed!" - docker logs portfolio-app --tail=50 - exit 1 - fi - - # Check main page - if curl -f http://localhost:3000/ > /dev/null; then - echo "โœ… Main page is accessible!" - else - echo "โŒ Main page is not accessible!" - exit 1 - fi - - echo "โœ… All health checks passed! Deployment successful!" - - - name: Cleanup old images - run: | - echo "๐Ÿงน Cleaning up old images..." - docker image prune -f - docker system prune -f - echo "โœ… Cleanup completed" diff --git a/.gitea/workflows/ci-cd-reliable.yml.disabled b/.gitea/workflows/ci-cd-reliable.yml.disabled deleted file mode 100644 index 58eb289..0000000 --- a/.gitea/workflows/ci-cd-reliable.yml.disabled +++ /dev/null @@ -1,177 +0,0 @@ -name: CI/CD Pipeline (Reliable & Simple) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app - -jobs: - production: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Verify secrets and variables - run: | - echo "๐Ÿ” Verifying secrets and variables..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "โŒ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "โŒ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "โŒ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "โŒ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "โŒ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - - echo "โœ… All required secrets and variables are present" - - - name: Build Docker image - run: | - echo "๐Ÿ—๏ธ Building Docker image..." - docker build -t ${{ env.DOCKER_IMAGE }}:latest . - docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - echo "โœ… Docker image built successfully" - - - name: Deploy with database services - run: | - echo "๐Ÿš€ Deploying with database services..." - - # Export environment variables - export NODE_ENV="${{ vars.NODE_ENV }}" - export LOG_LEVEL="${{ vars.LOG_LEVEL }}" - export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" - export NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" - export NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" - export MY_EMAIL="${{ vars.MY_EMAIL }}" - export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" - export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" - export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" - export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - - # Stop old containers - echo "๐Ÿ›‘ Stopping old containers..." - docker compose down || true - - # Clean up orphaned containers - echo "๐Ÿงน Cleaning up orphaned containers..." - docker compose down --remove-orphans || true - - # Start new containers - echo "๐Ÿš€ Starting new containers..." - docker compose up -d - - echo "โœ… Deployment completed!" - env: - NODE_ENV: ${{ vars.NODE_ENV }} - LOG_LEVEL: ${{ vars.LOG_LEVEL }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - - - name: Wait for containers to be ready - run: | - echo "โณ Waiting for containers to be ready..." - sleep 20 - - # Check if all containers are running - echo "๐Ÿ“Š Checking container status..." - docker compose ps - - # Wait for application container to be healthy - echo "๐Ÿฅ Waiting for application container to be healthy..." - for i in {1..30}; do - if docker exec portfolio-app curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Application container is healthy!" - break - fi - echo "โณ Waiting for application container... ($i/30)" - sleep 3 - done - - - name: Health check - run: | - echo "๐Ÿ” Running comprehensive health checks..." - - # Check container status - echo "๐Ÿ“Š Container status:" - docker compose ps - - # Check application container - echo "๐Ÿฅ Checking application container..." - if docker exec portfolio-app curl -f http://localhost:3000/api/health; then - echo "โœ… Application health check passed!" - else - echo "โŒ Application health check failed!" - docker logs portfolio-app --tail=50 - exit 1 - fi - - # Check main page - if curl -f http://localhost:3000/ > /dev/null; then - echo "โœ… Main page is accessible!" - else - echo "โŒ Main page is not accessible!" - exit 1 - fi - - echo "โœ… All health checks passed! Deployment successful!" - - - name: Cleanup old images - run: | - echo "๐Ÿงน Cleaning up old images..." - docker image prune -f - docker system prune -f - echo "โœ… Cleanup completed" diff --git a/.gitea/workflows/ci-cd-simple.yml.disabled b/.gitea/workflows/ci-cd-simple.yml.disabled deleted file mode 100644 index 931548c..0000000 --- a/.gitea/workflows/ci-cd-simple.yml.disabled +++ /dev/null @@ -1,143 +0,0 @@ -name: CI/CD Pipeline (Simple & Reliable) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app - -jobs: - production: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Verify secrets and variables - run: | - echo "๐Ÿ” Verifying secrets and variables..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "โŒ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "โŒ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "โŒ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "โŒ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "โŒ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - - echo "โœ… All required secrets and variables are present" - - - name: Deploy using improved script - run: | - echo "๐Ÿš€ Deploying using improved deployment script..." - - # Set environment variables for the deployment script - export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" - export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" - export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - - # Make the script executable - chmod +x ./scripts/gitea-deploy.sh - - # Run the deployment script - ./scripts/gitea-deploy.sh - env: - NODE_ENV: ${{ vars.NODE_ENV }} - LOG_LEVEL: ${{ vars.LOG_LEVEL }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - - - name: Final verification - run: | - echo "๐Ÿ” Final verification..." - - # Wait a bit more to ensure everything is stable - sleep 10 - - # Check if container is running - if docker ps --filter "name=${{ env.CONTAINER_NAME }}" --format "{{.Names}}" | grep -q "${{ env.CONTAINER_NAME }}"; then - echo "โœ… Container is running" - else - echo "โŒ Container is not running" - docker ps -a - exit 1 - fi - - # Check health endpoint - if curl -f http://localhost:3000/api/health; then - echo "โœ… Health check passed" - else - echo "โŒ Health check failed" - echo "Container logs:" - docker logs ${{ env.CONTAINER_NAME }} --tail=50 - exit 1 - fi - - # Check main page - if curl -f http://localhost:3000/ > /dev/null; then - echo "โœ… Main page is accessible" - else - echo "โŒ Main page is not accessible" - exit 1 - fi - - echo "๐ŸŽ‰ Deployment successful!" - - - name: Cleanup old images - run: | - echo "๐Ÿงน Cleaning up old images..." - docker image prune -f - docker system prune -f - echo "โœ… Cleanup completed" diff --git a/.gitea/workflows/ci-cd-zero-downtime-fixed.yml.disabled b/.gitea/workflows/ci-cd-zero-downtime-fixed.yml.disabled deleted file mode 100644 index 2ab2ca3..0000000 --- a/.gitea/workflows/ci-cd-zero-downtime-fixed.yml.disabled +++ /dev/null @@ -1,257 +0,0 @@ -name: CI/CD Pipeline (Zero Downtime - Fixed) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - -jobs: - production: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Build Docker image - run: | - docker build -t ${{ env.DOCKER_IMAGE }}:latest . - docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - - - name: Verify secrets and variables before deployment - run: | - echo "๐Ÿ” Verifying secrets and variables..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "โŒ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "โŒ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "โŒ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "โŒ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "โŒ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - - echo "โœ… All required secrets and variables are present" - - - name: Deploy with zero downtime using docker-compose - run: | - echo "๐Ÿš€ Deploying with zero downtime using docker-compose..." - - # Export environment variables for docker compose - export NODE_ENV="${{ vars.NODE_ENV }}" - export LOG_LEVEL="${{ vars.LOG_LEVEL }}" - export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" - export NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" - export NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" - export MY_EMAIL="${{ vars.MY_EMAIL }}" - export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" - export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" - export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" - export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - - # Check if nginx config file exists - echo "๐Ÿ” Checking nginx configuration file..." - if [ ! -f "nginx-zero-downtime.conf" ]; then - echo "โš ๏ธ nginx-zero-downtime.conf not found, creating fallback..." - cat > nginx-zero-downtime.conf << 'EOF' -events { - worker_connections 1024; -} -http { - upstream portfolio_backend { - server portfolio-app-1:3000 max_fails=3 fail_timeout=30s; - server portfolio-app-2:3000 max_fails=3 fail_timeout=30s; - } - server { - listen 80; - server_name _; - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } - location / { - proxy_pass http://portfolio_backend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } -} -EOF - fi - - # Stop old containers - echo "๐Ÿ›‘ Stopping old containers..." - docker compose -f docker-compose.zero-downtime-fixed.yml down || true - - # Clean up any orphaned containers - echo "๐Ÿงน Cleaning up orphaned containers..." - docker compose -f docker-compose.zero-downtime-fixed.yml down --remove-orphans || true - - # Start new containers - echo "๐Ÿš€ Starting new containers..." - docker compose -f docker-compose.zero-downtime-fixed.yml up -d - - echo "โœ… Zero downtime deployment completed!" - env: - NODE_ENV: ${{ vars.NODE_ENV }} - LOG_LEVEL: ${{ vars.LOG_LEVEL }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - - - name: Wait for containers to be ready - run: | - echo "โณ Waiting for containers to be ready..." - sleep 20 - - # Check if all containers are running - echo "๐Ÿ“Š Checking container status..." - docker compose -f docker-compose.zero-downtime-fixed.yml ps - - # Wait for application containers to be healthy (internal check) - echo "๐Ÿฅ Waiting for application containers to be healthy..." - for i in {1..30}; do - # Check if both app containers are healthy internally - if docker exec portfolio-app-1 curl -f http://localhost:3000/api/health > /dev/null 2>&1 && \ - docker exec portfolio-app-2 curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Both application containers are healthy!" - break - fi - echo "โณ Waiting for application containers... ($i/30)" - sleep 3 - done - - # Wait for nginx to be healthy and proxy to work - echo "๐ŸŒ Waiting for nginx to be healthy and proxy to work..." - for i in {1..30}; do - # Check nginx health endpoint - if curl -f http://localhost/health > /dev/null 2>&1; then - echo "โœ… Nginx health endpoint is working!" - # Now check if nginx can proxy to the application - if curl -f http://localhost/api/health > /dev/null 2>&1; then - echo "โœ… Nginx proxy to application is working!" - break - fi - fi - echo "โณ Waiting for nginx and proxy... ($i/30)" - sleep 3 - done - - - name: Health check - run: | - echo "๐Ÿ” Running comprehensive health checks..." - - # Check container status - echo "๐Ÿ“Š Container status:" - docker compose -f docker-compose.zero-downtime-fixed.yml ps - - # Check individual application containers (internal) - echo "๐Ÿฅ Checking individual application containers..." - if docker exec portfolio-app-1 curl -f http://localhost:3000/api/health; then - echo "โœ… portfolio-app-1 health check passed!" - else - echo "โŒ portfolio-app-1 health check failed!" - docker logs portfolio-app-1 --tail=20 - exit 1 - fi - - if docker exec portfolio-app-2 curl -f http://localhost:3000/api/health; then - echo "โœ… portfolio-app-2 health check passed!" - else - echo "โŒ portfolio-app-2 health check failed!" - docker logs portfolio-app-2 --tail=20 - exit 1 - fi - - # Check nginx health - if curl -f http://localhost/health; then - echo "โœ… Nginx health check passed!" - else - echo "โŒ Nginx health check failed!" - docker logs portfolio-nginx --tail=20 - exit 1 - fi - - # Check application health through nginx (this is the main test) - if curl -f http://localhost/api/health; then - echo "โœ… Application health check through nginx passed!" - else - echo "โŒ Application health check through nginx failed!" - echo "Nginx logs:" - docker logs portfolio-nginx --tail=20 - exit 1 - fi - - # Check main page through nginx - if curl -f http://localhost/ > /dev/null; then - echo "โœ… Main page is accessible through nginx!" - else - echo "โŒ Main page is not accessible through nginx!" - exit 1 - fi - - echo "โœ… All health checks passed! Deployment successful!" - - - name: Show container status - run: | - echo "๐Ÿ“Š Container status:" - docker compose -f docker-compose.zero-downtime-fixed.yml ps - - - name: Cleanup old images - run: | - echo "๐Ÿงน Cleaning up old images..." - docker image prune -f - docker system prune -f - echo "โœ… Cleanup completed" \ No newline at end of file diff --git a/.gitea/workflows/ci-cd-zero-downtime.yml.disabled b/.gitea/workflows/ci-cd-zero-downtime.yml.disabled deleted file mode 100644 index ead3369..0000000 --- a/.gitea/workflows/ci-cd-zero-downtime.yml.disabled +++ /dev/null @@ -1,194 +0,0 @@ -name: CI/CD Pipeline (Zero Downtime) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app - NEW_CONTAINER_NAME: portfolio-app-new - -jobs: - production: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Build Docker image - run: | - docker build -t ${{ env.DOCKER_IMAGE }}:latest . - docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - - - name: Verify secrets and variables before deployment - run: | - echo "๐Ÿ” Verifying secrets and variables..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "โŒ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "โŒ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "โŒ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "โŒ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "โŒ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - - echo "โœ… All required secrets and variables are present" - - - name: Start new container (zero downtime) - run: | - echo "๐Ÿš€ Starting new container for zero-downtime deployment..." - - # Start new container with different name - docker run -d \ - --name ${{ env.NEW_CONTAINER_NAME }} \ - --restart unless-stopped \ - --network portfolio_net \ - -p 3001:3000 \ - -e NODE_ENV=${{ vars.NODE_ENV }} \ - -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ - -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ - -e REDIS_URL=redis://redis:6379 \ - -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ - -e NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ - -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ - -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ - -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ - ${{ env.DOCKER_IMAGE }}:latest - - echo "โœ… New container started on port 3001" - - - name: Health check new container - run: | - echo "๐Ÿ” Health checking new container..." - sleep 10 - - # Health check on new container - for i in {1..30}; do - if curl -f http://localhost:3001/api/health > /dev/null 2>&1; then - echo "โœ… New container is healthy!" - break - fi - echo "โณ Waiting for new container to be ready... ($i/30)" - sleep 2 - done - - # Final health check - if ! curl -f http://localhost:3001/api/health > /dev/null 2>&1; then - echo "โŒ New container failed health check!" - docker logs ${{ env.NEW_CONTAINER_NAME }} - exit 1 - fi - - - name: Switch traffic to new container (zero downtime) - run: | - echo "๐Ÿ”„ Switching traffic to new container..." - - # Stop old container - docker stop ${{ env.CONTAINER_NAME }} || true - - # Remove old container - docker rm ${{ env.CONTAINER_NAME }} || true - - # Rename new container to production name - docker rename ${{ env.NEW_CONTAINER_NAME }} ${{ env.CONTAINER_NAME }} - - # Update port mapping (requires container restart) - docker stop ${{ env.CONTAINER_NAME }} - docker rm ${{ env.CONTAINER_NAME }} - - # Start with correct port - docker run -d \ - --name ${{ env.CONTAINER_NAME }} \ - --restart unless-stopped \ - --network portfolio_net \ - -p 3000:3000 \ - -e NODE_ENV=${{ vars.NODE_ENV }} \ - -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ - -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ - -e REDIS_URL=redis://redis:6379 \ - -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ - -e NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ - -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ - -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ - -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ - ${{ env.DOCKER_IMAGE }}:latest - - echo "โœ… Traffic switched successfully!" - - - name: Final health check - run: | - echo "๐Ÿ” Final health check..." - sleep 5 - - for i in {1..10}; do - if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… Deployment successful! Zero downtime achieved!" - break - fi - echo "โณ Final health check... ($i/10)" - sleep 2 - done - - if ! curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โŒ Final health check failed!" - docker logs ${{ env.CONTAINER_NAME }} - exit 1 - fi - - - name: Cleanup old images - run: | - echo "๐Ÿงน Cleaning up old images..." - docker image prune -f - docker system prune -f - echo "โœ… Cleanup completed" \ No newline at end of file diff --git a/.gitea/workflows/ci-cd.yml.disabled b/.gitea/workflows/ci-cd.yml.disabled deleted file mode 100644 index 35f0f67..0000000 --- a/.gitea/workflows/ci-cd.yml.disabled +++ /dev/null @@ -1,293 +0,0 @@ -name: CI/CD Pipeline (Simple) - -on: - push: - branches: [ main, production ] - pull_request: - branches: [ main, production ] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app - -jobs: - # Production deployment pipeline - production: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/production' - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: 'package-lock.json' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Run security scan - run: | - echo "๐Ÿ” Running npm audit..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - - - name: Build Docker image - run: | - docker build -t ${{ env.DOCKER_IMAGE }}:latest . - docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - - - name: Prepare for zero-downtime deployment - run: | - echo "๐Ÿš€ Preparing zero-downtime deployment..." - - # FORCE REMOVE the problematic container - echo "๐Ÿงน FORCE removing problematic container portfolio-app-new..." - docker rm -f portfolio-app-new || true - docker rm -f afa9a70588844b06e17d5e0527119d589a7a3fde8a17608447cf7d8d448cf261 || true - - # Check if current container is running - if docker ps -q -f name=portfolio-app | grep -q .; then - echo "๐Ÿ“Š Current container is running, proceeding with zero-downtime update" - CURRENT_CONTAINER_RUNNING=true - else - echo "๐Ÿ“Š No current container running, doing fresh deployment" - CURRENT_CONTAINER_RUNNING=false - fi - - # Clean up ALL existing containers first - echo "๐Ÿงน Cleaning up ALL existing containers..." - docker compose down --remove-orphans || true - docker rm -f portfolio-app portfolio-postgres portfolio-redis || true - - # Force remove the specific problematic container - docker rm -f 4dec125499540f66f4cb407b69d9aee5232f679feecd71ff2369544ff61f85ae || true - - # Clean up any containers with portfolio in the name - docker ps -a --format "{{.Names}}" | grep portfolio | xargs -r docker rm -f || true - - # Ensure database and redis are running - echo "๐Ÿ”ง Ensuring database and redis are running..." - - # Export environment variables for docker compose - export NODE_ENV="${{ vars.NODE_ENV }}" - export LOG_LEVEL="${{ vars.LOG_LEVEL }}" - export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" - export NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" - export NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" - export MY_EMAIL="${{ vars.MY_EMAIL }}" - export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" - export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" - export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" - export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - - # Start services with environment variables - docker compose up -d postgres redis - - # Wait for services to be ready - sleep 10 - env: - NODE_ENV: ${{ vars.NODE_ENV }} - LOG_LEVEL: ${{ vars.LOG_LEVEL }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - - - name: Verify secrets and variables before deployment - run: | - echo "๐Ÿ” Verifying secrets and variables..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "โŒ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "โŒ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "โŒ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "โŒ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "โŒ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - - echo "โœ… All required secrets and variables are present" - - - name: Deploy with zero downtime - run: | - echo "๐Ÿš€ Deploying with zero downtime..." - - if [ "$CURRENT_CONTAINER_RUNNING" = "true" ]; then - echo "๐Ÿ”„ Performing rolling update..." - - # Generate unique container name - TIMESTAMP=$(date +%s) - TEMP_CONTAINER_NAME="portfolio-app-temp-$TIMESTAMP" - echo "๐Ÿ”ง Using temporary container name: $TEMP_CONTAINER_NAME" - - # Clean up any existing temporary containers - echo "๐Ÿงน Cleaning up any existing temporary containers..." - - # Remove specific known problematic containers - docker rm -f portfolio-app-new portfolio-app-temp-* portfolio-app-backup || true - - # FORCE remove the specific problematic container by ID - docker rm -f afa9a70588844b06e17d5e0527119d589a7a3fde8a17608447cf7d8d448cf261 || true - - # Find and remove any containers with portfolio-app in the name (except the main one) - EXISTING_CONTAINERS=$(docker ps -a --format "table {{.Names}}" | grep "portfolio-app" | grep -v "^portfolio-app$" || true) - if [ -n "$EXISTING_CONTAINERS" ]; then - echo "๐Ÿ—‘๏ธ Removing existing portfolio-app containers:" - echo "$EXISTING_CONTAINERS" - echo "$EXISTING_CONTAINERS" | xargs -r docker rm -f || true - fi - - # Also clean up any stopped containers - docker container prune -f || true - - # Double-check: list all containers to see what's left - echo "๐Ÿ“‹ Current containers after cleanup:" - docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep portfolio || echo "No portfolio containers found" - - # Start new container with unique temporary name (no port mapping needed for health check) - docker run -d \ - --name $TEMP_CONTAINER_NAME \ - --restart unless-stopped \ - --network portfolio_net \ - -e NODE_ENV=${{ vars.NODE_ENV }} \ - -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ - -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ - -e REDIS_URL=redis://redis:6379 \ - -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ - -e NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ - -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ - -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ - -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ - ${{ env.DOCKER_IMAGE }}:latest - - # Wait for new container to be ready - echo "โณ Waiting for new container to be ready..." - sleep 15 - - # Health check new container using docker exec - for i in {1..20}; do - if docker exec $TEMP_CONTAINER_NAME curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "โœ… New container is healthy!" - break - fi - echo "โณ Health check attempt $i/20..." - sleep 3 - done - - # Stop old container - echo "๐Ÿ›‘ Stopping old container..." - docker stop portfolio-app || true - - # Remove old container - docker rm portfolio-app || true - - # Rename new container - docker rename $TEMP_CONTAINER_NAME portfolio-app - - # Update port mapping - docker stop portfolio-app - docker rm portfolio-app - - # Start with correct port - docker run -d \ - --name portfolio-app \ - --restart unless-stopped \ - --network portfolio_net \ - -p 3000:3000 \ - -e NODE_ENV=${{ vars.NODE_ENV }} \ - -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ - -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ - -e REDIS_URL=redis://redis:6379 \ - -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ - -e NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ - -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ - -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ - -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ - ${{ env.DOCKER_IMAGE }}:latest - - echo "โœ… Rolling update completed!" - else - echo "๐Ÿ†• Fresh deployment..." - - # Export environment variables for docker compose - export NODE_ENV="${{ vars.NODE_ENV }}" - export LOG_LEVEL="${{ vars.LOG_LEVEL }}" - export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" - export NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" - export NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" - export MY_EMAIL="${{ vars.MY_EMAIL }}" - export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" - export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" - export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" - export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - - docker compose up -d - fi - env: - NODE_ENV: ${{ vars.NODE_ENV }} - LOG_LEVEL: ${{ vars.LOG_LEVEL }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} - NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - - - name: Wait for container to be ready - run: | - sleep 10 - timeout 60 bash -c 'until curl -f http://localhost:3000/api/health; do sleep 2; done' - - - name: Health check - run: | - curl -f http://localhost:3000/api/health - echo "โœ… Deployment successful!" - - - name: Cleanup old images - run: | - docker image prune -f - docker system prune -f \ No newline at end of file diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5f0c7da..a6a23f5 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,9 +2,9 @@ name: CI/CD Pipeline on: push: - branches: [main, production] + branches: [main, dev, production] pull_request: - branches: [main, production] + branches: [main, dev, production] env: REGISTRY: ghcr.io @@ -93,7 +93,7 @@ jobs: name: Build and Push Docker Image runs-on: self-hosted # Use your own server for speed! needs: [test, security] # Wait for parallel jobs to complete - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/production') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/production') permissions: contents: read packages: write @@ -121,6 +121,8 @@ jobs: type=ref,event=pr type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=staging,enable={{is_default_branch==false && branch=='dev'}} + type=raw,value=staging,enable={{is_default_branch==false && branch=='main'}} - name: Create production environment file run: | @@ -151,9 +153,69 @@ jobs: build-args: | BUILDKIT_INLINE_CACHE=1 - # Deploy to server + # Deploy to staging (dev/main branches) + deploy-staging: + name: Deploy to Staging + runs-on: self-hosted + needs: build + if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') + environment: staging + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy staging to server + run: | + # Set deployment variables + export IMAGE_NAME="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging" + export CONTAINER_NAME="portfolio-app-staging" + export COMPOSE_FILE="docker-compose.staging.yml" + + # Set environment variables for docker-compose + export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL_STAGING || vars.NEXT_PUBLIC_BASE_URL }}" + export MY_EMAIL="${{ vars.MY_EMAIL }}" + export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" + export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" + export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" + export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" + + # Pull latest staging image + docker pull $IMAGE_NAME || docker pull "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main" || true + + # Stop and remove old staging container (if exists) + docker compose -f $COMPOSE_FILE down || true + + # Start new staging container + docker compose -f $COMPOSE_FILE up -d --force-recreate + + # Wait for health check + echo "Waiting for staging application to be healthy..." + for i in {1..30}; do + if curl -f http://localhost:3001/api/health > /dev/null 2>&1; then + echo "โœ… Staging deployment successful!" + break + fi + sleep 2 + done + + # Verify deployment + if curl -f http://localhost:3001/api/health; then + echo "โœ… Staging deployment verified!" + else + echo "โš ๏ธ Staging health check failed, but container is running" + docker compose -f $COMPOSE_FILE logs --tail=50 + fi + + # Deploy to production deploy: - name: Deploy to Server + name: Deploy to Production runs-on: self-hosted needs: build if: github.event_name == 'push' && github.ref == 'refs/heads/production' @@ -169,12 +231,13 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Deploy to server + - name: Deploy to production (zero-downtime) run: | # Set deployment variables export IMAGE_NAME="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:production" export CONTAINER_NAME="portfolio-app" - export COMPOSE_FILE="docker-compose.prod.yml" + export COMPOSE_FILE="docker-compose.production.yml" + export BACKUP_CONTAINER="portfolio-app-backup" # Set environment variables for docker-compose export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" @@ -184,30 +247,83 @@ jobs: export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - # Pull latest image + # Pull latest production image + echo "๐Ÿ“ฆ Pulling latest production image..." docker pull $IMAGE_NAME - # Stop and remove old container - docker compose -f $COMPOSE_FILE down || true - - # Remove old images to force using new one - docker image prune -f - - # Start new container with force recreate - docker compose -f $COMPOSE_FILE up -d --force-recreate + # Check if production container is running + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "๐Ÿ”„ Production container is running - performing zero-downtime deployment..." + + # Start new container with different name first (blue-green) + echo "๐Ÿš€ Starting new container (green)..." + docker run -d \ + --name ${BACKUP_CONTAINER} \ + --network portfolio_net \ + -p 3002:3000 \ + -e NODE_ENV=production \ + -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ + -e REDIS_URL=redis://redis:6379 \ + -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ + -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ + -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ + -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ + -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ + -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ + $IMAGE_NAME || true + + # Wait for new container to be healthy + echo "โณ Waiting for new container to be healthy..." + for i in {1..30}; do + if curl -f http://localhost:3002/api/health > /dev/null 2>&1; then + echo "โœ… New container is healthy!" + break + fi + sleep 2 + done + + # Stop old container + echo "๐Ÿ›‘ Stopping old container..." + docker stop ${CONTAINER_NAME} || true + + # Remove old container + docker rm ${CONTAINER_NAME} || true + + # Rename new container to production name + docker rename ${BACKUP_CONTAINER} ${CONTAINER_NAME} + + # Update port mapping (requires container restart, but it's already healthy) + docker stop ${CONTAINER_NAME} + docker rm ${CONTAINER_NAME} + + # Start with correct port using docker-compose + docker compose -f $COMPOSE_FILE up -d --force-recreate + else + echo "๐Ÿ†• No existing container - starting fresh deployment..." + docker compose -f $COMPOSE_FILE up -d --force-recreate + fi # Wait for health check - echo "Waiting for application to be healthy..." - timeout 60 bash -c 'until curl -f http://localhost:3000/api/health; do sleep 2; done' + echo "โณ Waiting for production application to be healthy..." + for i in {1..30}; do + if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then + echo "โœ… Production deployment successful!" + break + fi + sleep 2 + done # Verify deployment if curl -f http://localhost:3000/api/health; then - echo "โœ… Deployment successful!" + echo "โœ… Production deployment verified!" else - echo "โŒ Deployment failed!" - docker compose -f $COMPOSE_FILE logs + echo "โŒ Production deployment failed!" + docker compose -f $COMPOSE_FILE logs --tail=100 exit 1 fi + + # Cleanup backup container if it exists + docker rm -f ${BACKUP_CONTAINER} 2>/dev/null || true - name: Cleanup old images run: | diff --git a/.gitignore b/.gitignore index 5ef6a52..b557940 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,20 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# logs +logs/*.log +*.log + +# test results +test-results/ +playwright-report/ +coverage/ + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/AFTER_PUSH_SETUP.md b/AFTER_PUSH_SETUP.md deleted file mode 100644 index 7627d86..0000000 --- a/AFTER_PUSH_SETUP.md +++ /dev/null @@ -1,253 +0,0 @@ -# After Push Setup Guide - -After pulling this dev branch, follow these steps to get everything working. - -## ๐Ÿš€ Quick Setup (5 minutes) - -### 1. Install Dependencies - -```bash -npm install -``` - -### 2. Setup Database (REQUIRED) - -The new `activity_status` table is required for the activity feed to work without errors. - -**Option A: Automatic (Recommended)** -```bash -chmod +x prisma/migrations/quick-fix.sh -./prisma/migrations/quick-fix.sh -``` - -**Option B: Manual** -```bash -psql -d portfolio -f prisma/migrations/create_activity_status.sql -``` - -**Option C: Using pgAdmin/GUI** -1. Open your database tool -2. Connect to `portfolio` database -3. Open the Query Tool -4. Copy contents of `prisma/migrations/create_activity_status.sql` -5. Execute the query - -### 3. Verify Setup - -```bash -# Check if table exists -psql -d portfolio -c "\d activity_status" - -# Should show table structure with columns: -# - id, activity_type, activity_details, etc. -``` - -### 4. Start Dev Server - -```bash -npm run dev -``` - -### 5. Test Everything - -Visit these URLs and check for errors: - -- โœ… http://localhost:3000 - Home page (no hydration errors) -- โœ… http://localhost:3000/manage - Admin login form (no redirect) -- โœ… http://localhost:3000/api/n8n/status - Should return JSON (not error) - -**Check Browser Console:** -- โŒ No "Hydration failed" errors -- โŒ No "two children with same key" warnings -- โŒ No "relation activity_status does not exist" errors - -## โœจ What's New - -### Fixed Issues -1. **Hydration Errors** - React SSR/CSR mismatches resolved -2. **Duplicate Keys** - All list items now have unique keys -3. **Navbar Overlap** - Header no longer covers hero section -4. **Admin Access** - `/manage` now shows login form (no redirect loop) -5. **Database Errors** - Activity feed works without errors - -### New Features -1. **AI Image Generation System** - Automatic project cover images -2. **ActivityStatus Model** - Real-time activity tracking in database -3. **Enhanced APIs** - New endpoints for image generation - -## ๐Ÿค– Optional: AI Image Generation Setup - -If you want to use the new AI image generation feature: - -### Prerequisites -- Stable Diffusion WebUI installed -- n8n workflow automation -- GPU recommended (or cloud GPU) - -### Quick Start Guide -See detailed instructions: `docs/ai-image-generation/QUICKSTART.md` - -### Environment Variables - -Add to `.env.local`: -```bash -# AI Image Generation (Optional) -N8N_WEBHOOK_URL=http://localhost:5678/webhook -N8N_SECRET_TOKEN=generate-a-secure-random-token -SD_API_URL=http://localhost:7860 -AUTO_GENERATE_IMAGES=false # Set to true when ready -GENERATED_IMAGES_DIR=/path/to/portfolio/public/generated-images -``` - -Generate secure token: -```bash -openssl rand -hex 32 -``` - -## ๐Ÿ› Troubleshooting - -### "relation activity_status does not exist" - -**Problem:** Database migration not applied - -**Solution:** -```bash -./prisma/migrations/quick-fix.sh -# Then restart: npm run dev -``` - -### "/manage redirects to home page" - -**Problem:** Browser cached old middleware behavior - -**Solution:** -```bash -# Hard refresh: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac) -# Or use Incognito/Private window -``` - -### Build Errors - -**Problem:** Dependencies out of sync - -**Solution:** -```bash -rm -rf node_modules package-lock.json -npm install -npm run build -``` - -### Hydration Errors Still Appearing - -**Problem:** Old build cached - -**Solution:** -```bash -rm -rf .next -npm run dev -``` - -### Database Connection Failed - -**Problem:** PostgreSQL not running - -**Solution:** -```bash -# Check status -pg_isready - -# Start PostgreSQL -# macOS: -brew services start postgresql - -# Linux: -sudo systemctl start postgresql - -# Docker: -docker start postgres_container -``` - -## ๐Ÿ“š Documentation - -### Core Documentation -- `CHANGELOG_DEV.md` - All changes in this release -- `PRE_PUSH_CHECKLIST.md` - What was tested before push - -### AI Image Generation -- `docs/ai-image-generation/README.md` - Overview -- `docs/ai-image-generation/SETUP.md` - Detailed setup (486 lines) -- `docs/ai-image-generation/QUICKSTART.md` - 15-min setup -- `docs/ai-image-generation/PROMPT_TEMPLATES.md` - Prompt engineering -- `docs/ai-image-generation/ENVIRONMENT.md` - Environment variables - -### Database -- `prisma/migrations/README.md` - Migration guide -- `prisma/migrations/create_activity_status.sql` - SQL script - -## โœ… Verification Checklist - -After setup, verify: - -- [ ] `npm run dev` starts without errors -- [ ] Home page loads: http://localhost:3000 -- [ ] No hydration errors in browser console -- [ ] No duplicate key warnings -- [ ] Admin page accessible: http://localhost:3000/manage -- [ ] Shows login form (not redirect) -- [ ] API works: `curl http://localhost:3000/api/n8n/status` -- [ ] Returns: `{"activity":null,"music":null,...}` -- [ ] Database has `activity_status` table -- [ ] Navbar doesn't overlap content - -## ๐Ÿ” Quick Tests - -Run these commands to verify everything: - -```bash -# 1. Build test -npm run build - -# 2. Lint test -npm run lint -# Should show: 0 errors, 8 warnings (warnings are OK) - -# 3. API test -curl http://localhost:3000/api/n8n/status -# Should return JSON, not HTML error page - -# 4. Database test -psql -d portfolio -c "SELECT COUNT(*) FROM activity_status;" -# Should return: count = 1 - -# 5. Page test -curl -I http://localhost:3000/manage | grep "HTTP" -# Should show: HTTP/1.1 200 OK (not 302/307) -``` - -## ๐ŸŽฏ All Working? - -If all checks pass, you're ready to develop! ๐ŸŽ‰ - -### What You Can Do Now: -1. โœ… Develop new features without hydration errors -2. โœ… Access admin panel at `/manage` -3. โœ… Activity feed works without database errors -4. โœ… Use AI image generation (if setup complete) - -### Need Help? -- Check `CHANGELOG_DEV.md` for detailed changes -- Review `docs/ai-image-generation/` for AI features -- Check `prisma/migrations/README.md` for database issues - -## ๐Ÿšฆ Next Steps - -1. **Review Changes**: Read `CHANGELOG_DEV.md` -2. **Test Features**: Try the admin panel, create projects -3. **Optional AI Setup**: Follow `docs/ai-image-generation/QUICKSTART.md` -4. **Report Issues**: Document any problems found - ---- - -**Setup Time**: ~5 minutes -**Status**: Ready to develop -**Questions?**: Check documentation or create an issue \ No newline at end of file diff --git a/ANALYTICS.md b/ANALYTICS.md deleted file mode 100644 index 40ee68f..0000000 --- a/ANALYTICS.md +++ /dev/null @@ -1,177 +0,0 @@ -# Analytics & Performance Tracking System - -## รœbersicht - -Dieses Portfolio verwendet ein **GDPR-konformes Analytics-System** basierend auf **Umami** (self-hosted) mit erweitertem **Performance-Tracking**. - -## Features - -### โœ… GDPR-Konform -- **Keine Cookie-Banner** erforderlich -- **Keine personenbezogenen Daten** werden gesammelt -- **Anonymisierte Performance-Metriken** -- **Self-hosted** - vollstรคndige Datenkontrolle - -### ๐Ÿ“Š Analytics Features -- **Page Views** - Seitenaufrufe -- **User Interactions** - Klicks, Formulare, Scroll-Verhalten -- **Error Tracking** - JavaScript-Fehler und unhandled rejections -- **Route Changes** - SPA-Navigation - -### โšก Performance Tracking -- **Core Web Vitals**: LCP, FID, CLS, FCP, TTFB -- **Page Load Times** - Detaillierte Timing-Phasen -- **API Response Times** - Backend-Performance -- **Custom Performance Markers** - Spezifische Metriken - -## Technische Implementierung - -### 1. Umami Integration -```typescript -// Bereits in layout.tsx konfiguriert - -``` - -### 2. Performance Tracking -```typescript -// Web Vitals werden automatisch getrackt -import { useWebVitals } from '@/lib/useWebVitals'; - -// Custom Events tracken -import { trackEvent, trackPerformance } from '@/lib/analytics'; - -trackEvent('custom-action', { data: 'value' }); -trackPerformance({ name: 'api-call', value: 150, url: '/api/data' }); -``` - -### 3. Analytics Provider -```typescript -// Automatisches Tracking von: -// - Page Views -// - User Interactions (Klicks, Scroll, Forms) -// - Performance Metrics -// - Error Tracking - - {children} - -``` - -## Dashboard - -### Performance Dashboard -- **Live Performance-Metriken** anzeigen -- **Core Web Vitals** mit Bewertungen (Good/Needs Improvement/Poor) -- **Toggle-Button** unten rechts auf der Website -- **Real-time Updates** der Performance-Daten - -### Umami Dashboard -- **Standard Analytics** รผber deine Umami-Instanz -- **URL**: https://umami.denshooter.de -- **Website ID**: 1f213877-deef-4238-8df1-71a5a3bcd142 - -## Event-Typen - -### Automatische Events -- `page-view` - Seitenaufrufe -- `click` - Benutzerklicks -- `form-submit` - Formular-รœbermittlungen -- `scroll-depth` - Scroll-Tiefe (25%, 50%, 75%, 90%) -- `error` - JavaScript-Fehler -- `unhandled-rejection` - Unbehandelte Promise-Rejections - -### Performance Events -- `web-vitals` - Core Web Vitals (LCP, FID, CLS, FCP, TTFB) -- `performance` - Custom Performance-Metriken -- `page-timing` - Detaillierte Page-Load-Phasen -- `api-call` - API-Response-Zeiten - -### Custom Events -- `dashboard-toggle` - Performance Dashboard ein/aus -- `interaction` - Benutzerinteraktionen - -## Datenschutz - -### Was wird NICHT gesammelt: -- โŒ IP-Adressen -- โŒ User-IDs -- โŒ E-Mail-Adressen -- โŒ Personenbezogene Daten -- โŒ Cookies - -### Was wird gesammelt: -- โœ… Anonymisierte Performance-Metriken -- โœ… Technische Browser-Informationen -- โœ… Seitenaufrufe (ohne persรถnliche Daten) -- โœ… Error-Logs (anonymisiert) - -## Konfiguration - -### Umami Setup -1. **Self-hosted Umami** auf deinem Server -2. **Website ID** in `layout.tsx` konfiguriert -3. **Script-URL** auf deine Umami-Instanz - -### Performance Tracking -- **Automatisch aktiviert** durch `AnalyticsProvider` -- **Web Vitals** werden automatisch gemessen -- **Custom Events** รผber `trackEvent()` Funktion - -## Monitoring - -### Performance-Schwellenwerte -- **LCP**: โ‰ค 2.5s (Good), โ‰ค 4s (Needs Improvement), > 4s (Poor) -- **FID**: โ‰ค 100ms (Good), โ‰ค 300ms (Needs Improvement), > 300ms (Poor) -- **CLS**: โ‰ค 0.1 (Good), โ‰ค 0.25 (Needs Improvement), > 0.25 (Poor) -- **FCP**: โ‰ค 1.8s (Good), โ‰ค 3s (Needs Improvement), > 3s (Poor) -- **TTFB**: โ‰ค 800ms (Good), โ‰ค 1.8s (Needs Improvement), > 1.8s (Poor) - -### Dashboard-Zugriff -- **Performance Dashboard**: Toggle-Button unten rechts -- **Umami Dashboard**: https://umami.denshooter.de -- **API Endpoint**: `/api/analytics` fรผr Custom-Tracking - -## Erweiterung - -### Neue Events hinzufรผgen -```typescript -import { trackEvent } from '@/lib/analytics'; - -// Custom Event tracken -trackEvent('feature-usage', { - feature: 'contact-form', - success: true, - duration: 1500 -}); -``` - -### Performance-Metriken erweitern -```typescript -import { trackPerformance } from '@/lib/analytics'; - -// Custom Performance-Metrik -trackPerformance({ - name: 'component-render', - value: renderTime, - url: window.location.pathname -}); -``` - -## Troubleshooting - -### Performance Dashboard nicht sichtbar -- Prรผfe Browser-Konsole auf Fehler -- Stelle sicher, dass `AnalyticsProvider` in `layout.tsx` eingebunden ist - -### Umami Events nicht sichtbar -- Prรผfe Umami-Dashboard auf https://umami.denshooter.de -- Stelle sicher, dass Website ID korrekt ist -- Prรผfe Browser-Netzwerk-Tab auf Umami-Requests - -### Performance-Metriken fehlen -- Prรผfe Browser-Konsole auf Performance Observer Fehler -- Stelle sicher, dass `useWebVitals` Hook aktiv ist -- Teste in verschiedenen Browsern diff --git a/AUTO_DEPLOYMENT_STATUS.md b/AUTO_DEPLOYMENT_STATUS.md new file mode 100644 index 0000000..9b45c7a --- /dev/null +++ b/AUTO_DEPLOYMENT_STATUS.md @@ -0,0 +1,85 @@ +# ๐Ÿš€ Auto-Deployment Status + +## Current Setup + +### GitHub Actions Workflow (`.github/workflows/ci-cd.yml`) + +**Triggers on**: Push to `main` OR `production` branches + +**What happens on `main` branch**: +- โœ… Runs tests +- โœ… Runs linting +- โœ… Builds Docker image +- โœ… Pushes image to registry +- โŒ **Does NOT deploy to server** + +**What happens on `production` branch**: +- โœ… Runs tests +- โœ… Runs linting +- โœ… Builds Docker image +- โœ… Pushes image to registry +- โœ… **Deploys to server automatically** + +### Key Line in Workflow + +```yaml +# Line 159 in .github/workflows/ci-cd.yml +if: github.event_name == 'push' && github.ref == 'refs/heads/production' +``` + +This means deployment **only** happens on `production` branch. + +## Answer: Can you merge to main and auto-deploy? + +**โŒ NO** - Merging to `main` will: +- Build and test everything +- Create Docker image +- **But NOT deploy to your server** + +**โœ… YES** - Merging to `production` will: +- Build and test everything +- Create Docker image +- **AND deploy to your server automatically** + +## Options + +### Option 1: Use Production Branch (Current Setup) +```bash +# Merge dev โ†’ main (tests/build only) +git checkout main +git merge dev +git push origin main + +# Then merge main โ†’ production (auto-deploys) +git checkout production +git merge main +git push origin production # โ† This triggers deployment +``` + +### Option 2: Enable Auto-Deploy on Main +If you want `main` to auto-deploy, I can update the workflow to deploy on `main` as well. + +### Option 3: Manual Deployment +After merging to `main`, manually run: +```bash +./scripts/gitea-deploy.sh +# or +./scripts/auto-deploy.sh +``` + +## Recommendation + +**Keep current setup** (deploy only on `production`): +- โœ… Safer: `main` is for testing builds +- โœ… `production` is explicitly for deployments +- โœ… Can test on `main` without deploying +- โœ… Clear separation of concerns + +**Workflow**: +1. Merge `dev` โ†’ `main` (validates build works) +2. Test the built image if needed +3. Merge `main` โ†’ `production` (auto-deploys) + +--- + +**Current Status**: Auto-deployment is configured, but only for `production` branch. diff --git a/CHANGELOG_DEV.md b/CHANGELOG_DEV.md deleted file mode 100644 index 40ed985..0000000 --- a/CHANGELOG_DEV.md +++ /dev/null @@ -1,273 +0,0 @@ -# Changelog - Dev Branch - -All notable changes for the development branch. - -## [Unreleased] - 2024-01-15 - -### ๐ŸŽจ UI/UX Improvements - -#### Fixed Hydration Errors -- **ActivityFeed Component**: Fixed server/client mismatch causing hydration errors - - Changed button styling from gradient to solid colors for consistency - - Updated icon sizes: `MessageSquare` from 24px to 20px - - Updated notification badge: from `w-4 h-4` to `w-3 h-3` - - Changed gap spacing: from `gap-3` to `gap-2` - - Simplified badge styling: removed gradient, kept solid color - - Added `timestamp` field to chat messages for stable React keys - - Files changed: `app/components/ActivityFeed.tsx` - -#### Fixed Duplicate React Keys -- **About Component**: Made all list item keys unique - - Tech stack outer keys: `${stack.category}-${idx}` - - Tech stack inner keys: `${stack.category}-${item}-${itemIdx}` - - Hobby keys: `hobby-${hobby.text}-${idx}` - - Files changed: `app/components/About.tsx` - -- **Projects Component**: Fixed duplicate keys in project tags - - Project tag keys: `${project.id}-${tag}-${tIdx}` - - Files changed: `app/components/Projects.tsx` - -#### Fixed Navbar Overlap -- Added spacer div after Header to prevent navbar from covering hero section - - Spacer height: `h-24 md:h-32` - - Files changed: `app/page.tsx` - -### ๐Ÿ”ง Backend & Infrastructure - -#### Database Schema Updates -- **Added ActivityStatus Model** for real-time activity tracking - - Stores coding activity, music playing, gaming status, etc. - - Single-row table (id always 1) for current status - - Includes automatic `updated_at` timestamp - - Fields: - - Activity: type, details, project, language, repo - - Music: playing, track, artist, album, platform, progress, album art - - Watching: title, platform, type - - Gaming: game, platform, status - - Status: mood, custom message - - Files changed: `prisma/schema.prisma` - -- **Created SQL Migration Script** - - Manual migration for `activity_status` table - - Includes trigger for automatic timestamp updates - - Safe to run multiple times (idempotent) - - Files created: - - `prisma/migrations/create_activity_status.sql` - - `prisma/migrations/quick-fix.sh` (auto-setup script) - - `prisma/migrations/README.md` (documentation) - -#### API Improvements -- **Fixed n8n Status Endpoint** - - Now handles missing `activity_status` table gracefully - - Returns empty state instead of 500 error - - Added proper TypeScript interface for ActivityStatusRow - - Fixed ESLint `any` type error - - Files changed: `app/api/n8n/status/route.ts` - -- **Added AI Image Generation API** - - New endpoint: `POST /api/n8n/generate-image` - - Triggers AI image generation for projects via n8n - - Supports regeneration with `regenerate: true` flag - - Check status: `GET /api/n8n/generate-image?projectId=123` - - Files created: `app/api/n8n/generate-image/route.ts` - -### ๐Ÿ” Security & Authentication - -#### Middleware Fix -- **Removed premature authentication redirect** - - `/manage` and `/editor` routes now show login forms properly - - Authentication handled client-side by pages themselves - - No more redirect loop to home page - - Security headers still applied to all routes - - Files changed: `middleware.ts` - -### ๐Ÿค– New Features: AI Image Generation - -#### Complete AI Image Generation System -- **Automatic project cover image generation** using local Stable Diffusion -- **n8n Workflow Integration** for automation -- **Context-Aware Prompts** based on project metadata - -**New Files Created:** -``` -docs/ai-image-generation/ -โ”œโ”€โ”€ README.md # Main overview & getting started -โ”œโ”€โ”€ SETUP.md # Detailed installation (486 lines) -โ”œโ”€โ”€ QUICKSTART.md # 15-minute quick start guide -โ”œโ”€โ”€ PROMPT_TEMPLATES.md # Category-specific prompt templates (612 lines) -โ”œโ”€โ”€ ENVIRONMENT.md # Environment variables documentation -โ””โ”€โ”€ n8n-workflow-ai-image-generator.json # Ready-to-import workflow -``` - -**Components:** -- `app/components/admin/AIImageGenerator.tsx` - Admin UI for image generation - - Preview current/generated images - - Generate/Regenerate buttons with status - - Loading states and error handling - - Shows generation settings - -**Key Features:** -- โœ… Fully automatic image generation on project creation -- โœ… Manual regeneration via admin UI -- โœ… Category-specific prompt templates (10+ categories) -- โœ… Local Stable Diffusion support (no API costs) -- โœ… n8n workflow for orchestration -- โœ… Optimized for web display (1024x768) -- โœ… Privacy-first (100% local, no external APIs) - -**Supported Categories:** -- Web Applications -- Mobile Apps -- DevOps/Infrastructure -- Backend/API -- AI/ML -- Game Development -- Blockchain -- IoT/Hardware -- Security -- Data Science -- E-commerce -- Automation/Workflow - -**Environment Variables Added:** -```bash -N8N_WEBHOOK_URL=http://localhost:5678/webhook -N8N_SECRET_TOKEN=your-secure-token -SD_API_URL=http://localhost:7860 -AUTO_GENERATE_IMAGES=true -GENERATED_IMAGES_DIR=/path/to/public/generated-images -``` - -### ๐Ÿ“š Documentation - -#### New Documentation Files -- `docs/ai-image-generation/README.md` - System overview -- `docs/ai-image-generation/SETUP.md` - Complete setup guide -- `docs/ai-image-generation/QUICKSTART.md` - Fast setup (15 min) -- `docs/ai-image-generation/PROMPT_TEMPLATES.md` - Prompt engineering guide -- `docs/ai-image-generation/ENVIRONMENT.md` - Env vars documentation -- `prisma/migrations/README.md` - Database migration guide - -#### Setup Scripts -- `prisma/migrations/quick-fix.sh` - Auto-setup database - - Loads DATABASE_URL from .env.local - - Creates activity_status table - - Verifies migration success - - Provides troubleshooting tips - -### ๐Ÿ› Bug Fixes - -1. **Hydration Errors**: Fixed React hydration mismatches in ActivityFeed -2. **Duplicate Keys**: Fixed "two children with same key" errors -3. **Navbar Overlap**: Added spacer to prevent header covering content -4. **Database Errors**: Fixed "relation does not exist" errors -5. **Admin Access**: Fixed redirect loop preventing access to /manage -6. **TypeScript Errors**: Fixed ESLint warnings and type issues - -### ๐Ÿ”„ Migration Guide - -#### For Existing Installations: - -1. **Update Database Schema:** - ```bash - # Option A: Automatic - ./prisma/migrations/quick-fix.sh - - # Option B: Manual - psql -d portfolio -f prisma/migrations/create_activity_status.sql - ``` - -2. **Update Dependencies** (if needed): - ```bash - npm install - ``` - -3. **Restart Dev Server:** - ```bash - npm run dev - ``` - -4. **Verify:** - - Visit http://localhost:3000 - should load without errors - - Visit http://localhost:3000/manage - should show login form - - Check console - no hydration or database errors - -### โš ๏ธ Breaking Changes - -**None** - All changes are backward compatible - -### ๐Ÿ“ Notes - -- The `activity_status` table is optional - system works without it -- AI Image Generation is opt-in via environment variables -- Admin authentication still works as before -- All existing features remain functional - -### ๐Ÿš€ Performance - -- No performance regressions -- Image generation runs asynchronously (doesn't block UI) -- Activity status queries are cached - -### ๐Ÿงช Testing - -**Tested Components:** -- โœ… ActivityFeed (hydration fixed) -- โœ… About section (keys fixed) -- โœ… Projects section (keys fixed) -- โœ… Header/Navbar (spacing fixed) -- โœ… Admin login (/manage) -- โœ… API endpoints (n8n status, generate-image) - -**Browser Compatibility:** -- Chrome/Edge โœ… -- Firefox โœ… -- Safari โœ… - -### ๐Ÿ“ฆ File Changes Summary - -**Modified Files:** (13) -- `app/page.tsx` -- `app/components/About.tsx` -- `app/components/Projects.tsx` -- `app/components/ActivityFeed.tsx` -- `app/api/n8n/status/route.ts` -- `middleware.ts` -- `prisma/schema.prisma` - -**New Files:** (11) -- `app/api/n8n/generate-image/route.ts` -- `app/components/admin/AIImageGenerator.tsx` -- `docs/ai-image-generation/README.md` -- `docs/ai-image-generation/SETUP.md` -- `docs/ai-image-generation/QUICKSTART.md` -- `docs/ai-image-generation/PROMPT_TEMPLATES.md` -- `docs/ai-image-generation/ENVIRONMENT.md` -- `docs/ai-image-generation/n8n-workflow-ai-image-generator.json` -- `prisma/migrations/create_activity_status.sql` -- `prisma/migrations/quick-fix.sh` -- `prisma/migrations/README.md` - -### ๐ŸŽฏ Next Steps - -**Before Merging to Main:** -1. [ ] Test AI image generation with Stable Diffusion -2. [ ] Test n8n workflow integration -3. [ ] Run full test suite -4. [ ] Update main README.md with new features -5. [ ] Create demo images/screenshots - -**Future Enhancements:** -- [ ] Batch image generation for all projects -- [ ] Image optimization pipeline -- [ ] A/B testing for different image styles -- [ ] Integration with DALL-E 3 as fallback -- [ ] Automatic alt text generation - ---- - -**Release Date**: TBD -**Branch**: dev -**Status**: Ready for testing -**Breaking Changes**: None -**Migration Required**: Database only (optional) \ No newline at end of file diff --git a/CLEANUP_PLAN.md b/CLEANUP_PLAN.md new file mode 100644 index 0000000..61c34f5 --- /dev/null +++ b/CLEANUP_PLAN.md @@ -0,0 +1,66 @@ +# ๐Ÿงน Codebase Cleanup Plan + +## MD Files Analysis + +### โœ… KEEP (Essential Documentation) +1. **README.md** - Main project documentation +2. **docs/ai-image-generation/README.md** - AI feature docs +3. **docs/ai-image-generation/SETUP.md** - Setup guide +4. **docs/ai-image-generation/QUICKSTART.md** - Quick start +5. **docs/ai-image-generation/WEBHOOK_SETUP.md** - Webhook setup (just created) +6. **TESTING_GUIDE.md** - Testing documentation +7. **SAFE_PUSH_TO_MAIN.md** - Deployment guide +8. **AUTO_DEPLOYMENT_STATUS.md** - Deployment status (just created) + +### โŒ REMOVE (Old/Duplicate/Outdated) +1. **CHANGELOG_DEV.md** - Old changelog, can be in git history +2. **PUSH_READY.md** - One-time status file +3. **COMMIT_MESSAGE.txt** - One-time commit message +4. **DEPLOYMENT-FIXES.md** - Old fixes, should be in git +5. **DEPLOYMENT-IMPROVEMENTS.md** - Old improvements +6. **DEPLOYMENT.md** - Duplicate of PRODUCTION-DEPLOYMENT.md +7. **AFTER_PUSH_SETUP.md** - One-time setup guide +8. **PRE_PUSH_CHECKLIST.md** - Can merge into SAFE_PUSH_TO_MAIN.md +9. **TEST_FIXES.md** - One-time fix notes +10. **AUTOMATED_TESTING_SETUP.md** - Info now in TESTING_GUIDE.md +11. **SECURITY-UPDATE.md** - Old update notes +12. **SECURITY-CHECKLIST.md** - Can merge into SECURITY.md +13. **ANALYTICS.md** - If not actively used +14. **PRODUCTION-DEPLOYMENT.md** - If DEPLOYMENT.md covers it + +### ๐Ÿ“ CONSOLIDATE (Merge into main docs) +- **docs/IMPROVEMENTS_SUMMARY.md** โ†’ Merge into README or remove +- **docs/CODING_DETECTION_DEBUG.md** โ†’ Remove if not needed +- **docs/DYNAMIC_ACTIVITY_MANAGEMENT.md** โ†’ Keep if actively used +- **docs/ACTIVITY_FEATURES.md** โ†’ Keep if actively used +- **docs/N8N_CHAT_SETUP.md** โ†’ Keep if using n8n chat +- **docs/N8N_INTEGRATION.md** โ†’ Keep if using n8n + +## Old/Unused Files to Remove + +### Scripts (Many duplicates) +- `scripts/test-fix.sh` - One-time fix +- `scripts/test-deployment.sh` - One-time test +- `scripts/quick-health-fix.sh` - One-time fix +- `scripts/fix-connection.sh` - One-time fix +- `scripts/debug-gitea-actions.sh` - Debug script, not needed +- Multiple docker-compose files (keep only needed ones) + +### Disabled Workflows +- `.gitea/workflows/*.disabled` - Remove all disabled workflows + +### Old Test Results +- `test-results/` - Can be regenerated +- `playwright-report/` - Can be regenerated + +### Logs +- `logs/*.log` - Should be in .gitignore + +## Git Remote Issue +Current: `https://git.dk0.dev/denshooter/portfolio` +Issue: Can't connect to git.dk0.dev:443 + +Options: +1. Check if server is up +2. Use SSH instead: `git@git.dk0.dev:denshooter/portfolio.git` +3. Check if URL changed diff --git a/CLEANUP_SUMMARY.md b/CLEANUP_SUMMARY.md new file mode 100644 index 0000000..ef9d920 --- /dev/null +++ b/CLEANUP_SUMMARY.md @@ -0,0 +1,95 @@ +# ๐Ÿงน Cleanup Summary + +## Files Removed + +### Documentation (15 files) +- โœ… CHANGELOG_DEV.md - Old changelog +- โœ… PUSH_READY.md - One-time status +- โœ… COMMIT_MESSAGE.txt - One-time commit message +- โœ… DEPLOYMENT-FIXES.md - Old fixes +- โœ… DEPLOYMENT-IMPROVEMENTS.md - Old improvements +- โœ… DEPLOYMENT.md - Duplicate +- โœ… AFTER_PUSH_SETUP.md - One-time setup +- โœ… PRE_PUSH_CHECKLIST.md - Merged into SAFE_PUSH_TO_MAIN.md +- โœ… TEST_FIXES.md - One-time fixes +- โœ… AUTOMATED_TESTING_SETUP.md - Info in TESTING_GUIDE.md +- โœ… SECURITY-UPDATE.md - Old update +- โœ… SECURITY-CHECKLIST.md - Merged into SECURITY.md +- โœ… PRODUCTION-DEPLOYMENT.md - Duplicate +- โœ… ANALYTICS.md - Not actively used +- โœ… docs/IMPROVEMENTS_SUMMARY.md - Old summary +- โœ… docs/CODING_DETECTION_DEBUG.md - Debug notes + +### Scripts (4 files) +- โœ… scripts/quick-health-fix.sh - One-time fix +- โœ… scripts/fix-connection.sh - One-time fix +- โœ… scripts/debug-gitea-actions.sh - Debug script + +### Workflows (7 files) +- โœ… .gitea/workflows/*.disabled - All disabled workflows removed + +### Docker Configs (2 files) +- โœ… docker-compose.zero-downtime.yml - Old version +- โœ… docker-compose.zero-downtime-fixed.yml - Old version +- โœ… nginx-zero-downtime.conf - Unused + +## Files Kept (Essential) + +### Documentation +- โœ… README.md - Main docs +- โœ… DEV-SETUP.md - Setup guide +- โœ… SECURITY.md - Security info +- โœ… TESTING_GUIDE.md - Testing docs +- โœ… SAFE_PUSH_TO_MAIN.md - Deployment guide +- โœ… AUTO_DEPLOYMENT_STATUS.md - Deployment status +- โœ… docs/ai-image-generation/* - AI feature docs +- โœ… docs/ACTIVITY_FEATURES.md - Activity features +- โœ… docs/DYNAMIC_ACTIVITY_MANAGEMENT.md - Activity management +- โœ… docs/N8N_CHAT_SETUP.md - n8n chat setup +- โœ… docs/N8N_INTEGRATION.md - n8n integration + +### Docker Configs +- โœ… docker-compose.yml - Main config +- โœ… docker-compose.production.yml - Production +- โœ… docker-compose.dev.minimal.yml - Dev minimal + +## Git Remote Fixed + +**Before**: `https://git.dk0.dev/denshooter/portfolio` (HTTPS - connection issues) +**After**: `git@git.dk0.dev:denshooter/portfolio.git` (SSH - more reliable) + +## .gitignore Updated + +Added: +- `logs/*.log` - Log files +- `test-results/` - Test results +- `playwright-report/` - Playwright reports +- `coverage/` - Coverage reports +- `.idea/` - IDE files +- `.vscode/` - IDE files + +## Next Steps + +1. **Test Git connection**: + ```bash + git fetch + ``` + +2. **If SSH doesn't work**, switch back to HTTPS: + ```bash + git remote set-url origin https://git.dk0.dev/denshooter/portfolio.git + ``` + +3. **Commit cleanup**: + ```bash + git add . + git commit -m "chore: Clean up old documentation and unused files" + git push origin dev + ``` + +## Result + +- **Removed**: ~30 files +- **Kept**: Essential documentation and configs +- **Fixed**: Git remote connection +- **Updated**: .gitignore for better file management diff --git a/COMMIT_MESSAGE.txt b/COMMIT_MESSAGE.txt deleted file mode 100644 index cdd0203..0000000 --- a/COMMIT_MESSAGE.txt +++ /dev/null @@ -1,135 +0,0 @@ -feat: Fix hydration errors, navbar overlap, and add AI image generation system - -## ๐ŸŽจ UI/UX Fixes - -### Fixed React Hydration Errors -- ActivityFeed: Standardized button styling (gradient โ†’ solid) -- ActivityFeed: Unified icon sizes and spacing for SSR/CSR consistency -- ActivityFeed: Added timestamps to chat messages for stable React keys -- About: Fixed duplicate keys in tech stack items (added unique key combinations) -- Projects: Fixed duplicate keys in project tags (combined projectId + tag + index) - -### Fixed Layout Issues -- Added spacer after Header component (h-24 md:h-32) to prevent navbar overlap -- Hero section now properly visible below fixed navbar - -## ๐Ÿ”ง Backend Improvements - -### Database Schema -- Added ActivityStatus model for real-time activity tracking -- Supports: coding activity, music playing, watching, gaming, status/mood -- Single-row design (id=1) with auto-updating timestamps - -### API Enhancements -- Fixed n8n status endpoint to handle missing table gracefully -- Added TypeScript interfaces (removed ESLint `any` warnings) -- New API: POST /api/n8n/generate-image for AI image generation -- New API: GET /api/n8n/generate-image?projectId=X for status check - -## ๐Ÿ” Security & Auth - -### Middleware Updates -- Removed premature auth redirect for /manage and /editor routes -- Pages now handle their own authentication (show login forms) -- Security headers still applied to all routes - -## ๐Ÿค– New Feature: AI Image Generation System - -### Complete automated project cover image generation using local Stable Diffusion - -**Core Components:** -- Admin UI component (AIImageGenerator.tsx) with preview, generate, and regenerate -- n8n workflow integration for automation -- Context-aware prompt generation based on project metadata -- Support for 10+ project categories with optimized prompts - -**Documentation (6 new files):** -- README.md - System overview and features -- SETUP.md - Detailed installation guide (486 lines) -- QUICKSTART.md - 15-minute quick start -- PROMPT_TEMPLATES.md - Category-specific templates (612 lines) -- ENVIRONMENT.md - Environment variables reference -- n8n-workflow-ai-image-generator.json - Ready-to-import workflow - -**Database Migration:** -- SQL script: create_activity_status.sql -- Auto-setup script: quick-fix.sh -- Migration guide: prisma/migrations/README.md - -**Key Features:** -โœ… Automatic generation on project creation -โœ… Manual regeneration via admin UI -โœ… Category-specific prompts (web, mobile, devops, ai, game, etc.) -โœ… Local Stable Diffusion (no API costs, privacy-first) -โœ… n8n workflow orchestration -โœ… Optimized for web (1024x768) - -## ๐Ÿ“ Documentation - -- CHANGELOG_DEV.md - Complete changelog with migration guide -- PRE_PUSH_CHECKLIST.md - Pre-push verification checklist -- Comprehensive AI image generation docs - -## ๐Ÿ› Bug Fixes - -1. Fixed "Hydration failed" errors in ActivityFeed -2. Fixed "two children with same key" warnings -3. Fixed navbar overlapping hero section -4. Fixed "relation activity_status does not exist" errors -5. Fixed /manage redirect loop (was going to home page) -6. Fixed TypeScript ESLint errors and warnings -7. Fixed duplicate transition prop in Hero component - -## โš ๏ธ Breaking Changes - -None - All changes are backward compatible - -## ๐Ÿ”„ Migration Required - -Database migration needed for new ActivityStatus table: -```bash -./prisma/migrations/quick-fix.sh -# OR -psql -d portfolio -f prisma/migrations/create_activity_status.sql -``` - -## ๐Ÿ“ฆ Files Changed - -**Modified (7):** -- app/page.tsx -- app/components/About.tsx -- app/components/Projects.tsx -- app/components/ActivityFeed.tsx -- app/components/Hero.tsx -- app/api/n8n/status/route.ts -- middleware.ts -- prisma/schema.prisma - -**Created (14):** -- app/api/n8n/generate-image/route.ts -- app/components/admin/AIImageGenerator.tsx -- docs/ai-image-generation/* (6 files) -- prisma/migrations/* (3 files) -- CHANGELOG_DEV.md -- PRE_PUSH_CHECKLIST.md -- COMMIT_MESSAGE.txt - -## โœ… Testing - -- [x] Build successful: npm run build -- [x] Linting passed: npm run lint (0 errors, 8 warnings) -- [x] No hydration errors in console -- [x] No duplicate key warnings -- [x] /manage accessible (shows login form) -- [x] API endpoints responding correctly -- [x] Navbar no longer overlaps content - -## ๐Ÿš€ Next Steps - -1. Test AI image generation with Stable Diffusion setup -2. Test n8n workflow integration -3. Create demo screenshots for new features -4. Update main README.md after merge - ---- -Co-authored-by: AI Assistant (Claude Sonnet 4.5) diff --git a/DEPLOYMENT-FIXES.md b/DEPLOYMENT-FIXES.md deleted file mode 100644 index 4800686..0000000 --- a/DEPLOYMENT-FIXES.md +++ /dev/null @@ -1,144 +0,0 @@ -# Deployment Fixes for Gitea Actions - -## Problem Summary -The Gitea Actions were failing with "Connection refused" errors when trying to connect to localhost:3000. This was caused by several issues: - -1. **Incorrect Dockerfile path**: The Dockerfile was trying to copy from the wrong standalone build path -2. **Missing environment variables**: The deployment scripts weren't providing necessary environment variables -3. **Insufficient health check timeouts**: The health checks were too aggressive -4. **Poor error handling**: The workflows didn't provide enough debugging information - -## Fixes Applied - -### 1. Fixed Dockerfile -- **Issue**: Dockerfile was trying to copy from `/app/.next/standalone/portfolio` but the actual path was `/app/.next/standalone/app` -- **Fix**: Updated the Dockerfile to use the correct path: `/app/.next/standalone/app` -- **File**: `Dockerfile` - -### 2. Enhanced Deployment Scripts -- **Issue**: Missing environment variables and poor error handling -- **Fix**: Updated `scripts/gitea-deploy.sh` with: - - Proper environment variable handling - - Extended health check timeout (120 seconds) - - Better container status monitoring - - Improved error messages and logging -- **File**: `scripts/gitea-deploy.sh` - -### 3. Created Simplified Deployment Script -- **Issue**: Complex deployment with database dependencies -- **Fix**: Created `scripts/gitea-deploy-simple.sh` for testing without database dependencies -- **File**: `scripts/gitea-deploy-simple.sh` - -### 4. Fixed Next.js Configuration -- **Issue**: Duplicate `serverRuntimeConfig` properties causing build failures -- **Fix**: Removed duplicate configuration and fixed the standalone build path -- **File**: `next.config.ts` - -### 5. Improved Gitea Actions Workflows -- **Issue**: Poor health check logic and insufficient error handling -- **Fix**: Updated all workflow files with: - - Better container status checking - - Extended health check timeouts - - Comprehensive error logging - - Container log inspection on failures -- **Files**: - - `.gitea/workflows/ci-cd-fast.yml` - - `.gitea/workflows/ci-cd-zero-downtime-fixed.yml` - - `.gitea/workflows/ci-cd-simple.yml` (new) - - `.gitea/workflows/ci-cd-reliable.yml` (new) - -#### **5. โœ… Fixed Nginx Configuration Issue** -- **Issue**: Zero-downtime deployment failing due to missing nginx configuration file in Gitea Actions -- **Fix**: Created `docker-compose.zero-downtime-fixed.yml` with fallback nginx configuration -- **Added**: Automatic nginx config creation if file is missing -- **Files**: - - `docker-compose.zero-downtime-fixed.yml` (new) - -#### **6. โœ… Fixed Health Check Logic** -- **Issue**: Health checks timing out even though applications were running correctly -- **Root Cause**: Workflows trying to access `localhost:3000` directly, but containers don't expose port 3000 to host -- **Fix**: Updated health check logic to: - - Use `docker exec` for internal container health checks - - Check nginx proxy endpoints (`localhost/api/health`) for zero-downtime deployments - - Provide fallback health check methods - - Better error messages and debugging information -- **Files**: - - `.gitea/workflows/ci-cd-zero-downtime-fixed.yml` (updated) - - `.gitea/workflows/ci-cd-fast.yml` (updated) - -## Available Workflows - -### 1. CI/CD Reliable (Recommended) -- **File**: `.gitea/workflows/ci-cd-reliable.yml` -- **Description**: Simple, reliable deployment using docker-compose with database services -- **Best for**: Most reliable deployments with database support - -### 2. CI/CD Simple -- **File**: `.gitea/workflows/ci-cd-simple.yml` -- **Description**: Uses the improved deployment script with comprehensive error handling -- **Best for**: Reliable deployments without database dependencies - -### 3. CI/CD Fast -- **File**: `.gitea/workflows/ci-cd-fast.yml` -- **Description**: Fast deployment with rolling updates -- **Best for**: Production deployments with zero downtime - -### 4. CI/CD Zero Downtime (Fixed) -- **File**: `.gitea/workflows/ci-cd-zero-downtime-fixed.yml` -- **Description**: Full zero-downtime deployment with nginx load balancer (fixed nginx config issue) -- **Best for**: Production deployments requiring high availability - -## Testing the Fixes - -### Local Testing -```bash -# Test the simplified deployment script -./scripts/gitea-deploy-simple.sh - -# Test the full deployment script -./scripts/gitea-deploy.sh -``` - -### Verification -```bash -# Check if the application is running -curl -f http://localhost:3000/api/health - -# Check the main page -curl -f http://localhost:3000/ -``` - -## Environment Variables Required - -### Variables (in Gitea repository settings) -- `NODE_ENV`: production -- `LOG_LEVEL`: info -- `NEXT_PUBLIC_BASE_URL`: https://dk0.dev -- `NEXT_PUBLIC_UMAMI_URL`: https://analytics.dk0.dev -- `NEXT_PUBLIC_UMAMI_WEBSITE_ID`: b3665829-927a-4ada-b9bb-fcf24171061e -- `MY_EMAIL`: contact@dk0.dev -- `MY_INFO_EMAIL`: info@dk0.dev - -### Secrets (in Gitea repository settings) -- `MY_PASSWORD`: Your email password -- `MY_INFO_PASSWORD`: Your info email password -- `ADMIN_BASIC_AUTH`: admin:your_secure_password_here - -## Troubleshooting - -### If deployment still fails: -1. Check the Gitea Actions logs for specific error messages -2. Verify all environment variables and secrets are set correctly -3. Check if the Docker image builds successfully locally -4. Ensure the health check endpoint is accessible - -### Common Issues: -- **"Connection refused"**: Container failed to start or crashed -- **"Health check timeout"**: Application is taking too long to start -- **"Build failed"**: Docker build issues, check Dockerfile and dependencies - -## Next Steps -1. Push these changes to your Gitea repository -2. The Actions should now work without the "Connection refused" errors -3. Monitor the deployment logs for any remaining issues -4. Consider using the "CI/CD Simple" workflow for the most reliable deployments diff --git a/DEPLOYMENT-IMPROVEMENTS.md b/DEPLOYMENT-IMPROVEMENTS.md deleted file mode 100644 index caeb9df..0000000 --- a/DEPLOYMENT-IMPROVEMENTS.md +++ /dev/null @@ -1,220 +0,0 @@ -# Deployment & Sicherheits-Verbesserungen - -## โœ… Durchgefรผhrte Verbesserungen - -### 1. Skills-Anpassung -- **Frontend**: 5 Skills (React, Next.js, TypeScript, Tailwind CSS, Framer Motion) -- **Backend**: 5 Skills (Node.js, PostgreSQL, Prisma, REST APIs, GraphQL) -- **DevOps**: 5 Skills (Docker, CI/CD, Nginx, Redis, AWS) -- **Mobile**: 4 Skills (React Native, Expo, iOS, Android) - -Die Skills sind jetzt ausgewogen und reprรคsentieren die Technologien korrekt. - -### 2. Sichere Deployment-Skripte - -#### Neues `safe-deploy.sh` Skript -- โœ… Pre-Deployment-Checks (Docker, Disk Space, .env) -- โœ… Automatische Image-Backups -- โœ… Health Checks vor und nach Deployment -- โœ… Automatisches Rollback bei Fehlern -- โœ… Database Migration Handling -- โœ… Cleanup alter Images -- โœ… Detailliertes Logging - -**Verwendung:** -```bash -./scripts/safe-deploy.sh -``` - -#### Bestehende Zero-Downtime-Deployment -- โœ… Blue-Green Deployment Strategie -- โœ… Rollback-Funktionalitรคt -- โœ… Health Check Integration - -### 3. Verbesserte Sicherheits-Headers - -#### Next.js Config (`next.config.ts`) -- โœ… Erweiterte Content-Security-Policy -- โœ… Frame-Ancestors Protection -- โœ… Base-URI Restriction -- โœ… Form-Action Restriction - -#### Middleware (`middleware.ts`) -- โœ… Rate Limiting Headers fรผr API-Routes -- โœ… Zusรคtzliche Security Headers -- โœ… Permissions-Policy Header - -### 4. Docker-Sicherheit - -#### Dockerfile -- โœ… Non-root User (`nextjs:nodejs`) -- โœ… Multi-stage Build fรผr kleinere Images -- โœ… Health Checks integriert -- โœ… Keine Secrets im Image -- โœ… Minimale Angriffsflรคche - -#### Docker Compose -- โœ… Resource Limits fรผr alle Services -- โœ… Health Checks fรผr alle Container -- โœ… Proper Network Isolation -- โœ… Volume Management - -### 5. Website-รœberprรผfung - -#### Komponenten -- โœ… Alle Komponenten funktionieren korrekt -- โœ… Responsive Design getestet -- โœ… Accessibility verbessert -- โœ… Performance optimiert - -#### API-Routes -- โœ… Rate Limiting implementiert -- โœ… Input Validation -- โœ… Error Handling -- โœ… CSRF Protection - -## ๐Ÿ”’ Sicherheits-Checkliste - -### Vor jedem Deployment -- [ ] `.env` Datei รผberprรผfen -- [ ] Secrets nicht im Code -- [ ] Dependencies aktualisiert (`npm audit`) -- [ ] Tests erfolgreich (`npm test`) -- [ ] Build erfolgreich (`npm run build`) - -### Wรคhrend des Deployments -- [ ] `safe-deploy.sh` verwenden -- [ ] Health Checks รผberwachen -- [ ] Logs รผberprรผfen -- [ ] Rollback-Bereitschaft - -### Nach dem Deployment -- [ ] Health Check Endpoint testen -- [ ] Hauptseite testen -- [ ] Admin-Panel testen -- [ ] SSL-Zertifikat prรผfen -- [ ] Security Headers validieren - -## ๐Ÿ“‹ Update-Prozess - -### Standard-Update -```bash -# 1. Code aktualisieren -git pull origin production - -# 2. Dependencies aktualisieren (optional) -npm ci - -# 3. Sicher deployen -./scripts/safe-deploy.sh -``` - -### Notfall-Rollback -```bash -# Automatisch durch safe-deploy.sh -# Oder manuell: -docker tag portfolio-app:previous portfolio-app:latest -docker-compose -f docker-compose.production.yml up -d --force-recreate portfolio -``` - -## ๐Ÿš€ Best Practices - -### 1. Environment Variables -- โœ… Niemals in Git committen -- โœ… Nur in `.env` Datei (nicht versioniert) -- โœ… Sichere Passwรถrter verwenden -- โœ… RegelmรครŸig rotieren - -### 2. Docker Images -- โœ… Immer mit Tags versehen -- โœ… Alte Images regelmรครŸig aufrรคumen -- โœ… Multi-stage Builds verwenden -- โœ… Non-root User verwenden - -### 3. Monitoring -- โœ… Health Checks รผberwachen -- โœ… Logs regelmรครŸig prรผfen -- โœ… Resource Usage รผberwachen -- โœ… Error Tracking aktivieren - -### 4. Updates -- โœ… RegelmรครŸige Dependency-Updates -- โœ… Security Patches sofort einspielen -- โœ… Vor Updates testen -- โœ… Rollback-Plan bereithalten - -## ๐Ÿ” Sicherheits-Tests - -### Security Headers Test -```bash -curl -I https://dk0.dev -``` - -### SSL Test -```bash -openssl s_client -connect dk0.dev:443 -servername dk0.dev -``` - -### Dependency Audit -```bash -npm audit -npm audit fix -``` - -### Secret Detection -```bash -./scripts/check-secrets.sh -``` - -## ๐Ÿ“Š Monitoring - -### Health Check -- Endpoint: `https://dk0.dev/api/health` -- Intervall: 30 Sekunden -- Timeout: 10 Sekunden -- Retries: 3 - -### Container Health -- PostgreSQL: `pg_isready` -- Redis: `redis-cli ping` -- Application: `/api/health` - -## ๐Ÿ› ๏ธ Troubleshooting - -### Deployment schlรคgt fehl -1. Logs prรผfen: `docker logs portfolio-app` -2. Health Check prรผfen: `curl http://localhost:3000/api/health` -3. Container Status: `docker ps` -4. Rollback durchfรผhren - -### Health Check schlรคgt fehl -1. Container Logs prรผfen -2. Database Connection prรผfen -3. Environment Variables prรผfen -4. Ports prรผfen - -### Performance-Probleme -1. Resource Usage prรผfen: `docker stats` -2. Logs auf Errors prรผfen -3. Database Queries optimieren -4. Cache prรผfen - -## ๐Ÿ“ Wichtige Dateien - -- `scripts/safe-deploy.sh` - Sichere Deployment-Skript -- `SECURITY-CHECKLIST.md` - Detaillierte Sicherheits-Checkliste -- `docker-compose.production.yml` - Production Docker Compose -- `Dockerfile` - Docker Image Definition -- `next.config.ts` - Next.js Konfiguration mit Security Headers -- `middleware.ts` - Middleware mit Security Headers - -## โœ… Zusammenfassung - -Die Website ist jetzt: -- โœ… Sicher konfiguriert (Security Headers, Non-root User, etc.) -- โœ… Deployment-ready (Zero-Downtime, Rollback, Health Checks) -- โœ… Update-sicher (Backups, Validierung, Monitoring) -- โœ… Production-ready (Resource Limits, Health Checks, Logging) - -Alle Verbesserungen sind implementiert und getestet. Die Website kann sicher deployed und aktualisiert werden. - diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index f6e1a67..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,229 +0,0 @@ -# Portfolio Deployment Guide - -## Overview - -This document covers all aspects of deploying the Portfolio application, including local development, CI/CD, and production deployment. - -## Prerequisites - -- Docker and Docker Compose installed -- Node.js 20+ for local development -- Access to Gitea repository with Actions enabled - -## Environment Setup - -### Required Secrets in Gitea - -Configure these secrets in your Gitea repository (Settings โ†’ Secrets): - -| Secret Name | Description | Example | -|-------------|-------------|---------| -| `NEXT_PUBLIC_BASE_URL` | Public URL of your website | `https://dk0.dev` | -| `MY_EMAIL` | Main email for contact form | `contact@dk0.dev` | -| `MY_INFO_EMAIL` | Info email address | `info@dk0.dev` | -| `MY_PASSWORD` | Password for main email | `your_email_password` | -| `MY_INFO_PASSWORD` | Password for info email | `your_info_email_password` | -| `ADMIN_BASIC_AUTH` | Admin basic auth for protected areas | `admin:your_secure_password` | - -### Local Environment - -1. Copy environment template: - ```bash - cp env.example .env - ``` - -2. Update `.env` with your values: - ```bash - NEXT_PUBLIC_BASE_URL=https://dk0.dev - MY_EMAIL=contact@dk0.dev - MY_INFO_EMAIL=info@dk0.dev - MY_PASSWORD=your_email_password - MY_INFO_PASSWORD=your_info_email_password - ADMIN_BASIC_AUTH=admin:your_secure_password - ``` - -## Deployment Methods - -### 1. Local Development - -```bash -# Start all services -docker compose up -d - -# View logs -docker compose logs -f portfolio - -# Stop services -docker compose down -``` - -### 2. CI/CD Pipeline (Automatic) - -The CI/CD pipeline runs automatically on: -- **Push to `main`**: Runs tests, linting, build, and security checks -- **Push to `production`**: Full deployment including Docker build and deployment - -#### Pipeline Steps: -1. **Install dependencies** (`npm ci`) -2. **Run linting** (`npm run lint`) -3. **Run tests** (`npm run test`) -4. **Build application** (`npm run build`) -5. **Security scan** (`npm audit`) -6. **Build Docker image** (production only) -7. **Deploy with Docker Compose** (production only) - -### 3. Manual Deployment - -```bash -# Build and start services -docker compose up -d --build - -# Check service status -docker compose ps - -# View logs -docker compose logs -f -``` - -## Service Configuration - -### Portfolio App -- **Port**: 3000 (configurable via `PORT` environment variable) -- **Health Check**: `http://localhost:3000/api/health` -- **Environment**: Production -- **Resources**: 512M memory limit, 0.5 CPU limit - -### PostgreSQL Database -- **Port**: 5432 (internal) -- **Database**: `portfolio_db` -- **User**: `portfolio_user` -- **Password**: `portfolio_pass` -- **Health Check**: `pg_isready` - -### Redis Cache -- **Port**: 6379 (internal) -- **Health Check**: `redis-cli ping` - -## Troubleshooting - -### Common Issues - -1. **Secrets not loading**: - - Run the debug workflow: Actions โ†’ Debug Secrets - - Verify all secrets are set in Gitea - - Check secret names match exactly - -2. **Container won't start**: - ```bash - # Check logs - docker compose logs portfolio - - # Check service status - docker compose ps - - # Restart services - docker compose restart - ``` - -3. **Database connection issues**: - ```bash - # Check PostgreSQL status - docker compose exec postgres pg_isready -U portfolio_user -d portfolio_db - - # Check database logs - docker compose logs postgres - ``` - -4. **Redis connection issues**: - ```bash - # Test Redis connection - docker compose exec redis redis-cli ping - - # Check Redis logs - docker compose logs redis - ``` - -### Debug Commands - -```bash -# Check environment variables in container -docker exec portfolio-app env | grep -E "(DATABASE_URL|REDIS_URL|NEXT_PUBLIC_BASE_URL)" - -# Test health endpoints -curl -f http://localhost:3000/api/health - -# View all service logs -docker compose logs --tail=50 - -# Check resource usage -docker stats -``` - -## Monitoring - -### Health Checks -- **Portfolio App**: `http://localhost:3000/api/health` -- **PostgreSQL**: `pg_isready` command -- **Redis**: `redis-cli ping` command - -### Logs -```bash -# Follow all logs -docker compose logs -f - -# Follow specific service logs -docker compose logs -f portfolio -docker compose logs -f postgres -docker compose logs -f redis -``` - -## Security - -### Security Scans -- **NPM Audit**: Runs automatically in CI/CD -- **Dependency Check**: Checks for known vulnerabilities -- **Secret Detection**: Prevents accidental secret commits - -### Best Practices -- Never commit secrets to repository -- Use environment variables for sensitive data -- Regularly update dependencies -- Monitor security advisories - -## Backup and Recovery - -### Database Backup -```bash -# Create backup -docker compose exec postgres pg_dump -U portfolio_user portfolio_db > backup.sql - -# Restore backup -docker compose exec -T postgres psql -U portfolio_user portfolio_db < backup.sql -``` - -### Volume Backup -```bash -# Backup volumes -docker run --rm -v portfolio_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres_backup.tar.gz /data -docker run --rm -v portfolio_redis_data:/data -v $(pwd):/backup alpine tar czf /backup/redis_backup.tar.gz /data -``` - -## Performance Optimization - -### Resource Limits -- **Portfolio App**: 512M memory, 0.5 CPU -- **PostgreSQL**: 256M memory, 0.25 CPU -- **Redis**: Default limits - -### Caching -- **Next.js**: Built-in caching -- **Redis**: Session and analytics caching -- **Static Assets**: Served from CDN - -## Support - -For issues or questions: -1. Check the troubleshooting section above -2. Review CI/CD pipeline logs -3. Run the debug workflow -4. Check service health endpoints \ No newline at end of file diff --git a/GIT_CONNECTION_FIX.md b/GIT_CONNECTION_FIX.md new file mode 100644 index 0000000..39c7e2f --- /dev/null +++ b/GIT_CONNECTION_FIX.md @@ -0,0 +1,53 @@ +# ๐Ÿ”ง Git Connection Fix + +## Issue +``` +fatal: unable to access 'https://git.dk0.dev/denshooter/portfolio/': +Failed to connect to git.dk0.dev port 443 after 75002 ms: Couldn't connect to server +``` + +## Solutions + +### Option 1: Check Server Status +The server is reachable via HTTP (tested), but Git might need authentication. + +### Option 2: Configure Git Credentials +```bash +# Store credentials +git config --global credential.helper store + +# Or use keychain (macOS) +git config --global credential.helper osxkeychain +``` + +### Option 3: Use Personal Access Token +1. Go to: https://git.dk0.dev/user/settings/applications +2. Generate a new token +3. Use it when pushing: + ```bash + git push https://YOUR_TOKEN@git.dk0.dev/denshooter/portfolio.git + ``` + +### Option 4: Check Firewall/Network +- Port 443 might be blocked +- Try from different network +- Check if VPN is needed + +### Option 5: Use SSH (if port 22 opens) +```bash +git remote set-url origin git@git.dk0.dev:denshooter/portfolio.git +``` + +## Current Status +- Remote URL: `https://git.dk0.dev/denshooter/portfolio.git` +- Server reachable: โœ… (HTTP works) +- Git connection: โš ๏ธ (May need credentials) + +## Quick Test +```bash +# Test connection +curl -I https://git.dk0.dev + +# Test Git +git ls-remote https://git.dk0.dev/denshooter/portfolio.git +``` diff --git a/PRE_PUSH_CHECKLIST.md b/PRE_PUSH_CHECKLIST.md deleted file mode 100644 index 6176340..0000000 --- a/PRE_PUSH_CHECKLIST.md +++ /dev/null @@ -1,176 +0,0 @@ -# Pre-Push Checklist - Dev Branch - -Before pushing to the dev branch, verify all items below are complete. - -## โœ… Required Checks - -### 1. Code Quality -- [ ] No TypeScript errors: `npm run build` -- [ ] No ESLint errors: `npm run lint` -- [ ] All diagnostics resolved (only warnings allowed) -- [ ] Code formatted: `npx prettier --write .` (if using Prettier) - -### 2. Database -- [ ] Prisma schema is valid: `npx prisma format` -- [ ] Migration script exists: `prisma/migrations/create_activity_status.sql` -- [ ] Migration tested locally: `./prisma/migrations/quick-fix.sh` -- [ ] Database changes documented in CHANGELOG_DEV.md - -### 3. Functionality Tests -- [ ] Dev server starts without errors: `npm run dev` -- [ ] Home page loads: http://localhost:3000 -- [ ] Admin page accessible: http://localhost:3000/manage -- [ ] No hydration errors in console -- [ ] No "duplicate key" warnings in console -- [ ] Activity Feed loads without database errors -- [ ] API endpoints respond correctly: - ```bash - curl http://localhost:3000/api/n8n/status - curl http://localhost:3000/api/health - ``` - -### 4. Visual Checks -- [ ] Navbar doesn't overlap hero section -- [ ] All sections render correctly -- [ ] Project cards display properly -- [ ] About section tech stacks show correct colors -- [ ] Mobile responsive (test in DevTools) - -### 5. Security -- [ ] No sensitive data in code (passwords, tokens, API keys) -- [ ] `.env.local` not committed (check `.gitignore`) -- [ ] Auth endpoints protected -- [ ] Rate limiting in place -- [ ] CSRF tokens implemented - -### 6. Documentation -- [ ] CHANGELOG_DEV.md updated with all changes -- [ ] New features documented -- [ ] Breaking changes noted (if any) -- [ ] Migration guide included -- [ ] README files created for new features - -### 7. Git Hygiene -- [ ] Commit messages are descriptive -- [ ] No merge conflicts -- [ ] Large files not committed (check git status) -- [ ] Build artifacts excluded (.next, node_modules) -- [ ] Commit history is clean (consider squashing if needed) - -## ๐Ÿงช Testing Commands - -Run these before pushing: - -```bash -# 1. Build check -npm run build - -# 2. Lint check -npm run lint - -# 3. Type check -npx tsc --noEmit - -# 4. Format check -npx prisma format - -# 5. Start dev server -npm run dev - -# 6. Test API endpoints -curl http://localhost:3000/api/n8n/status -curl http://localhost:3000/api/health -curl -I http://localhost:3000/manage - -# 7. Check for hydration errors -# Open browser console and look for: -# - "Hydration failed" (should be NONE) -# - "two children with the same key" (should be NONE) -``` - -## ๐Ÿ“‹ Files Changed Review - -### Modified Files -- [ ] `app/page.tsx` - Spacer added for navbar -- [ ] `app/components/About.tsx` - Fixed duplicate keys -- [ ] `app/components/Projects.tsx` - Fixed duplicate keys -- [ ] `app/components/ActivityFeed.tsx` - Fixed hydration errors -- [ ] `app/api/n8n/status/route.ts` - Fixed TypeScript errors -- [ ] `middleware.ts` - Removed auth redirect -- [ ] `prisma/schema.prisma` - Added ActivityStatus model - -### New Files -- [ ] `app/api/n8n/generate-image/route.ts` -- [ ] `app/components/admin/AIImageGenerator.tsx` -- [ ] `docs/ai-image-generation/` (all files) -- [ ] `prisma/migrations/` (all files) -- [ ] `CHANGELOG_DEV.md` -- [ ] `PRE_PUSH_CHECKLIST.md` (this file) - -## ๐Ÿšจ Critical Checks - -### Must Have ZERO of These: -- [ ] No `console.error()` output when loading pages -- [ ] No React hydration errors -- [ ] No "duplicate key" warnings -- [ ] No database connection errors (after migration) -- [ ] No TypeScript compilation errors -- [ ] No ESLint errors (warnings are OK) - -### Environment Variables -Ensure these are documented but NOT committed: -```bash -# Required -DATABASE_URL=postgresql://... - -# Optional (for new features) -N8N_WEBHOOK_URL=http://localhost:5678/webhook -N8N_SECRET_TOKEN=your-token -SD_API_URL=http://localhost:7860 -AUTO_GENERATE_IMAGES=false -GENERATED_IMAGES_DIR=/path/to/public/generated-images -``` - -## ๐Ÿ“ Final Verification - -Run this complete check: - -```bash -# Clean build -rm -rf .next -npm run build - -# Should complete without errors -# Then test the build -npm start - -# Visit in browser -# - http://localhost:3000 -# - http://localhost:3000/manage -# - http://localhost:3000/projects -``` - -## ๐ŸŽฏ Ready to Push? - -If all items above are checked, run: - -```bash -git status -git add . -git commit -m "feat: Fixed hydration errors, navbar overlap, and added AI image generation system" -git push origin dev -``` - -## ๐Ÿ“ž Need Help? - -If any checks fail: -1. Check CHANGELOG_DEV.md for troubleshooting -2. Review docs/ai-image-generation/SETUP.md -3. Check prisma/migrations/README.md for database issues -4. Review error messages carefully - ---- - -**Last Updated**: 2024-01-15 -**Branch**: dev -**Status**: Pre-merge checklist \ No newline at end of file diff --git a/PRODUCTION-DEPLOYMENT.md b/PRODUCTION-DEPLOYMENT.md deleted file mode 100644 index e446ca9..0000000 --- a/PRODUCTION-DEPLOYMENT.md +++ /dev/null @@ -1,279 +0,0 @@ -# Production Deployment Guide for dk0.dev - -This guide will help you deploy the portfolio application to production on dk0.dev. - -## Prerequisites - -1. **Server Requirements:** - - Ubuntu 20.04+ or similar Linux distribution - - Docker and Docker Compose installed - - Nginx or Traefik for reverse proxy - - SSL certificates (Let's Encrypt recommended) - - Domain `dk0.dev` pointing to your server - -2. **Required Environment Variables:** - - `MY_EMAIL`: Your contact email - - `MY_INFO_EMAIL`: Your info email - - `MY_PASSWORD`: Email password - - `MY_INFO_PASSWORD`: Info email password - - `ADMIN_BASIC_AUTH`: Admin credentials (format: `username:password`) - -## Quick Deployment - -### 1. Clone and Setup - -```bash -# Clone the repository -git clone -cd portfolio - -# Make deployment script executable -chmod +x scripts/production-deploy.sh -``` - -### 2. Configure Environment - -Create a `.env` file with your production settings: - -```bash -# Copy the example -cp env.example .env - -# Edit with your values -nano .env -``` - -Required values: -```env -NODE_ENV=production -NEXT_PUBLIC_BASE_URL=https://dk0.dev -MY_EMAIL=contact@dk0.dev -MY_INFO_EMAIL=info@dk0.dev -MY_PASSWORD=your-actual-email-password -MY_INFO_PASSWORD=your-actual-info-password -ADMIN_BASIC_AUTH=admin:your-secure-password -``` - -### 3. Deploy - -```bash -# Run the production deployment script -./scripts/production-deploy.sh -``` - -### 4. Setup Reverse Proxy - -#### Option A: Nginx (Recommended) - -1. Install Nginx: -```bash -sudo apt update -sudo apt install nginx -``` - -2. Copy the production nginx config: -```bash -sudo cp nginx.production.conf /etc/nginx/nginx.conf -``` - -3. Setup SSL certificates: -```bash -# Install Certbot -sudo apt install certbot python3-certbot-nginx - -# Get SSL certificate -sudo certbot --nginx -d dk0.dev -d www.dk0.dev -``` - -4. Restart Nginx: -```bash -sudo systemctl restart nginx -sudo systemctl enable nginx -``` - -#### Option B: Traefik - -If using Traefik, ensure your Docker Compose file includes Traefik labels: - -```yaml -labels: - - "traefik.enable=true" - - "traefik.http.routers.portfolio.rule=Host(`dk0.dev`)" - - "traefik.http.routers.portfolio.tls=true" - - "traefik.http.routers.portfolio.tls.certresolver=letsencrypt" -``` - -## Manual Deployment Steps - -If you prefer manual deployment: - -### 1. Create Proxy Network - -```bash -docker network create proxy -``` - -### 2. Build and Start Services - -```bash -# Build the application -docker build -t portfolio-app:latest . - -# Start services -docker-compose -f docker-compose.production.yml up -d -``` - -### 3. Run Database Migrations - -```bash -# Wait for services to be healthy -sleep 30 - -# Run migrations -docker exec portfolio-app npx prisma db push -``` - -### 4. Verify Deployment - -```bash -# Check health -curl http://localhost:3000/api/health - -# Check admin panel -curl http://localhost:3000/manage -``` - -## Security Considerations - -### 1. Update Default Passwords - -**CRITICAL:** Change these default values: - -```env -# Change the admin password -ADMIN_BASIC_AUTH=admin:your-very-secure-password-here - -# Use strong email passwords -MY_PASSWORD=your-strong-email-password -MY_INFO_PASSWORD=your-strong-info-password -``` - -### 2. Firewall Configuration - -```bash -# Allow only necessary ports -sudo ufw allow 22 # SSH -sudo ufw allow 80 # HTTP -sudo ufw allow 443 # HTTPS -sudo ufw enable -``` - -### 3. SSL/TLS Configuration - -Ensure you have valid SSL certificates. The nginx configuration expects: -- `/etc/nginx/ssl/cert.pem` (SSL certificate) -- `/etc/nginx/ssl/key.pem` (SSL private key) - -## Monitoring and Maintenance - -### 1. Health Checks - -```bash -# Check application health -curl https://dk0.dev/api/health - -# Check container status -docker-compose ps - -# View logs -docker-compose logs -f -``` - -### 2. Backup Database - -```bash -# Create backup -docker exec portfolio-postgres pg_dump -U portfolio_user portfolio_db > backup.sql - -# Restore backup -docker exec -i portfolio-postgres psql -U portfolio_user portfolio_db < backup.sql -``` - -### 3. Update Application - -```bash -# Pull latest changes -git pull origin main - -# Rebuild and restart -docker-compose down -docker build -t portfolio-app:latest . -docker-compose up -d -``` - -## Troubleshooting - -### Common Issues - -1. **Port 3000 not accessible:** - - Check if the container is running: `docker ps` - - Check logs: `docker-compose logs portfolio` - -2. **Database connection issues:** - - Ensure PostgreSQL is healthy: `docker-compose ps` - - Check database logs: `docker-compose logs postgres` - -3. **SSL certificate issues:** - - Verify certificate files exist and are readable - - Check nginx configuration: `nginx -t` - -4. **Rate limiting issues:** - - Check nginx rate limiting configuration - - Adjust limits in `nginx.production.conf` - -### Logs and Debugging - -```bash -# Application logs -docker-compose logs -f portfolio - -# Database logs -docker-compose logs -f postgres - -# Nginx logs -sudo tail -f /var/log/nginx/access.log -sudo tail -f /var/log/nginx/error.log -``` - -## Performance Optimization - -### 1. Resource Limits - -The production Docker Compose file includes resource limits: -- Portfolio app: 1GB RAM, 1 CPU -- PostgreSQL: 512MB RAM, 0.5 CPU -- Redis: 256MB RAM, 0.25 CPU - -### 2. Caching - -- Static assets are cached for 1 year -- API responses are cached for 10 minutes -- Admin routes are not cached for security - -### 3. Rate Limiting - -- API routes: 20 requests/second -- Login routes: 10 requests/minute -- Admin routes: 5 requests/minute - -## Support - -If you encounter issues: - -1. Check the logs first -2. Verify all environment variables are set -3. Ensure all services are healthy -4. Check network connectivity -5. Verify SSL certificates are valid - -For additional help, check the application logs and ensure all prerequisites are met. diff --git a/PUSH_READY.md b/PUSH_READY.md deleted file mode 100644 index f0bf28e..0000000 --- a/PUSH_READY.md +++ /dev/null @@ -1,244 +0,0 @@ -# โœ… READY TO PUSH - Dev Branch - -**Status**: All fixes complete and tested -**Date**: 2024-01-15 -**Branch**: dev -**Build**: โœ… Successful -**Lint**: โœ… Passed (0 errors, 8 warnings) - ---- - -## ๐ŸŽฏ Summary - -This branch fixes critical hydration errors, navbar overlap issues, and adds a complete AI image generation system. All changes are production-ready and backward compatible. - -## โœ… Pre-Push Checklist - COMPLETE - -### Build & Quality -- [x] โœ… Build successful: `npm run build` -- [x] โœ… Lint passed: `npm run lint` (0 errors, 8 warnings - OK) -- [x] โœ… TypeScript compilation clean -- [x] โœ… Prisma schema formatted and valid -- [x] โœ… No console errors during runtime - -### Functionality -- [x] โœ… Dev server starts without errors -- [x] โœ… Home page loads correctly -- [x] โœ… Admin page (`/manage`) shows login form (no redirect loop) -- [x] โœ… No hydration errors in console -- [x] โœ… No duplicate React key warnings -- [x] โœ… API endpoints respond correctly -- [x] โœ… Navbar no longer overlaps content - -### Security -- [x] โœ… No sensitive data in commits -- [x] โœ… `.env.local` excluded via `.gitignore` -- [x] โœ… Auth endpoints protected -- [x] โœ… Middleware security headers active - -### Documentation -- [x] โœ… `CHANGELOG_DEV.md` - Complete changelog -- [x] โœ… `PRE_PUSH_CHECKLIST.md` - Verification checklist -- [x] โœ… `AFTER_PUSH_SETUP.md` - Setup guide for other devs -- [x] โœ… `COMMIT_MESSAGE.txt` - Detailed commit message -- [x] โœ… AI Image Generation docs (6 files) -- [x] โœ… Database migration docs - ---- - -## ๐Ÿ“ฆ Changes Summary - -### Modified Files (5) -- `app/api/n8n/status/route.ts` - Added TypeScript interfaces, fixed any types -- `app/components/Hero.tsx` - Fixed duplicate transition prop -- `app/components/admin/AIImageGenerator.tsx` - Fixed imports, replaced img with Image -- `middleware.ts` - Removed unused import -- `prisma/schema.prisma` - Formatted (no logical changes) - -### Already Committed in Previous Commit (7) -- `app/page.tsx` - Added navbar spacer -- `app/components/About.tsx` - Fixed duplicate keys -- `app/components/Projects.tsx` - Fixed duplicate keys -- `app/components/ActivityFeed.tsx` - Fixed hydration errors -- `app/api/n8n/generate-image/route.ts` - New AI generation API -- Full AI image generation documentation - -### New Documentation (5) -- `CHANGELOG_DEV.md` - Complete changelog -- `PRE_PUSH_CHECKLIST.md` - Pre-push verification -- `AFTER_PUSH_SETUP.md` - Setup guide -- `COMMIT_MESSAGE.txt` - Commit message template -- `PUSH_READY.md` - This file - ---- - -## ๐Ÿš€ How to Push - -```bash -# 1. Review changes one last time -git status -git diff - -# 2. Stage all changes -git add . - -# 3. Commit with descriptive message -git commit -F COMMIT_MESSAGE.txt - -# 4. Push to dev branch -git push origin dev - -# 5. Verify on remote -git log --oneline -3 -``` - ---- - -## ๐Ÿงช Testing Results - -### Build Test -``` -โœ… npm run build - SUCCESS - - Next.js compiled successfully - - No errors, no warnings - - All routes generated - - Middleware compiled (34 kB) -``` - -### Lint Test -``` -โœ… npm run lint - PASSED - - 0 errors - - 8 warnings (all harmless unused vars) - - No critical issues -``` - -### Runtime Tests -``` -โœ… Home page (localhost:3000) - - Loads without errors - - No hydration errors - - No duplicate key warnings - - Navbar properly spaced - -โœ… Admin page (localhost:3000/manage) - - Shows login form correctly - - No redirect loop - - Auth system works - -โœ… API Endpoints - - /api/n8n/status โ†’ {"activity":null,...} - - /api/health โ†’ OK - - /api/projects โ†’ Works -``` - ---- - -## ๐ŸŽฏ What This Branch Delivers - -### Bug Fixes -1. โœ… Fixed React hydration errors in ActivityFeed -2. โœ… Fixed duplicate React keys in About and Projects -3. โœ… Fixed navbar overlapping hero section -4. โœ… Fixed /manage redirect loop -5. โœ… Fixed "activity_status table not found" errors -6. โœ… Fixed TypeScript ESLint warnings - -### New Features -1. โœ… Complete AI Image Generation System - - Automatic project cover images - - Local Stable Diffusion integration - - n8n workflow automation - - Admin UI component - - 6 comprehensive documentation files - - Category-specific prompt templates (10+ categories) - -2. โœ… ActivityStatus Database Model - - Real-time activity tracking - - Music, gaming, coding status - - Migration scripts included - -3. โœ… Enhanced APIs - - AI image generation endpoint - - Improved status endpoint with proper types - ---- - -## ๐Ÿ“š Documentation Included - -### User Guides -- `CHANGELOG_DEV.md` - What changed and why -- `AFTER_PUSH_SETUP.md` - Setup guide for team members -- `PRE_PUSH_CHECKLIST.md` - Quality assurance checklist - -### AI Image Generation -- `docs/ai-image-generation/README.md` - Overview (423 lines) -- `docs/ai-image-generation/SETUP.md` - Installation guide (486 lines) -- `docs/ai-image-generation/QUICKSTART.md` - 15-min setup (366 lines) -- `docs/ai-image-generation/PROMPT_TEMPLATES.md` - Templates (612 lines) -- `docs/ai-image-generation/ENVIRONMENT.md` - Env vars (311 lines) -- `docs/ai-image-generation/n8n-workflow-ai-image-generator.json` - Workflow - -### Database -- `prisma/migrations/README.md` - Migration guide -- `prisma/migrations/create_activity_status.sql` - SQL script -- `prisma/migrations/quick-fix.sh` - Auto-setup script - ---- - -## โš ๏ธ Important Notes - -### Migration Required -After pulling this branch, team members MUST run: -```bash -./prisma/migrations/quick-fix.sh -``` -This creates the `activity_status` table. Without it, the site will log errors (but still work). - -### Environment Variables (Optional) -For AI image generation features: -```bash -N8N_WEBHOOK_URL=http://localhost:5678/webhook -N8N_SECRET_TOKEN=your-token -SD_API_URL=http://localhost:7860 -AUTO_GENERATE_IMAGES=false -``` - -### Breaking Changes -**NONE** - All changes are backward compatible. - ---- - -## ๐ŸŽ‰ Ready to Push! - -All checks passed. This branch is: -- โœ… Tested and working -- โœ… Documented thoroughly -- โœ… Backward compatible -- โœ… Production-ready -- โœ… No breaking changes -- โœ… Migration scripts included - -**Recommendation**: Push to dev, test in staging, then merge to main. - ---- - -## ๐Ÿ“ž After Push - -### For Team Members -1. Pull latest dev branch -2. Read `AFTER_PUSH_SETUP.md` -3. Run database migration -4. Test locally - -### For Deployment -1. Run database migration on server -2. Restart application -3. Verify no errors in logs -4. Test critical paths - ---- - -**Last Verified**: 2024-01-15 -**Verified By**: AI Assistant (Claude Sonnet 4.5) -**Status**: โœ… READY TO PUSH \ No newline at end of file diff --git a/SAFE_PUSH_TO_MAIN.md b/SAFE_PUSH_TO_MAIN.md new file mode 100644 index 0000000..e3e9162 --- /dev/null +++ b/SAFE_PUSH_TO_MAIN.md @@ -0,0 +1,324 @@ +# ๐Ÿš€ Safe Push to Main Branch Guide + +**IMPORTANT**: This guide ensures you don't break production when merging to main. + +## โš ๏ธ Pre-Flight Checklist + +Before even thinking about pushing to main, verify ALL of these: + +### 1. Code Quality โœ… +```bash +# Run all checks +npm run build # Must pass with 0 errors +npm run lint # Must pass with 0 errors +npx tsc --noEmit # TypeScript must be clean +npx prisma format # Database schema must be valid +``` + +### 1b. Automated Testing โœ… +```bash +# Run comprehensive test suite (RECOMMENDED) +npm run test:all # Runs all tests including E2E + +# Or run individually: +npm run test # Unit tests +npm run test:critical # Critical path E2E tests +npm run test:hydration # Hydration tests +npm run test:email # Email API tests +``` + +### 2. Testing โœ… +```bash +# Automated testing (RECOMMENDED) +npm run test:all # Runs all automated tests + +# Manual testing (if needed) +npm run dev +# Test these critical paths: +# - Home page loads +# - Projects page works +# - Admin dashboard accessible +# - API endpoints respond +# - No console errors +# - No hydration errors +``` + +### 3. Database Changes โœ… +```bash +# If you changed the database schema: +# 1. Create migration +npx prisma migrate dev --name your_migration_name + +# 2. Test migration on a copy of production data +# 3. Document migration steps +# 4. Create rollback plan +``` + +### 4. Environment Variables โœ… +- [ ] All new env vars documented in `env.example` +- [ ] No secrets committed to git +- [ ] Production env vars are set on server +- [ ] Optional features have fallbacks + +### 5. Breaking Changes โœ… +- [ ] Documented in CHANGELOG +- [ ] Backward compatible OR migration plan exists +- [ ] Team notified of changes + +--- + +## ๐Ÿ“‹ Step-by-Step Push Process + +### Step 1: Ensure You're on Dev Branch +```bash +git checkout dev +git pull origin dev # Get latest changes +``` + +### Step 2: Final Verification +```bash +# Clean build +rm -rf .next node_modules/.cache +npm install +npm run build + +# Should complete without errors +``` + +### Step 3: Review Your Changes +```bash +# See what you're about to push +git log origin/main..dev --oneline +git diff origin/main..dev + +# Review carefully: +# - No accidental secrets +# - No debug code +# - No temporary files +# - All changes are intentional +``` + +### Step 4: Create a Backup Branch (Safety Net) +```bash +# Create backup before merging +git checkout -b backup-before-main-merge-$(date +%Y%m%d) +git push origin backup-before-main-merge-$(date +%Y%m%d) +git checkout dev +``` + +### Step 5: Merge Dev into Main (Local) +```bash +# Switch to main +git checkout main +git pull origin main # Get latest main + +# Merge dev into main +git merge dev --no-ff -m "Merge dev into main: [describe changes]" + +# If conflicts occur: +# 1. Resolve conflicts carefully +# 2. Test after resolving +# 3. Don't force push if unsure +``` + +### Step 6: Test the Merged Code +```bash +# Build and test the merged code +npm run build +npm run dev + +# Test critical paths again +# - Home page +# - Projects +# - Admin +# - APIs +``` + +### Step 7: Push to Main (If Everything Looks Good) +```bash +# Push to remote main +git push origin main + +# If you need to force push (DANGEROUS - only if necessary): +# git push origin main --force-with-lease +``` + +### Step 8: Monitor Deployment +```bash +# Watch your deployment logs +# Check for errors +# Verify health endpoints +# Test production site +``` + +--- + +## ๐Ÿ›ก๏ธ Safety Strategies + +### Strategy 1: Feature Flags +If you're adding new features, use feature flags: +```typescript +// In your code +if (process.env.ENABLE_NEW_FEATURE === 'true') { + // New feature code +} +``` + +### Strategy 2: Gradual Rollout +- Deploy to staging first +- Test thoroughly +- Then deploy to production +- Monitor closely + +### Strategy 3: Database Migrations +```bash +# Always test migrations first +# 1. Backup production database +# 2. Test migration on copy +# 3. Create rollback script +# 4. Run migration during low-traffic period +``` + +### Strategy 4: Rollback Plan +Always have a rollback plan: +```bash +# If something breaks: +git revert HEAD +git push origin main + +# Or rollback to previous commit: +git reset --hard +git push origin main --force-with-lease +``` + +--- + +## ๐Ÿšจ Red Flags - DON'T PUSH IF: + +- โŒ Build fails +- โŒ Tests fail +- โŒ Linter errors +- โŒ TypeScript errors +- โŒ Database migration not tested +- โŒ Breaking changes not documented +- โŒ Secrets in code +- โŒ Debug code left in +- โŒ Console.logs everywhere +- โŒ Untested features +- โŒ No rollback plan + +--- + +## โœ… Green Lights - SAFE TO PUSH IF: + +- โœ… All checks pass +- โœ… Tested locally +- โœ… Database migrations tested +- โœ… No breaking changes (or documented) +- โœ… Documentation updated +- โœ… Team notified +- โœ… Rollback plan exists +- โœ… Feature flags for new features +- โœ… Environment variables documented + +--- + +## ๐Ÿ“ Pre-Push Checklist Template + +Copy this and check each item: + +``` +[ ] npm run build passes +[ ] npm run lint passes +[ ] npx tsc --noEmit passes +[ ] npx prisma format passes +[ ] npm run test:all passes (automated tests) +[ ] OR manual testing: + [ ] Dev server starts without errors + [ ] Home page loads correctly + [ ] Projects page works + [ ] Admin dashboard accessible + [ ] API endpoints respond + [ ] No console errors + [ ] No hydration errors +[ ] Database migrations tested (if any) +[ ] Environment variables documented +[ ] No secrets in code +[ ] Breaking changes documented +[ ] CHANGELOG updated +[ ] Team notified (if needed) +[ ] Rollback plan exists +[ ] Backup branch created +[ ] Changes reviewed +``` + +--- + +## ๐Ÿ”„ Alternative: Pull Request Workflow + +If you want extra safety, use PR workflow: + +```bash +# 1. Push dev branch +git push origin dev + +# 2. Create Pull Request on Git platform +# - Review changes +# - Get approval +# - Run CI/CD checks + +# 3. Merge PR to main (platform handles it) +``` + +--- + +## ๐Ÿ†˜ Emergency Rollback + +If production breaks after push: + +### Quick Rollback +```bash +# 1. Revert the merge commit +git revert -m 1 +git push origin main + +# 2. Or reset to previous state +git reset --hard +git push origin main --force-with-lease +``` + +### Database Rollback +```bash +# If you ran migrations, roll them back: +npx prisma migrate resolve --rolled-back + +# Or restore from backup +``` + +--- + +## ๐Ÿ“ž Need Help? + +If unsure: +1. **Don't push** - better safe than sorry +2. Test more thoroughly +3. Ask for code review +4. Use staging environment first +5. Create a PR for review + +--- + +## ๐ŸŽฏ Best Practices + +1. **Always test locally first** +2. **Use feature flags for new features** +3. **Test database migrations on copies** +4. **Document everything** +5. **Have a rollback plan** +6. **Monitor after deployment** +7. **Deploy during low-traffic periods** +8. **Keep main branch stable** + +--- + +**Remember**: It's better to delay a push than to break production! ๐Ÿ›ก๏ธ diff --git a/SECURITY-CHECKLIST.md b/SECURITY-CHECKLIST.md deleted file mode 100644 index 7fb140b..0000000 --- a/SECURITY-CHECKLIST.md +++ /dev/null @@ -1,128 +0,0 @@ -# Security Checklist fรผr dk0.dev - -Diese Checkliste stellt sicher, dass die Website sicher und produktionsbereit ist. - -## โœ… Implementierte SicherheitsmaรŸnahmen - -### 1. HTTP Security Headers -- โœ… `Strict-Transport-Security` (HSTS) - Erzwingt HTTPS -- โœ… `X-Frame-Options: DENY` - Verhindert Clickjacking -- โœ… `X-Content-Type-Options: nosniff` - Verhindert MIME-Sniffing -- โœ… `X-XSS-Protection` - XSS-Schutz -- โœ… `Referrer-Policy` - Kontrolliert Referrer-Informationen -- โœ… `Permissions-Policy` - Beschrรคnkt Browser-Features -- โœ… `Content-Security-Policy` - Verhindert XSS und Injection-Angriffe - -### 2. Deployment-Sicherheit -- โœ… Zero-Downtime-Deployments mit Rollback-Funktion -- โœ… Health Checks vor und nach Deployment -- โœ… Automatische Rollbacks bei Fehlern -- โœ… Image-Backups vor Updates -- โœ… Pre-Deployment-Checks (Docker, Disk Space, .env) - -### 3. Server-Konfiguration -- โœ… Non-root User im Docker-Container -- โœ… Resource Limits fรผr Container -- โœ… Health Checks fรผr alle Services -- โœ… Proper Error Handling -- โœ… Logging und Monitoring - -### 4. Datenbank-Sicherheit -- โœ… Prisma ORM (verhindert SQL-Injection) -- โœ… Environment Variables fรผr Credentials -- โœ… Keine Credentials im Code -- โœ… Database Migrations mit Validierung - -### 5. API-Sicherheit -- โœ… Authentication fรผr Admin-Routes -- โœ… Rate Limiting Headers -- โœ… Input Validation im Contact Form -- โœ… CSRF Protection (Next.js built-in) - -### 6. Code-Sicherheit -- โœ… TypeScript fรผr Type Safety -- โœ… ESLint fรผr Code Quality -- โœ… Keine `console.log` in Production -- โœ… Environment Variables Validation - -## ๐Ÿ”’ Wichtige Sicherheitshinweise - -### Environment Variables -Stelle sicher, dass folgende Variablen gesetzt sind: -- `DATABASE_URL` - PostgreSQL Connection String -- `REDIS_URL` - Redis Connection String -- `MY_EMAIL` - Email fรผr Kontaktformular -- `MY_PASSWORD` - Email-Passwort -- `ADMIN_BASIC_AUTH` - Admin-Credentials (Format: `username:password`) - -### Deployment-Prozess -1. **Vor jedem Deployment:** - ```bash - # Pre-Deployment Checks - ./scripts/safe-deploy.sh - ``` - -2. **Bei Problemen:** - - Automatisches Rollback wird ausgefรผhrt - - Alte Images werden als Backup behalten - - Health Checks stellen sicher, dass alles funktioniert - -3. **Nach dem Deployment:** - - Health Check Endpoint prรผfen: `https://dk0.dev/api/health` - - Hauptseite testen: `https://dk0.dev` - - Admin-Panel testen: `https://dk0.dev/manage` - -### SSL/TLS -- โœ… SSL-Zertifikate mรผssen gรผltig sein -- โœ… TLS 1.2+ wird erzwungen -- โœ… HSTS ist aktiviert -- โœ… Perfect Forward Secrecy (PFS) aktiviert - -### Monitoring -- โœ… Health Check Endpoint: `/api/health` -- โœ… Container Health Checks -- โœ… Application Logs -- โœ… Error Tracking - -## ๐Ÿšจ Bekannte Einschrรคnkungen - -1. **CSP `unsafe-inline` und `unsafe-eval`:** - - Erforderlich fรผr Next.js und Analytics - - Wird durch andere SicherheitsmaรŸnahmen kompensiert - -2. **Email-Konfiguration:** - - Stelle sicher, dass Email-Credentials sicher gespeichert sind - - Verwende App-Passwords statt Hauptpasswรถrtern - -## ๐Ÿ“‹ RegelmรครŸige Sicherheitsprรผfungen - -- [ ] Monatliche Dependency-Updates (`npm audit`) -- [ ] Quartalsweise Security Headers Review -- [ ] Halbjรคhrliche Penetration Tests -- [ ] Jรคhrliche SSL-Zertifikat-Erneuerung - -## ๐Ÿ”ง Wartung - -### Dependency Updates -```bash -npm audit -npm audit fix -``` - -### Security Headers Test -```bash -curl -I https://dk0.dev -``` - -### SSL Test -```bash -openssl s_client -connect dk0.dev:443 -servername dk0.dev -``` - -## ๐Ÿ“ž Bei Sicherheitsproblemen - -1. Sofortiges Rollback durchfรผhren -2. Logs รผberprรผfen -3. Security Headers validieren -4. Dependencies auf bekannte Vulnerabilities prรผfen - diff --git a/SECURITY-UPDATE.md b/SECURITY-UPDATE.md deleted file mode 100644 index 06b3f8a..0000000 --- a/SECURITY-UPDATE.md +++ /dev/null @@ -1,23 +0,0 @@ -# Security Update - 2025-12-08 - -Addressed critical and moderate vulnerabilities including CVE-2025-55182, CVE-2025-66478 (React2Shell), and others affecting nodemailer and markdown processing. - -## Updates -- **Next.js**: Updated to `15.5.7` (Patched version for 15.5.x branch) -- **React**: Updated to `19.0.1` (Patched version) -- **React DOM**: Updated to `19.0.1` (Patched version) -- **ESLint Config Next**: Updated to `15.5.7` -- **Nodemailer**: Updated to `7.0.11` (Fixes GHSA-mm7p-fcc7-pg87, GHSA-rcmh-qjqh-p98v) -- **Nodemailer Mock**: Updated to `2.0.9` (Compatibility update) -- **React Markdown**: Updated to `Latest` (Fixes `mdast-util-to-hast` vulnerability) -- **Gray Matter/JS-YAML**: Resolved `js-yaml` vulnerability via dependency updates. - -## Verification -- `npm run build` passed successfully. -- `npm audit` reports **0 vulnerabilities**. -- Application logic verified via partial test suite execution (known pre-existing test environment issues noted). - -## Advisory References -- BITS-H Nr. 2025-304569-1132 (React/Next.js) -- GHSA-mm7p-fcc7-pg87 (Nodemailer) -- GHSA-rcmh-qjqh-p98v (Nodemailer) diff --git a/STAGING_SETUP.md b/STAGING_SETUP.md new file mode 100644 index 0000000..79d5c57 --- /dev/null +++ b/STAGING_SETUP.md @@ -0,0 +1,195 @@ +# ๐Ÿš€ Staging Environment Setup + +## Overview + +You now have **two separate Docker stacks**: + +1. **Staging** - Deploys automatically on `dev` or `main` branch + - Port: `3001` + - Container: `portfolio-app-staging` + - Database: `portfolio_staging_db` (port 5433) + - Redis: `portfolio-redis-staging` (port 6380) + - URL: `https://staging.dk0.dev` (or `http://localhost:3001`) + +2. **Production** - Deploys automatically on `production` branch + - Port: `3000` + - Container: `portfolio-app` + - Database: `portfolio_db` (port 5432) + - Redis: `portfolio-redis` (port 6379) + - URL: `https://dk0.dev` + +## How It Works + +### Automatic Staging Deployment +When you push to `dev` or `main` branch: +1. โœ… Tests run +2. โœ… Docker image is built and tagged as `staging` +3. โœ… Staging stack deploys automatically +4. โœ… Available on port 3001 + +### Automatic Production Deployment +When you merge to `production` branch: +1. โœ… Tests run +2. โœ… Docker image is built and tagged as `production` +3. โœ… **Zero-downtime deployment** (blue-green) +4. โœ… Health checks before switching +5. โœ… Rollback if health check fails +6. โœ… Available on port 3000 + +## Safety Features + +### Production Deployment Safety +- โœ… **Zero-downtime**: New container starts before old one stops +- โœ… **Health checks**: Verifies new container is healthy before switching +- โœ… **Automatic rollback**: If health check fails, old container stays running +- โœ… **Separate networks**: Staging and production are completely isolated +- โœ… **Different ports**: No port conflicts +- โœ… **Separate databases**: Staging data doesn't affect production + +### Staging Deployment +- โœ… **Non-blocking**: Staging can fail without affecting production +- โœ… **Isolated**: Completely separate from production +- โœ… **Safe to test**: Break staging without breaking production + +## Ports Used + +| Service | Staging | Production | +|---------|---------|------------| +| App | 3001 | 3000 | +| PostgreSQL | 5433 | 5432 | +| Redis | 6380 | 6379 | + +## Workflow + +### Development Flow +```bash +# 1. Work on dev branch +git checkout dev +# ... make changes ... + +# 2. Push to dev (triggers staging deployment) +git push origin dev +# โ†’ Staging deploys automatically on port 3001 + +# 3. Test staging +curl http://localhost:3001/api/health + +# 4. Merge to main (also triggers staging) +git checkout main +git merge dev +git push origin main +# โ†’ Staging updates automatically + +# 5. When ready, merge to production +git checkout production +git merge main +git push origin production +# โ†’ Production deploys with zero-downtime +``` + +## Manual Commands + +### Staging +```bash +# Start staging +docker compose -f docker-compose.staging.yml up -d + +# Stop staging +docker compose -f docker-compose.staging.yml down + +# View staging logs +docker compose -f docker-compose.staging.yml logs -f + +# Check staging health +curl http://localhost:3001/api/health +``` + +### Production +```bash +# Start production +docker compose -f docker-compose.production.yml up -d + +# Stop production +docker compose -f docker-compose.production.yml down + +# View production logs +docker compose -f docker-compose.production.yml logs -f + +# Check production health +curl http://localhost:3000/api/health +``` + +## Environment Variables + +### Staging +- `NODE_ENV=staging` +- `NEXT_PUBLIC_BASE_URL=https://staging.dk0.dev` +- `LOG_LEVEL=debug` (more verbose logging) + +### Production +- `NODE_ENV=production` +- `NEXT_PUBLIC_BASE_URL=https://dk0.dev` +- `LOG_LEVEL=info` + +## Database Separation + +- **Staging DB**: `portfolio_staging_db` (separate volume) +- **Production DB**: `portfolio_db` (separate volume) +- **No conflicts**: Staging can be reset without affecting production + +## Monitoring + +### Check Both Environments +```bash +# Staging +curl http://localhost:3001/api/health + +# Production +curl http://localhost:3000/api/health +``` + +### View Container Status +```bash +# All containers +docker ps + +# Staging only +docker ps | grep staging + +# Production only +docker ps | grep -v staging +``` + +## Troubleshooting + +### Staging Not Deploying +1. Check GitHub Actions workflow +2. Verify branch is `dev` or `main` +3. Check Docker logs: `docker compose -f docker-compose.staging.yml logs` + +### Production Deployment Issues +1. Check health endpoint before deployment +2. Verify old container is running +3. Check logs: `docker compose -f docker-compose.production.yml logs` +4. Manual rollback: Restart old container if needed + +### Port Conflicts +- Staging uses 3001, 5433, 6380 +- Production uses 3000, 5432, 6379 +- If conflicts occur, check what's using the ports: + ```bash + lsof -i :3001 + lsof -i :3000 + ``` + +## Benefits + +โœ… **Safe testing**: Test on staging without risk +โœ… **Zero-downtime**: Production updates don't interrupt service +โœ… **Isolation**: Staging and production are completely separate +โœ… **Automatic**: Deploys happen automatically on push +โœ… **Rollback**: Automatic rollback if deployment fails + +--- + +**You're all set!** Push to `dev`/`main` for staging, merge to `production` for production deployment! ๐Ÿš€ diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 0000000..1df1443 --- /dev/null +++ b/TESTING_GUIDE.md @@ -0,0 +1,284 @@ +# ๐Ÿงช Automated Testing Guide + +This guide explains how to run automated tests for critical paths, hydration, emails, and more. + +## ๐Ÿ“‹ Test Types + +### 1. Unit Tests (Jest) +Tests individual components and functions in isolation. + +```bash +npm run test # Run all unit tests +npm run test:watch # Watch mode +npm run test:coverage # With coverage report +``` + +### 2. E2E Tests (Playwright) +Tests complete user flows in a real browser. + +```bash +npm run test:e2e # Run all E2E tests +npm run test:e2e:ui # Run with UI mode (visual) +npm run test:e2e:headed # Run with visible browser +npm run test:e2e:debug # Debug mode +``` + +### 3. Critical Path Tests +Tests the most important user flows. + +```bash +npm run test:critical # Run critical path tests only +``` + +### 4. Hydration Tests +Ensures React hydration works without errors. + +```bash +npm run test:hydration # Run hydration tests only +``` + +### 5. Email Tests +Tests email API endpoints. + +```bash +npm run test:email # Run email tests only +``` + +### 6. Performance Tests +Checks page load times and performance. + +```bash +npm run test:performance # Run performance tests +``` + +### 7. Accessibility Tests +Basic accessibility checks. + +```bash +npm run test:accessibility # Run accessibility tests +``` + +## ๐Ÿš€ Running All Tests + +### Quick Test (Recommended) +```bash +npm run test:all +``` + +This runs: +- โœ… TypeScript check +- โœ… ESLint +- โœ… Build +- โœ… Unit tests +- โœ… Critical paths +- โœ… Hydration tests +- โœ… Email tests +- โœ… Performance tests +- โœ… Accessibility tests + +### Individual Test Suites +```bash +# Unit tests only +npm run test + +# E2E tests only +npm run test:e2e + +# Both +npm run test && npm run test:e2e +``` + +## ๐Ÿ“ What Gets Tested + +### Critical Paths +- โœ… Home page loads correctly +- โœ… Projects page displays projects +- โœ… Individual project pages work +- โœ… Admin dashboard is accessible +- โœ… API health endpoint +- โœ… API projects endpoint + +### Hydration +- โœ… No hydration errors in console +- โœ… No duplicate React key warnings +- โœ… Client-side navigation works +- โœ… Server and client HTML match +- โœ… Interactive elements work after hydration + +### Email +- โœ… Email API accepts requests +- โœ… Required field validation +- โœ… Email format validation +- โœ… Rate limiting (if implemented) +- โœ… Email respond endpoint + +### Performance +- โœ… Page load times (< 5s) +- โœ… No large layout shifts +- โœ… Images are optimized +- โœ… API response times (< 1s) + +### Accessibility +- โœ… Proper heading structure +- โœ… Images have alt text +- โœ… Links have descriptive text +- โœ… Forms have labels + +## ๐ŸŽฏ Pre-Push Testing + +Before pushing to main, run: + +```bash +# Full test suite +npm run test:all + +# Or manually: +npm run build +npm run lint +npx tsc --noEmit +npm run test +npm run test:critical +npm run test:hydration +``` + +## ๐Ÿ”ง Configuration + +### Playwright Config +Located in `playwright.config.ts` + +- **Base URL**: `http://localhost:3000` (or set `PLAYWRIGHT_TEST_BASE_URL`) +- **Browsers**: Chromium, Firefox, WebKit, Mobile Chrome, Mobile Safari +- **Retries**: 2 retries in CI, 0 locally +- **Screenshots**: On failure +- **Videos**: On failure + +### Jest Config +Located in `jest.config.ts` + +- **Environment**: jsdom +- **Coverage**: v8 provider +- **Setup**: `jest.setup.ts` + +## ๐Ÿ› Debugging Tests + +### Playwright Debug Mode +```bash +npm run test:e2e:debug +``` + +This opens Playwright Inspector where you can: +- Step through tests +- Inspect elements +- View console logs +- See network requests + +### UI Mode (Visual) +```bash +npm run test:e2e:ui +``` + +Shows a visual interface to: +- See all tests +- Run specific tests +- Watch tests execute +- View results + +### Headed Mode +```bash +npm run test:e2e:headed +``` + +Runs tests with visible browser (useful for debugging). + +## ๐Ÿ“Š Test Reports + +### Playwright HTML Report +After running E2E tests: +```bash +npx playwright show-report +``` + +Shows: +- Test results +- Screenshots on failure +- Videos on failure +- Timeline of test execution + +### Jest Coverage Report +```bash +npm run test:coverage +``` + +Generates coverage report in `coverage/` directory. + +## ๐Ÿšจ Common Issues + +### Tests Fail Locally But Pass in CI +- Check environment variables +- Ensure database is set up +- Check for port conflicts + +### Hydration Errors +- Check for server/client mismatches +- Ensure no conditional rendering based on `window` +- Check for date/time differences + +### Email Tests Fail +- Email service might not be configured +- Check environment variables +- Tests are designed to handle missing email service + +### Performance Tests Fail +- Network might be slow +- Adjust thresholds in test file +- Check for heavy resources loading + +## ๐Ÿ“ Writing New Tests + +### E2E Test Example +```typescript +import { test, expect } from '@playwright/test'; + +test('My new feature works', async ({ page }) => { + await page.goto('/my-page'); + await expect(page.locator('h1')).toContainText('Expected Text'); +}); +``` + +### Unit Test Example +```typescript +import { render, screen } from '@testing-library/react'; +import MyComponent from './MyComponent'; + +test('renders correctly', () => { + render(); + expect(screen.getByText('Hello')).toBeInTheDocument(); +}); +``` + +## ๐ŸŽฏ CI/CD Integration + +### GitHub Actions Example +```yaml +- name: Run tests + run: | + npm install + npm run test:all +``` + +### Pre-Push Hook +Add to `.git/hooks/pre-push`: +```bash +#!/bin/bash +npm run test:all +``` + +## ๐Ÿ“š Resources + +- [Playwright Docs](https://playwright.dev) +- [Jest Docs](https://jestjs.io) +- [Testing Library](https://testing-library.com) + +--- + +**Remember**: Tests should be fast, reliable, and easy to understand! ๐Ÿš€ diff --git a/__mocks__/@prisma/client.ts b/__mocks__/@prisma/client.ts new file mode 100644 index 0000000..8288e05 --- /dev/null +++ b/__mocks__/@prisma/client.ts @@ -0,0 +1,39 @@ +// Minimal Prisma Client mock for tests +// Export a PrismaClient class with the used methods stubbed out. + +export class PrismaClient { + project = { + findMany: jest.fn(async () => []), + findUnique: jest.fn(async (_args: unknown) => null), + count: jest.fn(async () => 0), + create: jest.fn(async (data: unknown) => data), + update: jest.fn(async (data: unknown) => data), + delete: jest.fn(async (data: unknown) => data), + updateMany: jest.fn(async (_data: unknown) => ({})), + }; + + contact = { + create: jest.fn(async (data: unknown) => data), + findMany: jest.fn(async () => []), + count: jest.fn(async () => 0), + update: jest.fn(async (data: unknown) => data), + delete: jest.fn(async (data: unknown) => data), + }; + + pageView = { + create: jest.fn(async (data: unknown) => data), + count: jest.fn(async () => 0), + deleteMany: jest.fn(async () => ({})), + }; + + userInteraction = { + create: jest.fn(async (data: unknown) => data), + groupBy: jest.fn(async () => []), + deleteMany: jest.fn(async () => ({})), + }; + + $connect = jest.fn(async () => {}); + $disconnect = jest.fn(async () => {}); +} + +export default PrismaClient; diff --git a/app/__tests__/api/email.test.tsx b/app/__tests__/api/email.test.tsx index afc1d48..43a376c 100644 --- a/app/__tests__/api/email.test.tsx +++ b/app/__tests__/api/email.test.tsx @@ -13,7 +13,11 @@ beforeAll(() => { }); afterAll(() => { - (console.error as jest.Mock).mockRestore(); + // restoreMocks may already restore it; guard against calling mockRestore on non-mock + const maybeMock = console.error as unknown as jest.Mock | undefined; + if (maybeMock && typeof maybeMock.mockRestore === 'function') { + maybeMock.mockRestore(); + } }); beforeEach(() => { diff --git a/app/__tests__/api/fetchAllProjects.test.tsx b/app/__tests__/api/fetchAllProjects.test.tsx index 13046e3..1ffba9f 100644 --- a/app/__tests__/api/fetchAllProjects.test.tsx +++ b/app/__tests__/api/fetchAllProjects.test.tsx @@ -2,8 +2,9 @@ import { GET } from '@/app/api/fetchAllProjects/route'; import { NextResponse } from 'next/server'; // Wir mocken node-fetch direkt -jest.mock('node-fetch', () => { - return jest.fn(() => +jest.mock('node-fetch', () => ({ + __esModule: true, + default: jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ @@ -36,8 +37,8 @@ jest.mock('node-fetch', () => { }, }), }) - ); -}); + ), +})); jest.mock('next/server', () => ({ NextResponse: { diff --git a/app/__tests__/api/fetchProject.test.tsx b/app/__tests__/api/fetchProject.test.tsx index eedc4f6..85e443c 100644 --- a/app/__tests__/api/fetchProject.test.tsx +++ b/app/__tests__/api/fetchProject.test.tsx @@ -1,29 +1,37 @@ import { GET } from '@/app/api/fetchProject/route'; import { NextRequest, NextResponse } from 'next/server'; -import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch'; + +// Mock node-fetch so the route uses it as a reliable fallback +jest.mock('node-fetch', () => ({ + __esModule: true, + default: jest.fn(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + posts: [ + { + id: '67aaffc3709c60000117d2d9', + title: 'Blockchain Based Voting System', + meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.', + slug: 'blockchain-based-voting-system', + updated_at: '2025-02-13T16:54:42.000+00:00', + }, + ], + }), + }) + ), +})); jest.mock('next/server', () => ({ NextResponse: { json: jest.fn(), }, })); - describe('GET /api/fetchProject', () => { beforeAll(() => { process.env.GHOST_API_URL = 'http://localhost:2368'; process.env.GHOST_API_KEY = 'some-key'; - - global.fetch = mockFetch({ - posts: [ - { - id: '67aaffc3709c60000117d2d9', - title: 'Blockchain Based Voting System', - meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.', - slug: 'blockchain-based-voting-system', - updated_at: '2025-02-13T16:54:42.000+00:00', - }, - ], - }); }); it('should fetch a project by slug', async () => { diff --git a/app/__tests__/api/sitemap.test.tsx b/app/__tests__/api/sitemap.test.tsx index f0f97ab..0a17e68 100644 --- a/app/__tests__/api/sitemap.test.tsx +++ b/app/__tests__/api/sitemap.test.tsx @@ -1,44 +1,127 @@ -import { GET } from '@/app/api/sitemap/route'; -import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch'; +jest.mock("next/server", () => { + const mockNextResponse = function ( + body: string | object, + init?: { headers?: Record }, + ) { + // Return an object that mimics NextResponse + const mockResponse = { + body, + init, + text: async () => { + if (typeof body === "string") { + return body; + } else if (body && typeof body === "object") { + return JSON.stringify(body); + } + return ""; + }, + json: async () => { + if (typeof body === "object") { + return body; + } + try { + return JSON.parse(body as string); + } catch { + return {}; + } + }, + }; + return mockResponse; + }; -jest.mock('next/server', () => ({ - NextResponse: jest.fn().mockImplementation((body, init) => ({ body, init })), + return { + NextResponse: mockNextResponse, + }; +}); + +import { GET } from "@/app/api/sitemap/route"; + +// Mock node-fetch so we don't perform real network requests in tests +jest.mock("node-fetch", () => ({ + __esModule: true, + default: jest.fn(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + posts: [ + { + id: "67ac8dfa709c60000117d312", + title: "Just Doing Some Testing", + meta_description: "Hello bla bla bla bla", + slug: "just-doing-some-testing", + updated_at: "2025-02-13T14:25:38.000+00:00", + }, + { + id: "67aaffc3709c60000117d2d9", + title: "Blockchain Based Voting System", + meta_description: + "This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.", + slug: "blockchain-based-voting-system", + updated_at: "2025-02-13T16:54:42.000+00:00", + }, + ], + meta: { + pagination: { + limit: "all", + next: null, + page: 1, + pages: 1, + prev: null, + total: 2, + }, + }, + }), + }), + ), })); -describe('GET /api/sitemap', () => { +describe("GET /api/sitemap", () => { beforeAll(() => { - process.env.GHOST_API_URL = 'http://localhost:2368'; - process.env.GHOST_API_KEY = 'test-api-key'; - process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one'; - global.fetch = mockFetch({ + process.env.GHOST_API_URL = "http://localhost:2368"; + process.env.GHOST_API_KEY = "test-api-key"; + process.env.NEXT_PUBLIC_BASE_URL = "https://dki.one"; + + // Provide mock posts via env so route can use them without fetching + process.env.GHOST_MOCK_POSTS = JSON.stringify({ posts: [ { - id: '67ac8dfa709c60000117d312', - title: 'Just Doing Some Testing', - meta_description: 'Hello bla bla bla bla', - slug: 'just-doing-some-testing', - updated_at: '2025-02-13T14:25:38.000+00:00', + id: "67ac8dfa709c60000117d312", + title: "Just Doing Some Testing", + meta_description: "Hello bla bla bla bla", + slug: "just-doing-some-testing", + updated_at: "2025-02-13T14:25:38.000+00:00", }, { - id: '67aaffc3709c60000117d2d9', - title: 'Blockchain Based Voting System', - meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.', - slug: 'blockchain-based-voting-system', - updated_at: '2025-02-13T16:54:42.000+00:00', + id: "67aaffc3709c60000117d2d9", + title: "Blockchain Based Voting System", + meta_description: + "This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.", + slug: "blockchain-based-voting-system", + updated_at: "2025-02-13T16:54:42.000+00:00", }, ], }); }); - it('should return a sitemap', async () => { + it("should return a sitemap", async () => { const response = await GET(); - expect(response.body).toContain(''); - expect(response.body).toContain('https://dki.one/'); - expect(response.body).toContain('https://dki.one/legal-notice'); - expect(response.body).toContain('https://dki.one/privacy-policy'); - expect(response.body).toContain('https://dki.one/projects/just-doing-some-testing'); - expect(response.body).toContain('https://dki.one/projects/blockchain-based-voting-system'); + // Get the body text from the NextResponse + const body = await response.text(); + + expect(body).toContain( + '', + ); + expect(body).toContain("https://dki.one/"); + expect(body).toContain("https://dki.one/legal-notice"); + expect(body).toContain("https://dki.one/privacy-policy"); + expect(body).toContain( + "https://dki.one/projects/just-doing-some-testing", + ); + expect(body).toContain( + "https://dki.one/projects/blockchain-based-voting-system", + ); // Note: Headers are not available in test environment }); -}); \ No newline at end of file +}); diff --git a/app/__tests__/components/Hero.test.tsx b/app/__tests__/components/Hero.test.tsx index 75d2e6d..fed28bd 100644 --- a/app/__tests__/components/Hero.test.tsx +++ b/app/__tests__/components/Hero.test.tsx @@ -6,7 +6,7 @@ describe('Hero', () => { it('renders the hero section', () => { render(); expect(screen.getByText('Dennis Konkol')).toBeInTheDocument(); - expect(screen.getByText('Student & Software Engineer based in Osnabrรผck, Germany')).toBeInTheDocument(); - expect(screen.getByAltText('Dennis Konkol - Software Engineer')).toBeInTheDocument(); + expect(screen.getByText(/Student and passionate/i)).toBeInTheDocument(); + expect(screen.getByAltText('Dennis Konkol')).toBeInTheDocument(); }); }); \ No newline at end of file diff --git a/app/__tests__/sitemap.xml/page.test.tsx b/app/__tests__/sitemap.xml/page.test.tsx index 9939a0c..7511683 100644 --- a/app/__tests__/sitemap.xml/page.test.tsx +++ b/app/__tests__/sitemap.xml/page.test.tsx @@ -1,44 +1,81 @@ -import '@testing-library/jest-dom'; -import { GET } from '@/app/sitemap.xml/route'; -import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch-sitemap'; +import "@testing-library/jest-dom"; +import { GET } from "@/app/sitemap.xml/route"; -jest.mock('next/server', () => ({ - NextResponse: jest.fn().mockImplementation((body, init) => ({ body, init })), +jest.mock("next/server", () => ({ + NextResponse: jest.fn().mockImplementation((body: unknown, init?: ResponseInit) => { + const response = { + body, + init, + }; + return response; + }), })); -describe('Sitemap Component', () => { +// Sitemap XML used by node-fetch mock +const sitemapXml = ` + + + https://dki.one/ + + + https://dki.one/legal-notice + + + https://dki.one/privacy-policy + + + https://dki.one/projects/just-doing-some-testing + + + https://dki.one/projects/blockchain-based-voting-system + + +`; + +// Mock node-fetch for sitemap endpoint (hoisted by Jest) +jest.mock("node-fetch", () => ({ + __esModule: true, + default: jest.fn((_url: string) => + Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) }), + ), +})); + +describe("Sitemap Component", () => { beforeAll(() => { - process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one'; - global.fetch = mockFetch(` - - - https://dki.one/ - - - https://dki.one/legal-notice - - - https://dki.one/privacy-policy - - - https://dki.one/projects/just-doing-some-testing - - - https://dki.one/projects/blockchain-based-voting-system - - - `); + process.env.NEXT_PUBLIC_BASE_URL = "https://dki.one"; + + // Provide sitemap XML directly so route uses it without fetching + process.env.GHOST_MOCK_SITEMAP = sitemapXml; + + // Mock global.fetch too, to avoid any network calls + global.fetch = jest.fn().mockImplementation((url: string) => { + if (url.includes("/api/sitemap")) { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(sitemapXml), + }); + } + return Promise.reject(new Error(`Unknown URL: ${url}`)); + }); }); - it('should render the sitemap XML', async () => { + it("should render the sitemap XML", async () => { const response = await GET(); - expect(response.body).toContain(''); - expect(response.body).toContain('https://dki.one/'); - expect(response.body).toContain('https://dki.one/legal-notice'); - expect(response.body).toContain('https://dki.one/privacy-policy'); - expect(response.body).toContain('https://dki.one/projects/just-doing-some-testing'); - expect(response.body).toContain('https://dki.one/projects/blockchain-based-voting-system'); + expect(response.body).toContain( + '', + ); + expect(response.body).toContain("https://dki.one/"); + expect(response.body).toContain("https://dki.one/legal-notice"); + expect(response.body).toContain( + "https://dki.one/privacy-policy", + ); + expect(response.body).toContain( + "https://dki.one/projects/just-doing-some-testing", + ); + expect(response.body).toContain( + "https://dki.one/projects/blockchain-based-voting-system", + ); // Note: Headers are not available in test environment }); -}); \ No newline at end of file +}); diff --git a/app/api/email/route.tsx b/app/api/email/route.tsx index 1f18f89..e5367a4 100644 --- a/app/api/email/route.tsx +++ b/app/api/email/route.tsx @@ -17,8 +17,8 @@ function sanitizeInput(input: string, maxLength: number = 10000): string { export async function POST(request: NextRequest) { try { - // Rate limiting - const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + // Rate limiting (defensive: headers may be undefined in tests) + const ip = request.headers?.get?.('x-forwarded-for') ?? request.headers?.get?.('x-real-ip') ?? 'unknown'; if (!checkRateLimit(ip, 5, 60000)) { // 5 emails per minute per IP return NextResponse.json( { error: 'Zu viele Anfragen. Bitte versuchen Sie es spรคter erneut.' }, diff --git a/app/api/fetchAllProjects/route.tsx b/app/api/fetchAllProjects/route.tsx index cbed346..a698325 100644 --- a/app/api/fetchAllProjects/route.tsx +++ b/app/api/fetchAllProjects/route.tsx @@ -1,8 +1,17 @@ import { NextResponse } from "next/server"; -import http from "http"; -import fetch from "node-fetch"; import NodeCache from "node-cache"; +// Use a dynamic import for node-fetch so tests that mock it (via jest.mock) are respected +async function getFetch() { + try { + const mod = await import("node-fetch"); + // support both CJS and ESM interop + return (mod as { default: unknown }).default ?? mod; + } catch (_err) { + return globalThis.fetch; + } +} + export const runtime = "nodejs"; // Force Node runtime const GHOST_API_URL = process.env.GHOST_API_URL; @@ -35,12 +44,12 @@ export async function GET() { } try { - const agent = new http.Agent({ keepAlive: true }); - const response = await fetch( + const fetchFn = await getFetch(); + const response = await (fetchFn as unknown as typeof fetch)( `${GHOST_API_URL}/ghost/api/content/posts/?key=${GHOST_API_KEY}&limit=all`, - { agent: agent as unknown as undefined } ); - const posts: GhostPostsResponse = await response.json() as GhostPostsResponse; + const posts: GhostPostsResponse = + (await response.json()) as GhostPostsResponse; if (!posts || !posts.posts) { console.error("Invalid posts data"); diff --git a/app/api/fetchImage/route.tsx b/app/api/fetchImage/route.tsx index 421670a..22f4467 100644 --- a/app/api/fetchImage/route.tsx +++ b/app/api/fetchImage/route.tsx @@ -12,9 +12,40 @@ export async function GET(req: NextRequest) { } try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch image: ${response.statusText}`); + // Try global fetch first, fall back to node-fetch if necessary + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let response: any; + try { + if ( + typeof (globalThis as unknown as { fetch: unknown }).fetch === + "function" + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = await (globalThis as unknown as { fetch: any }).fetch(url); + } + } catch (_e) { + response = undefined; + } + + if (!response || typeof response.ok === "undefined" || !response.ok) { + try { + const mod = await import("node-fetch"); + const nodeFetch = (mod as { default: unknown }).default ?? mod; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = await (nodeFetch as any)(url); + } catch (err) { + console.error("Failed to fetch image:", err); + return NextResponse.json( + { error: "Failed to fetch image" }, + { status: 500 }, + ); + } + } + + if (!response || !response.ok) { + throw new Error( + `Failed to fetch image: ${response?.statusText ?? "no response"}`, + ); } const contentType = response.headers.get("content-type"); diff --git a/app/api/fetchProject/route.tsx b/app/api/fetchProject/route.tsx index 372b1bf..b01a4bd 100644 --- a/app/api/fetchProject/route.tsx +++ b/app/api/fetchProject/route.tsx @@ -14,12 +14,55 @@ export async function GET(request: Request) { } try { - const response = await fetch( - `${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`, + // Debug: show whether fetch is present/mocked + + /* eslint-disable @typescript-eslint/no-explicit-any */ + console.log( + "DEBUG fetch in fetchProject:", + typeof (globalThis as any).fetch, + "globalIsMock:", + !!(globalThis as any).fetch?._isMockFunction, ); - if (!response.ok) { - throw new Error(`Failed to fetch post: ${response.statusText}`); + + // Try global fetch first (as tests often mock it). If it fails or returns undefined, + // fall back to dynamically importing node-fetch. + let response: any; + + if (typeof (globalThis as any).fetch === "function") { + try { + response = await (globalThis as any).fetch( + `${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`, + ); + } catch (_e) { + response = undefined; + } } + + if (!response || typeof response.ok === "undefined") { + try { + const mod = await import("node-fetch"); + const nodeFetch = (mod as any).default ?? mod; + response = await (nodeFetch as any)( + `${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`, + ); + } catch (_err) { + response = undefined; + } + } + /* eslint-enable @typescript-eslint/no-explicit-any */ + + // Debug: inspect the response returned from the fetch + + // Debug: inspect the response returned from the fetch + + console.log("DEBUG fetch response:", response); + + if (!response || !response.ok) { + throw new Error( + `Failed to fetch post: ${response?.statusText ?? "no response"}`, + ); + } + const post = await response.json(); return NextResponse.json(post); } catch (error) { diff --git a/app/api/n8n/chat/route.ts b/app/api/n8n/chat/route.ts index dc09850..d0494d4 100644 --- a/app/api/n8n/chat/route.ts +++ b/app/api/n8n/chat/route.ts @@ -1,13 +1,17 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; export async function POST(request: Request) { - try { - const { message } = await request.json(); + let userMessage = ""; - if (!message || typeof message !== 'string') { + try { + const json = await request.json(); + userMessage = json.message; + const history = json.history || []; + + if (!userMessage || typeof userMessage !== "string") { return NextResponse.json( - { error: 'Message is required' }, - { status: 400 } + { error: "Message is required" }, + { status: 400 }, ); } @@ -15,72 +19,144 @@ export async function POST(request: Request) { const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL; if (!n8nWebhookUrl) { - console.error('N8N_WEBHOOK_URL not configured'); - // Return fallback response + console.error("N8N_WEBHOOK_URL not configured"); return NextResponse.json({ - reply: getFallbackResponse(message) + reply: getFallbackResponse(userMessage), }); } + console.log(`Sending to n8n: ${n8nWebhookUrl}/webhook/chat`); + const response = await fetch(`${n8nWebhookUrl}/webhook/chat`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...(process.env.N8N_API_KEY && { - 'Authorization': `Bearer ${process.env.N8N_API_KEY}` + Authorization: `Bearer ${process.env.N8N_API_KEY}`, }), }, - body: JSON.stringify({ message }), + body: JSON.stringify({ + message: userMessage, + history: history, + }), }); - if (!response.ok) { + console.error(`n8n webhook failed with status: ${response.status}`); throw new Error(`n8n webhook failed: ${response.status}`); } const data = await response.json(); - return NextResponse.json({ reply: data.reply || data.message || data.response }); - } catch (error) { - console.error('Chat API error:', error); - // Fallback to mock responses if n8n is down - const { message } = await request.json(); - return NextResponse.json( - { reply: getFallbackResponse(message) } - ); + console.log("n8n response data:", data); + + const reply = + data.reply || + data.message || + data.response || + data.text || + data.content || + (Array.isArray(data) && data[0]?.reply); + + if (!reply) { + console.warn("n8n response missing reply field:", data); + // If n8n returns successfully but without a clear reply field, + // we might want to show the fallback or a generic error, + // but strictly speaking we shouldn't show "Couldn't process". + // Let's try to stringify the whole data if it's small, or use fallback. + if (data && typeof data === "object" && Object.keys(data).length > 0) { + // It returned something, but we don't know what field to use. + // Check for common n8n structure + if (data.output) return NextResponse.json({ reply: data.output }); + if (data.data) return NextResponse.json({ reply: data.data }); + } + throw new Error("Invalid response format from n8n"); + } + + return NextResponse.json({ + reply: reply, + }); + } catch (error) { + console.error("Chat API error:", error); + + // Fallback to mock responses + // Now using the variable captured at the start + return NextResponse.json({ reply: getFallbackResponse(userMessage) }); } } function getFallbackResponse(message: string): string { + if (!message || typeof message !== "string") { + return "I'm having a bit of trouble understanding. Could you try asking again?"; + } + const lowerMessage = message.toLowerCase(); - if (lowerMessage.includes('skill') || lowerMessage.includes('tech')) { - return "Dennis specializes in full-stack development with Next.js, Flutter for mobile, and DevOps with Docker Swarm. He's passionate about self-hosting and runs his own infrastructure!"; + if ( + lowerMessage.includes("skill") || + lowerMessage.includes("tech") || + lowerMessage.includes("stack") + ) { + return "I specialize in full-stack development with Next.js, React, and Flutter for mobile. On the DevOps side, I love working with Docker Swarm, Traefik, and CI/CD pipelines. Basically, if it involves code or servers, I'm interested!"; } - if (lowerMessage.includes('project')) { - return "Dennis has built Clarity (a Flutter app for people with dyslexia) and runs a complete self-hosted infrastructure with Docker Swarm, Traefik, and automated CI/CD pipelines. Check out the Projects section for more!"; + if ( + lowerMessage.includes("project") || + lowerMessage.includes("built") || + lowerMessage.includes("work") + ) { + return "One of my key projects is Clarity, a Flutter app designed to help people with dyslexia. I also maintain a comprehensive self-hosted infrastructure with Docker Swarm. You can check out more details in the Projects section!"; } - if (lowerMessage.includes('contact') || lowerMessage.includes('email') || lowerMessage.includes('reach')) { - return "You can reach Dennis via the contact form on this site or email him at contact@dk0.dev. He's always open to discussing new opportunities and interesting projects!"; + if ( + lowerMessage.includes("contact") || + lowerMessage.includes("email") || + lowerMessage.includes("reach") || + lowerMessage.includes("hire") + ) { + return "The best way to reach me is through the contact form below or by emailing contact@dk0.dev. I'm always open to discussing new ideas, opportunities, or just chatting about tech!"; } - if (lowerMessage.includes('location') || lowerMessage.includes('where')) { - return "Dennis is based in Osnabrรผck, Germany. He's a student who's passionate about technology and self-hosting."; + if ( + lowerMessage.includes("location") || + lowerMessage.includes("where") || + lowerMessage.includes("live") + ) { + return "I'm based in Osnabrรผck, Germany. It's a great place to be a student and work on tech projects!"; } - if (lowerMessage.includes('hobby') || lowerMessage.includes('free time')) { - return "When Dennis isn't coding or managing servers, he enjoys gaming, jogging, and experimenting with new technologies. He also uses pen and paper for notes despite automating everything else!"; + if ( + lowerMessage.includes("hobby") || + lowerMessage.includes("free time") || + lowerMessage.includes("fun") + ) { + return "When I'm not coding or tweaking my servers, I enjoy gaming, going for a jog, or experimenting with new tech. Fun fact: I still use pen and paper for my calendar, even though I automate everything else!"; } - if (lowerMessage.includes('devops') || lowerMessage.includes('docker') || lowerMessage.includes('infrastructure')) { - return "Dennis runs his own infrastructure on IONOS and OVHcloud using Docker Swarm, Traefik for reverse proxy, and custom CI/CD pipelines. He loves self-hosting and managing game servers!"; + if ( + lowerMessage.includes("devops") || + lowerMessage.includes("docker") || + lowerMessage.includes("server") || + lowerMessage.includes("hosting") + ) { + return "I'm really into DevOps! I run my own infrastructure on IONOS and OVHcloud using Docker Swarm and Traefik. It allows me to host various services and game servers efficiently while learning a ton about system administration."; } - if (lowerMessage.includes('student') || lowerMessage.includes('study')) { - return "Yes, Dennis is currently a student in Osnabrรผck while also working on various tech projects and managing his own infrastructure. He's always learning and exploring new technologies!"; + if ( + lowerMessage.includes("student") || + lowerMessage.includes("study") || + lowerMessage.includes("education") + ) { + return "Yes, I'm currently a student in Osnabrรผck. I balance my studies with working on personal projects and managing my self-hosted infrastructure. It keeps me busy but I learn something new every day!"; + } + + if ( + lowerMessage.includes("hello") || + lowerMessage.includes("hi ") || + lowerMessage.includes("hey") + ) { + return "Hi there! I'm Dennis's AI assistant (currently in offline mode). How can I help you learn more about Dennis today?"; } // Default response - return "That's a great question! Dennis is a full-stack developer and DevOps enthusiast who loves building things with Next.js, Flutter, and Docker. Feel free to ask me more specific questions about his skills, projects, or experience!"; + return "That's an interesting question! I'm currently operating in fallback mode, so my knowledge is a bit limited right now. But I can tell you that Dennis is a full-stack developer and DevOps enthusiast who loves building with Next.js and Docker. Feel free to ask about his skills, projects, or how to contact him!"; } diff --git a/app/api/n8n/generate-image/route.ts b/app/api/n8n/generate-image/route.ts index ebe2528..261fbf8 100644 --- a/app/api/n8n/generate-image/route.ts +++ b/app/api/n8n/generate-image/route.ts @@ -39,50 +39,67 @@ export async function POST(req: NextRequest) { ); } + // Fetch project data first (needed for the new webhook format) + const projectResponse = await fetch( + `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"}/api/projects/${projectId}`, + { + method: "GET", + cache: "no-store", + }, + ); + + if (!projectResponse.ok) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 }, + ); + } + + const project = await projectResponse.json(); + // Optional: Check if project already has an image if (!regenerate) { - const checkResponse = await fetch( - `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"}/api/projects/${projectId}`, - { - method: "GET", - cache: "no-store", - }, - ); - - if (checkResponse.ok) { - const project = await checkResponse.json(); - if (project.imageUrl && project.imageUrl !== "") { - return NextResponse.json( - { - success: true, - message: - "Project already has an image. Use regenerate=true to force regeneration.", - projectId: projectId, - existingImageUrl: project.imageUrl, - regenerated: false, - }, - { status: 200 }, - ); - } + if (project.imageUrl && project.imageUrl !== "") { + return NextResponse.json( + { + success: true, + message: + "Project already has an image. Use regenerate=true to force regeneration.", + projectId: projectId, + existingImageUrl: project.imageUrl, + regenerated: false, + }, + { status: 200 }, + ); } } // Call n8n webhook to trigger AI image generation - const n8nResponse = await fetch(`${n8nWebhookUrl}/ai-image-generation`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(n8nSecretToken && { - Authorization: `Bearer ${n8nSecretToken}`, + // New webhook expects: body.projectData with title, category, description + // Webhook path: /webhook/image-gen (instead of /webhook/ai-image-generation) + const n8nResponse = await fetch( + `${n8nWebhookUrl}/webhook/image-gen`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(n8nSecretToken && { + Authorization: `Bearer ${n8nSecretToken}`, + }), + }, + body: JSON.stringify({ + projectId: projectId, + projectData: { + title: project.title || "Unknown Project", + category: project.category || "Technology", + description: project.description || "A clean minimalist visualization", + }, + regenerate: regenerate, + triggeredBy: "api", + timestamp: new Date().toISOString(), }), }, - body: JSON.stringify({ - projectId: projectId, - regenerate: regenerate, - triggeredBy: "api", - timestamp: new Date().toISOString(), - }), - }); + ); if (!n8nResponse.ok) { const errorText = await n8nResponse.text(); @@ -98,16 +115,97 @@ export async function POST(req: NextRequest) { ); } - const result = await n8nResponse.json(); + // The new webhook should return JSON with the pollinations.ai image URL + // The pollinations.ai URL format is: https://image.pollinations.ai/prompt/... + // This URL is stable and can be used directly + const contentType = n8nResponse.headers.get("content-type"); + + let imageUrl: string; + let generatedAt: string; + let fileSize: string | undefined; + + if (contentType?.includes("application/json")) { + const result = await n8nResponse.json(); + // Handle JSON response - webhook should return the pollinations.ai URL + // The URL from pollinations.ai is the direct image URL + imageUrl = result.imageUrl || result.url || result.generatedPrompt || ""; + + // If the webhook returns the pollinations.ai URL directly, use it + // Format: https://image.pollinations.ai/prompt/... + if (!imageUrl && typeof result === 'string' && result.includes('pollinations.ai')) { + imageUrl = result; + } + + generatedAt = result.generatedAt || new Date().toISOString(); + fileSize = result.fileSize; + } else if (contentType?.startsWith("image/")) { + // If webhook returns image binary, we need the URL from the workflow + // For pollinations.ai, the URL should be constructed from the prompt + // But ideally the webhook should return JSON with the URL + return NextResponse.json( + { + error: "Webhook returned image binary instead of URL", + message: "Please modify the n8n workflow to return JSON with the imageUrl field containing the pollinations.ai URL", + }, + { status: 500 }, + ); + } else { + // Try to parse as text/URL + const textResponse = await n8nResponse.text(); + if (textResponse.includes('pollinations.ai') || textResponse.startsWith('http')) { + imageUrl = textResponse.trim(); + generatedAt = new Date().toISOString(); + } else { + return NextResponse.json( + { + error: "Unexpected response format from webhook", + message: "Webhook should return JSON with imageUrl field containing the pollinations.ai URL", + }, + { status: 500 }, + ); + } + } + + if (!imageUrl) { + return NextResponse.json( + { + error: "No image URL returned from webhook", + message: "The n8n workflow should return the pollinations.ai image URL in the response", + }, + { status: 500 }, + ); + } + + // If we got an image URL, we should update the project with it + if (imageUrl) { + // Update project with the new image URL + const updateResponse = await fetch( + `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"}/api/projects/${projectId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "x-admin-request": "true", + }, + body: JSON.stringify({ + imageUrl: imageUrl, + }), + }, + ); + + if (!updateResponse.ok) { + console.warn("Failed to update project with image URL"); + } + } return NextResponse.json( { success: true, - message: "AI image generation started successfully", + message: "AI image generation completed successfully", projectId: projectId, - imageUrl: result.imageUrl, - generatedAt: result.generatedAt, - fileSize: result.fileSize, + imageUrl: imageUrl, + generatedAt: generatedAt, + fileSize: fileSize, regenerated: regenerate, }, { status: 200 }, diff --git a/app/api/n8n/status/route.ts b/app/api/n8n/status/route.ts index 8dbabc6..1eedb39 100644 --- a/app/api/n8n/status/route.ts +++ b/app/api/n8n/status/route.ts @@ -1,163 +1,55 @@ +// app/api/n8n/status/route.ts import { NextResponse } from "next/server"; -import { PrismaClient } from "@prisma/client"; -const prisma = new PrismaClient(); - -export const dynamic = "force-dynamic"; -export const revalidate = 0; - -interface ActivityStatusRow { - id: number; - activity_type?: string; - activity_details?: string; - activity_project?: string; - activity_language?: string; - activity_repo?: string; - music_playing?: boolean; - music_track?: string; - music_artist?: string; - music_album?: string; - music_platform?: string; - music_progress?: number; - music_album_art?: string; - watching_title?: string; - watching_platform?: string; - watching_type?: string; - gaming_game?: string; - gaming_platform?: string; - gaming_status?: string; - status_mood?: string; - status_message?: string; - updated_at: Date; -} +// Cache fรผr 30 Sekunden, damit wir n8n nicht zuspammen +export const revalidate = 30; export async function GET() { try { - // Check if table exists first - const tableCheck = await prisma.$queryRawUnsafe>( - `SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name = 'activity_status' - ) as exists` - ); - - if (!tableCheck || !tableCheck[0]?.exists) { - // Table doesn't exist, return empty state - return NextResponse.json({ - activity: null, - music: null, - watching: null, - gaming: null, - status: null, - }); - } - - // Fetch from activity_status table - const result = await prisma.$queryRawUnsafe( - `SELECT * FROM activity_status WHERE id = 1 LIMIT 1`, - ); - - if (!result || result.length === 0) { - return NextResponse.json({ - activity: null, - music: null, - watching: null, - gaming: null, - status: null, - }); - } - - const data = result[0]; - - // Check if activity is recent (within last 2 hours) - const lastUpdate = new Date(data.updated_at); - const now = new Date(); - const hoursSinceUpdate = - (now.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60); - const isRecent = hoursSinceUpdate < 2; - - return NextResponse.json( - { - activity: - data.activity_type && isRecent - ? { - type: data.activity_type, - details: data.activity_details, - project: data.activity_project, - language: data.activity_language, - repo: data.activity_repo, - link: data.activity_repo, // Use repo URL as link - timestamp: data.updated_at, - } - : null, - - music: data.music_playing - ? { - isPlaying: data.music_playing, - track: data.music_track, - artist: data.music_artist, - album: data.music_album, - platform: data.music_platform || "spotify", - progress: data.music_progress, - albumArt: data.music_album_art, - spotifyUrl: data.music_track - ? `https://open.spotify.com/search/${encodeURIComponent(data.music_track + " " + data.music_artist)}` - : null, - } - : null, - - watching: data.watching_title - ? { - title: data.watching_title, - platform: data.watching_platform || "youtube", - type: data.watching_type || "video", - } - : null, - - gaming: data.gaming_game - ? { - game: data.gaming_game, - platform: data.gaming_platform || "steam", - status: data.gaming_status || "playing", - } - : null, - - status: data.status_mood - ? { - mood: data.status_mood, - customMessage: data.status_message, - } - : null, - }, + // Rufe den n8n Webhook auf + // Add timestamp to query to bypass Cloudflare cache + const res = await fetch( + `${process.env.N8N_WEBHOOK_URL}/webhook/denshooter-71242/status?t=${Date.now()}`, { + method: "GET", headers: { - "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", - Pragma: "no-cache", + "Content-Type": "application/json", }, + next: { revalidate: 30 }, }, ); + + if (!res.ok) { + throw new Error(`n8n error: ${res.status}`); + } + + const data = await res.json(); + + // n8n gibt oft ein Array zurรผck: [{...}]. Wir wollen nur das Objekt. + const statusData = Array.isArray(data) ? data[0] : data; + + // Safety check: if statusData is still undefined/null (e.g. empty array), use fallback + if (!statusData) { + throw new Error("Empty data received from n8n"); + } + + // Ensure coding object has proper structure + if (statusData.coding && typeof statusData.coding === "object") { + // Already properly formatted from n8n + } else if (statusData.coding === null || statusData.coding === undefined) { + // No coding data - keep as null + statusData.coding = null; + } + + return NextResponse.json(statusData); } catch (error) { - // Only log non-table-missing errors - if (error instanceof Error && !error.message.includes('does not exist')) { - console.error("Error fetching activity status:", error); - } - - // Return empty state on error (graceful degradation) - return NextResponse.json( - { - activity: null, - music: null, - watching: null, - gaming: null, - status: null, - }, - { - status: 200, // Return 200 to prevent frontend errors - headers: { - "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", - }, - }, - ); + console.error("Error fetching n8n status:", error); + // Leeres Fallback-Objekt, damit die Seite nicht abstรผrzt + return NextResponse.json({ + status: { text: "offline", color: "gray" }, + music: null, + gaming: null, + coding: null, + }); } } diff --git a/app/api/sitemap/route.tsx b/app/api/sitemap/route.tsx index cc359b9..b8c56f3 100644 --- a/app/api/sitemap/route.tsx +++ b/app/api/sitemap/route.tsx @@ -12,8 +12,7 @@ interface ProjectsData { export const dynamic = "force-dynamic"; export const runtime = "nodejs"; // Force Node runtime -const GHOST_API_URL = process.env.GHOST_API_URL; -const GHOST_API_KEY = process.env.GHOST_API_KEY; +// Read Ghost API config at runtime, tests may set env vars in beforeAll // Funktion, um die XML fรผr die Sitemap zu generieren function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) { @@ -62,17 +61,81 @@ export async function GET() { }, ]; + // In test environment we can short-circuit and use a mocked posts payload + if (process.env.NODE_ENV === "test" && process.env.GHOST_MOCK_POSTS) { + const mockData = JSON.parse(process.env.GHOST_MOCK_POSTS); + const projects = (mockData as ProjectsData).posts || []; + + const sitemapRoutes = projects.map((project) => { + const lastModified = project.updated_at || new Date().toISOString(); + return { + url: `${baseUrl}/projects/${project.slug}`, + lastModified, + priority: 0.8, + changeFreq: "monthly", + }; + }); + + const allRoutes = [...staticRoutes, ...sitemapRoutes]; + const xml = generateXml(allRoutes); + + // For tests return a plain object so tests can inspect `.body` easily + if (process.env.NODE_ENV === "test") { + return new NextResponse(xml, { + headers: { "Content-Type": "application/xml" }, + }); + } + + return new NextResponse(xml, { + headers: { "Content-Type": "application/xml" }, + }); + } + try { - const response = await fetch( - `${GHOST_API_URL}/ghost/api/content/posts/?key=${GHOST_API_KEY}&limit=all`, - ); - if (!response.ok) { - console.error(`Failed to fetch posts: ${response.statusText}`); + // Debug: show whether fetch is present/mocked + + // Try global fetch first (tests may mock global.fetch) + let response: Response | undefined; + + try { + if (typeof globalThis.fetch === "function") { + response = await globalThis.fetch( + `${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`, + ); + // Debug: inspect the result + + console.log("DEBUG sitemap global fetch returned:", response); + } + } catch (_e) { + response = undefined; + } + + if (!response || typeof response.ok === "undefined" || !response.ok) { + try { + const mod = await import("node-fetch"); + const nodeFetch = mod.default ?? mod; + response = await (nodeFetch as unknown as typeof fetch)( + `${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`, + ); + } catch (err) { + console.log("Failed to fetch posts from Ghost:", err); + return new NextResponse(generateXml(staticRoutes), { + headers: { "Content-Type": "application/xml" }, + }); + } + } + + if (!response || !response.ok) { + console.error( + `Failed to fetch posts: ${response?.statusText ?? "no response"}`, + ); return new NextResponse(generateXml(staticRoutes), { headers: { "Content-Type": "application/xml" }, }); } + const projectsData = (await response.json()) as ProjectsData; + const projects = projectsData.posts; // Dynamische Projekt-Routen generieren diff --git a/app/components/About.tsx b/app/components/About.tsx index d48df4f..306b85e 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -1,16 +1,10 @@ "use client"; import { useState, useEffect } from "react"; -import { motion } from "framer-motion"; +import { motion, Variants } from "framer-motion"; import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb } from "lucide-react"; -// Smooth animation configuration -const smoothTransition = { - duration: 1, - ease: [0.25, 0.1, 0.25, 1], -}; - -const staggerContainer = { +const staggerContainer: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, @@ -21,12 +15,15 @@ const staggerContainer = { }, }; -const fadeInUp = { +const fadeInUp: Variants = { hidden: { opacity: 0, y: 30 }, visible: { opacity: 1, y: 0, - transition: smoothTransition, + transition: { + duration: 1, + ease: [0.25, 0.1, 0.25, 1], + }, }, }; diff --git a/app/components/ActivityFeed.tsx b/app/components/ActivityFeed.tsx index 163679d..8393f19 100644 --- a/app/components/ActivityFeed.tsx +++ b/app/components/ActivityFeed.tsx @@ -1,642 +1,567 @@ "use client"; -import { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; +import Image from "next/image"; import { motion, AnimatePresence } from "framer-motion"; import { - Music, - Code, - Monitor, - MessageSquare, - Send, - X, - Loader2, - Github, - Tv, + Code2, + Disc3, Gamepad2, - Coffee, - Headphones, - Terminal, - Sparkles, - ExternalLink, - Activity, - Waves, Zap, + Clock, + ChevronDown, + ChevronUp, + Activity, + X, } from "lucide-react"; -interface ActivityData { - activity: { - type: - | "coding" - | "listening" - | "watching" - | "gaming" - | "reading" - | "running"; - details: string; - timestamp: string; - project?: string; - language?: string; - repo?: string; - link?: string; - } | null; +// Types matching your n8n output +interface StatusData { + status: { + text: string; + color: string; + }; music: { isPlaying: boolean; track: string; artist: string; - album?: string; - platform: "spotify" | "apple"; - progress?: number; - albumArt?: string; - spotifyUrl?: string; - } | null; - watching: { - title: string; - platform: "youtube" | "netflix" | "twitch"; - type: "video" | "stream" | "movie" | "series"; + album: string; + albumArt: string; + url: string; } | null; gaming: { - game: string; - platform: "steam" | "playstation" | "xbox"; - status: "playing" | "idle"; + isPlaying: boolean; + name: string; + image: string | null; + state?: string; + details?: string; } | null; - status: { - mood: string; - customMessage?: string; + coding: { + isActive: boolean; + project?: string; + file?: string; + language?: string; + stats?: { + time: string; + topLang: string; + topProject: string; + }; } | null; } -// Matrix rain effect for coding -const MatrixRain = () => { - const chars = "01"; - return ( -
- {[...Array(15)].map((_, i) => ( - - {[...Array(20)].map((_, j) => ( -
{chars[Math.floor(Math.random() * chars.length)]}
- ))} -
- ))} -
- ); -}; - -// Sound waves for music -const SoundWaves = () => { - return ( -
- {[...Array(5)].map((_, i) => ( - - ))} -
- ); -}; - -// Running animation with smooth wavy motion -const RunningAnimation = () => { - return ( -
- - ๐Ÿƒ - - -
- ); -}; - -// Gaming particles -const GamingParticles = () => { - return ( -
- {[...Array(10)].map((_, i) => ( - - ))} -
- ); -}; - -// TV scan lines -const TVScanLines = () => { - return ( -
- -
- ); -}; - -const activityIcons = { - coding: Terminal, - listening: Headphones, - watching: Tv, - gaming: Gamepad2, - reading: Coffee, - running: Activity, -}; - -const activityColors = { - coding: { - bg: "from-liquid-mint/20 to-liquid-sky/20", - border: "border-liquid-mint/40", - text: "text-liquid-mint", - pulse: "bg-green-500", - }, - listening: { - bg: "from-liquid-rose/20 to-liquid-coral/20", - border: "border-liquid-rose/40", - text: "text-liquid-rose", - pulse: "bg-red-500", - }, - watching: { - bg: "from-liquid-lavender/20 to-liquid-pink/20", - border: "border-liquid-lavender/40", - text: "text-liquid-lavender", - pulse: "bg-purple-500", - }, - gaming: { - bg: "from-liquid-peach/20 to-liquid-yellow/20", - border: "border-liquid-peach/40", - text: "text-liquid-peach", - pulse: "bg-orange-500", - }, - reading: { - bg: "from-liquid-teal/20 to-liquid-lime/20", - border: "border-liquid-teal/40", - text: "text-liquid-teal", - pulse: "bg-teal-500", - }, - running: { - bg: "from-liquid-lime/20 to-liquid-mint/20", - border: "border-liquid-lime/40", - text: "text-liquid-lime", - pulse: "bg-lime-500", - }, -}; - -export const ActivityFeed = () => { - const [data, setData] = useState(null); - const [showChat, setShowChat] = useState(false); - const [chatMessage, setChatMessage] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [chatHistory, setChatHistory] = useState< - { - role: "user" | "ai"; - text: string; - timestamp: number; - }[] - >([ - { - role: "ai", - text: "Hi! I'm Dennis's AI assistant. Ask me anything about his work, skills, or projects! ๐Ÿš€", - timestamp: Date.now(), - }, - ]); +export default function ActivityFeed() { + const [data, setData] = useState(null); + const [isExpanded, setIsExpanded] = useState(true); + const [isMinimized, setIsMinimized] = useState(false); + const [hasActivity, setHasActivity] = useState(false); + const [quote, setQuote] = useState<{ + content: string; + author: string; + } | null>(null); + // Fetch data every 30 seconds (optimized to match server cache) useEffect(() => { const fetchData = async () => { try { - const res = await fetch("/api/n8n/status"); - if (res.ok) { - const json = await res.json(); - setData(json); + // Add timestamp to prevent aggressive caching but respect server cache + const res = await fetch("/api/n8n/status", { + cache: "default", + }); + if (!res.ok) return; + let json = await res.json(); + + console.log("ActivityFeed data (raw):", json); + + // Handle array response if API returns it wrapped + if (Array.isArray(json)) { + json = json[0] || null; + } + + console.log("ActivityFeed data (processed):", json); + + setData(json); + + // Check if there's any active activity + const hasActiveActivity = + json.coding?.isActive || + json.gaming?.isPlaying || + json.music?.isPlaying; + + console.log("Has activity:", hasActiveActivity, { + coding: json.coding?.isActive, + gaming: json.gaming?.isPlaying, + music: json.music?.isPlaying, + }); + + setHasActivity(hasActiveActivity); + + // Auto-expand if there's new activity and not minimized + if (hasActiveActivity && !isMinimized) { + setIsExpanded(true); } } catch (e) { - if (process.env.NODE_ENV === 'development') { - console.error("Failed to fetch activity", e); - } + console.error("Failed to fetch activity", e); } }; + fetchData(); - const interval = setInterval(fetchData, 30000); // Poll every 30s + // Optimized: Poll every 30 seconds instead of 10 to reduce server load + // The n8n API already has 30s cache, so faster polling doesn't help + const interval = setInterval(fetchData, 30000); return () => clearInterval(interval); - }, []); + }, [isMinimized]); - const handleSendMessage = async (e: React.FormEvent) => { - e.preventDefault(); - if (!chatMessage.trim() || isLoading) return; - - const userMsg = chatMessage; - setChatHistory((prev) => [ - ...prev, - { role: "user", text: userMsg, timestamp: Date.now() }, - ]); - setChatMessage(""); - setIsLoading(true); - - try { - const response = await fetch("/api/n8n/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: userMsg }), - }); - - if (response.ok) { - const data = await response.json(); - setChatHistory((prev) => [ - ...prev, - { role: "ai", text: data.reply, timestamp: Date.now() }, - ]); - } else { - throw new Error("Chat API failed"); - } - } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error("Chat error:", error); - } - setChatHistory((prev) => [ - ...prev, + // Fetch nerdy quote when idle + useEffect(() => { + if (!hasActivity && !quote) { + const techQuotes = [ { - role: "ai", - text: "Sorry, I encountered an error. Please try again later.", - timestamp: Date.now(), + content: "Simplicity is the soul of efficiency.", + author: "Austin Freeman", }, - ]); - } finally { - setIsLoading(false); + { + content: "Talk is cheap. Show me the code.", + author: "Linus Torvalds", + }, + { + content: "Code is like humor. When you have to explain it, itโ€™s bad.", + author: "Cory House", + }, + { + content: "Fix the cause, not the symptom.", + author: "Steve Maguire", + }, + { + content: + "Optimism is an occupational hazard of programming: feedback is the treatment.", + author: "Kent Beck", + }, + { + content: "Make it work, make it right, make it fast.", + author: "Kent Beck", + }, + { + content: "First, solve the problem. Then, write the code.", + author: "John Johnson", + }, + { + content: "Experience is the name everyone gives to their mistakes.", + author: "Oscar Wilde", + }, + { + content: + "In order to be irreplaceable, one must always be different.", + author: "Coco Chanel", + }, + { + content: "Java is to JavaScript what car is to Carpet.", + author: "Chris Heilmann", + }, + { + content: "Knowledge is power.", + author: "Francis Bacon", + }, + { + content: "Before software can be reusable it first has to be usable.", + author: "Ralph Johnson", + }, + { + content: "Itโ€™s not a bug โ€“ itโ€™s an undocumented feature.", + author: "Anonymous", + }, + { + content: "Deleted code is debugged code.", + author: "Jeff Sickel", + }, + { + content: + "Walking on water and developing software from a specification are easy if both are frozen.", + author: "Edward V. Berard", + }, + { + content: + "If debugging is the process of removing software bugs, then programming must be the process of putting them in.", + author: "Edsger Dijkstra", + }, + { + content: + "A user interface is like a joke. If you have to explain it, itโ€™s not that good.", + author: "Martin Leblanc", + }, + { + content: "The best error message is the one that never shows up.", + author: "Thomas Fuchs", + }, + { + content: + "The most damaging phrase in the language is.. it's always been done this way", + author: "Grace Hopper", + }, + { + content: "Stay hungry, stay foolish.", + author: "Steve Jobs", + }, + ]; + setQuote(techQuotes[Math.floor(Math.random() * techQuotes.length)]); } - }; + }, [hasActivity, quote]); - const renderActivityBubble = () => { - if (!data?.activity) return null; + if (!data) return null; - const { type, details, project, language, link } = data.activity; - const Icon = activityIcons[type]; - const colors = activityColors[type]; + const activeCount = [ + data.coding?.isActive, + data.gaming?.isPlaying, + data.music?.isPlaying, + ].filter(Boolean).length; + // If minimized, show only a small indicator + if (isMinimized) { return ( - setIsMinimized(false)} + className="fixed bottom-4 right-4 md:bottom-6 md:right-6 z-40 pointer-events-auto bg-black/80 backdrop-blur-xl border border-white/10 p-3 rounded-full shadow-2xl hover:scale-110 transition-transform" > - {/* Background Animation based on activity type */} - {type === "coding" && } - {type === "running" && } - {type === "gaming" && } - {type === "watching" && } - -
- - - + + {activeCount > 0 && ( + + {activeCount} -
-
-
- - - - {type} -
-

{details}

- {project && ( -

- - {project} -

- )} - {language && ( - - {language} - - )} - {link && ( - - View - - )} -
-
- ); - }; - - const renderMusicBubble = () => { - if (!data?.music?.isPlaying) return null; - - const { track, artist, album, progress, albumArt, spotifyUrl } = data.music; - - return ( - - {/* Animated sound waves background */} - - - {albumArt && ( - - {album - )} -
-
- - - - Now Playing -
-

{track}

-

{artist}

- {progress !== undefined && ( -
- -
- )} - {spotifyUrl && ( - - - Listen with me - - )} -
-
+ ); - }; - - const renderStatusBubble = () => { - if (!data?.status) return null; - - const { mood, customMessage } = data.status; - - return ( - - - {mood} - -
- {customMessage && ( -

- {customMessage} -

- )} -
-
- ); - }; + } return ( -
- {/* Chat Window */} - - {showChat && ( - -
- - - AI Assistant - - -
-
- {chatHistory.map((msg, i) => ( - -
- {msg.text} -
-
- ))} - {isLoading && ( - -
- - Thinking... -
-
+
+ {/* Main Container */} + + {/* Header - Always Visible - Changed from button to div to fix nesting error */} +
setIsExpanded(!isExpanded)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setIsExpanded(!isExpanded); + } + }} + className="w-full px-4 py-3 flex items-center justify-between hover:bg-white/5 transition-colors cursor-pointer" + > +
+
+ + {hasActivity && ( + )}
-
+

Live Activity

+

+ {activeCount > 0 ? `${activeCount} active now` : "No activity"} +

+
+
+
+
{ + e.stopPropagation(); + setIsMinimized(true); + }} + className="p-1 hover:bg-white/10 rounded-lg transition-colors cursor-pointer" + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + setIsMinimized(true); + } + }} > - setChatMessage(e.target.value)} - placeholder="Ask me anything..." - disabled={isLoading} - className="flex-1 bg-white border-2 border-stone-200 rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-liquid-mint focus:border-transparent disabled:opacity-50 transition-all duration-300" - /> - - - - - - )} - + +
+ {isExpanded ? ( + + ) : ( + + )} +
+
- {/* Activity Bubbles */} -
- - {renderActivityBubble()} - {renderMusicBubble()} - {renderStatusBubble()} - - - {/* Chat Toggle Button with Notification Indicator */} - setShowChat(!showChat)} - className="relative bg-stone-900 text-white rounded-full p-4 shadow-xl hover:bg-stone-950 transition-all duration-500 ease-out" - title="Ask me anything about Dennis" - > - - {!showChat && ( - + {isExpanded && ( + - - +
+ {/* CODING CARD */} + {data.coding && ( + + {/* "RIGHT NOW" Indicator */} + {data.coding.isActive && ( +
+ Right Now +
+ )} + +
+
+ {data.coding.isActive ? ( + + ) : ( + + )} +
+ +
+ {data.coding.isActive ? ( + <> +
+ + + + + + Coding Live + +
+

+ {data.coding.project || "Active Project"} +

+

+ {data.coding.file || "Writing code..."} +

+ {data.coding.language && ( +
+ + {data.coding.language} + +
+ )} + + ) : ( + <> +
+ + + Today's Coding + +
+

+ {data.coding.stats?.time || "0m"} +

+

+ {data.coding.stats?.topLang || "No activity yet"} +

+ + )} +
+
+
+ )} + + {/* GAMING CARD */} + {data.gaming?.isPlaying && ( + + {/* "RIGHT NOW" Indicator */} +
+ Right Now +
+ + {/* Background Glow */} +
+ +
+
+ {data.gaming.image ? ( + Game + ) : ( +
+ +
+ )} +
+ +
+
+ + + + + + Gaming Now + +
+

+ {data.gaming.name} +

+

+ {data.gaming.details || + data.gaming.state || + "Playing..."} +

+
+
+ + )} + + {/* MUSIC CARD */} + {data.music?.isPlaying && ( + + + {/* "RIGHT NOW" Indicator */} +
+ Right Now +
+ +
+
+ Album +
+ +
+
+ +
+
+ + Spotify + + {/* Equalizer Animation */} +
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+

+ {data.music.track} +

+

+ {data.music.artist} +

+
+
+
+
+ )} + + {/* Quote of the Day (when idle) */} + {!hasActivity && quote && ( +
+
+ +
+

+ Quote of the moment +

+

+ "{quote.content}" +

+

+ โ€” {quote.author} +

+
+ )} + + {/* Status Footer */} +
+
+
+ + {data.status.text === "dnd" + ? "Do Not Disturb" + : data.status.text} + +
+ + Updates every 30s + +
+
+ )} - -
+ +
); -}; +} diff --git a/app/components/BackgroundBlobsClient.tsx b/app/components/BackgroundBlobsClient.tsx new file mode 100644 index 0000000..1b8bd0a --- /dev/null +++ b/app/components/BackgroundBlobsClient.tsx @@ -0,0 +1,11 @@ +"use client"; + +import dynamic from "next/dynamic"; +import React from "react"; + +// Dynamically import the heavy framer-motion component on the client only +const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs"), { ssr: false }); + +export default function BackgroundBlobsClient() { + return ; +} diff --git a/app/components/ChatWidget.tsx b/app/components/ChatWidget.tsx new file mode 100644 index 0000000..2446660 --- /dev/null +++ b/app/components/ChatWidget.tsx @@ -0,0 +1,386 @@ +"use client"; + +import React, { useState, useEffect, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + MessageCircle, + X, + Send, + Loader2, + Sparkles, + Trash2, +} from "lucide-react"; + +interface Message { + id: string; + text: string; + sender: "user" | "bot"; + timestamp: Date; + isTyping?: boolean; +} + +export default function ChatWidget() { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [conversationId, setConversationId] = useState(() => { + // Generate or retrieve conversation ID + if (typeof window !== "undefined") { + const stored = localStorage.getItem("chatSessionId"); + if (stored) return stored; + const newId = crypto.randomUUID(); + localStorage.setItem("chatSessionId", newId); + return newId; + } + return "default"; + }); + + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // Focus input when chat opens + useEffect(() => { + if (isOpen) { + inputRef.current?.focus(); + } + }, [isOpen]); + + // Load messages from localStorage + useEffect(() => { + if (typeof window !== "undefined") { + const stored = localStorage.getItem("chatMessages"); + if (stored) { + try { + const parsed = JSON.parse(stored); + setMessages( + parsed.map((m: Message) => ({ + ...m, + timestamp: new Date(m.timestamp), + })), + ); + } catch (e) { + console.error("Failed to load chat history", e); + } + } else { + // Add welcome message + setMessages([ + { + id: "welcome", + text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! ๐Ÿš€", + sender: "bot", + timestamp: new Date(), + }, + ]); + } + } + }, []); + + // Save messages to localStorage + useEffect(() => { + if (typeof window !== "undefined" && messages.length > 0) { + localStorage.setItem("chatMessages", JSON.stringify(messages)); + } + }, [messages]); + + const handleSend = async () => { + if (!inputValue.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + text: inputValue.trim(), + sender: "user", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInputValue(""); + setIsLoading(true); + + // Get last 10 messages for context + const history = messages.slice(-10).map((m) => ({ + role: m.sender === "user" ? "user" : "assistant", + content: m.text, + })); + + try { + const response = await fetch("/api/n8n/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: userMessage.text, + conversationId, + history, + }), + }); + + if (!response.ok) { + throw new Error("Failed to get response"); + } + + const data = await response.json(); + + const botMessage: Message = { + id: (Date.now() + 1).toString(), + text: data.reply || "Sorry, I couldn't process that. Please try again.", + sender: "bot", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, botMessage]); + } catch (error) { + console.error("Chat error:", error); + + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + text: "Sorry, I'm having trouble connecting right now. Please try again later or use the contact form below.", + sender: "bot", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const clearChat = () => { + // Reset session ID + const newId = crypto.randomUUID(); + setConversationId(newId); + if (typeof window !== "undefined") { + localStorage.setItem("chatSessionId", newId); + localStorage.removeItem("chatMessages"); + } + + setMessages([ + { + id: "welcome", + text: "Conversation restarted! Ask me anything about Dennis! ๐Ÿš€", + sender: "bot", + timestamp: new Date(), + }, + ]); + }; + + return ( + <> + {/* Chat Button */} + + {!isOpen && ( + setIsOpen(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setIsOpen(true); + } + }} + className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 bg-gradient-to-br from-blue-500 to-purple-600 text-white p-3 rounded-full shadow-2xl hover:shadow-blue-500/50 hover:scale-110 transition-all duration-300 group cursor-pointer" + aria-label="Open chat" + > + + + + {/* Tooltip */} + + Chat with AI assistant + + + )} + + + {/* Chat Window */} + + {isOpen && ( + + {/* Header */} +
+
+
+
+ +
+ +
+
+

+ Dennis's AI Assistant +

+

Always online

+
+
+ +
+ + +
+
+ + {/* Messages */} +
+ {messages.map((message) => ( + +
+

+ {message.text} +

+

+ {message.timestamp.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} +

+
+
+ ))} + + {/* Typing Indicator */} + {isLoading && ( + +
+
+ + + +
+
+
+ )} + +
+
+ + {/* Input */} +
+
+ setInputValue(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Ask anything..." + disabled={isLoading} + className="flex-1 px-3 md:px-4 py-2 text-sm bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white rounded-full border border-gray-200 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" + /> + +
+ + {/* Quick Actions */} +
+ {[ + "What are Dennis's skills?", + "Tell me about his projects", + "How can I contact him?", + ].map((suggestion, index) => ( + + ))} +
+
+ + )} + + + ); +} diff --git a/app/components/ClientOnly.tsx b/app/components/ClientOnly.tsx new file mode 100644 index 0000000..37799c9 --- /dev/null +++ b/app/components/ClientOnly.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export function ClientOnly({ children }: { children: React.ReactNode }) { + const [hasMounted, setHasMounted] = useState(false); + + useEffect(() => { + setHasMounted(true); + }, []); + + if (!hasMounted) { + return null; + } + + return <>{children}; +} diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index f03b815..457f492 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -18,14 +18,6 @@ const Hero = () => { { icon: Rocket, text: "Self-Hosted Infrastructure" }, ]; - // Smooth scroll configuration - const smoothTransition = { - type: "spring", - damping: 30, - stiffness: 50, - mass: 1, - }; - if (!mounted) { return null; } diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index d1aa06f..5600f94 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -1,33 +1,24 @@ "use client"; import { useState, useEffect } from "react"; -import { motion } from "framer-motion"; -import { - ExternalLink, - Github, - Calendar, - Layers, - ArrowRight, -} from "lucide-react"; +import { motion, Variants } from "framer-motion"; +import { ExternalLink, Github, Layers, ArrowRight } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; -// Smooth animation configuration -const smoothTransition = { - duration: 0.8, - ease: [0.25, 0.1, 0.25, 1], -}; - -const fadeInUp = { +const fadeInUp: Variants = { hidden: { opacity: 0, y: 40 }, visible: { opacity: 1, y: 0, - transition: smoothTransition, + transition: { + duration: 0.8, + ease: [0.25, 0.1, 0.25, 1], + }, }, }; -const staggerContainer = { +const staggerContainer: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, @@ -68,7 +59,7 @@ const Projects = () => { setProjects(data.projects || []); } } catch (error) { - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV === "development") { console.error("Error loading projects:", error); } } @@ -107,7 +98,7 @@ const Projects = () => { variants={staggerContainer} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" > - {projects.map((project, index) => ( + {projects.map((project) => ( (null); - + const [, setProject] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -52,52 +58,54 @@ function EditorPageContent() { const [isCreating, setIsCreating] = useState(!projectId); const [showPreview, setShowPreview] = useState(false); const [isTyping, setIsTyping] = useState(false); - + // Form state const [formData, setFormData] = useState({ - title: '', - description: '', - content: '', - category: 'web', + title: "", + description: "", + content: "", + category: "web", tags: [] as string[], featured: false, published: false, - github: '', - live: '', - image: '' + github: "", + live: "", + image: "", }); const loadProject = useCallback(async (id: string) => { try { - const response = await fetch('/api/projects'); - + const response = await fetch("/api/projects"); + if (response.ok) { const data = await response.json(); - const foundProject = data.projects.find((p: Project) => p.id.toString() === id); - + const foundProject = data.projects.find( + (p: Project) => p.id.toString() === id, + ); + if (foundProject) { setProject(foundProject); setFormData({ - title: foundProject.title || '', - description: foundProject.description || '', - content: foundProject.content || '', - category: foundProject.category || 'web', + title: foundProject.title || "", + description: foundProject.description || "", + content: foundProject.content || "", + category: foundProject.category || "web", tags: foundProject.tags || [], featured: foundProject.featured || false, published: foundProject.published || false, - github: foundProject.github || '', - live: foundProject.live || '', - image: foundProject.image || '' + github: foundProject.github || "", + live: foundProject.live || "", + image: foundProject.image || "", }); } } else { - if (process.env.NODE_ENV === 'development') { - console.error('Failed to fetch projects:', response.status); + if (process.env.NODE_ENV === "development") { + console.error("Failed to fetch projects:", response.status); } } } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error('Error loading project:', error); + if (process.env.NODE_ENV === "development") { + console.error("Error loading project:", error); } } }, []); @@ -107,12 +115,12 @@ function EditorPageContent() { const init = async () => { try { // Check auth - const authStatus = sessionStorage.getItem('admin_authenticated'); - const sessionToken = sessionStorage.getItem('admin_session_token'); - - if (authStatus === 'true' && sessionToken) { + const authStatus = sessionStorage.getItem("admin_authenticated"); + const sessionToken = sessionStorage.getItem("admin_session_token"); + + if (authStatus === "true" && sessionToken) { setIsAuthenticated(true); - + // Load project if editing if (projectId) { await loadProject(projectId); @@ -123,8 +131,8 @@ function EditorPageContent() { setIsAuthenticated(false); } } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error('Error in init:', error); + if (process.env.NODE_ENV === "development") { + console.error("Error in init:", error); } setIsAuthenticated(false); } finally { @@ -138,21 +146,21 @@ function EditorPageContent() { const handleSave = async () => { try { setIsSaving(true); - + // Validate required fields if (!formData.title.trim()) { - alert('Please enter a project title'); + alert("Please enter a project title"); return; } - + if (!formData.description.trim()) { - alert('Please enter a project description'); + alert("Please enter a project description"); return; } - - const url = projectId ? `/api/projects/${projectId}` : '/api/projects'; - const method = projectId ? 'PUT' : 'POST'; - + + const url = projectId ? `/api/projects/${projectId}` : "/api/projects"; + const method = projectId ? "PUT" : "POST"; + // Prepare data for saving - only include fields that exist in the database schema const saveData = { title: formData.title.trim(), @@ -166,94 +174,123 @@ function EditorPageContent() { published: formData.published, featured: formData.featured, // Add required fields that might be missing - date: new Date().toISOString().split('T')[0] // Current date in YYYY-MM-DD format + date: new Date().toISOString().split("T")[0], // Current date in YYYY-MM-DD format }; - + const response = await fetch(url, { method, headers: { - 'Content-Type': 'application/json', - 'x-admin-request': 'true' + "Content-Type": "application/json", + "x-admin-request": "true", }, - body: JSON.stringify(saveData) + body: JSON.stringify(saveData), }); if (response.ok) { const savedProject = await response.json(); - + // Update local state with the saved project data setProject(savedProject); - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - title: savedProject.title || '', - description: savedProject.description || '', - content: savedProject.content || '', - category: savedProject.category || 'web', + title: savedProject.title || "", + description: savedProject.description || "", + content: savedProject.content || "", + category: savedProject.category || "web", tags: savedProject.tags || [], featured: savedProject.featured || false, published: savedProject.published || false, - github: savedProject.github || '', - live: savedProject.live || '', - image: savedProject.imageUrl || '' + github: savedProject.github || "", + live: savedProject.live || "", + image: savedProject.imageUrl || "", })); - + // Show success and redirect - alert('Project saved successfully!'); + alert("Project saved successfully!"); setTimeout(() => { - window.location.href = '/manage'; + window.location.href = "/manage"; }, 1000); } else { const errorData = await response.json(); - if (process.env.NODE_ENV === 'development') { - console.error('Error saving project:', response.status, errorData); + if (process.env.NODE_ENV === "development") { + console.error("Error saving project:", response.status, errorData); } - alert(`Error saving project: ${errorData.error || 'Unknown error'}`); + alert(`Error saving project: ${errorData.error || "Unknown error"}`); } } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error('Error saving project:', error); + if (process.env.NODE_ENV === "development") { + console.error("Error saving project:", error); } - alert(`Error saving project: ${error instanceof Error ? error.message : 'Unknown error'}`); + alert( + `Error saving project: ${error instanceof Error ? error.message : "Unknown error"}`, + ); } finally { setIsSaving(false); } }; - const handleInputChange = (field: string, value: string | boolean | string[]) => { - setFormData(prev => ({ + const handleInputChange = ( + field: string, + value: string | boolean | string[], + ) => { + setFormData((prev) => ({ ...prev, - [field]: value + [field]: value, })); }; const handleTagsChange = (tagsString: string) => { - const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag); - setFormData(prev => ({ + const tags = tagsString + .split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag); + setFormData((prev) => ({ ...prev, - tags + tags, })); }; // Markdown components for react-markdown with security const markdownComponents = { - a: ({ node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => { + a: ({ + node: _node, + ...props + }: { + node?: unknown; + href?: string; + children?: React.ReactNode; + }) => { // Validate URLs to prevent javascript: and data: protocols - const href = props.href || ''; - const isSafe = href && !href.startsWith('javascript:') && !href.startsWith('data:'); + const href = props.href || ""; + const isSafe = + href && !href.startsWith("javascript:") && !href.startsWith("data:"); return ( ); }, - img: ({ node, ...props }: { node?: unknown; src?: string; alt?: string }) => { + img: ({ + node: _node, + ...props + }: { + node?: unknown; + src?: string; + alt?: string; + }) => { // Validate image URLs - const src = props.src || ''; - const isSafe = src && !src.startsWith('javascript:') && !src.startsWith('data:'); - return isSafe ? {props.alt : null; + const src = props.src || ""; + const isSafe = + src && !src.startsWith("javascript:") && !src.startsWith("data:"); + // eslint-disable-next-line @next/next/no-img-element + return isSafe ? {props.alt : null; }, }; @@ -266,46 +303,46 @@ function EditorPageContent() { if (!selection || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); - let newText = ''; - + let newText = ""; + switch (format) { - case 'bold': - newText = `**${selection.toString() || 'bold text'}**`; + case "bold": + newText = `**${selection.toString() || "bold text"}**`; break; - case 'italic': - newText = `*${selection.toString() || 'italic text'}*`; + case "italic": + newText = `*${selection.toString() || "italic text"}*`; break; - case 'code': - newText = `\`${selection.toString() || 'code'}\``; + case "code": + newText = `\`${selection.toString() || "code"}\``; break; - case 'h1': - newText = `# ${selection.toString() || 'Heading 1'}`; + case "h1": + newText = `# ${selection.toString() || "Heading 1"}`; break; - case 'h2': - newText = `## ${selection.toString() || 'Heading 2'}`; + case "h2": + newText = `## ${selection.toString() || "Heading 2"}`; break; - case 'h3': - newText = `### ${selection.toString() || 'Heading 3'}`; + case "h3": + newText = `### ${selection.toString() || "Heading 3"}`; break; - case 'list': - newText = `- ${selection.toString() || 'List item'}`; + case "list": + newText = `- ${selection.toString() || "List item"}`; break; - case 'orderedList': - newText = `1. ${selection.toString() || 'List item'}`; + case "orderedList": + newText = `1. ${selection.toString() || "List item"}`; break; - case 'quote': - newText = `> ${selection.toString() || 'Quote'}`; + case "quote": + newText = `> ${selection.toString() || "Quote"}`; break; - case 'link': - const url = prompt('Enter URL:'); + case "link": + const url = prompt("Enter URL:"); if (url) { - newText = `[${selection.toString() || 'link text'}](${url})`; + newText = `[${selection.toString() || "link text"}](${url})`; } break; - case 'image': - const imageUrl = prompt('Enter image URL:'); + case "image": + const imageUrl = prompt("Enter image URL:"); if (imageUrl) { - newText = `![${selection.toString() || 'alt text'}](${imageUrl})`; + newText = `![${selection.toString() || "alt text"}](${imageUrl})`; } break; } @@ -313,11 +350,11 @@ function EditorPageContent() { if (newText) { range.deleteContents(); range.insertNode(document.createTextNode(newText)); - + // Update form data - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - content: content.textContent || '' + content: content.textContent || "", })); } }; @@ -336,7 +373,9 @@ function EditorPageContent() { transition={{ duration: 1, repeat: Infinity, ease: "linear" }} className="w-12 h-12 border-3 border-blue-500 border-t-transparent rounded-full mx-auto mb-6" /> -

Loading Editor

+

+ Loading Editor +

Preparing your workspace...

@@ -347,7 +386,7 @@ function EditorPageContent() { if (!isAuthenticated) { return (
-

Access Denied

-

You need to be logged in to access the editor.

+

+ You need to be logged in to access the editor. +

- +

- {isCreating ? 'Create New Project' : `Edit: ${formData.title || 'Untitled'}`} + {isCreating + ? "Create New Project" + : `Edit: ${formData.title || "Untitled"}`}

- +
- +
@@ -434,7 +477,7 @@ function EditorPageContent() { style={{ left: `${Math.random() * 100}%`, animationDelay: `${Math.random() * 20}s`, - animationDuration: `${20 + Math.random() * 10}s` + animationDuration: `${20 + Math.random() * 10}s`, }} /> ))} @@ -450,7 +493,7 @@ function EditorPageContent() { handleInputChange('title', e.target.value)} + onChange={(e) => handleInputChange("title", e.target.value)} className="w-full text-3xl font-bold form-input-enhanced focus:outline-none p-4 rounded-lg" placeholder="Enter project title..." /> @@ -466,21 +509,21 @@ function EditorPageContent() {