feat: enhance README with comprehensive screenshot showcase

- Add organized screenshot sections for better visual presentation
- Include all 12 available screenshots from assets/screenshots/
- Group screenshots into logical categories:
  * Core Application Views (Dashboard, Projects, Tasks, Clients)
  * Management & Analytics (Reports, Visual Analytics, Task Management, Admin)
  * Data Entry & Creation (Log Time, New Task, New Client, New Project)
- Improve visual layout with proper spacing and responsive design
- Enhance user experience by showcasing full application capabilities
This commit is contained in:
Dries Peeters
2025-09-02 14:42:54 +02:00
parent 8e3e5c195c
commit d9dab3a49c
34 changed files with 4992 additions and 191 deletions

View File

@@ -52,7 +52,7 @@ RUN mkdir -p /app/app/static/uploads/logos /app/static/uploads/logos && \
COPY docker/start-fixed.py /app/start.py
# Make startup scripts executable
RUN chmod +x /app/start.py /app/docker/init-database.py /app/docker/init-database-sql.py /app/docker/init-database-enhanced.py /app/docker/verify-database.py /app/docker/test-db.py /app/docker/test-routing.py
RUN chmod +x /app/start.py /app/docker/init-database.py /app/docker/init-database-sql.py /app/docker/init-database-enhanced.py /app/docker/verify-database.py /app/docker/test-db.py /app/docker/test-routing.py /app/docker/entrypoint.sh /app/docker/entrypoint_fixed.sh /app/docker/startup_with_migration.py /app/docker/test_db_connection.py /app/docker/debug_startup.sh /app/docker/simple_test.sh
# Create non-root user
RUN useradd -m -u 1000 timetracker && \
@@ -71,5 +71,8 @@ EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/_health || exit 1
# Set the entrypoint
ENTRYPOINT ["/app/docker/entrypoint_fixed.sh"]
# Run the application
CMD ["python", "/app/start.py"]

View File

@@ -4,12 +4,28 @@ A comprehensive web-based time tracking application built with Flask, featuring
## 📸 Screenshots
### Core Application Views
<div align="center">
<img src="assets/screenshots/Dashboard.png" alt="Dashboard" width="300" style="margin: 10px;">
<img src="assets/screenshots/Projects.png" alt="Projects" width="300" style="margin: 10px;">
<img src="assets/screenshots/Tasks.png" alt="Tasks" width="300" style="margin: 10px;">
<img src="assets/screenshots/Clients.png" alt="Clients" width="300" style="margin: 10px;">
</div>
### Management & Analytics
<div align="center">
<img src="assets/screenshots/Reports.png" alt="Reports" width="300" style="margin: 10px;">
<img src="assets/screenshots/VisualAnalytics.png" alt="Visual Analytics" width="300" style="margin: 10px;">
<img src="assets/screenshots/Task_Management.png" alt="Task Management" width="300" style="margin: 10px;">
<img src="assets/screenshots/Admin.png" alt="Admin Panel" width="300" style="margin: 10px;">
</div>
### Data Entry & Creation
<div align="center">
<img src="assets/screenshots/LogTime.png" alt="Log Time" width="300" style="margin: 10px;">
<img src="assets/screenshots/New-Task.png" alt="New Task Creation" width="300" style="margin: 10px;">
<img src="assets/screenshots/New-Client.png" alt="New Client Creation" width="300" style="margin: 10px;">
<img src="assets/screenshots/New-Project.png" alt="New Project Creation" width="300" style="margin: 10px;">
</div>
## 🌐 Platform Support
@@ -110,9 +126,9 @@ A comprehensive web-based time tracking application built with Flask, featuring
### Import Capabilities
- **Database Schema**: PostgreSQL and SQLite support
- **Migration Scripts**: Automated database schema updates
- **Migration System**: Flask-Migrate with version tracking and rollback support
- **Backup/Restore**: Database backup and restoration tools
- **CLI Management**: Command-line database operations
- **CLI Management**: Command-line database operations with migration commands
### API Integration
- **RESTful Endpoints**: Standard HTTP API for external access
@@ -149,9 +165,13 @@ TimeTracker/
├── docker/ # Docker-related scripts and utilities
│ ├── config/ # Configuration files (Caddyfile, supervisord)
│ ├── fixes/ # Database and permission fix scripts
│ ├── migrations/ # Database migration scripts
│ ├── startup/ # Startup and initialization scripts
│ └── tests/ # Docker environment test scripts
├── migrations/ # Database migrations with Flask-Migrate
│ ├── versions/ # Migration version files
│ ├── env.py # Migration environment configuration
│ ├── script.py.mako # Migration template
│ └── README.md # Migration documentation
├── scripts/ # Deployment and utility scripts
├── tests/ # Application test suite
├── templates/ # Additional templates
@@ -184,6 +204,72 @@ Multiple Docker configurations are available for different deployment scenarios:
- Secure cookie settings enabled
- Suitable for testing pre-release versions
### Database Migration System
The application now uses **Flask-Migrate** for standardized database migrations with:
- **Version Tracking**: Complete history of all database schema changes
- **Rollback Support**: Ability to revert to previous database versions
- **Automatic Schema Detection**: Migrations generated from SQLAlchemy models
- **Cross-Database Support**: Works with both PostgreSQL and SQLite
- **CLI Commands**: Simple commands for migration management
#### Migration Commands
```bash
# Initialize migrations (first time only)
flask db init
# Create a new migration
flask db migrate -m "Description of changes"
# Apply pending migrations
flask db upgrade
# Rollback last migration
flask db downgrade
# Check migration status
flask db current
# View migration history
flask db history
```
#### Quick Migration Setup
```bash
# Use the migration management script
python migrations/manage_migrations.py
# Or manually initialize
flask db init
flask db migrate -m "Initial schema"
flask db upgrade
```
#### **Comprehensive Migration for Any Existing Database:**
```bash
# For ANY existing database (recommended)
python migrations/migrate_existing_database.py
# For legacy schema migration
python migrations/legacy_schema_migration.py
```
#### **Migration Support:**
-**Fresh Installation**: No existing database
-**Legacy Databases**: Old custom migration systems
-**Mixed Schema**: Some tables exist, some missing
-**Production Data**: Existing databases with user data
-**Cross-Version**: Databases from different TimeTracker versions
#### **🚀 Automatic Container Migration:**
-**Zero Configuration**: Container automatically detects database state
-**Smart Strategy Selection**: Chooses best migration approach
-**Automatic Startup**: Handles migration during container startup
-**Production Ready**: Safe migration with automatic fallbacks
See [Migration Documentation](migrations/README.md), [Complete Migration Guide](migrations/MIGRATION_GUIDE.md), and [Container Startup Configuration](docker/STARTUP_MIGRATION_CONFIG.md) for comprehensive details.
### Enhanced Database Startup
The application now includes an enhanced database startup procedure that automatically:

View File

@@ -79,192 +79,53 @@ def register_cli_commands(app):
click.echo("For PostgreSQL, please use pg_dump, e.g.: pg_dump --format=custom --dbname=\"$DATABASE_URL\" --file=backup.dump")
# Clean up old backups
backup_dir = os.getenv('BACKUP_DIR', '/data/backups')
if os.path.exists(backup_dir):
cleanup_old_backups(backup_dir)
@app.cli.command()
@with_appcontext
def cleanup_old_entries():
"""Clean up old time entries (older than specified days)"""
days = click.prompt("Delete entries older than (days)", type=int, default=365)
cutoff_date = datetime.utcnow() - timedelta(days=days)
old_entries = TimeEntry.query.filter(
TimeEntry.end_time < cutoff_date
).all()
if not old_entries:
click.echo("No old entries found")
return
count = len(old_entries)
if click.confirm(f"Delete {count} old entries?"):
for entry in old_entries:
db.session.delete(entry)
db.session.commit()
click.echo(f"Deleted {count} old entries")
else:
click.echo("Operation cancelled")
@app.cli.command()
@with_appcontext
def stats():
"""Show database statistics"""
total_users = User.query.count()
active_users = User.query.filter_by(is_active=True).count()
total_projects = Project.query.count()
active_projects = Project.query.filter_by(status='active').count()
total_entries = TimeEntry.query.count()
completed_entries = TimeEntry.query.filter(TimeEntry.end_time.isnot(None)).count()
active_timers = TimeEntry.query.filter_by(end_time=None).count()
click.echo("Database Statistics:")
click.echo(f" Users: {total_users} (active: {active_users})")
click.echo(f" Projects: {total_projects} (active: {active_projects})")
click.echo(f" Time Entries: {total_entries} (completed: {completed_entries}, active: {active_timers})")
# Calculate total hours
total_hours = db.session.query(
db.func.sum(TimeEntry.duration_seconds)
).filter(
TimeEntry.end_time.isnot(None)
).scalar() or 0
total_hours = round(total_hours / 3600, 2)
click.echo(f" Total Hours: {total_hours}")
@app.cli.command()
def license_status():
"""Show license server client status"""
try:
from app.utils.license_server import get_license_client
client = get_license_client()
if client:
status = client.get_status()
click.echo("License Server Client Status:")
click.echo(f" Registered: {status['is_registered']}")
click.echo(f" Instance ID: {status['instance_id']}")
click.echo(f" Running: {status['is_running']}")
click.echo(f" Server Healthy: {status['server_healthy']}")
click.echo(f" Offline Data: {status['offline_data_count']}")
click.echo(f" App ID: {status['app_identifier']}")
click.echo(f" App Version: {status['app_version']}")
else:
click.echo("License server client not initialized")
except Exception as e:
click.echo(f"Error getting license status: {e}")
@app.cli.command()
def license_test():
"""Test license server communication"""
try:
from app.utils.license_server import get_license_client, send_usage_event
client = get_license_client()
if client:
click.echo("Testing license server communication...")
# Test server health
if client.check_server_health():
click.echo("✓ Server is healthy")
else:
click.echo("✗ Server is not responding")
# Test usage event
if send_usage_event("test_event", {"test": "data"}):
click.echo("✓ Usage event sent successfully")
else:
click.echo("✗ Failed to send usage event")
else:
click.echo("License server client not initialized")
except Exception as e:
click.echo(f"Error testing license server: {e}")
@app.cli.command()
def license_restart():
"""Restart the license server client"""
try:
from app.utils.license_server import get_license_client, start_license_client
client = get_license_client()
if client:
click.echo("Restarting license server client...")
if start_license_client():
click.echo("✓ License server client restarted successfully")
else:
click.echo("✗ Failed to restart license server client")
else:
click.echo("License server client not initialized")
except Exception as e:
click.echo(f"Error restarting license server client: {e}")
def cleanup_old_backups(backup_dir, retention_days=30):
"""Clean up old backup files"""
cutoff_date = datetime.now() - timedelta(days=retention_days)
for filename in os.listdir(backup_dir):
file_path = os.path.join(backup_dir, filename)
if os.path.isfile(file_path):
file_time = datetime.fromtimestamp(os.path.getctime(file_path))
if file_time < cutoff_date:
os.remove(file_path)
click.echo(f"Removed old backup: {filename}")
def cleanup_old_entries():
"""Clean up old time entries (older than specified days)"""
try:
days = 365 # Default to 1 year
cutoff_date = datetime.utcnow() - timedelta(days=days)
old_entries = TimeEntry.query.filter(
TimeEntry.end_time < cutoff_date
).all()
if not old_entries:
click.echo("No old entries found")
return
count = len(old_entries)
click.echo(f"Found {count} old entries older than {days} days")
# For automated cleanup, we'll just log the count
# In interactive mode, you could add confirmation here
click.echo(f"Would delete {count} old entries (use interactive mode for confirmation)")
except Exception as e:
click.echo(f"Error cleaning up old entries: {e}")
def create_backup():
"""Create a database backup"""
try:
from app.config import Config
url = Config.SQLALCHEMY_DATABASE_URI
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if url.startswith('sqlite:///'):
# SQLite file copy
db_path = url.replace('sqlite:///', '')
if not os.path.exists(db_path):
click.echo(f"Database file not found: {db_path}")
return
backup_dir = os.path.join(os.path.dirname(db_path), 'backups')
os.makedirs(backup_dir, exist_ok=True)
backup_filename = f"timetracker_backup_{timestamp}.db"
backup_path = os.path.join(backup_dir, backup_filename)
shutil.copy2(db_path, backup_path)
click.echo(f"Database backed up to: {backup_path}")
else:
click.echo("For PostgreSQL, please use pg_dump, e.g.: pg_dump --format=custom --dbname=\"$DATABASE_URL\" --file=backup.dump")
except Exception as e:
click.echo(f"Error creating backup: {e}")
try:
backup_retention_days = int(os.getenv('BACKUP_RETENTION_DAYS', 30))
cutoff_date = datetime.now() - timedelta(days=backup_retention_days)
for backup_file in os.listdir(backup_dir):
backup_file_path = os.path.join(backup_dir, backup_file)
if os.path.isfile(backup_file_path):
file_time = datetime.fromtimestamp(os.path.getctime(backup_file_path))
if file_time < cutoff_date:
os.remove(backup_file_path)
click.echo(f"Removed old backup: {backup_file}")
except Exception as e:
click.echo(f"Warning: Could not clean up old backups: {e}")
def restore_backup():
"""Restore database from backup"""
try:
click.echo("Database restore functionality not implemented yet.")
click.echo("Please restore manually using your database management tools.")
except Exception as e:
click.echo(f"Error restoring backup: {e}")
@app.cli.command()
@with_appcontext
def migrate_to_flask_migrate():
"""Migrate from custom migration system to Flask-Migrate"""
click.echo("This command is deprecated. Use the migration management script instead:")
click.echo("python migrations/manage_migrations.py")
click.echo("\nOr use Flask-Migrate commands directly:")
click.echo("flask db init # Initialize migrations (first time only)")
click.echo("flask db migrate # Create a new migration")
click.echo("flask db upgrade # Apply pending migrations")
click.echo("flask db downgrade # Rollback last migration")
click.echo("flask db current # Show current migration")
click.echo("flask db history # Show migration history")
@app.cli.command()
@with_appcontext
def db_status():
"""Show database migration status"""
try:
from flask_migrate import current
current()
except Exception as e:
click.echo(f"Error getting migration status: {e}")
click.echo("Make sure Flask-Migrate is properly initialized")
@app.cli.command()
@with_appcontext
def db_history():
"""Show database migration history"""
try:
from flask_migrate import history
history()
except Exception as e:
click.echo(f"Error getting migration history: {e}")
click.echo("Make sure Flask-Migrate is properly initialized")

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,281 @@
# Container Startup with Automatic Migration Detection
This document explains how the TimeTracker Docker container automatically detects database state and chooses the correct migration strategy during startup.
## 🎯 **Automatic Migration Detection**
The container startup system automatically detects the current state of your database and chooses the appropriate migration strategy:
### **Database States Detected:**
| **State** | **Description** | **Detection Method** | **Migration Strategy** |
|-----------|----------------|---------------------|------------------------|
| **Fresh** | No database or empty database | No tables exist | `fresh_init` |
| **Migrated** | Database already uses Flask-Migrate | `alembic_version` table exists | `check_migrations` |
| **Legacy** | Database with old custom migrations | Tables exist but no `alembic_version` | `comprehensive_migration` |
| **Unknown** | Cannot determine state | Detection failed | `comprehensive_migration` (fallback) |
## 🚀 **Migration Strategies**
### **1. Fresh Initialization (`fresh_init`)**
- **When Used**: New database, no existing tables
- **Actions**:
- Initialize Flask-Migrate
- Create initial migration
- Apply migration to create schema
- **Result**: Complete new database with Flask-Migrate
### **2. Migration Check (`check_migrations`)**
- **When Used**: Database already migrated, check for updates
- **Actions**:
- Check current migration revision
- Apply any pending migrations
- Verify database integrity
- **Result**: Database updated to latest migration
### **3. Comprehensive Migration (`comprehensive_migration`)**
- **When Used**: Legacy database with old custom migrations
- **Actions**:
- Run enhanced startup script (`startup_with_migration.py`)
- Fallback to manual migration if script fails
- Preserve all existing data
- Create migration baseline
- **Result**: Legacy database converted to Flask-Migrate
## 🔧 **Startup Process Flow**
```
Container Start
Wait for Database
Detect Database State
Choose Migration Strategy
Execute Migration
Verify Database Integrity
Start Application
```
## 📋 **Startup Scripts**
### **Primary Entrypoint: `docker/entrypoint.sh`**
- **Purpose**: Main container entrypoint with migration detection
- **Features**:
- Database availability check
- State detection (PostgreSQL/SQLite)
- Strategy selection
- Migration execution
- Integrity verification
- **Fallbacks**: Multiple fallback methods for each step
### **Enhanced Startup: `docker/startup_with_migration.py`**
- **Purpose**: Advanced migration handling for complex scenarios
- **Features**:
- Comprehensive database analysis
- Automatic backup creation
- Schema migration
- Data preservation
- Error recovery
## 🛡️ **Safety Features**
### **Automatic Protection:**
-**Database Wait**: Waits for database to be available
-**State Detection**: Analyzes existing database structure
-**Strategy Selection**: Chooses safest migration approach
-**Fallback Methods**: Multiple fallback options for each step
-**Integrity Verification**: Confirms database is working after migration
### **Error Handling:**
-**Graceful Failures**: Detailed error logging and recovery
-**Retry Logic**: Automatic retries for database connections
-**Fallback Strategies**: Alternative approaches if primary method fails
-**Logging**: Comprehensive logging for troubleshooting
## 🔍 **Detection Methods**
### **PostgreSQL Detection:**
```bash
# Check if alembic_version table exists
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'alembic_version'
);
# Get list of existing tables
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
```
### **SQLite Detection:**
```bash
# Check if alembic_version table exists
SELECT name FROM sqlite_master
WHERE type='table' AND name='alembic_version';
# Get list of existing tables
SELECT name FROM sqlite_master WHERE type='table';
```
## 📊 **Migration Strategy Selection Logic**
```python
def choose_migration_strategy(db_state):
if db_state == 'fresh':
return 'fresh_init' # New database
elif db_state == 'migrated':
return 'check_migrations' # Already migrated
elif db_state == 'legacy':
return 'comprehensive_migration' # Old system
else:
return 'comprehensive_migration' # Fallback
```
## 🚀 **Usage Examples**
### **Fresh Database:**
```bash
# Container will automatically:
# 1. Detect no tables exist
# 2. Choose 'fresh_init' strategy
# 3. Initialize Flask-Migrate
# 4. Create and apply initial migration
# 5. Start application
```
### **Existing Migrated Database:**
```bash
# Container will automatically:
# 1. Detect alembic_version table exists
# 2. Choose 'check_migrations' strategy
# 3. Check for pending migrations
# 4. Apply any updates
# 5. Start application
```
### **Legacy Database:**
```bash
# Container will automatically:
# 1. Detect tables exist but no alembic_version
# 2. Choose 'comprehensive_migration' strategy
# 3. Run enhanced migration script
# 4. Preserve all existing data
# 5. Convert to Flask-Migrate
# 6. Start application
```
## 🔧 **Configuration Options**
### **Environment Variables:**
```bash
# Required
DATABASE_URL=postgresql://user:pass@host:port/db
# or
DATABASE_URL=sqlite:///path/to/database.db
# Optional
FLASK_APP=/app/app.py
TZ=Europe/Rome
```
### **Database Connection Settings:**
- **PostgreSQL**: Full connection string with credentials
- **SQLite**: File path with write permissions
- **Connection Retries**: 30 attempts with 2-second delays
- **Timeout**: 60 seconds total wait time
## 📝 **Logging and Monitoring**
### **Log Locations:**
- **Container Logs**: `docker logs <container_name>`
- **Startup Logs**: `/var/log/timetracker_startup.log`
- **Application Logs**: `/app/logs/`
### **Log Levels:**
- **INFO**: Normal operation and progress
- **WARNING**: Non-critical issues
- **ERROR**: Critical failures and errors
### **Key Log Messages:**
```
[2025-01-15 10:00:00] === TimeTracker Docker Container Starting ===
[2025-01-15 10:00:01] Waiting for database to be available...
[2025-01-15 10:00:02] ✓ PostgreSQL database is available
[2025-01-15 10:00:03] Analyzing database state...
[2025-01-15 10:00:04] Detected database state: legacy with 8 tables
[2025-01-15 10:00:05] Selected migration strategy: comprehensive_migration
[2025-01-15 10:00:06] Executing migration strategy: comprehensive_migration
[2025-01-15 10:00:10] ✓ Enhanced startup script completed successfully
[2025-01-15 10:00:11] Verifying database integrity...
[2025-01-15 10:00:12] ✓ Database integrity verified
[2025-01-15 10:00:13] === Startup and Migration Complete ===
[2025-01-15 10:00:14] Starting TimeTracker application...
```
## 🔍 **Troubleshooting**
### **Common Issues:**
#### **1. Database Connection Failed:**
```bash
# Check environment variables
echo $DATABASE_URL
# Check database service
docker-compose ps db
# Check logs
docker logs <container_name>
```
#### **2. Migration Strategy Failed:**
```bash
# Check migration logs
docker exec <container_name> cat /var/log/timetracker_startup.log
# Check migration status
docker exec <container_name> flask db current
# Check database state manually
docker exec <container_name> flask db history
```
#### **3. Database Integrity Check Failed:**
```bash
# Check if key tables exist
docker exec <container_name> psql $DATABASE_URL -c "\dt"
# Check table structure
docker exec <container_name> psql $DATABASE_URL -c "\d users"
```
### **Recovery Options:**
1. **Check Logs**: Review startup and migration logs
2. **Verify Database**: Ensure database is accessible
3. **Check Permissions**: Verify database user permissions
4. **Restart Container**: Restart with fresh migration attempt
5. **Manual Migration**: Use migration scripts manually if needed
## 🎉 **Benefits**
### **Automatic Operation:**
-**Zero Configuration**: Works with any existing database
-**Smart Detection**: Automatically chooses best migration approach
-**Data Preservation**: Never loses existing data
-**Error Recovery**: Multiple fallback methods
### **Production Ready:**
-**Safe Migration**: Automatic backups and verification
-**Rollback Support**: Can revert to previous state
-**Monitoring**: Comprehensive logging and health checks
-**Scalability**: Works with any database size
---
**Result**: Your TimeTracker container will automatically handle any database scenario during startup, ensuring a smooth transition to the new Flask-Migrate system regardless of the existing database state! 🚀

