diff --git a/.gitea/workflows/ci-cd-dev-staging.yml b/.gitea/workflows/ci-cd-dev-staging.yml deleted file mode 100644 index 1a0004e..0000000 --- a/.gitea/workflows/ci-cd-dev-staging.yml +++ /dev/null @@ -1,209 +0,0 @@ -name: CI/CD Pipeline (Dev/Staging) - -on: - push: - branches: [dev, main] - -env: - NODE_VERSION: '20' - DOCKER_IMAGE: portfolio-app - CONTAINER_NAME: portfolio-app-staging - -jobs: - staging: - 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:production - - - 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 Gitea Variables and Secrets - run: | - echo "πŸ” Verifying Gitea Variables and Secrets..." - - # Check Variables - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "❌ NEXT_PUBLIC_BASE_URL variable is missing!" - echo "Please set this variable in Gitea repository settings" - exit 1 - fi - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "❌ MY_EMAIL variable is missing!" - echo "Please set this variable in Gitea repository settings" - exit 1 - fi - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "❌ MY_INFO_EMAIL variable is missing!" - echo "Please set this variable in Gitea repository settings" - exit 1 - fi - - # Check Secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "❌ MY_PASSWORD secret is missing!" - echo "Please set this secret in Gitea repository settings" - exit 1 - fi - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "❌ MY_INFO_PASSWORD secret is missing!" - echo "Please set this secret in Gitea repository settings" - exit 1 - fi - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "❌ ADMIN_BASIC_AUTH secret is missing!" - echo "Please set this secret in Gitea repository settings" - exit 1 - fi - - echo "βœ… All required Gitea variables and secrets are present" - echo "πŸ“ Variables found:" - echo " - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }}" - echo " - MY_EMAIL: ${{ vars.MY_EMAIL }}" - echo " - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }}" - echo " - NODE_ENV: staging" - echo " - LOG_LEVEL: ${{ vars.LOG_LEVEL }}" - - - name: Build Docker image - run: | - echo "πŸ—οΈ Building Docker image..." - docker build -t ${{ env.DOCKER_IMAGE }}:staging . - docker tag ${{ env.DOCKER_IMAGE }}:staging ${{ env.DOCKER_IMAGE }}:staging-$(date +%Y%m%d-%H%M%S) - echo "βœ… Docker image built successfully" - - - name: Deploy Staging using Gitea Variables and Secrets - run: | - echo "πŸš€ Deploying Staging using Gitea Variables and Secrets..." - - echo "πŸ“ Using Gitea Variables and Secrets:" - echo " - NODE_ENV: staging" - echo " - LOG_LEVEL: ${LOG_LEVEL}" - 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 FROM GITEA SECRET]" - echo " - MY_INFO_PASSWORD: [SET FROM GITEA SECRET]" - echo " - ADMIN_BASIC_AUTH: [SET FROM GITEA SECRET]" - echo " - N8N_WEBHOOK_URL: ${N8N_WEBHOOK_URL}" - - # Stop old staging containers - echo "πŸ›‘ Stopping old staging containers..." - docker compose -f docker-compose.staging.yml down || true - - # Clean up orphaned containers - echo "🧹 Cleaning up orphaned containers..." - docker compose -f docker-compose.staging.yml down --remove-orphans || true - - # Start new staging containers - echo "πŸš€ Starting new staging containers..." - docker compose -f docker-compose.staging.yml up -d - - # Wait a moment for containers to start - echo "⏳ Waiting for containers to start..." - sleep 10 - - # Check container logs for debugging - echo "πŸ“‹ Container logs (first 20 lines):" - docker compose -f docker-compose.staging.yml logs --tail=20 - - echo "βœ… Staging deployment completed!" - env: - NODE_ENV: staging - 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 }} - N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL }} - N8N_API_KEY: ${{ secrets.N8N_API_KEY }} - - - name: Wait for containers to be ready - run: | - echo "⏳ Waiting for containers to be ready..." - sleep 45 - - # Check if all containers are running - echo "πŸ“Š Checking container status..." - docker compose -f docker-compose.staging.yml ps - - # Wait for application container to be healthy - echo "πŸ₯ Waiting for application container to be healthy..." - for i in {1..60}; do - if docker exec portfolio-app-staging 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/60)" - sleep 5 - done - - # Additional wait for main page to be accessible - echo "🌐 Waiting for main page to be accessible..." - for i in {1..30}; do - if curl -f http://localhost:3001/ > /dev/null 2>&1; then - echo "βœ… Main page is accessible!" - break - fi - echo "⏳ Waiting for main page... ($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.staging.yml ps - - # Check application container - echo "πŸ₯ Checking application container..." - if docker exec portfolio-app-staging curl -f http://localhost:3000/api/health; then - echo "βœ… Application health check passed!" - else - echo "❌ Application health check failed!" - docker logs portfolio-app-staging --tail=50 - exit 1 - fi - - # Check main page - if curl -f http://localhost:3001/ > /dev/null; then - echo "βœ… Main page is accessible!" - else - echo "❌ Main page is not accessible!" - exit 1 - fi - - echo "βœ… All health checks passed! Staging 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-with-gitea-vars.yml b/.gitea/workflows/ci-cd-with-gitea-vars.yml.disabled similarity index 100% rename from .gitea/workflows/ci-cd-with-gitea-vars.yml rename to .gitea/workflows/ci-cd-with-gitea-vars.yml.disabled diff --git a/.gitea/workflows/ci-cd-woodpecker.yml b/.gitea/workflows/ci-cd-woodpecker.yml deleted file mode 100644 index f4cd42a..0000000 --- a/.gitea/workflows/ci-cd-woodpecker.yml +++ /dev/null @@ -1,232 +0,0 @@ -name: CI/CD Pipeline (Woodpecker) - -when: - event: push - branch: production - -steps: - build: - image: node:20-alpine - commands: - - echo "πŸš€ Starting CI/CD Pipeline" - - echo "πŸ“‹ Step 1: Installing dependencies..." - - npm ci --prefer-offline --no-audit - - echo "πŸ” Step 2: Running linting..." - - npm run lint - - echo "πŸ§ͺ Step 3: Running tests..." - - npm run test - - echo "πŸ—οΈ Step 4: Building application..." - - npm run build - - echo "πŸ”’ Step 5: Running security scan..." - - npm audit --audit-level=high || echo "⚠️ Some vulnerabilities found, but continuing..." - volumes: - - node_modules:/app/node_modules - - docker-build: - image: docker:latest - commands: - - echo "🐳 Building Docker image..." - - docker build -t portfolio-app:latest . - - docker tag portfolio-app:latest portfolio-app:$(date +%Y%m%d-%H%M%S) - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - deploy: - image: docker:latest - commands: - - echo "πŸš€ Deploying application..." - - # Verify secrets and variables - - echo "πŸ” Verifying secrets and variables..." - - | - if [ -z "$NEXT_PUBLIC_BASE_URL" ]; then - echo "❌ NEXT_PUBLIC_BASE_URL variable is missing!" - exit 1 - fi - if [ -z "$MY_EMAIL" ]; then - echo "❌ MY_EMAIL variable is missing!" - exit 1 - fi - if [ -z "$MY_INFO_EMAIL" ]; then - echo "❌ MY_INFO_EMAIL variable is missing!" - exit 1 - fi - if [ -z "$MY_PASSWORD" ]; then - echo "❌ MY_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "$MY_INFO_PASSWORD" ]; then - echo "❌ MY_INFO_PASSWORD secret is missing!" - exit 1 - fi - if [ -z "$ADMIN_BASIC_AUTH" ]; then - echo "❌ ADMIN_BASIC_AUTH secret is missing!" - exit 1 - fi - echo "βœ… All required secrets and variables are present" - - # 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 - - sleep 10 - - # Deploy 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..." - 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 - docker run -d \ - --name $TEMP_CONTAINER_NAME \ - --restart unless-stopped \ - --network portfolio_net \ - -e NODE_ENV=$NODE_ENV \ - -e LOG_LEVEL=$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="$NEXT_PUBLIC_BASE_URL" \ - -e NEXT_PUBLIC_UMAMI_URL="$NEXT_PUBLIC_UMAMI_URL" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ - -e MY_EMAIL="$MY_EMAIL" \ - -e MY_INFO_EMAIL="$MY_INFO_EMAIL" \ - -e MY_PASSWORD="$MY_PASSWORD" \ - -e MY_INFO_PASSWORD="$MY_INFO_PASSWORD" \ - -e ADMIN_BASIC_AUTH="$ADMIN_BASIC_AUTH" \ - portfolio-app:latest - - # Wait for new container to be ready - echo "⏳ Waiting for new container to be ready..." - sleep 15 - - # Health check new container - 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 - 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=$NODE_ENV \ - -e LOG_LEVEL=$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="$NEXT_PUBLIC_BASE_URL" \ - -e NEXT_PUBLIC_UMAMI_URL="$NEXT_PUBLIC_UMAMI_URL" \ - -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ - -e MY_EMAIL="$MY_EMAIL" \ - -e MY_INFO_EMAIL="$MY_INFO_EMAIL" \ - -e MY_PASSWORD="$MY_PASSWORD" \ - -e MY_INFO_PASSWORD="$MY_INFO_PASSWORD" \ - -e ADMIN_BASIC_AUTH="$ADMIN_BASIC_AUTH" \ - portfolio-app:latest - - echo "βœ… Rolling update completed!" - else - echo "πŸ†• Fresh deployment..." - docker compose up -d - fi - - # Wait for container to be ready - - echo "⏳ Waiting for container to be ready..." - - sleep 15 - - # Health check - - | - echo "πŸ₯ Performing health check..." - for i in {1..40}; do - if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then - echo "βœ… Application is healthy!" - break - fi - echo "⏳ Health check attempt $i/40..." - sleep 3 - done - - # Final verification - - echo "πŸ” Final health verification..." - - docker ps --filter "name=portfolio-app" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - - | - if curl -f http://localhost:3000/api/health; then - echo "βœ… Health endpoint accessible" - else - echo "❌ Health endpoint not accessible" - exit 1 - fi - - | - 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!" - - # Cleanup - - docker image prune -f - - docker system prune -f - volumes: - - /var/run/docker.sock:/var/run/docker.sock - environment: - - NODE_ENV - - LOG_LEVEL - - NEXT_PUBLIC_BASE_URL - - NEXT_PUBLIC_UMAMI_URL - - NEXT_PUBLIC_UMAMI_WEBSITE_ID - - MY_EMAIL - - MY_INFO_EMAIL - - MY_PASSWORD - - MY_INFO_PASSWORD - - ADMIN_BASIC_AUTH - -volumes: - node_modules: diff --git a/.gitea/workflows/debug-secrets.yml b/.gitea/workflows/debug-secrets.yml deleted file mode 100644 index 7825c7a..0000000 --- a/.gitea/workflows/debug-secrets.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Debug Secrets - -on: - workflow_dispatch: - push: - branches: [ main ] - -jobs: - debug-secrets: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Debug Environment Variables - run: | - echo "πŸ” Checking if secrets are available..." - echo "" - - echo "πŸ“Š VARIABLES:" - echo "βœ… NODE_ENV: ${{ vars.NODE_ENV }}" - echo "βœ… LOG_LEVEL: ${{ vars.LOG_LEVEL }}" - echo "βœ… NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }}" - echo "βœ… NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }}" - echo "βœ… NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" - echo "βœ… MY_EMAIL: ${{ vars.MY_EMAIL }}" - echo "βœ… MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }}" - - echo "" - echo "πŸ” SECRETS:" - if [ -n "${{ secrets.MY_PASSWORD }}" ]; then - echo "βœ… MY_PASSWORD: Set (length: ${#MY_PASSWORD})" - else - echo "❌ MY_PASSWORD: Not set" - fi - - if [ -n "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "βœ… MY_INFO_PASSWORD: Set (length: ${#MY_INFO_PASSWORD})" - else - echo "❌ MY_INFO_PASSWORD: Not set" - fi - - if [ -n "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "βœ… ADMIN_BASIC_AUTH: Set (length: ${#ADMIN_BASIC_AUTH})" - else - echo "❌ ADMIN_BASIC_AUTH: Not set" - fi - - echo "" - echo "πŸ“‹ Summary:" - echo "Variables: 7 configured" - echo "Secrets: 3 configured" - echo "Total environment variables: 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: Test Docker Environment - run: | - echo "🐳 Testing Docker environment with secrets..." - - # Create a test container to verify environment variables - docker run --rm \ - -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="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \ - -e MY_EMAIL="${{ secrets.MY_EMAIL }}" \ - -e MY_INFO_EMAIL="${{ secrets.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 }}" \ - alpine:latest sh -c ' - echo "Environment variables in container:" - echo "NODE_ENV: $NODE_ENV" - echo "DATABASE_URL: $DATABASE_URL" - echo "REDIS_URL: $REDIS_URL" - echo "NEXT_PUBLIC_BASE_URL: $NEXT_PUBLIC_BASE_URL" - echo "MY_EMAIL: $MY_EMAIL" - echo "MY_INFO_EMAIL: $MY_INFO_EMAIL" - echo "MY_PASSWORD: [HIDDEN - length: ${#MY_PASSWORD}]" - echo "MY_INFO_PASSWORD: [HIDDEN - length: ${#MY_INFO_PASSWORD}]" - echo "ADMIN_BASIC_AUTH: [HIDDEN - length: ${#ADMIN_BASIC_AUTH}]" - ' - - - name: Validate Secret Formats - run: | - echo "πŸ” Validating secret formats..." - - # Check NEXT_PUBLIC_BASE_URL format - if [[ "${{ secrets.NEXT_PUBLIC_BASE_URL }}" =~ ^https?:// ]]; then - echo "βœ… NEXT_PUBLIC_BASE_URL: Valid URL format" - else - echo "❌ NEXT_PUBLIC_BASE_URL: Invalid URL format (should start with http:// or https://)" - fi - - # Check email formats - if [[ "${{ secrets.MY_EMAIL }}" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - echo "βœ… MY_EMAIL: Valid email format" - else - echo "❌ MY_EMAIL: Invalid email format" - fi - - if [[ "${{ secrets.MY_INFO_EMAIL }}" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - echo "βœ… MY_INFO_EMAIL: Valid email format" - else - echo "❌ MY_INFO_EMAIL: Invalid email format" - fi - - # Check ADMIN_BASIC_AUTH format (should be username:password) - if [[ "${{ secrets.ADMIN_BASIC_AUTH }}" =~ ^[^:]+:.+$ ]]; then - echo "βœ… ADMIN_BASIC_AUTH: Valid format (username:password)" - else - echo "❌ ADMIN_BASIC_AUTH: Invalid format (should be username:password)" - fi \ No newline at end of file diff --git a/.gitea/workflows/dev-deploy.yml b/.gitea/workflows/dev-deploy.yml new file mode 100644 index 0000000..39563a4 --- /dev/null +++ b/.gitea/workflows/dev-deploy.yml @@ -0,0 +1,126 @@ +name: Dev Deployment (Zero Downtime) + +on: + push: + branches: [ dev ] + +env: + NODE_VERSION: '20' + DOCKER_IMAGE: portfolio-app + IMAGE_TAG: staging + +jobs: + deploy-dev: + 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: Build Docker image + run: | + echo "πŸ—οΈ Building dev Docker image..." + docker build -t ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} . + echo "βœ… Docker image built successfully" + + - name: Zero-Downtime Dev Deployment + run: | + echo "πŸš€ Starting zero-downtime dev deployment..." + + COMPOSE_FILE="docker-compose.staging.yml" + CONTAINER_NAME="portfolio-app-staging" + HEALTH_PORT="3002" + + # Backup current container ID if running + OLD_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME || echo "") + + # Start new container with updated image + echo "πŸ†• Starting new dev container..." + docker compose -f $COMPOSE_FILE up -d --no-deps --build portfolio-staging + + # Wait for new container to be healthy + echo "⏳ Waiting for new container to be healthy..." + for i in {1..60}; do + NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) + if [ ! -z "$NEW_CONTAINER" ]; then + # Check health status + HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") + if [ "$HEALTH" == "healthy" ]; then + echo "βœ… New container is healthy!" + break + fi + # Also check HTTP health endpoint + if curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then + echo "βœ… New container is responding!" + break + fi + fi + echo "⏳ Waiting... ($i/60)" + sleep 2 + done + + # Verify new container is working + if ! curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then + echo "⚠️ New dev container health check failed, but continuing (non-blocking)..." + docker compose -f $COMPOSE_FILE logs --tail=50 portfolio-staging + fi + + # Remove old container if it exists and is different + if [ ! -z "$OLD_CONTAINER" ]; then + NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) + if [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then + echo "🧹 Removing old container..." + docker stop $OLD_CONTAINER 2>/dev/null || true + docker rm $OLD_CONTAINER 2>/dev/null || true + fi + fi + + echo "βœ… Dev deployment completed!" + env: + NODE_ENV: staging + LOG_LEVEL: ${{ vars.LOG_LEVEL || 'debug' }} + NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL || 'https://dev.dk0.dev' }} + 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 }} + N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} + N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} + + - name: Dev Health Check + run: | + echo "πŸ” Running dev health checks..." + for i in {1..20}; do + if curl -f http://localhost:3002/api/health && curl -f http://localhost:3002/ > /dev/null; then + echo "βœ… Dev is fully operational!" + exit 0 + fi + echo "⏳ Waiting for dev... ($i/20)" + sleep 3 + done + echo "⚠️ Dev health check failed, but continuing (non-blocking)..." + docker compose -f docker-compose.staging.yml logs --tail=50 + + - name: Cleanup + run: | + echo "🧹 Cleaning up old images..." + docker image prune -f + echo "βœ… Cleanup completed" diff --git a/.gitea/workflows/production-deploy.yml b/.gitea/workflows/production-deploy.yml new file mode 100644 index 0000000..9cdbc9d --- /dev/null +++ b/.gitea/workflows/production-deploy.yml @@ -0,0 +1,129 @@ +name: Production Deployment (Zero Downtime) + +on: + push: + branches: [ production ] + +env: + NODE_VERSION: '20' + DOCKER_IMAGE: portfolio-app + IMAGE_TAG: production + +jobs: + deploy-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:production + + - name: Build application + run: npm run build + + - name: Build Docker image + run: | + echo "πŸ—οΈ Building production Docker image..." + docker build -t ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} . + docker tag ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} ${{ env.DOCKER_IMAGE }}:latest + echo "βœ… Docker image built successfully" + + - name: Zero-Downtime Production Deployment + run: | + echo "πŸš€ Starting zero-downtime production deployment..." + + COMPOSE_FILE="docker-compose.production.yml" + CONTAINER_NAME="portfolio-app" + HEALTH_PORT="3000" + + # Backup current container ID if running + OLD_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME || echo "") + + # Start new container with updated image (docker-compose will handle this) + echo "πŸ†• Starting new production container..." + docker compose -f $COMPOSE_FILE up -d --no-deps --build portfolio + + # Wait for new container to be healthy + echo "⏳ Waiting for new container to be healthy..." + for i in {1..60}; do + NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) + if [ ! -z "$NEW_CONTAINER" ]; then + # Check health status + HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") + if [ "$HEALTH" == "healthy" ]; then + echo "βœ… New container is healthy!" + break + fi + # Also check HTTP health endpoint + if curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then + echo "βœ… New container is responding!" + break + fi + fi + echo "⏳ Waiting... ($i/60)" + sleep 2 + done + + # Verify new container is working + if ! curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then + echo "❌ New container failed health check!" + docker compose -f $COMPOSE_FILE logs --tail=50 portfolio + exit 1 + fi + + # Remove old container if it exists and is different + if [ ! -z "$OLD_CONTAINER" ]; then + NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) + if [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then + echo "🧹 Removing old container..." + docker stop $OLD_CONTAINER 2>/dev/null || true + docker rm $OLD_CONTAINER 2>/dev/null || true + fi + fi + + echo "βœ… Production deployment completed with zero downtime!" + env: + NODE_ENV: production + LOG_LEVEL: ${{ vars.LOG_LEVEL || 'info' }} + NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL || 'https://dk0.dev' }} + 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 }} + N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} + N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} + + - name: Production Health Check + run: | + echo "πŸ” Running production health checks..." + for i in {1..20}; do + if curl -f http://localhost:3000/api/health && curl -f http://localhost:3000/ > /dev/null; then + echo "βœ… Production is fully operational!" + exit 0 + fi + echo "⏳ Waiting for production... ($i/20)" + sleep 3 + done + echo "❌ Production health check failed!" + docker compose -f docker-compose.production.yml logs --tail=50 + exit 1 + + - name: Cleanup + run: | + echo "🧹 Cleaning up old images..." + docker image prune -f + echo "βœ… Cleanup completed" diff --git a/.gitea/workflows/staging-deploy.yml b/.gitea/workflows/staging-deploy.yml.disabled similarity index 100% rename from .gitea/workflows/staging-deploy.yml rename to .gitea/workflows/staging-deploy.yml.disabled diff --git a/.gitea/workflows/test-and-build.yml b/.gitea/workflows/test-and-build.yml deleted file mode 100644 index 8a1db70..0000000 --- a/.gitea/workflows/test-and-build.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Test and Build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -env: - NODE_VERSION: '20' - -jobs: - test-and-build: - 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' - 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..." \ No newline at end of file diff --git a/.gitea/workflows/test-gitea-variables.yml b/.gitea/workflows/test-gitea-variables.yml deleted file mode 100644 index 0f4ac08..0000000 --- a/.gitea/workflows/test-gitea-variables.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Test Gitea Variables and Secrets - -on: - push: - branches: [ production ] - workflow_dispatch: - -jobs: - test-variables: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Test Variables and Secrets Access - run: | - echo "πŸ” Testing Gitea Variables and Secrets access..." - - # Test Variables - echo "πŸ“ Testing Variables:" - echo "NEXT_PUBLIC_BASE_URL: '${{ vars.NEXT_PUBLIC_BASE_URL }}'" - echo "MY_EMAIL: '${{ vars.MY_EMAIL }}'" - echo "MY_INFO_EMAIL: '${{ vars.MY_INFO_EMAIL }}'" - echo "NODE_ENV: '${{ vars.NODE_ENV }}'" - echo "LOG_LEVEL: '${{ vars.LOG_LEVEL }}'" - echo "NEXT_PUBLIC_UMAMI_URL: '${{ vars.NEXT_PUBLIC_UMAMI_URL }}'" - echo "NEXT_PUBLIC_UMAMI_WEBSITE_ID: '${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}'" - - # Test Secrets (without revealing values) - echo "" - echo "πŸ” Testing Secrets:" - echo "MY_PASSWORD: '$([ -n "${{ secrets.MY_PASSWORD }}" ] && echo "[SET]" || echo "[NOT SET]")'" - echo "MY_INFO_PASSWORD: '$([ -n "${{ secrets.MY_INFO_PASSWORD }}" ] && echo "[SET]" || echo "[NOT SET]")'" - echo "ADMIN_BASIC_AUTH: '$([ -n "${{ secrets.ADMIN_BASIC_AUTH }}" ] && echo "[SET]" || echo "[NOT SET]")'" - - # Check if variables are empty - echo "" - echo "πŸ” Checking for empty variables:" - if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then - echo "❌ NEXT_PUBLIC_BASE_URL is empty or not set" - else - echo "βœ… NEXT_PUBLIC_BASE_URL is set" - fi - - if [ -z "${{ vars.MY_EMAIL }}" ]; then - echo "❌ MY_EMAIL is empty or not set" - else - echo "βœ… MY_EMAIL is set" - fi - - if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then - echo "❌ MY_INFO_EMAIL is empty or not set" - else - echo "βœ… MY_INFO_EMAIL is set" - fi - - # Check secrets - if [ -z "${{ secrets.MY_PASSWORD }}" ]; then - echo "❌ MY_PASSWORD secret is empty or not set" - else - echo "βœ… MY_PASSWORD secret is set" - fi - - if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then - echo "❌ MY_INFO_PASSWORD secret is empty or not set" - else - echo "βœ… MY_INFO_PASSWORD secret is set" - fi - - if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then - echo "❌ ADMIN_BASIC_AUTH secret is empty or not set" - else - echo "βœ… ADMIN_BASIC_AUTH secret is set" - fi - - echo "" - echo "πŸ“Š Summary:" - echo "Variables set: $(echo '${{ vars.NEXT_PUBLIC_BASE_URL }}' | wc -c)" - echo "Secrets set: $(echo '${{ secrets.MY_PASSWORD }}' | wc -c)" - - - name: Test Environment Variable Export - run: | - echo "πŸ§ͺ Testing environment variable export..." - - # Export variables as 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 }}" - - echo "πŸ“ Exported environment variables:" - echo "NODE_ENV: ${NODE_ENV:-[NOT SET]}" - echo "LOG_LEVEL: ${LOG_LEVEL:-[NOT SET]}" - echo "NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL:-[NOT SET]}" - echo "MY_EMAIL: ${MY_EMAIL:-[NOT SET]}" - echo "MY_INFO_EMAIL: ${MY_INFO_EMAIL:-[NOT SET]}" - echo "MY_PASSWORD: $([ -n "${MY_PASSWORD}" ] && echo "[SET]" || echo "[NOT SET]")" - echo "MY_INFO_PASSWORD: $([ -n "${MY_INFO_PASSWORD}" ] && echo "[SET]" || echo "[NOT SET]")" - echo "ADMIN_BASIC_AUTH: $([ -n "${ADMIN_BASIC_AUTH}" ] && echo "[SET]" || echo "[NOT SET]")" diff --git a/AUTO_DEPLOYMENT_STATUS.md b/AUTO_DEPLOYMENT_STATUS.md deleted file mode 100644 index 9b45c7a..0000000 --- a/AUTO_DEPLOYMENT_STATUS.md +++ /dev/null @@ -1,85 +0,0 @@ -# πŸš€ 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/CLEANUP_PLAN.md b/CLEANUP_PLAN.md deleted file mode 100644 index 61c34f5..0000000 --- a/CLEANUP_PLAN.md +++ /dev/null @@ -1,66 +0,0 @@ -# 🧹 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 deleted file mode 100644 index ef9d920..0000000 --- a/CLEANUP_SUMMARY.md +++ /dev/null @@ -1,95 +0,0 @@ -# 🧹 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/DEPLOYMENT_FIX.md b/DEPLOYMENT_FIX.md deleted file mode 100644 index ecc3bf4..0000000 --- a/DEPLOYMENT_FIX.md +++ /dev/null @@ -1,89 +0,0 @@ -# πŸ”§ Deployment Fixes Applied - -## Issues Fixed - -### 1. Port 3001 Already Allocated ❌ β†’ βœ… -**Problem**: Port 3001 was already in use, causing staging deployment to fail. - -**Fix**: -- Changed staging port from `3001` to `3002` -- Changed PostgreSQL staging port from `5433` to `5434` -- Changed Redis staging port from `6380` to `6381` - -### 2. Docker Compose Version Warning ❌ β†’ βœ… -**Problem**: `version: '3.8'` is obsolete in newer Docker Compose. - -**Fix**: Removed `version` line from `docker-compose.staging.yml` - -### 3. Missing N8N Environment Variables ❌ β†’ βœ… -**Problem**: `N8N_SECRET_TOKEN` warning appeared. - -**Fix**: Added `N8N_WEBHOOK_URL` and `N8N_SECRET_TOKEN` to staging compose file - -### 4. Wrong Compose File Used ❌ β†’ βœ… -**Problem**: Gitea workflow was using wrong compose file (stopping production containers). - -**Fix**: -- Updated `ci-cd-with-gitea-vars.yml` to detect branch and use correct compose file -- Created dedicated `staging-deploy.yml` workflow -- Staging now uses `docker-compose.staging.yml` -- Production uses `docker-compose.production.yml` - -## Updated Ports - -| Service | Staging | Production | -|---------|---------|------------| -| App | **3002** βœ… | **3000** | -| PostgreSQL | **5434** βœ… | **5432** | -| Redis | **6381** βœ… | **6379** | - -## How It Works Now - -### Staging (dev/main branch) -```bash -git push origin dev -# β†’ Uses docker-compose.staging.yml -# β†’ Deploys to port 3002 -# β†’ Does NOT touch production containers -``` - -### Production (production branch) -```bash -git push origin production -# β†’ Uses docker-compose.production.yml -# β†’ Deploys to port 3000 -# β†’ Zero-downtime deployment -# β†’ Does NOT touch staging containers -``` - -## Files Updated - -- βœ… `docker-compose.staging.yml` - Fixed ports, removed version, added N8N vars -- βœ… `.gitea/workflows/ci-cd-with-gitea-vars.yml` - Branch detection, correct compose files -- βœ… `.gitea/workflows/staging-deploy.yml` - New dedicated staging workflow -- βœ… `STAGING_SETUP.md` - Updated port references - -## Next Steps - -1. **Test staging deployment**: - ```bash - git push origin dev - # Should deploy to port 3002 without errors - ``` - -2. **Verify staging**: - ```bash - curl http://localhost:3002/api/health - ``` - -3. **When ready for production**: - ```bash - git checkout production - git merge main - git push origin production - # Deploys safely to port 3000 - ``` - ---- - -**All fixes applied!** Staging and production are now completely isolated. πŸš€ diff --git a/DEPLOYMENT_SETUP.md b/DEPLOYMENT_SETUP.md new file mode 100644 index 0000000..20636a2 --- /dev/null +++ b/DEPLOYMENT_SETUP.md @@ -0,0 +1,200 @@ +# πŸš€ Deployment Setup Guide + +## Overview + +This project uses a **dual-branch deployment strategy** with zero-downtime deployments: + +- **Production Branch** (`production`) β†’ Serves `https://dk0.dev` on port 3000 +- **Dev Branch** (`dev`) β†’ Serves `https://dev.dk0.dev` on port 3002 + +Both environments are completely isolated with separate: +- Docker containers +- Databases (PostgreSQL) +- Redis instances +- Networks +- Volumes + +## Branch Strategy + +### Production Branch +- **Branch**: `production` +- **Domain**: `https://dk0.dev` +- **Port**: `3000` +- **Container**: `portfolio-app` +- **Database**: `portfolio_db` (port 5432) +- **Redis**: `portfolio-redis` (port 6379) +- **Image Tag**: `portfolio-app:production` / `portfolio-app:latest` + +### Dev Branch +- **Branch**: `dev` +- **Domain**: `https://dev.dk0.dev` +- **Port**: `3002` +- **Container**: `portfolio-app-staging` +- **Database**: `portfolio_staging_db` (port 5434) +- **Redis**: `portfolio-redis-staging` (port 6381) +- **Image Tag**: `portfolio-app:staging` + +## Automatic Deployment + +### How It Works + +1. **Push to `production` branch**: + - Triggers `.gitea/workflows/production-deploy.yml` + - Runs tests, builds, and deploys to production + - Zero-downtime deployment (starts new container, waits for health, removes old) + +2. **Push to `dev` branch**: + - Triggers `.gitea/workflows/dev-deploy.yml` + - Runs tests, builds, and deploys to dev/staging + - Zero-downtime deployment + +### Zero-Downtime Process + +1. Build new Docker image +2. Start new container with updated image +3. Wait for new container to be healthy (health checks) +4. Verify HTTP endpoints respond correctly +5. Remove old container (if different) +6. Cleanup old images + +## Manual Deployment + +### Production +```bash +# Build and deploy production +docker build -t portfolio-app:latest . +docker compose -f docker-compose.production.yml up -d --build +``` + +### Dev/Staging +```bash +# Build and deploy dev +docker build -t portfolio-app:staging . +docker compose -f docker-compose.staging.yml up -d --build +``` + +## Environment Variables + +### Required Gitea Variables +- `NEXT_PUBLIC_BASE_URL` - Base URL for the application +- `MY_EMAIL` - Email address for contact +- `MY_INFO_EMAIL` - Info email address +- `LOG_LEVEL` - Logging level (info/debug) + +### Required Gitea Secrets +- `MY_PASSWORD` - Email password +- `MY_INFO_PASSWORD` - Info email password +- `ADMIN_BASIC_AUTH` - Admin basic auth credentials +- `N8N_SECRET_TOKEN` - Optional: n8n webhook secret + +### Optional Variables +- `N8N_WEBHOOK_URL` - n8n webhook URL for automation + +## Health Checks + +Both environments have health check endpoints: +- Production: `http://localhost:3000/api/health` +- Dev: `http://localhost:3002/api/health` + +## Monitoring + +### Check Container Status +```bash +# Production +docker compose -f docker-compose.production.yml ps + +# Dev +docker compose -f docker-compose.staging.yml ps +``` + +### View Logs +```bash +# Production +docker logs portfolio-app --tail=100 -f + +# Dev +docker logs portfolio-app-staging --tail=100 -f +``` + +### Health Check +```bash +# Production +curl http://localhost:3000/api/health + +# Dev +curl http://localhost:3002/api/health +``` + +## Troubleshooting + +### Container Won't Start +1. Check logs: `docker logs ` +2. Verify environment variables are set +3. Check database/redis connectivity +4. Verify ports aren't already in use + +### Deployment Fails +1. Check Gitea Actions logs +2. Verify all required secrets/variables are set +3. Check if old containers are blocking ports +4. Verify Docker image builds successfully + +### Zero-Downtime Issues +- Old container might still be running - check with `docker ps` +- Health checks might be failing - check container logs +- Port conflicts - verify ports 3000 and 3002 are available + +## Rollback + +If a deployment fails or causes issues: + +```bash +# Production rollback +docker compose -f docker-compose.production.yml down +docker tag portfolio-app:previous portfolio-app:latest +docker compose -f docker-compose.production.yml up -d + +# Dev rollback +docker compose -f docker-compose.staging.yml down +docker tag portfolio-app:staging-previous portfolio-app:staging +docker compose -f docker-compose.staging.yml up -d +``` + +## Best Practices + +1. **Always test on dev branch first** before pushing to production +2. **Monitor health checks** after deployment +3. **Keep old images** for quick rollback (last 3 versions) +4. **Use feature flags** for new features +5. **Document breaking changes** before deploying +6. **Run tests locally** before pushing + +## Network Configuration + +- **Production Network**: `portfolio_net` + `proxy` (external) +- **Dev Network**: `portfolio_staging_net` +- **Isolation**: Complete separation ensures no interference + +## Database Management + +### Production Database +- **Container**: `portfolio-postgres` +- **Port**: `5432` (internal only) +- **Database**: `portfolio_db` +- **User**: `portfolio_user` + +### Dev Database +- **Container**: `portfolio-postgres-staging` +- **Port**: `5434` (external), `5432` (internal) +- **Database**: `portfolio_staging_db` +- **User**: `portfolio_user` + +## Redis Configuration + +### Production Redis +- **Container**: `portfolio-redis` +- **Port**: `6379` (internal only) + +### Dev Redis +- **Container**: `portfolio-redis-staging` +- **Port**: `6381` (external), `6379` (internal) diff --git a/DEV_TESTING.md b/DEV_TESTING.md deleted file mode 100644 index 03d3bf6..0000000 --- a/DEV_TESTING.md +++ /dev/null @@ -1,236 +0,0 @@ -# πŸ§ͺ Dev Branch Testing Guide - -## Übersicht - -Dieses Dokument erklΓ€rt, wie du dein Portfolio-Projekt auf dem `dev` Branch testen kannst, bevor du es in Production deployst. - -## Voraussetzungen - -1. βœ… n8n lΓ€uft bereits auf `n8n.dk0.dev` -2. βœ… Gitea Repository ist eingerichtet -3. βœ… Docker und Docker Compose sind installiert - -## Setup fΓΌr lokales Testen mit n8n - -### 1. Environment Variables konfigurieren - -Erstelle eine `.env.local` Datei (oder aktualisiere deine bestehende `.env`): - -```bash -# n8n Integration -N8N_WEBHOOK_URL=https://n8n.dk0.dev -N8N_API_KEY=dein-n8n-api-key -N8N_SECRET_TOKEN=dein-n8n-secret-token - -# Application -NODE_ENV=development -NEXT_PUBLIC_BASE_URL=http://localhost:3000 - -# Database (wird automatisch von docker-compose.dev.minimal.yml gesetzt) -# DATABASE_URL=postgresql://portfolio_user:portfolio_dev_pass@localhost:5432/portfolio_dev?schema=public - -# Redis (wird automatisch von docker-compose.dev.minimal.yml gesetzt) -# REDIS_URL=redis://localhost:6379 - -# Email Configuration -MY_EMAIL=contact@dk0.dev -MY_INFO_EMAIL=info@dk0.dev -MY_PASSWORD=dein-email-passwort -MY_INFO_PASSWORD=dein-info-email-passwort - -# Analytics -NEXT_PUBLIC_UMAMI_URL=https://analytics.dk0.dev -NEXT_PUBLIC_UMAMI_WEBSITE_ID=b3665829-927a-4ada-b9bb-fcf24171061e - -# Security -ADMIN_BASIC_AUTH=admin:dein-sicheres-passwort -LOG_LEVEL=debug -``` - -### 2. Lokal testen - -```bash -# 1. Starte Datenbank und Redis -npm run dev:minimal - -# 2. In einem neuen Terminal: Starte die Next.js App -npm run dev - -# 3. Γ–ffne http://localhost:3000 -``` - -### 3. n8n Webhook testen - -Teste die Verbindung zu deinem n8n Server: - -```bash -# Teste den Status Endpoint -curl https://n8n.dk0.dev/webhook/denshooter-71242/status - -# Teste den Chat Endpoint (wenn konfiguriert) -curl -X POST https://n8n.dk0.dev/webhook/chat \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer DEIN_N8N_SECRET_TOKEN" \ - -d '{"message": "Hallo", "history": []}' -``` - -## Staging Deployment auf dem Server - -### 1. Gitea Variables und Secrets konfigurieren - -Gehe zu deinem Gitea Repository β†’ Settings β†’ Secrets/Variables und fΓΌge hinzu: - -**Variables:** -- `NEXT_PUBLIC_BASE_URL` = `https://staging.dk0.dev` (oder deine Staging URL) -- `MY_EMAIL` = `contact@dk0.dev` -- `MY_INFO_EMAIL` = `info@dk0.dev` -- `NEXT_PUBLIC_UMAMI_URL` = `https://analytics.dk0.dev` -- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` = `b3665829-927a-4ada-b9bb-fcf24171061e` -- `N8N_WEBHOOK_URL` = `https://n8n.dk0.dev` -- `LOG_LEVEL` = `debug` - -**Secrets:** -- `MY_PASSWORD` = Dein Email Passwort -- `MY_INFO_PASSWORD` = Dein Info Email Passwort -- `ADMIN_BASIC_AUTH` = `admin:dein-sicheres-passwort` -- `N8N_API_KEY` = Dein n8n API Key (optional) -- `N8N_SECRET_TOKEN` = Dein n8n Secret Token (optional) - -### 2. Push zum dev Branch - -```bash -# Stelle sicher, dass du auf dem dev Branch bist -git checkout dev - -# Committe deine Γ„nderungen -git add . -git commit -m "Test: Dev deployment" - -# Push zum dev Branch (triggert automatisch Staging Deployment) -git push origin dev -``` - -### 3. Deployment ΓΌberwachen - -Nach dem Push: -1. Gehe zu deinem Gitea Repository β†’ Actions -2. Überwache den Workflow `CI/CD Pipeline (Dev/Staging)` -3. Der Workflow wird: - - Tests ausfΓΌhren - - Docker Image bauen - - Staging Container auf Port 3001 deployen - -### 4. Staging testen - -```bash -# Auf deinem Server: PrΓΌfe Container Status -docker ps | grep staging - -# PrΓΌfe Health Endpoint -curl http://localhost:3001/api/health - -# PrΓΌfe n8n Status Endpoint -curl http://localhost:3001/api/n8n/status - -# Logs ansehen -docker logs portfolio-app-staging -f -``` - -### 5. Staging URL konfigurieren - -Falls du eine Subdomain fΓΌr Staging hast (z.B. `staging.dk0.dev`): -- Konfiguriere deinen Reverse Proxy (Nginx/Traefik) um auf Port 3001 zu zeigen -- Oder verwende direkt `http://dein-server-ip:3001` - -## Troubleshooting - -### Dev Container wird nicht erstellt - -1. **PrΓΌfe Gitea Workflow:** - - Gehe zu Repository β†’ Actions - - PrΓΌfe ob der Workflow `ci-cd-dev-staging.yml` existiert - - PrΓΌfe ob der Workflow auf `dev` Branch Push reagiert - -2. **PrΓΌfe Gitea Variables:** - - Stelle sicher, dass alle erforderlichen Variables und Secrets gesetzt sind - - PrΓΌfe die Workflow Logs fΓΌr fehlende Variablen - -3. **PrΓΌfe Docker:** - ```bash - # Auf deinem Server - docker ps -a - docker images | grep portfolio-app - ``` - -### n8n Verbindungsfehler - -1. **PrΓΌfe n8n URL:** - ```bash - # Teste ob n8n erreichbar ist - curl https://n8n.dk0.dev/webhook/denshooter-71242/status - ``` - -2. **PrΓΌfe Environment Variables:** - ```bash - # Im Container - docker exec portfolio-app-staging env | grep N8N - ``` - -3. **PrΓΌfe n8n Webhook Konfiguration:** - - Stelle sicher, dass der Webhook in n8n aktiviert ist - - PrΓΌfe ob der Webhook-Pfad korrekt ist (`/webhook/denshooter-71242/status`) - -### Datenbank Fehler - -```bash -# PrΓΌfe ob die Datenbank lΓ€uft -docker ps | grep postgres-staging - -# PrΓΌfe Datenbank Logs -docker logs portfolio-postgres-staging - -# PrΓΌfe Verbindung -docker exec portfolio-postgres-staging pg_isready -U portfolio_user -d portfolio_staging_db -``` - -## Workflow Übersicht - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Push to dev β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Run Tests β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Build Docker β”‚ -β”‚ Image (staging) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Deploy Staging β”‚ -β”‚ (Port 3001) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Health Check β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -## NΓ€chste Schritte - -1. βœ… Teste lokal mit `npm run dev` -2. βœ… Konfiguriere Gitea Variables und Secrets -3. βœ… Push zum `dev` Branch -4. βœ… Teste Staging auf Port 3001 -5. βœ… Wenn alles funktioniert: Merge zu `production` Branch - ---- - -**Tipp:** Verwende `LOG_LEVEL=debug` in Staging um mehr Informationen zu sehen! diff --git a/DOCKER_BUILD_FIX.md b/DOCKER_BUILD_FIX.md deleted file mode 100644 index a36528b..0000000 --- a/DOCKER_BUILD_FIX.md +++ /dev/null @@ -1,150 +0,0 @@ -# Docker Build Fix - Standalone Output Issue - -## Problem - -Der Docker Build schlΓ€gt fehl mit: -``` -ERROR: failed to calculate checksum of ref ... "/app/.next/standalone/app": not found -``` - -## Ursache - -Next.js erstellt das `standalone` Output nur, wenn: -1. `output: "standalone"` in `next.config.ts` gesetzt ist βœ… (bereits konfiguriert) -2. Der Build erfolgreich abgeschlossen wird -3. Alle AbhΓ€ngigkeiten korrekt aufgelΓΆst werden - -## LΓΆsung - -### 1. n8n Status Route Fix - -Die Route wurde angepasst, um wΓ€hrend des Builds nicht zu fehlschlagen, wenn `N8N_WEBHOOK_URL` nicht gesetzt ist: - -```typescript -// PrΓΌft jetzt, ob N8N_WEBHOOK_URL gesetzt ist -if (!n8nWebhookUrl) { - return NextResponse.json({ /* fallback */ }); -} -``` - -### 2. Dockerfile Verbesserungen - -- **Verification Step**: PrΓΌft, ob das standalone Verzeichnis existiert -- **Debug Output**: Zeigt die Verzeichnisstruktur, falls Probleme auftreten -- **Robustere Fehlerbehandlung**: Bessere Fehlermeldungen - -### 3. MΓΆgliche Ursachen und LΓΆsungen - -#### Problem: Standalone Output wird nicht erstellt - -**LΓΆsung 1: PrΓΌfe next.config.ts** -```typescript -// Stelle sicher, dass dies gesetzt ist: -output: "standalone", -outputFileTracingRoot: path.join(process.cwd()), -``` - -**LΓΆsung 2: PrΓΌfe Build-Logs** -```bash -# Schaue in die Build-Logs, ob es Fehler gibt -docker build . 2>&1 | grep -i "standalone\|error" -``` - -**LΓΆsung 3: Lokaler Test** -```bash -# Teste lokal, ob standalone erstellt wird -npm run build -ls -la .next/standalone/ -``` - -#### Problem: Falsche Verzeichnisstruktur - -**βœ… GELΓ–ST**: Die Debug-Ausgabe zeigt, dass Next.js 15 die Struktur `.next/standalone/` direkt verwendet: -- `.next/standalone/server.js` βœ… -- `.next/standalone/.next/` βœ… -- `.next/standalone/node_modules/` βœ… -- `.next/standalone/package.json` βœ… - -**NICHT**: `.next/standalone/app/server.js` ❌ - -Das Dockerfile wurde korrigiert, um `.next/standalone/` direkt zu kopieren. - -## Debugging - -### 1. Lokaler Build Test - -```bash -# Baue lokal -npm run build - -# PrΓΌfe ob standalone existiert -test -d .next/standalone && echo "βœ… Standalone exists" || echo "❌ Standalone missing" - -# Zeige Struktur -ls -la .next/standalone/ -find .next/standalone -name "server.js" -``` - -### 2. Docker Build mit Debug - -```bash -# Baue mit mehr Output -docker build --progress=plain -t portfolio-app:test . - -# Oder baue nur bis zum Builder Stage -docker build --target builder -t portfolio-builder:test . -docker run --rm portfolio-builder:test ls -la .next/standalone/ -``` - -### 3. PrΓΌfe Build-Logs - -Der aktualisierte Dockerfile gibt jetzt Debug-Informationen aus: -- Zeigt `.next/` Verzeichnisstruktur -- Sucht nach `standalone` Verzeichnis -- Zeigt `server.js` Location - -## Alternative: Fallback ohne Standalone - -Falls das standalone Output weiterhin Probleme macht, kann man auf ein vollstΓ€ndiges Image zurΓΌckgreifen: - -```dockerfile -# Statt standalone zu kopieren, kopiere alles -COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder --chown=nextjs:nodejs /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json -COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma - -CMD ["npm", "start"] -``` - -**Nachteil**: Grâßeres Image, aber funktioniert immer. - -## NΓ€chste Schritte - -1. βœ… n8n Status Route Fix (bereits gemacht) -2. βœ… Dockerfile Debug-Verbesserungen (bereits gemacht) -3. πŸ”„ Push zum dev Branch und Build testen -4. πŸ“Š Build-Logs analysieren -5. πŸ”§ Falls nΓΆtig: Dockerfile weiter anpassen - -## Workflow Test - -```bash -# 1. Committe Γ„nderungen -git add . -git commit -m "Fix: Docker build standalone output issue" - -# 2. Push zum dev Branch -git push origin dev - -# 3. Überwache Gitea Actions -# Gehe zu Repository β†’ Actions β†’ CI/CD Pipeline (Dev/Staging) - -# 4. PrΓΌfe Build-Logs -# Schaue nach den Debug-Ausgaben im Build-Step -``` - ---- - -**Hinweis**: Falls das Problem weiterhin besteht, schaue in die Build-Logs nach den Debug-Ausgaben, die der aktualisierte Dockerfile jetzt ausgibt. Diese zeigen genau, wo das Problem liegt. diff --git a/GIT_CONNECTION_FIX.md b/GIT_CONNECTION_FIX.md deleted file mode 100644 index 39c7e2f..0000000 --- a/GIT_CONNECTION_FIX.md +++ /dev/null @@ -1,53 +0,0 @@ -# πŸ”§ 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/app/editor/page.tsx b/app/editor/page.tsx index 9455555..4f1ed8b 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -251,19 +251,13 @@ function EditorPageContent() { }; // Markdown components for react-markdown with security - const markdownComponents = { - a: ({ - node: _node, - ...props - }: { - node?: unknown; - href?: string; - children?: React.ReactNode; - }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const markdownComponents: any = { + 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:"); + href && typeof href === 'string' && !href.startsWith("javascript:") && !href.startsWith("data:"); return ( ); }, - img: ({ - node: _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 src = props.src; const isSafe = - src && !src.startsWith("javascript:") && !src.startsWith("data:"); + src && typeof src === 'string' && !src.startsWith("javascript:") && !src.startsWith("data:"); // eslint-disable-next-line @next/next/no-img-element return isSafe ? {props.alt : null; }, diff --git a/components/GhostEditor.tsx b/components/GhostEditor.tsx deleted file mode 100644 index 4ee96fe..0000000 --- a/components/GhostEditor.tsx +++ /dev/null @@ -1,733 +0,0 @@ -'use client'; - -import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { - Save, - X, - Eye, - Settings, - Globe, - Github, - Image as ImageIcon, - Bold, - Italic, - List, - Quote, - Code, - Link2, - ListOrdered, - Underline, - Strikethrough, - Type, - Columns -} from 'lucide-react'; - -interface Project { - id: string; - title: string; - description: string; - content?: string; - category: string; - difficulty?: string; - tags?: string[]; - featured: boolean; - published: boolean; - github?: string; - live?: string; - image?: string; - createdAt: string; - updatedAt: string; -} - -interface GhostEditorProps { - isOpen: boolean; - onClose: () => void; - project?: Project | null; - onSave: (projectData: Partial) => void; - isCreating: boolean; -} - -export const GhostEditor: React.FC = ({ - isOpen, - onClose, - project, - onSave, - isCreating -}) => { - const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); - const [content, setContent] = useState(''); - const [category, setCategory] = useState('Web Development'); - const [tags, setTags] = useState([]); - const [github, setGithub] = useState(''); - const [live, setLive] = useState(''); - const [featured, setFeatured] = useState(false); - const [published, setPublished] = useState(false); - const [difficulty, setDifficulty] = useState('Intermediate'); - - // Editor UI state - const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'split'>('split'); - const [showSettings, setShowSettings] = useState(false); - const [wordCount, setWordCount] = useState(0); - const [readingTime, setReadingTime] = useState(0); - - const titleRef = useRef(null); - const contentRef = useRef(null); - const previewRef = useRef(null); - - const categories = ['Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design']; - const difficulties = ['Beginner', 'Intermediate', 'Advanced', 'Expert']; - - useEffect(() => { - if (project && !isCreating) { - setTitle(project.title); - setDescription(project.description); - setContent(project.content || ''); - setCategory(project.category); - setTags(project.tags || []); - setGithub(project.github || ''); - setLive(project.live || ''); - setFeatured(project.featured); - setPublished(project.published); - setDifficulty(project.difficulty || 'Intermediate'); - } else { - // Reset for new project - setTitle(''); - setDescription(''); - setContent(''); - setCategory('Web Development'); - setTags([]); - setGithub(''); - setLive(''); - setFeatured(false); - setPublished(false); - setDifficulty('Intermediate'); - } - }, [project, isCreating, isOpen]); - - // Calculate word count and reading time - useEffect(() => { - const words = content.trim().split(/\s+/).filter(word => word.length > 0).length; - setWordCount(words); - setReadingTime(Math.ceil(words / 200)); // Average reading speed: 200 words/minute - }, [content]); - - const handleSave = () => { - const projectData = { - title, - description, - content, - category, - tags, - github, - live, - featured, - published, - difficulty - }; - onSave(projectData); - }; - - const addTag = (tag: string) => { - if (tag.trim() && !tags.includes(tag.trim())) { - setTags([...tags, tag.trim()]); - } - }; - - const removeTag = (tagToRemove: string) => { - setTags(tags.filter(tag => tag !== tagToRemove)); - }; - - const insertMarkdown = useCallback((syntax: string, selectedText: string = '') => { - if (!contentRef.current) return; - - const textarea = contentRef.current; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const selection = selectedText || content.substring(start, end); - - let newText = ''; - let cursorOffset = 0; - - switch (syntax) { - case 'bold': - newText = `**${selection || 'bold text'}**`; - cursorOffset = selection ? newText.length : 2; - break; - case 'italic': - newText = `*${selection || 'italic text'}*`; - cursorOffset = selection ? newText.length : 1; - break; - case 'underline': - newText = `${selection || 'underlined text'}`; - cursorOffset = selection ? newText.length : 3; - break; - case 'strikethrough': - newText = `~~${selection || 'strikethrough text'}~~`; - cursorOffset = selection ? newText.length : 2; - break; - case 'heading1': - newText = `# ${selection || 'Heading 1'}`; - cursorOffset = selection ? newText.length : 2; - break; - case 'heading2': - newText = `## ${selection || 'Heading 2'}`; - cursorOffset = selection ? newText.length : 3; - break; - case 'heading3': - newText = `### ${selection || 'Heading 3'}`; - cursorOffset = selection ? newText.length : 4; - break; - case 'list': - newText = `- ${selection || 'List item'}`; - cursorOffset = selection ? newText.length : 2; - break; - case 'list-ordered': - newText = `1. ${selection || 'List item'}`; - cursorOffset = selection ? newText.length : 3; - break; - case 'quote': - newText = `> ${selection || 'Quote'}`; - cursorOffset = selection ? newText.length : 2; - break; - case 'code': - if (selection.includes('\n')) { - newText = `\`\`\`\n${selection || 'code block'}\n\`\`\``; - cursorOffset = selection ? newText.length : 4; - } else { - newText = `\`${selection || 'code'}\``; - cursorOffset = selection ? newText.length : 1; - } - break; - case 'link': - newText = `[${selection || 'link text'}](url)`; - cursorOffset = selection ? newText.length - 4 : newText.length - 4; - break; - case 'image': - newText = `![${selection || 'alt text'}](image-url)`; - cursorOffset = selection ? newText.length - 11 : newText.length - 11; - break; - case 'divider': - newText = '\n---\n'; - cursorOffset = newText.length; - break; - default: - return; - } - - const newContent = content.substring(0, start) + newText + content.substring(end); - setContent(newContent); - - // Focus and set cursor position - setTimeout(() => { - textarea.focus(); - const newPosition = start + cursorOffset; - textarea.setSelectionRange(newPosition, newPosition); - }, 0); - }, [content]); - - const autoResizeTextarea = (element: HTMLTextAreaElement) => { - element.style.height = 'auto'; - element.style.height = element.scrollHeight + 'px'; - }; - - // Render markdown preview - const renderMarkdownPreview = (markdown: string) => { - // Simple markdown renderer for preview - const html = markdown - // Headers - .replace(/^### (.*$)/gim, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^# (.*$)/gim, '

