Files
TimeTracker/docker/entrypoint.sh
Dries Peeters d9dab3a49c 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
2025-09-02 14:42:54 +02:00

480 lines
14 KiB
Bash

#!/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 "$@"