View File

@@ -0,0 +1,286 @@
# Database Connection Troubleshooting Guide
This guide helps resolve database connection issues during TimeTracker container startup.
## 🚨 **Common Error: Database Connection Failed**
### **Error Symptoms:**
```
[2025-09-01 19:02:16] Database not ready (attempt 1/30)
[2025-09-01 19:02:18] Database not ready (attempt 2/30)
...
[2025-09-01 19:02:46] Database not ready (attempt 17/30)
```
### **Root Causes:**
1. **PostgreSQL service not fully initialized**
2. **Database container not ready**
3. **Network connectivity issues**
4. **Authentication problems**
5. **Connection string format issues**
## 🔧 **Immediate Solutions**
### **1. Check Database Service Status**
```bash
# Check if database container is running
docker-compose ps db
# Check database container logs
docker-compose logs db
# Check if database is accepting connections
docker-compose exec db pg_isready -U timetracker
```
### **2. Test Database Connection Manually**
```bash
# Test connection from host
docker-compose exec app python /app/docker/test_db_connection.py
# Or test from outside container
docker exec <container_name> python /app/docker/test_db_connection.py
```
### **3. Check Environment Variables**
```bash
# Verify DATABASE_URL is set correctly
docker-compose exec app env | grep DATABASE_URL
# Check if the URL format is correct
echo $DATABASE_URL
```
## 🔍 **Diagnostic Steps**
### **Step 1: Verify Database Container**
```bash
# Check if PostgreSQL container is healthy
docker-compose ps
# Look for these indicators:
# - Status: Up (healthy)
# - Health: healthy
```
### **Step 2: Check Database Logs**
```bash
# View PostgreSQL startup logs
docker-compose logs db | tail -50
# Look for these success indicators:
# - "database system is ready to accept connections"
# - "PostgreSQL init process complete"
# - "database system is ready to accept connections"
```
### **Step 3: Test Network Connectivity**
```bash
# Test if app container can reach database
docker-compose exec app ping db
# Test if database port is accessible
docker-compose exec app nc -zv db 5432
```
### **Step 4: Verify Database Credentials**
```bash
# Check if database user exists
docker-compose exec db psql -U postgres -c "\du"
# Verify database exists
docker-compose exec db psql -U postgres -c "\l"
```
## 🛠️ **Common Fixes**
### **Fix 1: Wait for Database to be Ready**
```bash
# Stop all services
docker-compose down
# Start database first and wait
docker-compose up -d db
# Wait for database to be healthy
docker-compose ps db
# Then start app
docker-compose up -d app
```
### **Fix 2: Check Connection String Format**
```bash
# Correct format for PostgreSQL
DATABASE_URL=postgresql://user:password@host:port/database
# If using psycopg2 (automatic)
DATABASE_URL=postgresql+psycopg2://user:password@host:port/database
# Common issues:
# - Missing password
# - Wrong port number
# - Database name doesn't exist
```
### **Fix 3: Verify Database Initialization**
```bash
# Check if database was initialized
docker-compose exec db psql -U timetracker -d timetracker -c "\dt"
# If no tables exist, database might not be initialized
# Check docker/init-database.py or similar scripts
```
### **Fix 4: Check Docker Compose Configuration**
```yaml
# Ensure proper service dependencies
services:
app:
depends_on:
db:
condition: service_healthy
# ... other config
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U timetracker"]
interval: 10s
timeout: 5s
retries: 5
```
## 📋 **Troubleshooting Checklist**
### **Before Starting Container:**
- [ ] Database container is running and healthy
- [ ] DATABASE_URL environment variable is set correctly
- [ ] Database user has proper permissions
- [ ] Database exists and is accessible
- [ ] Network connectivity between containers works
### **During Startup:**
- [ ] Container waits for database to be ready
- [ ] Connection string is parsed correctly
- [ ] Authentication succeeds
- [ ] Basic queries can be executed
- [ ] Migration system can access database
### **After Startup:**
- [ ] Database tables are accessible
- [ ] Migration system works correctly
- [ ] Application can read/write data
- [ ] Health checks pass
## 🔧 **Advanced Debugging**
### **Enable Verbose Logging**
```bash
# Set environment variable for verbose logging
export FLASK_DEBUG=1
export PYTHONVERBOSE=1
# Restart container with verbose logging
docker-compose up -d app
```
### **Test Connection Step by Step**
```bash
# 1. Test basic connectivity
docker-compose exec app ping db
# 2. Test port accessibility
docker-compose exec app nc -zv db 5432
# 3. Test PostgreSQL connection
docker-compose exec app python -c "
import psycopg2
try:
conn = psycopg2.connect('postgresql://timetracker:timetracker@db:5432/timetracker')
print('Connection successful')
conn.close()
except Exception as e:
print(f'Connection failed: {e}')
"
# 4. Test with psycopg2 URL
docker-compose exec app python -c "
import psycopg2
try:
conn = psycopg2.connect('postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker')
print('Connection successful')
conn.close()
except Exception as e:
print(f'Connection failed: {e}')
"
```
### **Check Container Resources**
```bash
# Check if containers have enough resources
docker stats
# Check container logs for resource issues
docker-compose logs app | grep -i "memory\|cpu\|disk"
```
## 🚀 **Prevention Strategies**
### **1. Use Health Checks**
```yaml
# In docker-compose.yml
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U timetracker"]
interval: 10s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthy
```
### **2. Proper Service Dependencies**
```yaml
# Ensure app waits for database
services:
app:
depends_on:
db:
condition: service_healthy
restart: unless-stopped
```
### **3. Connection Retry Logic**
```bash
# The entrypoint script already includes:
# - 60 retry attempts
# - 3-second delays
# - Multiple connection methods
# - Fallback strategies
```
## 📞 **Getting Help**
### **If Problems Persist:**
1. **Check all logs**: `docker-compose logs`
2. **Verify environment**: `docker-compose config`
3. **Test manually**: Use the test script
4. **Check documentation**: See `docker/STARTUP_MIGRATION_CONFIG.md`
### **Useful Commands:**
```bash
# Comprehensive debugging
docker-compose logs -f
docker-compose exec app python /app/docker/test_db_connection.py
docker-compose exec db pg_isready -U timetracker
docker-compose ps
docker network ls
```
---
**Remember**: Most database connection issues are resolved by ensuring the PostgreSQL service is fully initialized before the application container tries to connect. The enhanced entrypoint script includes multiple fallback methods and increased retry logic to handle this automatically.

286
docker/debug_startup.sh Normal file
View File