$1

') - // Bold and Italic - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - // Underline and Strikethrough - .replace(/(.*?)<\/u>/g, '$1') - .replace(/~~(.*?)~~/g, '$1') - // Code - .replace(/```([^`]+)```/g, '
$1
') - .replace(/`([^`]+)`/g, '$1') - // Lists - .replace(/^\- (.*$)/gim, '
  • β€’ $1
  • ') - .replace(/^\d+\. (.*$)/gim, '
  • $1
  • ') - // Links - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '
    $1') - // Images - .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') - // Quotes - .replace(/^> (.*$)/gim, '
    $1
    ') - // Dividers - .replace(/^---$/gim, '
    ') - // Paragraphs - .replace(/\n\n/g, '

    ') - .replace(/\n/g, '
    '); - - return `

    ${html}

    `; - }; - - if (!isOpen) return null; - - return ( - - - {/* Professional Ghost Editor */} -
    - {/* Top Navigation Bar */} -
    -
    - - -
    -
    - - {isCreating ? 'New Project' : 'Editing Project'} - -
    - -
    - {published ? ( - - Published - - ) : ( - - Draft - - )} - {featured && ( - - Featured - - )} -
    -
    - - {/* View Mode Toggle */} -
    -
    - - - -
    - - - - -
    -
    - - {/* Rich Text Toolbar */} -
    -
    - {/* Text Formatting */} -
    - - - - -
    - - {/* Headers */} -
    - - - -
    - - {/* Lists */} -
    - - -
    - - {/* Insert Elements */} -
    - - - - -
    -
    - - {/* Stats */} -
    - {wordCount} words - {readingTime} min read -
    -
    - - {/* Main Editor Area */} -
    - {/* Content Area */} -
    - {/* Editor Pane */} - {(viewMode === 'edit' || viewMode === 'split') && ( -
    - {/* Title & Description */} -
    -