8 Commits

Author SHA1 Message Date
Ryan Chen
bef2577e13 feat: add comprehensive database migration system
- Create migrations/ directory with proper structure
- Add migration runner (migrations/migrate.py) with tracking
- Implement 001_add_description_column.py migration
- Add comprehensive README with usage and best practices
- Support for ordered execution and rollback capabilities
- Integration ready for Docker and CI/CD workflows

The migration system provides:
- Ordered execution by filename (001_, 002_, etc.)
- Applied migration tracking in database
- Idempotent operations (safe to re-run)
- Standalone and batch execution modes
- Comprehensive error handling and logging

Restored description column that was missing after merge.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-08 00:05:37 -04:00
ryan
ed896a2bdf Merge pull request 'feat: Update Docker setup for refactored Flask architecture' (#2) from docker-refactor-updates into main
Reviewed-on: #2
2025-08-08 00:00:03 -04:00
ryan
7deeb681c0 Merge branch 'main' into docker-refactor-updates 2025-08-07 23:59:57 -04:00
Ryan Chen
8814dd8994 perf: reduce Gunicorn workers to 1 for stability
- Change from 4 workers to 1 worker in both Dockerfile and docker-compose.yml
- Helps reduce resource usage and connection reset issues
- Better for single-user or low-traffic deployment scenarios

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 23:59:00 -04:00
Ryan Chen
02247340d4 fix: resolve image display issues after refactor
- Fix upload folder path to be relative to Flask app static folder
- Update app initialization to use proper static folder structure
- Change default development port to 5001 to avoid AirPlay conflicts
- Images now display correctly in both public and admin views

The refactored app now properly serves uploaded images from the
correct static file location.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 23:56:14 -04:00
ryan
f3c14d649c Merge pull request 'Updating Dockerfile and docker-compose following' (#1) from docker-refactor-updates into main
Reviewed-on: #1
2025-08-07 23:41:21 -04:00
Ryan Chen
b71187009b feat: update Docker setup for refactored app structure
- Update Dockerfile to copy new app/ directory structure
- Fix docker-compose.yml volume paths for app/static/uploads
- Add environment variable support for new config system
- Update migration service to use migrate_session_changes.py
- Add .dockerignore for optimized builds
- Remove obsolete version field from docker-compose.yml
- Create comprehensive README_DOCKER.md documentation

The Docker setup now fully supports the refactored Flask application
with modular structure, authentication, and like system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 23:40:12 -04:00
Ryan Chen
77cea8cbc5 feat: complete codebase refactor with Flask best practices
- Implement application factory pattern with blueprints
- Add modular structure (models, routes, utils, templates)
- Create configuration management system
- Add comprehensive error handling and logging
- Implement cookie-based like system with database migration
- Add proper authentication decorators and session management
- Update .gitignore to exclude database files and OS artifacts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 18:29:15 -04:00
12 changed files with 626 additions and 49 deletions

View File

@@ -1,46 +1,44 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
venv/
# Database files (will be mounted as volume)
*.db
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git
.git
.gitignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
ENV/
# IDE
.idea/
# IDE files
.vscode/
.idea/
*.swp
*.swo
# Database
*.db
# Logs
logs/
*.log
# Uploads
static/uploads/*
!static/uploads/.gitkeep
# Development files
.env
.env.local
# Docker
Dockerfile
.dockerignore
# Cache directories
.cache/

13
.gitignore vendored
View File

@@ -8,3 +8,16 @@ wheels/
# Virtual environments
.venv
# Database files
*.db
pet_pictures.db
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@@ -15,11 +15,13 @@ RUN pip install uv
# Copy project files
COPY pyproject.toml .
COPY main.py .
COPY templates/ templates/
COPY app/ app/
COPY migrate_session_changes.py .
COPY README_MIGRATION.md .
COPY README.md .
# Create uploads directory
RUN mkdir -p static/uploads
# Create uploads directory in the correct location
RUN mkdir -p app/static/uploads
# Create and activate virtual environment, then install dependencies
RUN uv venv && \
@@ -30,7 +32,7 @@ RUN uv venv && \
ENV FLASK_APP=main.py
ENV FLASK_ENV=production
ENV PATH="/app/.venv/bin:$PATH"
ENV GUNICORN_CMD_ARGS="--workers=4 --bind=0.0.0.0:5000 --timeout=120"
ENV GUNICORN_CMD_ARGS="--workers=1 --bind=0.0.0.0:5000 --timeout=120"
# Expose port
EXPOSE 5000

160
README_DOCKER.md Normal file
View File

@@ -0,0 +1,160 @@
# Docker Setup for Pets of Powerwashing
## Updated for Refactored Architecture
The Docker setup has been updated to work with the refactored Flask application structure.
## Quick Start
### 1. Build and Run
```bash
# Build and start the application
docker compose up --build
# Or run in detached mode
docker compose up --build -d
```
### 2. Run Database Migration
```bash
# Run the migration (first time setup or after schema changes)
docker compose --profile migrate up migrate
```
### 3. Access the Application
- **Web Interface**: http://localhost:54321
- **Admin Login**: username: `admin`, password: `password123`
## File Structure Changes
The refactored application now uses this structure:
```
app/
├── static/uploads/ # Upload directory (mounted as volume)
├── templates/ # HTML templates
├── models/ # Database models
├── routes/ # Route handlers (blueprints)
├── utils/ # Utilities and helpers
└── ...
```
## Docker Configuration
### Environment Variables
You can customize the application through environment variables:
```yaml
environment:
# Flask settings
- FLASK_ENV=production
# Application settings
- UPLOAD_FOLDER=app/static/uploads
- DATABASE_PATH=pet_pictures.db
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=password123
# Optional: Override secret key for production
- SECRET_KEY=your-secret-key-here
```
### Volume Mounts
- `./app/static/uploads:/app/app/static/uploads` - Persistent file uploads
- `./pet_pictures.db:/app/pet_pictures.db` - Persistent database
## Services
### Web Service (`web`)
- **Port**: 54321 (maps to internal port 5000)
- **Process**: Gunicorn with 4 workers
- **Health Check**: HTTP check every 30 seconds
- **Restart Policy**: unless-stopped
### Migration Service (`migrate`)
- **Purpose**: Database schema updates
- **Usage**: `docker compose --profile migrate up migrate`
- **Script**: Uses `migrate_session_changes.py`
## Commands
### Development
```bash
# Build only
docker compose build
# View logs
docker compose logs -f
# Stop services
docker compose down
# Remove everything including volumes
docker compose down -v
```
### Production
```bash
# Start in production mode
FLASK_ENV=production docker compose up -d
# Update and restart
docker compose pull
docker compose up --build -d
```
## Troubleshooting
### Common Issues
**1. Permission Issues with Uploads**
```bash
# Fix upload directory permissions
sudo chown -R 1000:1000 ./app/static/uploads
```
**2. Database Migration Fails**
```bash
# Run migration manually
docker compose exec web python migrate_session_changes.py
```
**3. Build Fails**
```bash
# Clean build
docker compose build --no-cache
```
### Health Check
The application includes a health check that:
- Tests HTTP connectivity on port 5000
- Retries 3 times with 5-second delays
- Runs every 30 seconds
- Allows 40 seconds for startup
## Security Notes
**For Production:**
1. Change default admin credentials via environment variables
2. Set a secure `SECRET_KEY`
3. Use HTTPS reverse proxy (nginx/traefik)
4. Limit file upload sizes
5. Regular database backups
## File Locations in Container
- **Application**: `/app/`
- **Database**: `/app/pet_pictures.db`
- **Uploads**: `/app/app/static/uploads/`
- **Logs**: `/app/logs/` (if logging enabled)
## Changes from Previous Version
**Updated paths** for refactored app structure
**New migration script** (`migrate_session_changes.py`)
**Added environment variables** for configuration
**Improved .dockerignore** for smaller builds
**Removed obsolete version** from docker-compose.yml
**Better volume mapping** for uploads directory

View File

@@ -15,7 +15,10 @@ def create_app():
app.config.from_object(Config)
# Ensure upload directory exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
upload_path = os.path.join(app.static_folder, 'uploads')
os.makedirs(upload_path, exist_ok=True)
# Update config to use absolute path for file operations
app.config['UPLOAD_FOLDER'] = upload_path
# Setup logging
from app.utils.logging_config import setup_logging

View File

@@ -12,7 +12,7 @@ class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24)
# Upload settings
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'app/static/uploads'
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'static/uploads'
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

View File

@@ -1,17 +1,20 @@
version: "3.8"
services:
web:
build: .
ports:
- "54321:5000"
volumes:
- ./static/uploads:/app/static/uploads
- ./app/static/uploads:/app/app/static/uploads
- ./pet_pictures.db:/app/pet_pictures.db
environment:
- FLASK_APP=main.py
- FLASK_ENV=production
- GUNICORN_CMD_ARGS=--workers=4 --bind=0.0.0.0:5000 --timeout=120 --keep-alive=5 --worker-class=sync --worker-connections=1000 --max-requests=1000 --max-requests-jitter=50
- GUNICORN_CMD_ARGS=--workers=1 --bind=0.0.0.0:5000 --timeout=120 --keep-alive=5 --worker-class=sync --worker-connections=1000 --max-requests=1000 --max-requests-jitter=50
# Application configuration
- UPLOAD_FOLDER=app/static/uploads
- DATABASE_PATH=pet_pictures.db
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=password123
restart: unless-stopped
healthcheck:
test:
@@ -36,6 +39,6 @@ services:
build: .
volumes:
- ./pet_pictures.db:/app/pet_pictures.db
command: python migrate.py
command: python migrate_session_changes.py
profiles:
- migrate

View File

@@ -15,4 +15,5 @@ app = create_app()
if __name__ == "__main__":
# Development server configuration
debug_mode = os.environ.get('FLASK_ENV') == 'development'
app.run(debug=debug_mode, host='0.0.0.0', port=5000)
port = int(os.environ.get('PORT', 5001)) # Use port 5001 to avoid AirPlay conflict
app.run(debug=debug_mode, host='0.0.0.0', port=port)

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Migration 001: Add description column
Date: 2025-08-08
Description: Add description field to pet_pictures table for optional image descriptions
"""
import sqlite3
import sys
from datetime import datetime
def check_column_exists(cursor, table_name, column_name):
"""Check if a column exists in a table"""
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [column[1] for column in cursor.fetchall()]
return column_name in columns
def migrate_up(cursor):
"""Apply the migration"""
if not check_column_exists(cursor, 'pet_pictures', 'description'):
print("Adding description column to pet_pictures table...")
cursor.execute("ALTER TABLE pet_pictures ADD COLUMN description TEXT")
return True
else:
print("Description column already exists, skipping...")
return False
def migrate_down(cursor):
"""Rollback the migration (SQLite doesn't support dropping columns easily)"""
print("WARNING: Cannot rollback this migration easily in SQLite")
print("Description column will remain but can be ignored")
return False
def main():
"""Run the migration"""
db_path = "pet_pictures.db"
try:
db = sqlite3.connect(db_path)
cursor = db.cursor()
print(f"Running migration 001: Add description column")
print(f"Database: {db_path}")
print(f"Timestamp: {datetime.now().isoformat()}")
print("-" * 50)
if migrate_up(cursor):
db.commit()
print("✓ Migration 001 applied successfully")
else:
print("• Migration 001 already applied")
# Verify the migration
cursor.execute("PRAGMA table_info(pet_pictures)")
columns = cursor.fetchall()
print("\nFinal schema:")
for col in columns:
nullable = "NOT NULL" if col[3] else "NULL"
default = f"DEFAULT {col[4]}" if col[4] else ""
print(f" {col[1]} {col[2]} {nullable} {default}")
except sqlite3.Error as e:
print(f"❌ Migration failed: {e}")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
return 1
finally:
if 'db' in locals():
db.close()
return 0
if __name__ == "__main__":
sys.exit(main())

142
migrations/README.md Normal file
View File

@@ -0,0 +1,142 @@
# Database Migrations
This directory contains database migrations for the Pet Picture Queue application.
## Migration System
The migration system provides:
-**Ordered execution** - Migrations run in filename order (001_, 002_, etc.)
-**Tracking** - Applied migrations are tracked in the database
-**Idempotent** - Safe to run multiple times
-**Rollback support** - Individual migrations can implement rollback logic
## Usage
### Run All Pending Migrations
```bash
# From project root
python migrations/migrate.py
# Or make it executable and run directly
chmod +x migrations/migrate.py
./migrations/migrate.py
```
### Run Individual Migration
```bash
python migrations/001_add_description_column.py
```
## Migration Structure
Each migration file should follow this pattern:
```python
#!/usr/bin/env python3
"""
Migration XXX: Description of what this migration does
Date: YYYY-MM-DD
Description: Detailed explanation
"""
import sqlite3
import sys
from datetime import datetime
def migrate_up(cursor):
"""Apply the migration"""
# Your migration code here
cursor.execute("ALTER TABLE ...")
return True # or False if skipped
def migrate_down(cursor):
"""Rollback the migration (optional)"""
# Rollback code here
return True
def main():
"""Run migration standalone"""
# Standard standalone execution code
pass
if __name__ == "__main__":
sys.exit(main())
```
## Existing Migrations
### 001_add_description_column.py
- **Purpose**: Add description field to pet_pictures table
- **Date**: 2025-08-08
- **Changes**: Adds `description TEXT NULL` column
## Migration Naming Convention
- Use 3-digit numbers: `001_`, `002_`, `003_`, etc.
- Descriptive names: `add_description_column`, `create_user_table`
- Full format: `XXX_descriptive_name.py`
## Database Schema Tracking
The migration system creates a `migrations` table to track applied migrations:
```sql
CREATE TABLE migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration_name TEXT NOT NULL UNIQUE,
applied_at TIMESTAMP NOT NULL
);
```
## Best Practices
1. **Always backup** before running migrations on production data
2. **Test migrations** on development data first
3. **Make migrations idempotent** - check if changes already exist
4. **Keep migrations small** - one logical change per migration
5. **Never edit** existing migration files after they've been applied
6. **Document breaking changes** in migration comments
## Development Workflow
1. **Create migration**: Copy template and modify
2. **Test locally**: Run migration on development database
3. **Add to version control**: Commit the migration file
4. **Deploy**: Run migrations on staging/production
5. **Verify**: Check database schema and data integrity
## Troubleshooting
**Migration fails:**
```bash
# Check database connection
ls -la pet_pictures.db
# Check migration table
sqlite3 pet_pictures.db "SELECT * FROM migrations;"
# Run specific migration for debugging
python migrations/001_add_description_column.py
```
**Reset migration tracking:**
```sql
-- Careful! This will re-run all migrations
DELETE FROM migrations WHERE migration_name = '001_add_description_column';
```
## Integration
### Docker
The migration runner is integrated with Docker:
```bash
# Run migrations in Docker
docker compose --profile migrate up migrate
```
### CI/CD
Add to deployment scripts:
```bash
# Run migrations before starting app
python migrations/migrate.py && python main.py
```

1
migrations/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Migrations package

174
migrations/migrate.py Executable file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Migration runner for Pet Picture Queue
Handles database schema migrations in order
"""
import os
import sys
import importlib.util
import sqlite3
from datetime import datetime
def get_database_path():
"""Get the correct database path"""
if os.path.exists("pet_pictures.db"):
return "pet_pictures.db"
elif os.path.exists("app/pet_pictures.db"):
return "app/pet_pictures.db"
else:
return "pet_pictures.db"
def create_migration_table(cursor):
"""Create migrations tracking table"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration_name TEXT NOT NULL UNIQUE,
applied_at TIMESTAMP NOT NULL
)
""")
def get_applied_migrations(cursor):
"""Get list of applied migrations"""
try:
cursor.execute("SELECT migration_name FROM migrations ORDER BY id")
return [row[0] for row in cursor.fetchall()]
except sqlite3.OperationalError:
# Migrations table doesn't exist yet
return []
def get_migration_files():
"""Get list of migration files in order"""
migrations_dir = os.path.dirname(__file__)
migration_files = []
for file in os.listdir(migrations_dir):
if file.endswith('.py') and file != 'migrate.py' and file != '__init__.py':
migration_files.append(file)
# Sort by filename (assumes numbered naming like 001_xxx.py)
migration_files.sort()
return migration_files
def load_migration_module(migration_file):
"""Load a migration module dynamically"""
migrations_dir = os.path.dirname(__file__)
file_path = os.path.join(migrations_dir, migration_file)
spec = importlib.util.spec_from_file_location("migration", file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def run_migrations():
"""Run pending migrations"""
db_path = get_database_path()
print(f"Using database: {db_path}")
try:
db = sqlite3.connect(db_path)
cursor = db.cursor()
# Create migrations table
create_migration_table(cursor)
db.commit()
# Get current state
applied_migrations = get_applied_migrations(cursor)
available_migrations = get_migration_files()
print(f"Applied migrations: {len(applied_migrations)}")
print(f"Available migrations: {len(available_migrations)}")
# Find pending migrations
pending_migrations = []
for migration_file in available_migrations:
migration_name = migration_file.replace('.py', '')
if migration_name not in applied_migrations:
pending_migrations.append(migration_file)
if not pending_migrations:
print("✓ No pending migrations")
return 0
print(f"\nRunning {len(pending_migrations)} pending migrations:")
# Run each pending migration
for migration_file in pending_migrations:
migration_name = migration_file.replace('.py', '')
print(f"\n📦 Running migration: {migration_name}")
try:
# Load and run migration
module = load_migration_module(migration_file)
if hasattr(module, 'migrate_up'):
success = module.migrate_up(cursor)
if success is not False: # Allow None or True
# Mark as applied
cursor.execute(
"INSERT INTO migrations (migration_name, applied_at) VALUES (?, ?)",
(migration_name, datetime.now())
)
db.commit()
print(f"✓ Migration {migration_name} applied successfully")
else:
print(f"• Migration {migration_name} skipped (already applied)")
else:
print(f"❌ Migration {migration_name} has no migrate_up function")
except Exception as e:
print(f"❌ Migration {migration_name} failed: {e}")
db.rollback()
return 1
print(f"\n🎉 All migrations completed successfully!")
# Show final schema
cursor.execute("PRAGMA table_info(pet_pictures)")
columns = cursor.fetchall()
print(f"\nFinal pet_pictures schema:")
for col in columns:
nullable = "NOT NULL" if col[3] else "NULL"
default = f"DEFAULT {col[4]}" if col[4] else ""
print(f" {col[1]} {col[2]} {nullable} {default}")
return 0
except Exception as e:
print(f"❌ Migration runner failed: {e}")
return 1
finally:
if 'db' in locals():
db.close()
def main():
"""Main entry point"""
print("=" * 60)
print("Pet Picture Queue - Database Migration Runner")
print(f"Started at: {datetime.now().isoformat()}")
print("=" * 60)
result = run_migrations()
print("=" * 60)
if result == 0:
print("✅ Migration process completed successfully!")
else:
print("❌ Migration process failed!")
print("=" * 60)
return result
if __name__ == "__main__":
sys.exit(main())