@@ -0,0 +1,286 @@
#!/bin/bash
set -e
# TimeTracker Startup Debug Script
# This script helps debug startup issues step by step
echo "=== TimeTracker Startup Debug Script ==="
echo "Timestamp: $(date)"
echo "Container ID: $(hostname)"
echo
# Function to log messages with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to test basic connectivity
test_basic_connectivity() {
log "=== Testing Basic Connectivity ==="
# Test if we can resolve the database hostname
if ping -c 1 db >/dev/null 2>&1; then
log "✓ Can ping database host 'db'"
else
log "✗ Cannot ping database host 'db'"
return 1
fi
# Test if database port is accessible
if command_exists nc; then
if nc -zv db 5432 2>/dev/null; then
log "✓ Database port 5432 is accessible"
else
log "✗ Database port 5432 is not accessible"
return 1
fi
else
log "⚠ netcat not available, skipping port test"
fi
return 0
}
# Function to test environment variables
test_environment() {
log "=== Testing Environment Variables ==="
# Check if DATABASE_URL is set
if [[ -n "${DATABASE_URL}" ]]; then
log "✓ DATABASE_URL is set: ${DATABASE_URL}"
# Check if it's a valid PostgreSQL URL
if [[ "${DATABASE_URL}" == postgresql* ]]; then
log "✓ DATABASE_URL format is valid PostgreSQL"
else
log "✗ DATABASE_URL format is not valid PostgreSQL"
return 1
fi
else
log "✗ DATABASE_URL is not set"
return 1
fi
# Check other important variables
if [[ -n "${FLASK_APP}" ]]; then
log "✓ FLASK_APP is set: ${FLASK_APP}"
else
log "⚠ FLASK_APP is not set"
fi
return 0
}
# Function to test Python dependencies
test_python_dependencies() {
log "=== Testing Python Dependencies ==="
# Check if psycopg2 is available
if python -c "import psycopg2; print('✓ psycopg2 is available')" 2>/dev/null; then
log "✓ psycopg2 is available"
else
log "✗ psycopg2 is not available"
return 1
fi
# Check if Flask is available
if python -c "import flask; print('✓ Flask is available')" 2>/dev/null; then
log "✓ Flask is available"
else
log "✗ Flask is not available"
return 1
fi
# Check if Flask-Migrate is available
if python -c "import flask_migrate; print('✓ Flask-Migrate is available')" 2>/dev/null; then
log "✓ Flask-Migrate is available"
else
log "✗ Flask-Migrate is not available"
return 1
fi
return 0
}
# Function to test database connection
test_database_connection() {
log "=== Testing Database Connection ==="
# Run the connection test script
if [[ -f "/app/docker/test_db_connection.py" ]]; then
log "Running database connection test..."
if python /app/docker/test_db_connection.py; then
log "✓ Database connection test successful"
return 0
else
log "✗ Database connection test failed"
return 1
fi
else
log "⚠ Database connection test script not found"
# Fallback: test connection manually
log "Testing connection manually..."
if python -c "
import psycopg2
import sys
try:
# Parse connection string to remove +psycopg2 if present
conn_str = '${DATABASE_URL}'.replace('+psycopg2://', 'postgresql://')
print(f'Trying to connect to: {conn_str}')
conn = psycopg2.connect(conn_str)
cursor = conn.cursor()
cursor.execute('SELECT 1')
result = cursor.fetchone()
print(f'✓ Connection successful, test query result: {result}')
conn.close()
sys.exit(0)
except Exception as e:
print(f'✗ Connection failed: {e}')
sys.exit(1)
"; then
log "✓ Manual connection test successful"
return 0
else
log "✗ Manual connection test failed"
return 1
fi
fi
}
# Function to test Flask-Migrate commands
test_flask_migrate() {
log "=== Testing Flask-Migrate Commands ==="
# Check if flask db command is available
if flask db --help >/dev/null 2>&1; then
log "✓ Flask-Migrate commands are available"
# Test current command
if flask db current >/dev/null 2>&1; then
current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
log "✓ Current migration revision: $current_revision"
else
log "⚠ Could not get current migration revision"
fi
else
log "✗ Flask-Migrate commands are not available"
return 1
fi
return 0
}
# Function to show system information
show_system_info() {
log "=== System Information ==="
log "Python version: $(python --version)"
log "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
log "Working directory: $(pwd)"
log "Current user: $(whoami)"
log "Environment: $(env | grep -E '(FLASK|DATABASE|PYTHON)' | sort)"
# Check if we're in a container
if [[ -f /.dockerenv ]]; then
log "✓ Running in Docker container"
else
log "⚠ Not running in Docker container"
fi
# Check if we can access the app directory
if [[ -d "/app" ]]; then
log "✓ /app directory is accessible"
log " Contents: $(ls -la /app | head -5)"
else
log "✗ /app directory is not accessible"
fi
}
# Main execution
main() {
log "Starting TimeTracker startup debug..."
# Show system information
show_system_info
echo
# Test basic connectivity
if ! test_basic_connectivity; then
log "❌ Basic connectivity test failed"
echo
log "Troubleshooting connectivity issues:"
log "1. Check if database container is running: docker-compose ps db"
log "2. Check database logs: docker-compose logs db"
log "3. Check network: docker network ls"
echo
return 1
fi
echo
# Test environment variables
if ! test_environment; then
log "❌ Environment test failed"
echo
log "Troubleshooting environment issues:"
log "1. Check .env file exists and has correct values"
log "2. Verify DATABASE_URL format"
log "3. Check docker-compose environment section"
echo
return 1
fi
echo
# Test Python dependencies
if ! test_python_dependencies; then
log "❌ Python dependencies test failed"
echo
log "Troubleshooting dependency issues:"
log "1. Check requirements.txt is installed"
log "2. Verify Python packages are available"
log "3. Check container build process"
echo
return 1
fi
echo
# Test database connection
if ! test_database_connection; then
log "❌ Database connection test failed"
echo
log "Troubleshooting connection issues:"
log "1. Check database container health: docker-compose ps db"
log "2. Verify database credentials"
log "3. Check database initialization"
log "4. See: docker/TROUBLESHOOTING_DB_CONNECTION.md"
echo
return 1
fi
echo
# Test Flask-Migrate
if ! test_flask_migrate; then
log "❌ Flask-Migrate test failed"
echo
log "Troubleshooting Flask-Migrate issues:"
log "1. Check if migrations directory exists"
log "2. Verify Flask-Migrate is properly installed"
log "3. Check application configuration"
echo
return 1
fi
echo
log "🎉 All tests passed! System appears to be ready."
log "You can now try starting the application normally."
return 0
}
# Run main function
main "$@"

