diff --git a/DATABASE_MANAGEMENT.md b/DATABASE_MANAGEMENT.md new file mode 100644 index 0000000..0aada15 --- /dev/null +++ b/DATABASE_MANAGEMENT.md @@ -0,0 +1,103 @@ +# ChefBible Database Management + +Easy backup and restore for your recipes using the `db-manager.sh` script. + +## Quick Start + +### 1. Setup (One-time) +```bash +# Create the data directory on your server +sudo mkdir -p /home/chefbible/data +sudo chown 1001:1001 /home/chefbible/data +sudo chmod 755 /home/chefbible/data +``` + +### 2. Before Updating Your App +```bash +# Create a backup +./db-manager.sh backup +``` + +### 3. After Updating (if data is lost) +```bash +# Restore from the latest backup +./db-manager.sh restore +``` + +## Available Commands + +| Command | Description | Example | +|---------|-------------|---------| +| `backup` | Create a backup of the database | `./db-manager.sh backup` | +| `restore` | Restore from the most recent backup | `./db-manager.sh restore` | +| `list` | List all available backups | `./db-manager.sh list` | +| `status` | Show current database status | `./db-manager.sh status` | +| `help` | Show help message | `./db-manager.sh help` | + +## Typical Workflow + +### Before App Updates: +```bash +# 1. Check current status +./db-manager.sh status + +# 2. Create backup +./db-manager.sh backup + +# 3. Update your app in Portainer +# (Your data should be preserved, but backup is safety net) +``` + +### If Data Gets Lost: +```bash +# 1. Check available backups +./db-manager.sh list + +# 2. Restore from latest backup +./db-manager.sh restore + +# 3. Restart your container in Portainer +``` + +## Backup Storage + +- **Location**: `/home/chefbible/backups/` +- **Format**: `chefbible_backup_YYYYMMDD_HHMMSS.db` +- **Retention**: Keeps last 5 backups automatically +- **Size**: Typically 1-10MB per backup + +## Troubleshooting + +### Permission Issues +```bash +# If you get permission errors, run with sudo: +sudo ./db-manager.sh backup +sudo ./db-manager.sh restore +``` + +### Container Not Found +```bash +# If container name is different, edit the script: +# Change CONTAINER_NAME="chefbible" to your actual container name +``` + +### Database Path Issues +```bash +# Check if the database path exists: +ls -la /home/chefbible/data/ +``` + +## Safety Features + +✅ **Automatic cleanup** - Keeps only last 5 backups +✅ **Size reporting** - Shows backup sizes +✅ **Status checking** - Verifies database and container state +✅ **Error handling** - Clear error messages and suggestions +✅ **Color output** - Easy to read status messages + +## Pro Tips + +1. **Always backup before major updates** +2. **Test restore process** on a test environment first +3. **Keep backups in a separate location** for extra safety +4. **Monitor backup sizes** - large backups might indicate issues diff --git a/db-manager.sh b/db-manager.sh new file mode 100755 index 0000000..64a5149 --- /dev/null +++ b/db-manager.sh @@ -0,0 +1,169 @@ +#!/bin/bash + +# ChefBible Database Manager +# Easy backup and restore for your recipes + +BACKUP_DIR="/home/chefbible/backups" +DB_PATH="/home/chefbible/data/database.db" +CONTAINER_NAME="chefbible" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +show_help() { + echo -e "${BLUE}ChefBible Database Manager${NC}" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " backup - Create a backup of the database" + echo " restore - Restore from the most recent backup" + echo " list - List all available backups" + echo " status - Show current database status" + echo " help - Show this help message" + echo "" + echo "Examples:" + echo " $0 backup # Create backup" + echo " $0 restore # Restore from latest backup" + echo " $0 restore backup_20240101 # Restore from specific backup" +} + +backup_database() { + echo -e "${BLUE}🔄 Creating backup of ChefBible database...${NC}" + + # Create backup directory if it doesn't exist + mkdir -p "$BACKUP_DIR" + + # Check if database exists + if [ -f "$DB_PATH" ]; then + TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + BACKUP_FILE="$BACKUP_DIR/chefbible_backup_$TIMESTAMP.db" + + # Create backup + cp "$DB_PATH" "$BACKUP_FILE" + echo -e "${GREEN}✅ Backup created: $(basename "$BACKUP_FILE")${NC}" + + # Show backup size + BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) + echo -e "${GREEN}📊 Backup size: $BACKUP_SIZE${NC}" + + # Keep only last 5 backups + ls -t "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null | tail -n +6 | xargs -r rm + echo -e "${GREEN}🧹 Cleaned up old backups (keeping last 5)${NC}" + else + echo -e "${YELLOW}⚠️ No database found at $DB_PATH${NC}" + echo -e "${YELLOW} This might be a fresh installation${NC}" + fi + + echo -e "${GREEN}🎉 Backup process complete!${NC}" +} + +restore_database() { + echo -e "${BLUE}🔄 Restoring ChefBible database...${NC}" + + # Create data directory if it doesn't exist + mkdir -p "$(dirname "$DB_PATH")" + + # If no backup file specified, use the most recent one + if [ -z "$1" ]; then + BACKUP_FILE=$(ls -t "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null | head -n1) + if [ -z "$BACKUP_FILE" ]; then + echo -e "${RED}❌ No backup files found in $BACKUP_DIR${NC}" + echo -e "${YELLOW} Run '$0 backup' first to create a backup${NC}" + exit 1 + fi + echo -e "${BLUE}📁 Using most recent backup: $(basename "$BACKUP_FILE")${NC}" + else + BACKUP_FILE="$BACKUP_DIR/chefbible_backup_$1.db" + if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}❌ Backup file not found: $BACKUP_FILE${NC}" + echo -e "${YELLOW} Available backups:${NC}" + list_backups + exit 1 + fi + fi + + # Stop the container if it's running + echo -e "${YELLOW}⏹️ Stopping ChefBible container...${NC}" + docker stop "$CONTAINER_NAME" 2>/dev/null || echo -e "${YELLOW} Container not running${NC}" + + # Restore the database + echo -e "${BLUE}📥 Restoring database from: $(basename "$BACKUP_FILE")${NC}" + cp "$BACKUP_FILE" "$DB_PATH" + + # Set proper permissions + chown 1001:1001 "$DB_PATH" 2>/dev/null || echo -e "${YELLOW} Note: Run with sudo to set proper permissions${NC}" + + echo -e "${GREEN}✅ Database restored successfully!${NC}" + echo -e "${GREEN}🚀 You can now start the ChefBible container${NC}" + + # Show restored database size + if [ -f "$DB_PATH" ]; then + DB_SIZE=$(du -h "$DB_PATH" | cut -f1) + echo -e "${GREEN}📊 Restored database size: $DB_SIZE${NC}" + fi +} + +list_backups() { + echo -e "${BLUE}📋 Available backups:${NC}" + if [ -d "$BACKUP_DIR" ] && [ "$(ls -A "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null)" ]; then + ls -lh "$BACKUP_DIR"/chefbible_backup_*.db | awk '{print " " $9 " (" $5 ", " $6 " " $7 " " $8 ")"}' + else + echo -e "${YELLOW} No backups found${NC}" + echo -e "${YELLOW} Run '$0 backup' to create your first backup${NC}" + fi +} + +show_status() { + echo -e "${BLUE}📊 ChefBible Database Status${NC}" + echo "" + + # Check if database exists + if [ -f "$DB_PATH" ]; then + DB_SIZE=$(du -h "$DB_PATH" | cut -f1) + echo -e "${GREEN}✅ Database exists: $DB_PATH${NC}" + echo -e "${GREEN}📊 Size: $DB_SIZE${NC}" + else + echo -e "${RED}❌ No database found at $DB_PATH${NC}" + fi + + # Check container status + if docker ps | grep -q "$CONTAINER_NAME"; then + echo -e "${GREEN}✅ Container is running${NC}" + else + echo -e "${YELLOW}⚠️ Container is not running${NC}" + fi + + # Show backup count + BACKUP_COUNT=$(ls "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null | wc -l) + echo -e "${BLUE}📋 Backups available: $BACKUP_COUNT${NC}" +} + +# Main script logic +case "$1" in + "backup") + backup_database + ;; + "restore") + restore_database "$2" + ;; + "list") + list_backups + ;; + "status") + show_status + ;; + "help"|"--help"|"-h"|"") + show_help + ;; + *) + echo -e "${RED}❌ Unknown command: $1${NC}" + echo "" + show_help + exit 1 + ;; +esac diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c68768a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Super Simple ChefBible Deploy +# Just run: ./deploy.sh + +echo "🚀 Building and pushing ChefBible..." + +# Build and push in one go +docker build -t git.redbackpack.ca/taogaetz/chefbible:latest . && \ +docker push git.redbackpack.ca/taogaetz/chefbible:latest + +echo "✅ Done! Now go to Portainer and click 'Update the stack'" +echo "💾 If data gets lost, run './db-manager.sh restore' on your server" diff --git a/docker-compose.yml b/docker-compose.yml index 4bfe5a7..0336778 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: container_name: chefbible restart: unless-stopped ports: - - "3000:3000" + - "4000:3000" environment: - NODE_ENV=production - DATABASE_URL=file:/app/data/database.db @@ -14,14 +14,10 @@ services: - CLOUDINARY_URL=${CLOUDINARY_URL} - ORIGIN=${ORIGIN} volumes: - - chefbible_data:/app/data + - /home/chefbible/data:/app/data healthcheck: test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" ] interval: 30s timeout: 10s retries: 3 start_period: 40s - -volumes: - chefbible_data: - driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index de411fc..914838a 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -11,8 +11,14 @@ if [ -z "$DATABASE_URL" ]; then fi npx prisma migrate deploy -echo "Seeding database..." -npx prisma db seed +echo "Checking if database needs seeding..." +# Check if database file exists and has content +if [ ! -f "/app/data/database.db" ] || [ ! -s "/app/data/database.db" ]; then + echo "Database is empty or doesn't exist, seeding with sample data..." + npx prisma db seed +else + echo "Database already exists with data, skipping seed..." +fi echo "Starting application..." exec "$@" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9e52d53..46e4ae3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,7 +14,7 @@ generator client { datasource db { provider = "sqlite" - url = "file:./database.db" + url = env("DATABASE_URL") } model Recipe { diff --git a/prisma/seed.ts b/prisma/seed.ts index 52a7332..9903f43 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3,13 +3,14 @@ import { PrismaClient } from '@prisma-app/client'; const prisma = new PrismaClient(); async function main() { - console.log('🧹 Clearing existing records...'); - await prisma.menuRecipe.deleteMany(); - await prisma.recipeIngredient.deleteMany(); - await prisma.menu.deleteMany(); - await prisma.recipe.deleteMany(); - await prisma.ingredient.deleteMany(); - await prisma.allergen.deleteMany(); + // Check if database already has data + const existingRecipes = await prisma.recipe.count(); + + if (existingRecipes > 0) { + console.log('📊 Database already contains data, skipping seed...'); + console.log(`Found ${existingRecipes} existing recipes`); + return; + } console.log('🌱 Seeding database with new recipes...'); diff --git a/src/routes/recipe/[id]/edit/+server.ts b/src/routes/recipe/[id]/edit/+server.ts index f06eb8f..d3d3e43 100644 --- a/src/routes/recipe/[id]/edit/+server.ts +++ b/src/routes/recipe/[id]/edit/+server.ts @@ -4,7 +4,7 @@ import prisma from '$lib/server/prisma'; // Configure Cloudinary conditionally let cloudinary: any = null; try { - const { CLOUDINARY_URL } = await import('$env/static/private'); + const CLOUDINARY_URL = process.env.CLOUDINARY_URL; if (CLOUDINARY_URL) { const { v2 } = await import('cloudinary'); cloudinary = v2; @@ -62,7 +62,7 @@ export const POST: RequestHandler = async ({ request, params }) => { { quality: 'auto:good' } ] }, - (error, result) => { + (error: any, result: any) => { if (error) reject(error); else if (result) resolve(result as { secure_url: string }); else reject(new Error('No result from Cloudinary')); diff --git a/src/routes/recipe/new/+server.ts b/src/routes/recipe/new/+server.ts index b11b980..bb93b5e 100644 --- a/src/routes/recipe/new/+server.ts +++ b/src/routes/recipe/new/+server.ts @@ -4,7 +4,7 @@ import prisma from '$lib/server/prisma'; // Configure Cloudinary conditionally let cloudinary: any = null; try { - const { CLOUDINARY_URL } = await import('$env/static/private'); + const CLOUDINARY_URL = process.env.CLOUDINARY_URL; if (CLOUDINARY_URL) { const { v2 } = await import('cloudinary'); cloudinary = v2; @@ -62,7 +62,7 @@ export const POST: RequestHandler = async ({ request }) => { { quality: 'auto:good' } ] }, - (error, result) => { + (error: any, result: any) => { if (error) reject(error); else if (result) resolve(result as { secure_url: string }); else reject(new Error('No result from Cloudinary'));