479
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,479 @@
#!/bin/bash
set -e
# TimeTracker Docker Entrypoint with Automatic Migration Detection
# This script automatically detects database state and chooses the correct migration strategy
echo "=== TimeTracker Docker Container Starting ==="
echo "Timestamp: $(date)"
echo "Container ID: $(hostname)"
echo "Python version: $(python --version)"
echo "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
# Function to log messages with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to wait for database
wait_for_database() {
local db_url="$1"
local max_retries=60 # Increased retries
local retry_delay=3 # Increased delay
log "Waiting for database to be available..."
for attempt in $(seq 1 $max_retries); do
if [[ "$db_url" == postgresql://* ]]; then
# PostgreSQL connection test
if command_exists psql; then
if psql "$db_url" -c "SELECT 1" >/dev/null 2>&1; then
log "✓ PostgreSQL database is available (via psql)"
return 0
fi
fi
# Always try Python connection as primary method
if python -c "
import psycopg2
import sys
try:
# Parse connection string to remove +psycopg2 if present
conn_str = '$db_url'.replace('+psycopg2://', 'postgresql://')
conn = psycopg2.connect(conn_str)
conn.close()
print('Connection successful')
sys.exit(0)
except Exception as e:
print(f'Connection failed: {e}')
sys.exit(1)
" >/dev/null 2>&1; then
log "✓ PostgreSQL database is available (via Python)"
return 0
fi
elif [[ "$db_url" == sqlite://* ]]; then
# SQLite file check
local db_file="${db_url#sqlite://}"
if [[ -f "$db_file" ]] || [[ -w "$(dirname "$db_file")" ]]; then
log "✓ SQLite database is available"
return 0
fi
fi
log "Database not ready (attempt $attempt/$max_retries)"
if [[ $attempt -lt $max_retries ]]; then
sleep $retry_delay
fi
done
log "✗ Database is not available after maximum retries"
return 1
}
# Function to detect database state
detect_database_state() {
local db_url="$1"
log "Analyzing database state..."
if [[ "$db_url" == postgresql://* ]]; then
# PostgreSQL state detection
if command_exists psql; then
# Check if alembic_version table exists
local has_alembic=$(psql "$db_url" -t -c "
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'alembic_version'
);
" 2>/dev/null | tr -d ' ')
# Get list of existing tables
local existing_tables=$(psql "$db_url" -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
" 2>/dev/null | grep -v '^$' | tr '\n' ' ')
log "PostgreSQL state: has_alembic=$has_alembic, tables=[$existing_tables]"
if [[ "$has_alembic" == "t" ]]; then
echo "migrated"
elif [[ -n "$existing_tables" ]]; then
echo "legacy"
else
echo "fresh"
fi
else
# Fallback to Python detection
local state=$(python -c "
import psycopg2
try:
conn = psycopg2.connect('$db_url')
cursor = conn.cursor()
# Check if alembic_version table exists
cursor.execute(\"\"\"
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'alembic_version'
)
\"\"\")
has_alembic = cursor.fetchone()[0]
# Get list of existing tables
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
\"\"\")
existing_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if has_alembic:
print('migrated')
elif existing_tables:
print('legacy')
else:
print('fresh')
except Exception as e:
print('unknown')
" 2>/dev/null)
echo "$state"
fi
elif [[ "$db_url" == sqlite://* ]]; then
# SQLite state detection
local db_file="${db_url#sqlite://}"
if [[ ! -f "$db_file" ]]; then
echo "fresh"
return
fi
local state=$(python -c "
import sqlite3
try:
conn = sqlite3.connect('$db_file')
cursor = conn.cursor()
# Check if alembic_version table exists
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"alembic_version\"')
has_alembic = cursor.fetchone() is not None
# Get list of existing tables
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\"')
existing_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if has_alembic:
print('migrated')
elif existing_tables:
print('legacy')
else:
print('fresh')
except Exception as e:
print('unknown')
" 2>/dev/null)
echo "$state"
else
echo "unknown"
fi
}
# Function to choose migration strategy
choose_migration_strategy() {
local db_state="$1"
log "Choosing migration strategy for state: $db_state"
case "$db_state" in
"fresh")
log "Fresh database detected - using standard initialization"
echo "fresh_init"
;;
"migrated")
log "Database already migrated - checking for pending migrations"
echo "check_migrations"
;;
"legacy")
log "Legacy database detected - using comprehensive migration"
echo "comprehensive_migration"
;;
*)
log "Unknown database state - using comprehensive migration as fallback"
echo "comprehensive_migration"
;;
esac
}
# Function to execute migration strategy
execute_migration_strategy() {
local strategy="$1"
local db_url="$2"
log "Executing migration strategy: $strategy"
case "$strategy" in
"fresh_init")
execute_fresh_init "$db_url"
;;
"check_migrations")
execute_check_migrations "$db_url"
;;
"comprehensive_migration")
execute_comprehensive_migration "$db_url"
;;
*)
log "✗ Unknown migration strategy: $strategy"
return 1
;;
esac
}
# Function to execute fresh database initialization
execute_fresh_init() {
local db_url="$1"
log "Executing fresh database initialization..."
# Initialize Flask-Migrate
if ! flask db init >/dev/null 2>&1; then
log "✗ Flask-Migrate initialization failed"
return 1
fi
log "✓ Flask-Migrate initialized"
# Create initial migration
if ! flask db migrate -m "Initial database schema" >/dev/null 2>&1; then
log "✗ Initial migration creation failed"
return 1
fi
log "✓ Initial migration created"
# Apply migration
if ! flask db upgrade >/dev/null 2>&1; then
log "✗ Initial migration application failed"
return 1
fi
log "✓ Initial migration applied"
return 0
}
# Function to check and apply pending migrations
execute_check_migrations() {
local db_url="$1"
log "Checking for pending migrations..."
# Check current migration status
local current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
log "Current migration revision: $current_revision"
# Check for pending migrations
if ! flask db upgrade >/dev/null 2>&1; then
log "✗ Migration check failed"
return 1
fi
log "✓ Migrations checked and applied"
return 0
}
# Function to execute comprehensive migration
execute_comprehensive_migration() {
local db_url="$1"
log "Executing comprehensive migration..."
# Try to run the enhanced startup script
if [[ -f "/app/docker/startup_with_migration.py" ]]; then
log "Running enhanced startup script..."
if python /app/docker/startup_with_migration.py; then
log "✓ Enhanced startup script completed successfully"
return 0
else
log "⚠ Enhanced startup script failed, falling back to manual migration"
fi
fi
# Fallback to manual migration
log "Executing manual migration fallback..."
# Initialize Flask-Migrate if not already done
if [[ ! -f "/app/migrations/env.py" ]]; then
if ! flask db init >/dev/null 2>&1; then
log "✗ Flask-Migrate initialization failed"
return 1
fi
log "✓ Flask-Migrate initialized"
fi
# Create baseline migration
if ! flask db migrate -m "Baseline from existing database" >/dev/null 2>&1; then
log "✗ Baseline migration creation failed"
return 1
fi
log "✓ Baseline migration created"
# Stamp database as current
if ! flask db stamp head >/dev/null 2>&1; then
log "✗ Database stamping failed"
return 1
fi
log "✓ Database stamped as current"
return 0
}
# Function to verify database integrity
verify_database_integrity() {
local db_url="$1"
log "Verifying database integrity..."
# Test basic database operations
if [[ "$db_url" == postgresql://* ]]; then
if command_exists psql; then
# Check if key tables exist
local key_tables=$(psql "$db_url" -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_name IN ('users', 'projects', 'time_entries')
AND table_schema = 'public';
" 2>/dev/null | grep -v '^$' | tr '\n' ' ')
if [[ $(echo "$key_tables" | wc -w) -ge 2 ]]; then
log "✓ Database integrity verified (PostgreSQL)"
return 0
else
log "✗ Missing key tables: [$key_tables]"
return 1
fi
else
# Fallback to Python verification
if python -c "
import psycopg2
try:
conn = psycopg2.connect('$db_url')
cursor = conn.cursor()
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_name IN ('users', 'projects', 'time_entries')
AND table_schema = 'public'
\"\"\")
key_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if len(key_tables) >= 2:
exit(0)
else:
exit(1)
except:
exit(1)
" >/dev/null 2>&1; then
log "✓ Database integrity verified (PostgreSQL via Python)"
return 0
else
log "✗ Database integrity check failed (PostgreSQL)"
return 1
fi
fi
elif [[ "$db_url" == sqlite://* ]]; then
local db_file="${db_url#sqlite://}"
if [[ ! -f "$db_file" ]]; then
log "✗ SQLite database file not found"
return 1
fi
if python -c "
import sqlite3
try:
conn = sqlite3.connect('$db_file')
cursor = conn.cursor()
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name IN (\"users\", \"projects\", \"time_entries\")')
key_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if len(key_tables) >= 2:
exit(0)
else:
exit(1)
except:
exit(1)
" >/dev/null 2>&1; then
log "✓ Database integrity verified (SQLite)"
return 0
else
log "✗ Database integrity check failed (SQLite)"
return 1
fi
fi
return 1
}
# Main execution
main() {
log "=== TimeTracker Docker Entrypoint with Migration Detection ==="
# Set environment variables
export FLASK_APP=${FLASK_APP:-/app/app.py}
# Get database URL from environment
local db_url="${DATABASE_URL}"
if [[ -z "$db_url" ]]; then
log "✗ DATABASE_URL environment variable not set"
exit 1
fi
log "Database URL: $db_url"
# Wait for database to be available
if ! wait_for_database "$db_url"; then
log "✗ Failed to connect to database"
exit 1
fi
# Detect database state
local db_state=$(detect_database_state "$db_url")
log "Detected database state: $db_state"
# Choose migration strategy
local strategy=$(choose_migration_strategy "$db_state")
log "Selected migration strategy: $strategy"
# Execute migration strategy
if ! execute_migration_strategy "$strategy" "$db_url"; then
log "✗ Migration strategy execution failed"
exit 1
fi
# Verify database integrity
if ! verify_database_integrity "$db_url"; then
log "✗ Database integrity verification failed"
exit 1
fi
log "=== Startup and Migration Complete ==="
log "Database is ready for use"
# Show final migration status
local final_status=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
log "Final migration status: $final_status"
# Start the application
log "Starting TimeTracker application..."
exec "$@"
}
# Run main function with all arguments
main "$@"

660
docker/entrypoint_fixed.sh Normal file
View File

@@ -0,0 +1,660 @@
#!/bin/bash
# TimeTracker Docker Entrypoint with Automatic Migration Detection
# This script automatically detects database state and chooses the correct migration strategy
# Don't exit on errors - let the script continue and show what's happening
# set -e
echo "=== TimeTracker Docker Container Starting ==="
echo "Timestamp: $(date)"
echo "Container ID: $(hostname)"
echo "Python version: $(python --version 2>/dev/null || echo 'Python not available')"
echo "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
echo "Current directory: $(pwd)"
echo "User: $(whoami)"
echo
# Function to log messages with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to wait for database
wait_for_database() {
local db_url="$1"
local max_retries=60 # Increased retries
local retry_delay=3 # Increased delay
log "Waiting for database to be available..."
log "Database URL: $db_url"
for attempt in $(seq 1 $max_retries); do
log "Attempt $attempt/$max_retries to connect to database..."
if [[ "$db_url" == postgresql* ]]; then
log "Testing PostgreSQL connection..."
# Test 1: Try psql if available
if command_exists psql; then
log "Testing with psql..."
if psql "$db_url" -c "SELECT 1" >/dev/null 2>&1; then
log "✓ PostgreSQL database is available (via psql)"
return 0
else
log "psql connection failed"
fi
else
log "psql not available, skipping psql test"
fi
# Test 2: Always try Python connection
log "Testing with Python psycopg2..."
if python -c "
import psycopg2
import sys
try:
# Parse connection string to remove +psycopg2 if present
conn_str = '$db_url'.replace('+psycopg2://', '://')
print(f'Attempting connection to: {conn_str}')
conn = psycopg2.connect(conn_str)
conn.close()
print('Connection successful')
sys.exit(0)
except Exception as e:
print(f'Connection failed: {e}')
sys.exit(1)
" 2>/dev/null; then
log "✓ PostgreSQL database is available (via Python)"
return 0
else
log "Python connection failed"
fi
elif [[ "$db_url" == sqlite://* ]]; then
log "Testing SQLite connection..."
local db_file="${db_url#sqlite://}"
if [[ -f "$db_file" ]] || [[ -w "$(dirname "$db_file")" ]]; then
log "✓ SQLite database is available"
return 0
else
log "SQLite file not accessible"
fi
else
log "Unknown database URL format: $db_url"
fi
log "Database not ready (attempt $attempt/$max_retries)"
if [[ $attempt -lt $max_retries ]]; then
log "Waiting $retry_delay seconds before next attempt..."
sleep $retry_delay
fi
done
log "✗ Database is not available after maximum retries"
return 1
}
# Function to detect database state
detect_database_state() {
local db_url="$1"
if [[ "$db_url" == postgresql* ]]; then
# Simple direct Python execution without temp files
python -c "
import psycopg2
try:
conn_str = '$db_url'.replace('+psycopg2://', '://')
conn = psycopg2.connect(conn_str)
cursor = conn.cursor()
cursor.execute(\"\"\"
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'alembic_version'
)
\"\"\")
has_alembic = cursor.fetchone()[0]
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
\"\"\")
existing_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if has_alembic:
print('migrated')
elif existing_tables:
print('legacy')
else:
print('fresh')
except Exception as e:
print('unknown')
" 2>/dev/null
elif [[ "$db_url" == sqlite://* ]]; then
local db_file="${db_url#sqlite://}"
if [[ ! -f "$db_file" ]]; then
echo "fresh"
return
fi
python -c "
import sqlite3
try:
conn = sqlite3.connect('$db_file')
cursor = conn.cursor()
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"alembic_version\"')
has_alembic = cursor.fetchone() is not None
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\"')
existing_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if has_alembic:
print('migrated')
elif existing_tables:
print('legacy')
else:
print('fresh')
except Exception as e:
print('unknown')
" 2>/dev/null
else
echo "unknown"
fi
}
# Function to choose migration strategy
choose_migration_strategy() {
local db_state="$1"
case "$db_state" in
"fresh")
echo "fresh_init"
;;
"migrated")
echo "check_migrations"
;;
"legacy")
echo "comprehensive_migration"
;;
*)
echo "comprehensive_migration"
;;
esac
}
# Function to execute migration strategy
execute_migration_strategy() {
local strategy="$1"
local db_url="$2"
log "Executing migration strategy: '$strategy'"
case "$strategy" in
"fresh_init")
log "Executing fresh_init strategy..."
execute_fresh_init "$db_url"
;;
"check_migrations")
log "Executing check_migrations strategy..."
execute_check_migrations "$db_url"
;;
"comprehensive_migration")
log "Executing comprehensive_migration strategy..."
execute_comprehensive_migration "$db_url"
;;
*)
log "✗ Unknown migration strategy: '$strategy'"
return 1
;;
esac
}
# Function to execute fresh database initialization
execute_fresh_init() {
local db_url="$1"
log "Executing fresh database initialization..."
# Set FLASK_APP if not already set
if [[ -z "$FLASK_APP" ]]; then
log "⚠ FLASK_APP not set, setting it to app.py"
export FLASK_APP=app.py
fi
# Check if migrations directory already exists
if [[ -d "/app/migrations" ]]; then
log "⚠ Migrations directory already exists, checking if it's properly configured..."
# Check if we have the required files
if [[ -f "/app/migrations/env.py" && -f "/app/migrations/alembic.ini" ]]; then
log "✓ Migrations directory is properly configured, skipping init"
log "Checking if we need to create initial migration..."
# Check if we have any migration versions
if [[ ! -d "/app/migrations/versions" ]] || [[ -z "$(ls -A /app/migrations/versions 2>/dev/null)" ]]; then
log "No migration versions found, creating initial migration..."
if ! flask db migrate -m "Initial database schema"; then
log "✗ Initial migration creation failed"
log "Error details:"
flask db migrate -m "Initial database schema" 2>&1 | head -20
return 1
fi
log "✓ Initial migration created"
else
log "✓ Migration versions already exist"
fi
# Check current migration status
log "Checking current migration status..."
local current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "none")
log "Current migration revision: $current_revision"
if [[ "$current_revision" == "none" ]]; then
log "Database not stamped, stamping with current revision..."
local head_revision=$(flask db heads 2>/dev/null | tr -d '\n' || echo "")
if [[ -n "$head_revision" ]]; then
if ! flask db stamp "$head_revision"; then
log "✗ Database stamping failed"
log "Error details:"
flask db stamp "$head_revision" 2>&1 | head -20
return 1
fi
log "✓ Database stamped with revision: $head_revision"
fi
fi
# Apply any pending migrations
log "Applying pending migrations..."
if ! flask db upgrade; then
log "✗ Migration application failed"
log "Error details:"
flask db upgrade 2>&1 | head -20
return 1
fi
log "✓ Migrations applied"
# Wait a moment for tables to be fully committed
log "Waiting for tables to be fully committed..."
sleep 2
return 0
else
log "⚠ Migrations directory exists but is incomplete, removing it..."
rm -rf /app/migrations
fi
fi
# Check if we're in the right directory
if [[ ! -f "/app/app.py" ]]; then
log "⚠ Not in correct directory, changing to /app"
cd /app
fi
# Initialize Flask-Migrate
log "Initializing Flask-Migrate..."
if ! flask db init; then
log "✗ Flask-Migrate initialization failed"
log "Error details:"
flask db init 2>&1 | head -20
return 1
fi
log "✓ Flask-Migrate initialized"
# Create initial migration
log "Creating initial migration..."
if ! flask db migrate -m "Initial database schema"; then
log "✗ Initial migration creation failed"
log "Error details:"
flask db migrate -m "Initial database schema" 2>&1 | head -20
return 1
fi
log "✓ Initial migration created"
# Apply migration
log "Applying initial migration..."
if ! flask db upgrade; then
log "✗ Initial migration application failed"
log "Error details:"
flask db upgrade 2>&1 | head -20
return 1
fi
log "✓ Initial migration applied"
return 0
}
# Function to check and apply pending migrations
execute_check_migrations() {
local db_url="$1"
log "Checking for pending migrations..."
# Check current migration status
local current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
log "Current migration revision: $current_revision"
# Check for pending migrations
if ! flask db upgrade; then
log "✗ Migration check failed"
log "Error details:"
flask db upgrade 2>&1 | head -20
return 1
fi
log "✓ Migrations checked and applied"
return 0
}
# Function to execute comprehensive migration
execute_comprehensive_migration() {
local db_url="$1"
log "Executing comprehensive migration..."
# Try to run the enhanced startup script
if [[ -f "/app/docker/startup_with_migration.py" ]]; then
log "Running enhanced startup script..."
if python /app/docker/startup_with_migration.py; then
log "✓ Enhanced startup script completed successfully"
return 0
else
log "⚠ Enhanced startup script failed, falling back to manual migration"
fi
fi
# Fallback to manual migration
log "Executing manual migration fallback..."
# Initialize Flask-Migrate if not already done
if [[ ! -f "/app/migrations/env.py" ]]; then
if ! flask db init; then
log "✗ Flask-Migrate initialization failed"
log "Error details:"
flask db init 2>&1 | head -20
return 1
fi
log "✓ Flask-Migrate initialized"
fi
# Create baseline migration
if ! flask db migrate -m "Baseline from existing database"; then
log "✗ Baseline migration creation failed"
log "Error details:"
flask db migrate -m "Baseline from existing database" 2>&1 | head -20
return 1
fi
log "✓ Baseline migration created"
# Stamp database as current
if ! flask db stamp head; then
log "✗ Database stamping failed"
log "Error details:"
flask db stamp head 2>&1 | head -20
return 1
fi
log "✓ Database stamped as current"
return 0
}
# Function to verify database integrity
verify_database_integrity() {
local db_url="$1"
log "Verifying database integrity..."
# Try up to 3 times with delays
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
log "Database integrity check attempt $attempt/$max_attempts..."
# Test basic database operations
if [[ "$db_url" == postgresql* ]]; then
log "Checking PostgreSQL database integrity..."
if python -c "
import psycopg2
try:
# Parse connection string to remove +psycopg2 if present
conn_str = '$db_url'.replace('+psycopg2://', '://')
conn = psycopg2.connect(conn_str)
cursor = conn.cursor()
# Check for key tables that should exist after migration
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_name IN ('clients', 'users', 'projects', 'tasks', 'time_entries', 'settings', 'invoices', 'invoice_items')
AND table_schema = 'public'
ORDER BY table_name
\"\"\")
key_tables = [row[0] for row in cursor.fetchall()]
# Also check if alembic_version table exists
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_name = 'alembic_version'
AND table_schema = 'public'
\"\"\")
alembic_table = cursor.fetchone()
conn.close()
print(f'Found tables: {key_tables}')
print(f'Alembic version table: {alembic_table[0] if alembic_table else \"missing\"}')
# Require at least the core tables and alembic_version
if len(key_tables) >= 5 and alembic_table:
exit(0)
else:
print(f'Expected at least 5 core tables, found {len(key_tables)}')
print(f'Expected alembic_version table: {bool(alembic_table)}')
exit(1)
except Exception as e:
print(f'Error during integrity check: {e}')
exit(1)
"; then
log "✓ Database integrity verified (PostgreSQL via Python)"
return 0
else
log "✗ Database integrity check failed (PostgreSQL)"
log "Error details:"
python -c "
import psycopg2
try:
conn_str = '$db_url'.replace('+psycopg2://', '://')
conn = psycopg2.connect(conn_str)
cursor = conn.cursor()
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
\"\"\")
all_tables = [row[0] for row in cursor.fetchall()]
cursor.execute(\"\"\"
SELECT table_name
FROM information_schema.tables
WHERE table_name = 'alembic_version'
AND table_schema = 'public'
\"\"\)
alembic_table = cursor.fetchone()
conn.close()
print(f'All tables in database: {all_tables}')
print(f'Alembic version table exists: {bool(alembic_table)}')
except Exception as e:
print(f'Error getting table list: {e}')
" 2>&1 | head -20
if [[ $attempt -lt $max_attempts ]]; then
log "Waiting 3 seconds before retry..."
sleep 3
attempt=$((attempt + 1))
continue
else
return 1
fi
fi
elif [[ "$db_url" == sqlite://* ]]; then
local db_file="${db_url#sqlite://}"
if [[ ! -f "$db_file" ]]; then
log "✗ SQLite database file not found"
return 1
fi
log "Checking SQLite database integrity..."
if python -c "
import sqlite3
try:
conn = sqlite3.connect('$db_file')
cursor = conn.cursor()
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name IN (\"clients\", \"users\", \"projects\", \"tasks\", \"time_entries\", \"settings\", \"invoices\", \"invoice_items\")')
key_tables = [row[0] for row in cursor.fetchall()]
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"alembic_version\"')
alembic_table = cursor.fetchone()
conn.close()
print(f'Found tables: {key_tables}')
print(f'Alembic version table: {alembic_table[0] if alembic_table else \"missing\"}')
if len(key_tables) >= 5 and alembic_table:
exit(0)
else:
print(f'Expected at least 5 core tables, found {len(key_tables)}')
print(f'Expected alembic_version table: {bool(alembic_table)}')
exit(1)
except Exception as e:
print(f'Error during integrity check: {e}')
exit(1)
"; then
log "✓ Database integrity verified (SQLite)"
return 0
else
log "✗ Database integrity check failed (SQLite)"
if [[ $attempt -lt $max_attempts ]]; then
log "Waiting 3 seconds before retry..."
sleep 3
attempt=$((attempt + 1))
continue
else
return 1
fi
fi
else
log "✗ Unsupported database type: $db_url"
return 1
fi
# If we get here, the check failed
if [[ $attempt -lt $max_attempts ]]; then
log "Waiting 3 seconds before retry..."
sleep 3
attempt=$((attempt + 1))
else
return 1
fi
done
return 1
}
# Main execution
main() {
log "=== TimeTracker Docker Entrypoint with Migration Detection ==="
# Set environment variables
export FLASK_APP=${FLASK_APP:-/app/app.py}
# Get database URL from environment
local db_url="${DATABASE_URL}"
if [[ -z "$db_url" ]]; then
log "✗ DATABASE_URL environment variable not set"
log "Available environment variables:"
env | grep -E "(DATABASE|FLASK|PYTHON)" | sort
exit 1
fi
log "Database URL: $db_url"
# Wait for database to be available
if ! wait_for_database "$db_url"; then
log "✗ Failed to connect to database"
log "Trying to run simple test script for debugging..."
if [[ -f "/app/docker/simple_test.sh" ]]; then
/app/docker/simple_test.sh
fi
exit 1
fi
# Detect database state
local db_state=$(detect_database_state "$db_url")
log "Raw db_state output: '${db_state}'"
log "Detected database state: '$db_state'"
# Choose migration strategy
local strategy=$(choose_migration_strategy "$db_state")
log "Raw strategy output: '${strategy}'"
log "Selected migration strategy: '$strategy'"
# Log the strategy selection details
case "$db_state" in
"fresh")
log "Fresh database detected - using standard initialization"
;;
"migrated")
log "Database already migrated - checking for pending migrations"
;;
"legacy")
log "Legacy database detected - using comprehensive migration"
;;
*)
log "Unknown database state: '$db_state' - using comprehensive migration as fallback"
;;
esac
# Execute migration strategy
if ! execute_migration_strategy "$strategy" "$db_url"; then
log "✗ Migration strategy execution failed"
exit 1
fi
# Verify database integrity
if ! verify_database_integrity "$db_url"; then
log "✗ Database integrity verification failed"
exit 1
fi
log "=== Startup and Migration Complete ==="
log "Database is ready for use"
# Show final migration status
local final_status=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
log "Final migration status: $final_status"
# Start the application
log "Starting TimeTracker application..."
exec "$@"
}
# Run main function with all arguments
main "$@"

85
docker/simple_test.sh Normal file
View File

@@ -0,0 +1,85 @@
#!/bin/bash
set -e
echo "=== Simple Test Script ==="
echo "Timestamp: $(date)"
echo "Container ID: $(hostname)"
echo "Current directory: $(pwd)"
echo "User: $(whoami)"
echo
# Test 1: Basic environment
echo "=== Test 1: Environment ==="
echo "DATABASE_URL: ${DATABASE_URL:-'NOT SET'}"
echo "FLASK_APP: ${FLASK_APP:-'NOT SET'}"
echo "PATH: $PATH"
echo
# Test 2: Basic commands
echo "=== Test 2: Basic Commands ==="
echo "Python version: $(python --version)"
echo "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
echo "psql available: $(command -v psql >/dev/null 2>&1 && echo 'YES' || echo 'NO')"
echo
# Test 3: File access
echo "=== Test 3: File Access ==="
echo "Can access /app: $(test -d /app && echo 'YES' || echo 'NO')"
echo "Can access /app/docker: $(test -d /app/docker && echo 'YES' || echo 'NO')"
echo "Entrypoint script exists: $(test -f /app/docker/entrypoint.sh && echo 'YES' || echo 'NO')"
echo "Entrypoint script executable: $(test -x /app/docker/entrypoint.sh && echo 'YES' || echo 'NO')"
echo
# Test 4: Network connectivity
echo "=== Test 4: Network Connectivity ==="
if ping -c 1 db >/dev/null 2>&1; then
echo "✓ Can ping database host 'db'"
else
echo "✗ Cannot ping database host 'db'"
fi
if command -v nc >/dev/null 2>&1; then
if nc -zv db 5432 2>/dev/null; then
echo "✓ Database port 5432 is accessible"
else
echo "✗ Database port 5432 is not accessible"
fi
else
echo "⚠ netcat not available, skipping port test"
fi
echo
# Test 5: Python connection test
echo "=== Test 5: Python Connection Test ==="
if [[ -n "${DATABASE_URL}" ]]; then
echo "Testing connection with Python..."
if python -c "
import psycopg2
import sys
try:
# Parse connection string to remove +psycopg2 if present
conn_str = '${DATABASE_URL}'.replace('+psycopg2://', '://')
print(f'Trying to connect to: {conn_str}')
conn = psycopg2.connect(conn_str)
cursor = conn.cursor()
cursor.execute('SELECT 1')
result = cursor.fetchone()
print(f'✓ Connection successful, test query result: {result}')
conn.close()
sys.exit(0)
except Exception as e:
print(f'✗ Connection failed: {e}')
sys.exit(1)
"; then
echo "✓ Python connection test successful"
else
echo "✗ Python connection test failed"
fi
else
echo "⚠ DATABASE_URL not set, skipping connection test"
fi
echo
echo "=== Test Complete ==="
echo "If you see this message, the basic script execution is working."
echo "Check the output above for any specific failures."

View File

@@ -0,0 +1,382 @@
#!/usr/bin/env python3
"""
Enhanced Startup Script with Automatic Migration Detection
This script automatically detects database state and chooses the correct migration strategy
"""
import os
import sys
import time
import subprocess
import sqlite3
import psycopg2
from pathlib import Path
from datetime import datetime
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('/var/log/timetracker_startup.log')
]
)
logger = logging.getLogger(__name__)
def wait_for_database(db_url, max_retries=60, retry_delay=3):
"""Wait for database to be available"""
logger.info(f"Waiting for database to be available: {db_url}")
for attempt in range(max_retries):
try:
if db_url.startswith('postgresql'):
# Handle both postgresql:// and postgresql+psycopg2:// URLs
clean_url = db_url.replace('+psycopg2://', '://')
conn = psycopg2.connect(clean_url)
conn.close()
logger.info("✓ PostgreSQL database is available")
return True
elif db_url.startswith('sqlite'):
# For SQLite, just check if the file exists or can be created
db_file = db_url.replace('sqlite:///', '')
if os.path.exists(db_file) or os.access(os.path.dirname(db_file), os.W_OK):
logger.info("✓ SQLite database is available")
return True
except Exception as e:
logger.info(f"Database not ready (attempt {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
logger.error("Database is not available after maximum retries")
return False
def detect_database_state(db_url):
"""Detect the current state of the database"""
logger.info("Analyzing database state...")
try:
if db_url.startswith('postgresql'):
# Handle both postgresql:// and postgresql+psycopg2:// URLs
clean_url = db_url.replace('+psycopg2://', '://')
conn = psycopg2.connect(clean_url)
cursor = conn.cursor()
# Check if alembic_version table exists
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'alembic_version'
)
""")
has_alembic = cursor.fetchone()[0]
# Get list of existing tables
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
""")
existing_tables = [row[0] for row in cursor.fetchall()]
# Check if this is a fresh database
is_fresh = len(existing_tables) == 0
conn.close()
logger.info(f"Database state: has_alembic={has_alembic}, tables={existing_tables}, is_fresh={is_fresh}")
if has_alembic:
return 'migrated', existing_tables
elif existing_tables:
return 'legacy', existing_tables
else:
return 'fresh', []
elif db_url.startswith('sqlite'):
db_file = db_url.replace('sqlite:///', '')
if not os.path.exists(db_file):
return 'fresh', []
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# Check if alembic_version table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='alembic_version'")
has_alembic = cursor.fetchone() is not None
# Get list of existing tables
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
existing_tables = [row[0] for row in cursor.fetchall()]
conn.close()
logger.info(f"Database state: has_alembic={has_alembic}, tables={existing_tables}")
if has_alembic:
return 'migrated', existing_tables
elif existing_tables:
return 'legacy', existing_tables
else:
return 'fresh', []
except Exception as e:
logger.error(f"Error detecting database state: {e}")
return 'unknown', []
return 'unknown', []
def choose_migration_strategy(db_state, existing_tables):
"""Choose the appropriate migration strategy based on database state"""
logger.info(f"Choosing migration strategy for state: {db_state}")
if db_state == 'fresh':
logger.info("Fresh database detected - using standard initialization")
return 'fresh_init'
elif db_state == 'migrated':
logger.info("Database already migrated - checking for pending migrations")
return 'check_migrations'
elif db_state == 'legacy':
logger.info("Legacy database detected - using comprehensive migration")
return 'comprehensive_migration'
else:
logger.warning("Unknown database state - using comprehensive migration as fallback")
return 'comprehensive_migration'
def execute_migration_strategy(strategy, db_url):
"""Execute the chosen migration strategy"""
logger.info(f"Executing migration strategy: {strategy}")
try:
if strategy == 'fresh_init':
return execute_fresh_init(db_url)
elif strategy == 'check_migrations':
return execute_check_migrations(db_url)
elif strategy == 'comprehensive_migration':
return execute_comprehensive_migration(db_url)
else:
logger.error(f"Unknown migration strategy: {strategy}")
return False
except Exception as e:
logger.error(f"Error executing migration strategy: {e}")
return False
def execute_fresh_init(db_url):
"""Execute fresh database initialization"""
logger.info("Executing fresh database initialization...")
try:
# Initialize Flask-Migrate
result = subprocess.run(['flask', 'db', 'init'],
capture_output=True, text=True, check=True)
logger.info("✓ Flask-Migrate initialized")
# Create initial migration
result = subprocess.run(['flask', 'db', 'migrate', '-m', 'Initial database schema'],
capture_output=True, text=True, check=True)
logger.info("✓ Initial migration created")
# Apply migration
result = subprocess.run(['flask', 'db', 'upgrade'],
capture_output=True, text=True, check=True)
logger.info("✓ Initial migration applied")
return True
except subprocess.CalledProcessError as e:
logger.error(f"Fresh init failed: {e}")
logger.error(f"STDOUT: {e.stdout}")
logger.error(f"STDERR: {e.stderr}")
return False
def execute_check_migrations(db_url):
"""Check and apply any pending migrations"""
logger.info("Checking for pending migrations...")
try:
# Check current migration status
result = subprocess.run(['flask', 'db', 'current'],
capture_output=True, text=True, check=True)
current_revision = result.stdout.strip()
logger.info(f"Current migration revision: {current_revision}")
# Check for pending migrations
result = subprocess.run(['flask', 'db', 'upgrade'],
capture_output=True, text=True, check=True)
logger.info("✓ Migrations checked and applied")
return True
except subprocess.CalledProcessError as e:
logger.error(f"Migration check failed: {e}")
return False
def execute_comprehensive_migration(db_url):
"""Execute comprehensive migration for legacy databases"""
logger.info("Executing comprehensive migration...")
try:
# Run the comprehensive migration script
migration_script = '/app/migrations/migrate_existing_database.py'
if os.path.exists(migration_script):
result = subprocess.run(['python', migration_script],
capture_output=True, text=True, check=True)
logger.info("✓ Comprehensive migration completed")
return True
else:
logger.warning("Comprehensive migration script not found, falling back to manual migration")
return execute_manual_migration(db_url)
except subprocess.CalledProcessError as e:
logger.error(f"Comprehensive migration failed: {e}")
logger.error(f"STDOUT: {e.stdout}")
logger.error(f"STDERR: {e.stderr}")
return False
def execute_manual_migration(db_url):
"""Execute manual migration as fallback"""
logger.info("Executing manual migration fallback...")
try:
# Initialize Flask-Migrate if not already done
if not os.path.exists('/app/migrations/env.py'):
result = subprocess.run(['flask', 'db', 'init'],
capture_output=True, text=True, check=True)
logger.info("✓ Flask-Migrate initialized")
# Create baseline migration
result = subprocess.run(['flask', 'db', 'migrate', '-m', 'Baseline from existing database'],
capture_output=True, text=True, check=True)
logger.info("✓ Baseline migration created")
# Stamp database as current
result = subprocess.run(['flask', 'db', 'stamp', 'head'],
capture_output=True, text=True, check=True)
logger.info("✓ Database stamped as current")
return True
except subprocess.CalledProcessError as e:
logger.error(f"Manual migration failed: {e}")
return False
def verify_database_integrity(db_url):
"""Verify that the database is working correctly after migration"""
logger.info("Verifying database integrity...")
try:
# Test basic database operations
if db_url.startswith('postgresql'):
# Handle both postgresql:// and postgresql+psycopg2:// URLs
clean_url = db_url.replace('+psycopg2://', '://')
conn = psycopg2.connect(clean_url)
cursor = conn.cursor()
# Check if key tables exist
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_name IN ('users', 'projects', 'time_entries')
AND table_schema = 'public'
""")
key_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if len(key_tables) >= 2: # At least users and projects
logger.info("✓ Database integrity verified")
return True
else:
logger.error(f"Missing key tables: {key_tables}")
return False
elif db_url.startswith('sqlite'):
db_file = db_url.replace('sqlite:///', '')
if not os.path.exists(db_file):
logger.error("SQLite database file not found")
return False
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# Check if key tables exist
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name IN ('users', 'projects', 'time_entries')
""")
key_tables = [row[0] for row in cursor.fetchall()]
conn.close()
if len(key_tables) >= 2: # At least users and projects
logger.info("✓ Database integrity verified")
return True
else:
logger.error(f"Missing key tables: {key_tables}")
return False
except Exception as e:
logger.error(f"Database integrity check failed: {e}")
return False
return False
def main():
"""Main startup function"""
logger.info("=== TimeTracker Enhanced Startup with Migration Detection ===")
# Set environment variables
os.environ.setdefault('FLASK_APP', '/app/app.py')
# Get database URL from environment
db_url = os.getenv('DATABASE_URL')
if not db_url:
logger.error("DATABASE_URL environment variable not set")
sys.exit(1)
logger.info(f"Database URL: {db_url}")
# Wait for database to be available
if not wait_for_database(db_url):
logger.error("Failed to connect to database")
sys.exit(1)
# Detect database state
db_state, existing_tables = detect_database_state(db_url)
logger.info(f"Detected database state: {db_state} with {len(existing_tables)} tables")
# Choose migration strategy
strategy = choose_migration_strategy(db_state, existing_tables)
logger.info(f"Selected migration strategy: {strategy}")
# Execute migration strategy
if not execute_migration_strategy(strategy, db_url):
logger.error("Migration strategy execution failed")
sys.exit(1)
# Verify database integrity
if not verify_database_integrity(db_url):
logger.error("Database integrity verification failed")
sys.exit(1)
logger.info("=== Startup and Migration Complete ===")
logger.info("Database is ready for use")
# Show final migration status
try:
result = subprocess.run(['flask', 'db', 'current'],
capture_output=True, text=True, check=True)
logger.info(f"Final migration status: {result.stdout.strip()}")
except:
logger.info("Could not determine final migration status")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Simple Database Connection Test Script
This script tests database connectivity to help debug connection issues
"""
import os
import sys
import psycopg2
import sqlite3
from datetime import datetime
def test_postgresql_connection(db_url):
"""Test PostgreSQL connection"""
print(f"Testing PostgreSQL connection: {db_url}")
try:
# Handle both postgresql:// and postgresql+psycopg2:// URLs
clean_url = db_url.replace('+psycopg2://', '://')
print(f"Cleaned URL: {clean_url}")
# Test connection
conn = psycopg2.connect(clean_url)
cursor = conn.cursor()
# Test basic query
cursor.execute("SELECT version()")
version = cursor.fetchone()[0]
print(f"✓ PostgreSQL connection successful")
print(f" Server version: {version}")
# Test if we can access information_schema
cursor.execute("SELECT current_database(), current_user")
db_info = cursor.fetchone()
print(f" Database: {db_info[0]}")
print(f" User: {db_info[1]}")
# Check if we can list tables
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
""")
tables = [row[0] for row in cursor.fetchall()]
print(f" Tables found: {len(tables)}")
if tables:
print(f" Table names: {tables[:5]}{'...' if len(tables) > 5 else ''}")
conn.close()
return True
except Exception as e:
print(f"✗ PostgreSQL connection failed: {e}")
return False
def test_sqlite_connection(db_url):
"""Test SQLite connection"""
print(f"Testing SQLite connection: {db_url}")
try:
db_file = db_url.replace('sqlite:///', '')
print(f"Database file: {db_file}")
if not os.path.exists(db_file):
print(f" Database file does not exist, checking if directory is writable...")
dir_path = os.path.dirname(db_file) if os.path.dirname(db_file) else '.'
if os.access(dir_path, os.W_OK):
print(f" ✓ Directory is writable: {dir_path}")
return True
else:
print(f" ✗ Directory is not writable: {dir_path}")
return False
# Test connection
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# Test basic query
cursor.execute("SELECT sqlite_version()")
version = cursor.fetchone()[0]
print(f"✓ SQLite connection successful")
print(f" SQLite version: {version}")
# Check if we can list tables
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [row[0] for row in cursor.fetchall()]
print(f" Tables found: {len(tables)}")
if tables:
print(f" Table names: {tables[:5]}{'...' if len(tables) > 5 else ''}")
conn.close()
return True
except Exception as e:
print(f"✗ SQLite connection failed: {e}")
return False
def main():
"""Main function"""
print("=== Database Connection Test ===")
print(f"Timestamp: {datetime.now()}")
print()
# Get database URL from environment
db_url = os.getenv('DATABASE_URL')
if not db_url:
print("✗ DATABASE_URL environment variable not set")
print("Please set DATABASE_URL to test database connection")
sys.exit(1)
print(f"Database URL: {db_url}")
print()
# Test connection based on database type
success = False
if db_url.startswith('postgresql'):
success = test_postgresql_connection(db_url)
elif db_url.startswith('sqlite'):
success = test_sqlite_connection(db_url)
else:
print(f"✗ Unknown database type: {db_url}")
print("Supported types: postgresql://, sqlite://")
sys.exit(1)
print()
if success:
print("🎉 Database connection test successful!")
sys.exit(0)
else:
print("❌ Database connection test failed!")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,358 @@
# Complete Database Migration Guide
This guide covers **ALL** possible migration scenarios from any previous database state to the new Flask-Migrate system.
## 🎯 Migration Scenarios Covered
### 1. **Fresh Installation** (No existing database)
- New project setup
- First-time deployment
### 2. **Existing Database with Old Custom Migrations**
- Legacy migration scripts
- Custom Python migration files
- Manual schema changes
### 3. **Existing Database with Mixed Schema**
- Some tables exist, some missing
- Incomplete schema
- Partial migrations
### 4. **Existing Database with Data**
- Production databases with user data
- Development databases with test data
- Staging databases
### 5. **Database from Different Versions**
- Old TimeTracker versions
- Different schema versions
- Incompatible table structures
## 🚀 Migration Methods
### **Method 1: Automated Migration (Recommended)**
#### **For Any Existing Database:**
```bash
# Run the comprehensive migration script
python migrations/migrate_existing_database.py
```
This script will:
- ✅ Detect your database type (PostgreSQL/SQLite)
- ✅ Create a complete backup
- ✅ Analyze existing schema
- ✅ Create migration strategy
- ✅ Initialize Flask-Migrate
- ✅ Create baseline migration
- ✅ Preserve all existing data
- ✅ Verify migration success
#### **For Legacy Schema Migration:**
```bash
# Run the legacy schema migration script
python migrations/legacy_schema_migration.py
```
This script handles:
- ✅ Old `projects.client``projects.client_id` conversion
- ✅ Missing columns in settings table
- ✅ Legacy table structures
- ✅ Data preservation
### **Method 2: Manual Migration**
#### **Step-by-Step Process:**
1. **Backup Your Database**
```bash
# PostgreSQL
pg_dump --format=custom --dbname="$DATABASE_URL" --file=backup_$(date +%Y%m%d_%H%M%S).dump
# SQLite
cp instance/timetracker.db backup_timetracker_$(date +%Y%m%d_%H%M%S).db
```
2. **Initialize Flask-Migrate**
```bash
flask db init
```
3. **Create Baseline Migration**
```bash
flask db migrate -m "Baseline from existing database"
```
4. **Review Generated Migration**
```bash
# Check the generated file in migrations/versions/
cat migrations/versions/*.py
```
5. **Stamp Database as Current**
```bash
flask db stamp head
```
6. **Apply Any Pending Migrations**
```bash
flask db upgrade
```
### **Method 3: Setup Scripts**
#### **Windows:**
```bash
scripts\setup-migrations.bat
```
#### **Linux/Mac:**
```bash
scripts/setup-migrations.sh
```
#### **Python (Cross-platform):**
```bash
python migrations/manage_migrations.py
```
## 🔍 Pre-Migration Analysis
### **Check Your Current State:**
```bash
# Check if Flask-Migrate is available
python -c "import flask_migrate; print('✓ Flask-Migrate available')"
# Check database connection
python -c "from app import create_app; app = create_app(); print('✓ Database accessible')"
# Check existing tables (if any)
python -c "
from app import create_app, db
app = create_app()
with app.app_context():
inspector = db.inspect(db.engine)
tables = inspector.get_table_names()
print(f'Existing tables: {tables}')
"
```
### **Common Database States:**
| State | Description | Migration Approach |
|-------|-------------|-------------------|
| **No Database** | Fresh installation | `flask db init` → `flask db migrate` → `flask db upgrade` |
| **Empty Database** | Tables exist but no data | `flask db stamp head` → `flask db upgrade` |
| **Partial Schema** | Some tables missing | Run comprehensive migration script |
| **Legacy Schema** | Old table structures | Run legacy schema migration script |
| **Complete Schema** | All tables exist | `flask db stamp head` |
## 🛡️ Safety Measures
### **Automatic Backups:**
- ✅ Database backup before any migration
- ✅ Timestamped backup files
- ✅ Multiple backup formats (dump, copy)
- ✅ Backup verification
### **Migration Verification:**
- ✅ Schema analysis before migration
- ✅ Data integrity checks
- ✅ Rollback capability
- ✅ Migration status verification
### **Error Handling:**
- ✅ Graceful failure handling
- ✅ Detailed error messages
- ✅ Recovery instructions
- ✅ Safe fallback options
## 📋 Migration Checklist
### **Before Migration:**
- [ ] **Backup your database** (automatic with scripts)
- [ ] **Check disk space** (need space for backup + migration)
- [ ] **Stop application** (if running)
- [ ] **Verify dependencies** (Flask-Migrate installed)
- [ ] **Check permissions** (database access)
### **During Migration:**
- [ ] **Run migration script** (automatic or manual)
- [ ] **Review generated files** (check migrations/versions/)
- [ ] **Verify backup creation** (check backup files)
- [ ] **Monitor progress** (watch for errors)
### **After Migration:**
- [ ] **Test application** (start and verify functionality)
- [ ] **Check migration status** (`flask db current`)
- [ ] **Verify data integrity** (check key tables)
- [ ] **Update deployment scripts** (if using CI/CD)
## 🔧 Troubleshooting
### **Common Issues & Solutions:**
#### **1. Migration Already Applied**
```bash
# Check current status
flask db current
# If migration is already applied, stamp the database
flask db stamp head
```
#### **2. Schema Conflicts**
```bash
# Show migration heads
flask db heads
# Merge branches if needed
flask db merge -m "Merge migration branches" <revision1> <revision2>
```
#### **3. Database Out of Sync**
```bash
# Check migration history
flask db history
# Reset to specific revision
flask db stamp <revision>
```
#### **4. Permission Errors**
```bash
# Check database permissions
# Ensure user has CREATE, ALTER, INSERT privileges
# For PostgreSQL: GRANT ALL PRIVILEGES ON DATABASE timetracker TO timetracker;
```
#### **5. Connection Issues**
```bash
# Verify DATABASE_URL environment variable
echo $DATABASE_URL
# Test connection manually
python -c "from app import create_app; app = create_app(); print('Connection OK')"
```
### **Recovery Options:**
#### **If Migration Fails:**
1. **Check backup files** - your data is safe
2. **Review error logs** - identify the issue
3. **Fix the problem** - resolve conflicts/errors
4. **Restart migration** - run script again
#### **If Data is Lost:**
1. **Restore from backup** - use pg_restore or copy SQLite file
2. **Verify restoration** - check data integrity
3. **Contact support** - if issues persist
## 📚 Advanced Migration Scenarios
### **Handling Complex Schema Changes:**
#### **Custom Data Migrations:**
```python
# In your migration file
def upgrade():
# Custom data transformation
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
# Complex table modifications
op.execute("""
INSERT INTO new_table (id, name, status)
SELECT id, name, 'active' FROM old_table
WHERE status = 'enabled'
""")
```
#### **Conditional Migrations:**
```python
def upgrade():
# Handle different database types
if op.get_bind().dialect.name == 'postgresql':
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
elif op.get_bind().dialect.name == 'sqlite':
# SQLite-specific operations
pass
```
#### **Rollback Strategies:**
```python
def downgrade():
# Always provide rollback capability
op.drop_table('new_table')
op.drop_column('existing_table', 'new_column')
```
## 🚀 Post-Migration
### **Verification Steps:**
```bash
# Check migration status
flask db current
# View migration history
flask db history
# Test database connection
python -c "from app import create_app, db; app = create_app(); print('OK')"
# Verify key tables
python -c "
from app import create_app, db
from app.models import User, Project, TimeEntry
app = create_app()
with app.app_context():
users = User.query.count()
projects = Project.query.count()
entries = TimeEntry.query.count()
print(f'Users: {users}, Projects: {projects}, Entries: {entries}')
"
```
### **Future Migrations:**
```bash
# Create new migration
flask db migrate -m "Add new feature"
# Apply migration
flask db upgrade
# Rollback if needed
flask db downgrade
```
## 📞 Support & Help
### **Getting Help:**
1. **Check this guide** - covers most scenarios
2. **Review migration logs** - detailed error information
3. **Check Flask-Migrate docs** - https://flask-migrate.readthedocs.io/
4. **Check Alembic docs** - https://alembic.sqlalchemy.org/
### **Emergency Recovery:**
```bash
# If everything fails, restore from backup
# PostgreSQL:
pg_restore --clean --dbname="$DATABASE_URL" backup_file.dump
# SQLite:
cp backup_file.db instance/timetracker.db
```
## 🎉 Success Indicators
Your migration is successful when:
- ✅ `flask db current` shows a revision number
- ✅ `flask db history` shows migration timeline
- ✅ Application starts without database errors
- ✅ All existing data is accessible
- ✅ New features work correctly
- ✅ Backup files are created and accessible
---
**Remember**: The migration scripts are designed to be **safe** and **reversible**. Your data is automatically backed up, and you can always restore if needed. When in doubt, run the comprehensive migration script - it handles all scenarios automatically!

269
migrations/README.md Normal file
View File

@@ -0,0 +1,269 @@
# Database Migrations with Flask-Migrate
This directory contains the database migration system for TimeTracker, now standardized on Flask-Migrate with proper versioning.
## Overview
The migration system has been updated from custom Python scripts to use Flask-Migrate, which provides:
- **Standardized migrations** using Alembic
- **Version tracking** for all database changes
- **Rollback capabilities** to previous versions
- **Automatic schema detection** from SQLAlchemy models
- **Cross-database compatibility** (PostgreSQL, SQLite)
## Quick Start
### 1. Initialize Migrations (First Time Only)
```bash
flask db init
```
### 2. Create Your First Migration
```bash
flask db migrate -m "Initial database schema"
```
### 3. Apply Migrations
```bash
flask db upgrade
```
## Migration Commands
### Basic Commands
- `flask db init` - Initialize migrations directory
- `flask db migrate -m "Description"` - Create a new migration
- `flask db upgrade` - Apply pending migrations
- `flask db downgrade` - Rollback last migration
- `flask db current` - Show current migration version
- `flask db history` - Show migration history
### Advanced Commands
- `flask db show <revision>` - Show specific migration details
- `flask db stamp <revision>` - Mark database as being at specific revision
- `flask db heads` - Show current heads (for branched migrations)
## Migration Workflow
### 1. Make Model Changes
Edit your SQLAlchemy models in `app/models/`:
```python
# Example: Add a new column
class User(db.Model):
# ... existing fields ...
phone_number = db.Column(db.String(20), nullable=True)
```
### 2. Generate Migration
```bash
flask db migrate -m "Add phone number to users"
```
### 3. Review Generated Migration
Check the generated migration file in `migrations/versions/`:
```python
def upgrade():
op.add_column('users', sa.Column('phone_number', sa.String(length=20), nullable=True))
def downgrade():
op.drop_column('users', 'phone_number')
```
### 4. Apply Migration
```bash
flask db upgrade
```
### 5. Verify Changes
Check the migration status:
```bash
flask db current
```
## Migration Files Structure
```
migrations/
├── versions/ # Migration files
│ ├── 001_initial_schema.py
│ ├── 002_add_phone_number.py
│ └── ...
├── env.py # Migration environment
├── script.py.mako # Migration template
├── alembic.ini # Alembic configuration
└── README.md # This file
```
## Transition from Old System
If you're migrating from the old custom migration system:
### 1. Backup Your Database
```bash
# PostgreSQL
pg_dump --format=custom --dbname="$DATABASE_URL" --file=backup_$(date +%Y%m%d_%H%M%S).dump
# SQLite
cp instance/timetracker.db backup_timetracker_$(date +%Y%m%d_%H%M%S).db
```
### 2. Use Migration Management Script
```bash
python migrations/manage_migrations.py
```
### 3. Or Manual Migration
```bash
# Initialize Flask-Migrate
flask db init
# Create initial migration (captures current schema)
flask db migrate -m "Initial schema from existing database"
# Apply migration
flask db upgrade
```
## Best Practices
### 1. Migration Naming
Use descriptive names for migrations:
```bash
flask db migrate -m "Add user profile fields"
flask db migrate -m "Create project categories table"
flask db migrate -m "Add invoice payment tracking"
```
### 2. Testing Migrations
Always test migrations on a copy of your production data:
```bash
# Test upgrade
flask db upgrade
# Test downgrade
flask db downgrade
# Verify data integrity
```
### 3. Backup Before Migrations
```bash
# Always backup before major migrations
flask db backup # Custom command
# or
pg_dump --format=custom --dbname="$DATABASE_URL" --file=pre_migration_backup.dump
```
### 4. Review Generated Code
Always review auto-generated migrations before applying:
- Check the `upgrade()` function
- Verify the `downgrade()` function
- Ensure data types and constraints are correct
## Troubleshooting
### Common Issues
#### 1. Migration Already Applied
```bash
# Check current status
flask db current
# If migration is already applied, stamp the database
flask db stamp <revision>
```
#### 2. Migration Conflicts
```bash
# Show migration heads
flask db heads
# Merge branches if needed
flask db merge -m "Merge migration branches" <revision1> <revision2>
```
#### 3. Database Out of Sync
```bash
# Check migration history
flask db history
# Reset to specific revision
flask db stamp <revision>
```
#### 4. Model Import Errors
Ensure all models are imported in your application:
```python
# In app/__init__.py or similar
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
```
### Getting Help
1. Check the migration status: `flask db current`
2. Review migration history: `flask db history`
3. Check Alembic logs for detailed error messages
4. Verify database connection and permissions
## Advanced Features
### Custom Migration Operations
You can add custom operations in your migrations:
```python
def upgrade():
# Custom data migration
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
# Custom table operations
op.create_index('custom_idx', 'table_name', ['column_name'])
```
### Data Migrations
For complex data migrations, use the `op.execute()` method:
```python
def upgrade():
# Migrate existing data
op.execute("""
INSERT INTO new_table (id, name)
SELECT id, name FROM old_table
""")
```
### Conditional Migrations
Handle different database types:
```python
def upgrade():
# PostgreSQL-specific operations
if op.get_bind().dialect.name == 'postgresql':
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
```
## Environment Variables
Ensure these environment variables are set:
```bash
export FLASK_APP=app.py
export DATABASE_URL="postgresql://user:pass@localhost/dbname"
# or
export DATABASE_URL="sqlite:///instance/timetracker.db"
```
## CI/CD Integration
For automated deployments, include migration steps:
```yaml
# Example GitHub Actions step
- name: Run database migrations
run: |
flask db upgrade
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
```
## Support
For migration-related issues:
1. Check this README
2. Review Flask-Migrate documentation: https://flask-migrate.readthedocs.io/
3. Check Alembic documentation: https://alembic.sqlalchemy.org/
4. Review generated migration files for errors

107
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,107 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version number format
version_num_format = %04d
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
# behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

91
migrations/env.py Normal file
View File

@@ -0,0 +1,91 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
Legacy Schema Migration Script for TimeTracker
This script handles migration from old custom migration system to Flask-Migrate
"""
import os
import sys
from pathlib import Path
def migrate_legacy_schema():
"""Migrate from legacy schema to new Flask-Migrate system"""
print("=== Legacy Schema Migration ===")
# Check if we're in the right directory
if not Path("app.py").exists():
print("Error: Please run this script from the TimeTracker root directory")
return False
# Set environment variables
os.environ.setdefault('FLASK_APP', 'app.py')
try:
from app import create_app, db
app = create_app()
with app.app_context():
print("✓ Application context created")
# Check current database state
inspector = db.inspect(db.engine)
existing_tables = inspector.get_table_names()
print(f"Found {len(existing_tables)} existing tables: {existing_tables}")
# Handle legacy schema issues
if 'projects' in existing_tables:
migrate_projects_table(db)
if 'settings' in existing_tables:
migrate_settings_table(db)
print("✓ Legacy schema migration completed")
return True
except Exception as e:
print(f"✗ Error during legacy schema migration: {e}")
return False
def migrate_projects_table(db):
"""Migrate projects table from legacy schema"""
print("Migrating projects table...")
try:
# Check if projects table has old 'client' column
inspector = db.inspect(db.engine)
project_columns = [col['name'] for col in inspector.get_columns('projects')]
if 'client' in project_columns and 'client_id' not in project_columns:
print(" - Converting projects.client to projects.client_id")
# Create clients table if it doesn't exist
if 'clients' not in inspector.get_table_names():
print(" - Creating clients table")
db.session.execute(db.text("""
CREATE TABLE IF NOT EXISTS clients (
id SERIAL PRIMARY KEY,
name VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
contact_person VARCHAR(200),
email VARCHAR(200),
phone VARCHAR(50),
address TEXT,
default_hourly_rate NUMERIC(9, 2),
status VARCHAR(20) DEFAULT 'active' NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
"""))
# Add client_id column
db.session.execute(db.text("""
ALTER TABLE projects ADD COLUMN IF NOT EXISTS client_id INTEGER
"""))
# Migrate existing client names to clients table
db.session.execute(db.text("""
INSERT INTO clients (name, status)
SELECT DISTINCT client, 'active' FROM projects
WHERE client IS NOT NULL AND client <> ''
ON CONFLICT (name) DO NOTHING
"""))
# Update projects to reference clients
db.session.execute(db.text("""
UPDATE projects p
SET client_id = c.id
FROM clients c
WHERE p.client_id IS NULL AND p.client = c.name
"""))
# Create index
db.session.execute(db.text("""
CREATE INDEX IF NOT EXISTS idx_projects_client_id ON projects(client_id)
"""))
print(" - Projects table migration completed")
except Exception as e:
print(f" - Warning: Projects table migration failed: {e}")
def migrate_settings_table(db):
"""Migrate settings table from legacy schema"""
print("Migrating settings table...")
try:
inspector = db.inspect(db.engine)
settings_columns = [col['name'] for col in inspector.get_columns('settings')]
# Add missing columns that the new system expects
missing_columns = [
('allow_analytics', 'BOOLEAN DEFAULT TRUE'),
('logo_path', 'VARCHAR(500)'),
('company_website', 'VARCHAR(200)')
]
for col_name, col_def in missing_columns:
if col_name not in settings_columns:
print(f" - Adding missing column: {col_name}")
db.session.execute(db.text(f"""
ALTER TABLE settings ADD COLUMN {col_name} {col_def}
"""))
print(" - Settings table migration completed")
except Exception as e:
print(f" - Warning: Settings table migration failed: {e}")
def create_migration_baseline():
"""Create a migration baseline for the current database state"""
print("Creating migration baseline...")
try:
# Initialize Flask-Migrate if not already done
if not Path("migrations/env.py").exists():
print(" - Initializing Flask-Migrate")
os.system("flask db init")
# Create initial migration
print(" - Creating initial migration")
os.system('flask db migrate -m "Baseline from legacy schema"')
# Stamp database as being at this revision
print(" - Stamping database")
os.system("flask db stamp head")
print("✓ Migration baseline created")
return True
except Exception as e:
print(f"✗ Error creating migration baseline: {e}")
return False
def main():
"""Main function"""
print("=== TimeTracker Legacy Schema Migration ===")
print("This script migrates from the old custom migration system to Flask-Migrate")
# Step 1: Migrate legacy schema
if not migrate_legacy_schema():
print("Legacy schema migration failed")
sys.exit(1)
# Step 2: Create migration baseline
if not create_migration_baseline():
print("Migration baseline creation failed")
sys.exit(1)
print("\n=== Migration Complete ===")
print("🎉 Your legacy database has been successfully migrated!")
print("\nNext steps:")
print("1. Test your application to ensure everything works")
print("2. Review the generated migration files in migrations/versions/")
print("3. For future schema changes, use:")
print(" - flask db migrate -m 'Description of changes'")
print(" - flask db upgrade")
print("4. To check status: flask db current")
print("5. To view history: flask db history")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Migration Management Script for TimeTracker
This script helps manage the transition from custom migrations to Flask-Migrate
"""
import os
import sys
import subprocess
from pathlib import Path
def run_command(command, description):
"""Run a command and handle errors"""
print(f"\n--- {description} ---")
print(f"Running: {command}")
try:
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
print(f"{description} completed successfully")
if result.stdout:
print(result.stdout)
return True
except subprocess.CalledProcessError as e:
print(f"{description} failed:")
print(f"Error code: {e.returncode}")
if e.stdout:
print(f"STDOUT: {e.stdout}")
if e.stderr:
print(f"STDERR: {e.stderr}")
return False
def check_flask_migrate_installed():
"""Check if Flask-Migrate is properly installed"""
try:
import flask_migrate
print("✓ Flask-Migrate is installed")
return True
except ImportError:
print("✗ Flask-Migrate is not installed")
print("Please install it with: pip install Flask-Migrate")
return False
def initialize_migrations():
"""Initialize Flask-Migrate if not already initialized"""
migrations_dir = Path("migrations")
if not migrations_dir.exists():
print("Initializing Flask-Migrate...")
return run_command("flask db init", "Initialize Flask-Migrate")
else:
print("✓ Migrations directory already exists")
return True
def create_initial_migration():
"""Create the initial migration if it doesn't exist"""
versions_dir = Path("migrations/versions")
if not versions_dir.exists() or not list(versions_dir.glob("*.py")):
print("Creating initial migration...")
return run_command("flask db migrate -m 'Initial database schema'", "Create initial migration")
else:
print("✓ Initial migration already exists")
return True
def apply_migrations():
"""Apply all pending migrations"""
return run_command("flask db upgrade", "Apply database migrations")
def show_migration_status():
"""Show current migration status"""
return run_command("flask db current", "Show current migration")
def show_migration_history():
"""Show migration history"""
return run_command("flask db history", "Show migration history")
def backup_database():
"""Create a backup of the current database"""
print("\n--- Creating Database Backup ---")
# Check if we're using PostgreSQL or SQLite
db_url = os.getenv('DATABASE_URL', '')
if db_url.startswith('postgresql'):
print("PostgreSQL database detected")
backup_cmd = "pg_dump --format=custom --dbname=\"$DATABASE_URL\" --file=backup_$(date +%Y%m%d_%H%M%S).dump"
print(f"Please run: {backup_cmd}")
return True
else:
print("SQLite database detected")
# For SQLite, we'll just copy the file
if os.path.exists('instance/timetracker.db'):
import shutil
from datetime import datetime
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = f"backup_timetracker_{timestamp}.db"
shutil.copy2('instance/timetracker.db', backup_file)
print(f"✓ Database backed up to: {backup_file}")
return True
else:
print("SQLite database file not found")
return False
def main():
"""Main migration management function"""
print("=== TimeTracker Migration Management ===")
print("This script will help you transition to Flask-Migrate")
# Check prerequisites
if not check_flask_migrate_installed():
sys.exit(1)
# Create backup
print("\n⚠️ IMPORTANT: Creating database backup before proceeding...")
if not backup_database():
print("Failed to create backup. Please create one manually before proceeding.")
response = input("Continue anyway? (y/N): ")
if response.lower() != 'y':
print("Migration cancelled.")
sys.exit(1)
# Initialize migrations
if not initialize_migrations():
print("Failed to initialize migrations")
sys.exit(1)
# Create initial migration
if not create_initial_migration():
print("Failed to create initial migration")
sys.exit(1)
# Show current status
show_migration_status()
# Apply migrations
print("\n⚠️ About to apply migrations to your database...")
response = input("Continue? (y/N): ")
if response.lower() != 'y':
print("Migration cancelled. You can run 'flask db upgrade' manually later.")
return
if not apply_migrations():
print("Failed to apply migrations")
sys.exit(1)
# Show final status
print("\n=== Migration Complete ===")
show_migration_status()
show_migration_history()
print("\n🎉 Migration to Flask-Migrate completed successfully!")
print("\nNext steps:")
print("1. Test your application to ensure everything works")
print("2. For future schema changes, use:")
print(" - flask db migrate -m 'Description of changes'")
print(" - flask db upgrade")
print("3. To rollback: flask db downgrade")
print("4. To check status: flask db current")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,383 @@
#!/usr/bin/env python3
"""
Comprehensive Database Migration Script for TimeTracker
This script can migrate ANY existing database to the new Flask-Migrate system
"""
import os
import sys
import subprocess
import sqlite3
import psycopg2
from pathlib import Path
from datetime import datetime
import shutil
def run_command(command, description, capture_output=True):
"""Run a command and handle errors"""
print(f"\n--- {description} ---")
print(f"Running: {command}")
try:
if capture_output:
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
print(f"{description} completed successfully")
if result.stdout:
print(result.stdout)
return True
else:
subprocess.run(command, shell=True, check=True)
print(f"{description} completed successfully")
return True
except subprocess.CmdProcessError as e:
print(f"{description} failed:")
print(f"Error code: {e.returncode}")
if e.stdout:
print(f"STDOUT: {e.stdout}")
if e.stderr:
print(f"STDERR: {e.stderr}")
return False
def check_flask_migrate_installed():
"""Check if Flask-Migrate is properly installed"""
try:
import flask_migrate
print("✓ Flask-Migrate is installed")
return True
except ImportError:
print("✗ Flask-Migrate is not installed")
print("Please install it with: pip install Flask-Migrate")
return False
def detect_database_type():
"""Detect the type of database being used"""
db_url = os.getenv('DATABASE_URL', '')
if db_url.startswith('postgresql'):
return 'postgresql', db_url
elif db_url.startswith('sqlite'):
return 'sqlite', db_url
else:
# Check for common database files
if os.path.exists('instance/timetracker.db'):
return 'sqlite', 'sqlite:///instance/timetracker.db'
elif os.path.exists('timetracker.db'):
return 'sqlite', 'sqlite:///timetracker.db'
else:
return 'unknown', None
def backup_database(db_type, db_url):
"""Create a comprehensive backup of the current database"""
print("\n--- Creating Database Backup ---")
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if db_type == 'postgresql':
backup_file = f"backup_postgresql_{timestamp}.dump"
backup_cmd = f'pg_dump --format=custom --dbname="{db_url}" --file={backup_file}'
print(f"PostgreSQL database detected")
print(f"Running: {backup_cmd}")
try:
subprocess.run(backup_cmd, shell=True, check=True)
print(f"✓ Database backed up to: {backup_file}")
return backup_file
except subprocess.CalledProcessError as e:
print(f"✗ PostgreSQL backup failed: {e}")
print("Please ensure pg_dump is available and you have proper permissions")
return None
elif db_type == 'sqlite':
# Find the actual database file
if 'instance/timetracker.db' in db_url:
db_file = 'instance/timetracker.db'
elif 'timetracker.db' in db_url:
db_file = 'timetracker.db'
else:
db_file = db_url.replace('sqlite:///', '')
if os.path.exists(db_file):
backup_file = f"backup_sqlite_{timestamp}.db"
shutil.copy2(db_file, backup_file)
print(f"✓ SQLite database backed up to: {backup_file}")
return backup_file
else:
print(f"✗ SQLite database file not found: {db_file}")
return None
else:
print("✗ Unknown database type")
return None
def analyze_existing_schema(db_type, db_url):
"""Analyze the existing database schema to understand what needs to be migrated"""
print("\n--- Analyzing Existing Database Schema ---")
if db_type == 'postgresql':
try:
conn = psycopg2.connect(db_url)
cursor = conn.cursor()
# Get list of existing tables
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
""")
existing_tables = [row[0] for row in cursor.fetchall()]
# Get table schemas
table_schemas = {}
for table in existing_tables:
cursor.execute(f"""
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = '{table}'
ORDER BY ordinal_position
""")
columns = cursor.fetchall()
table_schemas[table] = columns
conn.close()
print(f"✓ Found {len(existing_tables)} existing tables: {existing_tables}")
return existing_tables, table_schemas
except Exception as e:
print(f"✗ Error analyzing PostgreSQL schema: {e}")
return [], {}
elif db_type == 'sqlite':
try:
# Find the actual database file
if 'instance/timetracker.db' in db_url:
db_file = 'instance/timetracker.db'
elif 'timetracker.db' in db_url:
db_file = 'timetracker.db'
else:
db_file = db_url.replace('sqlite:///', '')
if not os.path.exists(db_file):
print(f"✗ SQLite database file not found: {db_file}")
return [], {}
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# Get list of existing tables
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
existing_tables = [row[0] for row in cursor.fetchall()]
# Get table schemas
table_schemas = {}
for table in existing_tables:
cursor.execute(f"PRAGMA table_info({table})")
columns = cursor.fetchall()
table_schemas[table] = columns
conn.close()
print(f"✓ Found {len(existing_tables)} existing tables: {existing_tables}")
return existing_tables, table_schemas
except Exception as e:
print(f"✗ Error analyzing SQLite schema: {e}")
return [], {}
return [], {}
def create_migration_strategy(existing_tables, table_schemas):
"""Create a migration strategy based on existing schema"""
print("\n--- Creating Migration Strategy ---")
# Define expected tables and their priority
expected_tables = [
'clients', 'users', 'projects', 'tasks', 'time_entries',
'settings', 'invoices', 'invoice_items'
]
missing_tables = [table for table in expected_tables if table not in existing_tables]
existing_but_modified = []
if missing_tables:
print(f"Tables to create: {missing_tables}")
# Check for schema modifications
for table in existing_tables:
if table in expected_tables:
# This table exists, check if it needs modifications
print(f"Table '{table}' exists - will preserve data")
return missing_tables, existing_but_modified
def initialize_flask_migrate():
"""Initialize Flask-Migrate if not already initialized"""
migrations_dir = Path("migrations")
if not migrations_dir.exists():
print("Initializing Flask-Migrate...")
return run_command("flask db init", "Initialize Flask-Migrate")
else:
print("✓ Migrations directory already exists")
return True
def create_initial_migration():
"""Create the initial migration that captures the current state"""
versions_dir = Path("migrations/versions")
if not versions_dir.exists() or not list(versions_dir.glob("*.py")):
print("Creating initial migration...")
return run_command("flask db migrate -m 'Initial schema from existing database'", "Create initial migration")
else:
print("✓ Initial migration already exists")
return True
def stamp_database_with_current_revision():
"""Mark the database as being at the current migration revision"""
print("Stamping database with current migration revision...")
return run_command("flask db stamp head", "Stamp database with current revision")
def apply_migrations():
"""Apply any pending migrations"""
return run_command("flask db upgrade", "Apply database migrations")
def verify_migration_success():
"""Verify that the migration was successful"""
print("\n--- Verifying Migration Success ---")
# Check migration status
print("Current migration status:")
run_command("flask db current", "Show current migration", capture_output=False)
# Check migration history
print("\nMigration history:")
run_command("flask db history", "Show migration history", capture_output=False)
# Test database connection
try:
from app import create_app, db
app = create_app()
with app.app_context():
# Try to query the database
result = db.session.execute(db.text("SELECT 1"))
print("✓ Database connection test successful")
return True
except Exception as e:
print(f"✗ Database connection test failed: {e}")
return False
def create_data_migration_script(existing_tables, table_schemas):
"""Create a data migration script for any existing data"""
print("\n--- Creating Data Migration Script ---")
script_content = """#!/usr/bin/env python3
\"\"\"
Data Migration Script for Existing Database
This script handles data migration from old schema to new schema
\"\"\"
from app import create_app, db
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
def migrate_existing_data():
\"\"\"Migrate existing data to new schema\"\"\"
app = create_app()
with app.app_context():
print("Starting data migration...")
# Add your data migration logic here
# Example: Migrate old client names to new client table
print("Data migration completed")
if __name__ == "__main__":
migrate_existing_data()
"""
script_path = "migrations/migrate_existing_data.py"
with open(script_path, 'w') as f:
f.write(script_content)
print(f"✓ Data migration script created: {script_path}")
return script_path
def main():
"""Main migration function"""
print("=== TimeTracker Comprehensive Database Migration ===")
print("This script will migrate ANY existing database to the new Flask-Migrate system")
# Check prerequisites
if not check_flask_migrate_installed():
sys.exit(1)
# Detect database type
db_type, db_url = detect_database_type()
if not db_url:
print("✗ Could not detect database configuration")
print("Please set DATABASE_URL environment variable or ensure database files exist")
sys.exit(1)
print(f"✓ Detected {db_type} database")
# Create backup
print("\n⚠️ IMPORTANT: Creating database backup before proceeding...")
backup_file = backup_database(db_type, db_url)
if not backup_file:
response = input("Failed to create backup. Continue anyway? (y/N): ")
if response.lower() != 'y':
print("Migration cancelled.")
sys.exit(1)
# Analyze existing schema
existing_tables, table_schemas = analyze_existing_schema(db_type, db_url)
# Create migration strategy
missing_tables, modified_tables = create_migration_strategy(existing_tables, table_schemas)
# Initialize Flask-Migrate
if not initialize_flask_migrate():
print("Failed to initialize Flask-Migrate")
sys.exit(1)
# Create initial migration
if not create_initial_migration():
print("Failed to create initial migration")
sys.exit(1)
# Create data migration script if needed
if existing_tables:
create_data_migration_script(existing_tables, table_schemas)
# Stamp database with current revision
if not stamp_database_with_current_revision():
print("Failed to stamp database")
sys.exit(1)
# Apply any pending migrations
if not apply_migrations():
print("Failed to apply migrations")
sys.exit(1)
# Verify migration success
if not verify_migration_success():
print("Migration verification failed")
sys.exit(1)
# Show final status
print("\n=== Migration Complete ===")
print("🎉 Your database has been successfully migrated to Flask-Migrate!")
print("\nNext steps:")
print("1. Test your application to ensure everything works")
print("2. Review the generated migration files in migrations/versions/")
print("3. If you have existing data, run the data migration script:")
print(" python migrations/migrate_existing_data.py")
print("4. For future schema changes, use:")
print(" - flask db migrate -m 'Description of changes'")
print(" - flask db upgrade")
print("\nBackup created at:", backup_file)
print("For more information, see: migrations/README.md")
if __name__ == "__main__":
main()

24
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Test Script for TimeTracker Migration System
This script verifies that the migration system is working correctly
"""
import os
import sys
from pathlib import Path
def test_flask_migrate_installation():
"""Test if Flask-Migrate is properly installed"""
try:
import flask_migrate
print("✓ Flask-Migrate is installed")
return True
except ImportError:
print("✗ Flask-Migrate is not installed")
return False
def test_database_connection():
"""Test database connection"""
try:
from app import create_app, db
app = create_app()
with app.app_context():
# Test basic connection
result = db.session.execute(db.text("SELECT 1"))
print("✓ Database connection successful")
return True
except Exception as e:
print(f"✗ Database connection failed: {e}")
return False
def test_migration_files():
"""Test if migration files exist and are valid"""
migrations_dir = Path("migrations")
if not migrations_dir.exists():
print("✗ Migrations directory does not exist")
return False
required_files = [
"env.py",
"script.py.mako",
"alembic.ini",
"README.md"
]
for file in required_files:
if not (migrations_dir / file).exists():
print(f"✗ Required file missing: {file}")
return False
print("✓ All required migration files exist")
return True
def test_migration_commands():
"""Test if migration commands are available"""
try:
# Test flask db init (should not fail if already initialized)
result = os.system("flask db --help > /dev/null 2>&1")
if result == 0:
print("✓ Flask migration commands available")
return True
else:
print("✗ Flask migration commands not available")
return False
except Exception as e:
print(f"✗ Error testing migration commands: {e}")
return False
def test_models_import():
"""Test if all models can be imported"""
try:
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
print("✓ All models imported successfully")
return True
except Exception as e:
print(f"✗ Error importing models: {e}")
return False
def test_migration_scripts():
"""Test if migration scripts exist and are valid"""
scripts = [
"migrate_existing_database.py",
"legacy_schema_migration.py",
"manage_migrations.py"
]
for script in scripts:
script_path = Path("migrations") / script
if not script_path.exists():
print(f"✗ Migration script missing: {script}")
return False
print("✓ All migration scripts exist")
return True
def run_comprehensive_test():
"""Run all tests"""
print("=== Testing TimeTracker Migration System ===\n")
tests = [
("Flask-Migrate Installation", test_flask_migrate_installation),
("Database Connection", test_database_connection),
("Migration Files", test_migration_files),
("Migration Commands", test_migration_commands),
("Models Import", test_models_import),
("Migration Scripts", test_migration_scripts)
]
passed = 0
total = len(tests)
for test_name, test_func in tests:
print(f"Testing: {test_name}")
if test_func():
passed += 1
print()
print(f"=== Test Results ===")
print(f"Passed: {passed}/{total}")
if passed == total:
print("🎉 All tests passed! Migration system is ready.")
return True
else:
print("⚠️ Some tests failed. Please fix issues before proceeding.")
return False
def main():
"""Main function"""
if not Path("app.py").exists():
print("Error: Please run this script from the TimeTracker root directory")
sys.exit(1)
# Set environment variables
os.environ.setdefault('FLASK_APP', 'app.py')
success = run_comprehensive_test()
if success:
print("\n✅ Migration system is ready for use!")
print("\nNext steps:")
print("1. For fresh installation: python migrations/manage_migrations.py")
print("2. For existing database: python migrations/migrate_existing_database.py")
print("3. For legacy schema: python migrations/legacy_schema_migration.py")
else:
print("\n❌ Migration system has issues that need to be resolved.")
print("\nTroubleshooting:")
print("1. Install Flask-Migrate: pip install Flask-Migrate")
print("2. Check database connection settings")
print("3. Verify all required files exist")
print("4. Check application configuration")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,187 @@
"""Initial database schema
Revision ID: 001
Revises:
Create Date: 2025-01-15 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create clients table
op.create_table('clients',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('contact_person', sa.String(length=200), nullable=True),
sa.Column('email', sa.String(length=200), nullable=True),
sa.Column('phone', sa.String(length=50), nullable=True),
sa.Column('address', sa.Text(), nullable=True),
sa.Column('default_hourly_rate', sa.Numeric(precision=9, scale=2), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False, server_default='active'),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_clients_name', 'clients', ['name'], unique=True)
# Create users table
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=80), nullable=False),
sa.Column('role', sa.String(length=20), nullable=False, server_default='user'),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('last_login', sa.TIMESTAMP(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_users_username', 'users', ['username'], unique=True)
op.create_index('idx_users_role', 'users', ['role'])
# Create projects table
op.create_table('projects',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('client_id', sa.Integer(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('billable', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('hourly_rate', sa.Numeric(precision=9, scale=2), nullable=True),
sa.Column('billing_ref', sa.String(length=100), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False, server_default='active'),
sa.Column('created_at', sa.TIMESTAMP(), nullable=True, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=True, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_projects_client_id', 'projects', ['client_id'])
op.create_index('idx_projects_status', 'projects', ['status'])
# Create tasks table
op.create_table('tasks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False, server_default='pending'),
sa.Column('priority', sa.String(length=20), nullable=False, server_default='medium'),
sa.Column('assigned_to', sa.Integer(), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('due_date', sa.Date(), nullable=True),
sa.Column('estimated_hours', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['assigned_to'], ['users.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_tasks_project_id', 'tasks', ['project_id'])
op.create_index('idx_tasks_status', 'tasks', ['status'])
op.create_index('idx_tasks_priority', 'tasks', ['priority'])
# Create time_entries table
op.create_table('time_entries',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.Integer(), nullable=True),
sa.Column('start_time', sa.TIMESTAMP(), nullable=False),
sa.Column('end_time', sa.TIMESTAMP(), nullable=True),
sa.Column('duration', sa.Interval(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('tags', sa.String(length=500), nullable=True),
sa.Column('billable', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('source', sa.String(length=50), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_time_entries_user_id', 'time_entries', ['user_id'])
op.create_index('idx_time_entries_project_id', 'time_entries', ['project_id'])
op.create_index('idx_time_entries_start_time', 'time_entries', ['start_time'])
# Create settings table
op.create_table('settings',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('company_name', sa.String(length=200), nullable=True),
sa.Column('company_address', sa.Text(), nullable=True),
sa.Column('company_phone', sa.String(length=50), nullable=True),
sa.Column('company_email', sa.String(length=200), nullable=True),
sa.Column('company_website', sa.String(length=200), nullable=True),
sa.Column('currency', sa.String(length=3), nullable=False, server_default='EUR'),
sa.Column('timezone', sa.String(length=50), nullable=False, server_default='Europe/Rome'),
sa.Column('rounding_minutes', sa.Integer(), nullable=False, server_default='1'),
sa.Column('single_active_timer', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('idle_timeout_minutes', sa.Integer(), nullable=False, server_default='30'),
sa.Column('allow_self_register', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('allow_analytics', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('logo_path', sa.String(length=500), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.PrimaryKeyConstraint('id')
)
# Create invoices table
op.create_table('invoices',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('invoice_number', sa.String(length=50), nullable=False),
sa.Column('client_id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=True),
sa.Column('issue_date', sa.Date(), nullable=False),
sa.Column('due_date', sa.Date(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False, server_default='draft'),
sa.Column('subtotal', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
sa.Column('tax_rate', sa.Numeric(precision=5, scale=2), nullable=False, server_default='0'),
sa.Column('tax_amount', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
sa.Column('total', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('terms', sa.Text(), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_invoices_client_id', 'invoices', ['client_id'])
op.create_index('idx_invoices_status', 'invoices', ['status'])
# Create invoice_items table
op.create_table('invoice_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('invoice_id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=500), nullable=False),
sa.Column('quantity', sa.Numeric(precision=10, scale=2), nullable=False, server_default='1'),
sa.Column('unit_price', sa.Numeric(precision=9, scale=2), nullable=False, server_default='0'),
sa.Column('total', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
sa.Column('time_entry_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['time_entry_id'], ['time_entries.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_invoice_items_invoice_id', 'invoice_items', ['invoice_id'])
def downgrade():
# Drop tables in reverse order
op.drop_table('invoice_items')
op.drop_table('invoices')
op.drop_table('settings')
op.drop_table('time_entries')
op.drop_table('tasks')
op.drop_table('projects')
op.drop_table('users')
op.drop_table('clients')

View File

@@ -0,0 +1,166 @@
@echo off
REM TimeTracker Migration Setup Script for Windows
REM This script helps set up Flask-Migrate for database migrations
echo === TimeTracker Migration Setup ===
echo This script will help you set up Flask-Migrate for database migrations
echo.
REM Check if we're in the right directory
if not exist "app.py" (
echo Error: Please run this script from the TimeTracker root directory
pause
exit /b 1
)
REM Check if Python is available
python --version >nul 2>&1
if errorlevel 1 (
echo Error: Python is required but not installed or not in PATH
pause
exit /b 1
)
REM Check if Flask is available
python -c "import flask" >nul 2>&1
if errorlevel 1 (
echo Error: Flask is required but not installed
echo Please install dependencies with: pip install -r requirements.txt
pause
exit /b 1
)
REM Check if Flask-Migrate is available
python -c "import flask_migrate" >nul 2>&1
if errorlevel 1 (
echo Error: Flask-Migrate is required but not installed
echo Please install dependencies with: pip install -r requirements.txt
pause
exit /b 1
)
echo ✓ Prerequisites check passed
echo.
REM Set environment variables if not already set
if "%FLASK_APP%"=="" (
set FLASK_APP=app.py
echo Set FLASK_APP=app.py
)
REM Check if migrations directory exists
if exist "migrations" (
echo ✓ Migrations directory already exists
REM Check if it's properly initialized
if exist "migrations\env.py" if exist "migrations\alembic.ini" (
echo ✓ Flask-Migrate is already initialized
REM Show current status
echo.
echo Current migration status:
flask db current 2>nul || echo No migrations applied yet
echo.
echo Migration history:
flask db history 2>nul || echo No migration history
echo.
echo To create a new migration:
echo flask db migrate -m "Description of changes"
echo.
echo To apply pending migrations:
echo flask db upgrade
pause
exit /b 0
) else (
echo ⚠ Migrations directory exists but appears incomplete
echo Removing and reinitializing...
rmdir /s /q migrations
)
)
REM Initialize Flask-Migrate
echo Initializing Flask-Migrate...
flask db init
if errorlevel 1 (
echo ✗ Failed to initialize Flask-Migrate
pause
exit /b 1
) else (
echo ✓ Flask-Migrate initialized successfully
)
REM Create initial migration
echo.
echo Creating initial migration...
flask db migrate -m "Initial database schema"
if errorlevel 1 (
echo ✗ Failed to create initial migration
pause
exit /b 1
) else (
echo ✓ Initial migration created successfully
)
REM Show the generated migration
echo.
echo Generated migration file:
dir migrations\versions
echo.
echo Review the migration file before applying:
echo type migrations\versions\*.py
REM Ask user if they want to apply the migration
echo.
set /p "apply_migration=Do you want to apply this migration now? (y/N): "
if /i "%apply_migration%"=="y" (
echo Applying migration...
flask db upgrade
if errorlevel 1 (
echo ✗ Failed to apply migration
pause
exit /b 1
) else (
echo ✓ Migration applied successfully
echo.
echo Current migration status:
flask db current
echo.
echo Migration history:
flask db history
)
) else (
echo Migration not applied. You can apply it later with:
echo flask db upgrade
)
echo.
echo === Setup Complete ===
echo.
echo Your Flask-Migrate system is now set up!
echo.
echo Next steps:
echo 1. Test your application to ensure everything works
echo 2. For future schema changes:
echo - Edit your models in app\models\
echo - Run: flask db migrate -m "Description of changes"
echo - Review the generated migration file
echo - Run: flask db upgrade
echo.
echo Useful commands:
echo flask db current # Show current migration
echo flask db history # Show migration history
echo flask db downgrade # Rollback last migration
echo.
echo For more information, see: migrations\README.md
pause

157
scripts/setup-migrations.sh Normal file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
# TimeTracker Migration Setup Script
# This script helps set up Flask-Migrate for database migrations
set -e
echo "=== TimeTracker Migration Setup ==="
echo "This script will help you set up Flask-Migrate for database migrations"
echo ""
# Check if we're in the right directory
if [ ! -f "app.py" ]; then
echo "Error: Please run this script from the TimeTracker root directory"
exit 1
fi
# Check if Python is available
if ! command -v python3 &> /dev/null; then
echo "Error: Python 3 is required but not installed"
exit 1
fi
# Check if Flask is available
if ! python3 -c "import flask" &> /dev/null; then
echo "Error: Flask is required but not installed"
echo "Please install dependencies with: pip install -r requirements.txt"
exit 1
fi
# Check if Flask-Migrate is available
if ! python3 -c "import flask_migrate" &> /dev/null; then
echo "Error: Flask-Migrate is required but not installed"
echo "Please install dependencies with: pip install -r requirements.txt"
exit 1
fi
echo "✓ Prerequisites check passed"
echo ""
# Set environment variables if not already set
if [ -z "$FLASK_APP" ]; then
export FLASK_APP=app.py
echo "Set FLASK_APP=app.py"
fi
# Check if migrations directory exists
if [ -d "migrations" ]; then
echo "✓ Migrations directory already exists"
# Check if it's properly initialized
if [ -f "migrations/env.py" ] && [ -f "migrations/alembic.ini" ]; then
echo "✓ Flask-Migrate is already initialized"
# Show current status
echo ""
echo "Current migration status:"
flask db current || echo "No migrations applied yet"
echo ""
echo "Migration history:"
flask db history || echo "No migration history"
echo ""
echo "To create a new migration:"
echo " flask db migrate -m 'Description of changes'"
echo ""
echo "To apply pending migrations:"
echo " flask db upgrade"
exit 0
else
echo "⚠ Migrations directory exists but appears incomplete"
echo "Removing and reinitializing..."
rm -rf migrations
fi
fi
# Initialize Flask-Migrate
echo "Initializing Flask-Migrate..."
flask db init
if [ $? -eq 0 ]; then
echo "✓ Flask-Migrate initialized successfully"
else
echo "✗ Failed to initialize Flask-Migrate"
exit 1
fi
# Create initial migration
echo ""
echo "Creating initial migration..."
flask db migrate -m "Initial database schema"
if [ $? -eq 0 ]; then
echo "✓ Initial migration created successfully"
else
echo "✗ Failed to create initial migration"
exit 1
fi
# Show the generated migration
echo ""
echo "Generated migration file:"
ls -la migrations/versions/
echo ""
echo "Review the migration file before applying:"
echo " cat migrations/versions/*.py"
# Ask user if they want to apply the migration
echo ""
read -p "Do you want to apply this migration now? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Applying migration..."
flask db upgrade
if [ $? -eq 0 ]; then
echo "✓ Migration applied successfully"
echo ""
echo "Current migration status:"
flask db current
echo ""
echo "Migration history:"
flask db history
else
echo "✗ Failed to apply migration"
exit 1
fi
else
echo "Migration not applied. You can apply it later with:"
echo " flask db upgrade"
fi
echo ""
echo "=== Setup Complete ==="
echo ""
echo "Your Flask-Migrate system is now set up!"
echo ""
echo "Next steps:"
echo "1. Test your application to ensure everything works"
echo "2. For future schema changes:"
echo " - Edit your models in app/models/"
echo " - Run: flask db migrate -m 'Description of changes'"
echo " - Review the generated migration file"
echo " - Run: flask db upgrade"
echo ""
echo "Useful commands:"
echo " flask db current # Show current migration"
echo " flask db history # Show migration history"
echo " flask db downgrade # Rollback last migration"
echo ""
echo "For more information, see: migrations/README.md"