Files
TimeTracker/docker/entrypoint.py
Dries Peeters 7791e6ada0 feat: Add comprehensive issue/bug tracking system
Implement a complete issue management system with client portal integration
and internal admin interface for tracking and resolving client-reported issues.

Features:
- New Issue model with full lifecycle management (open, in_progress, resolved, closed, cancelled)
- Priority levels (low, medium, high, urgent) with visual indicators
- Issue linking to projects and tasks
- Create tasks directly from issues
- Client portal integration for issue reporting and viewing
- Internal admin routes for issue management, filtering, and assignment
- Comprehensive templates for both client and admin views
- Status filtering and search functionality
- Issue assignment to internal users
- Automatic timestamp tracking (created, updated, resolved, closed)

Client Portal:
- Clients can report new issues with project association
- View all issues with status filtering
- View individual issue details
- Submit issues with optional submitter name/email

Admin Interface:
- List all issues with advanced filtering (status, priority, client, project, assignee, search)
- View, edit, and delete issues
- Link issues to existing tasks
- Create tasks from issues
- Update issue status, priority, and assignment
- Issue statistics dashboard

Technical:
- Added Issue model with relationships to Client, Project, Task, and User
- New issues blueprint for internal management
- Extended client_portal routes with issue endpoints
- Updated model imports and relationships
- Added navigation links in base templates
- Version bump to 4.6.0
- Code cleanup in docker scripts and schema verification
2025-12-14 07:25:42 +01:00

233 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""
Python entrypoint script for TimeTracker Docker container
This avoids shell script line ending issues and provides better error handling
"""
import os
import sys
import time
import subprocess
import traceback
import psycopg2
from urllib.parse import urlparse
def log(message):
"""Log message with timestamp"""
from datetime import datetime
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"[{timestamp}] {message}")
def wait_for_database():
"""Wait for database to be ready"""
log("Waiting for database to be available...")
db_url = os.getenv('DATABASE_URL')
if not db_url:
log("✗ DATABASE_URL environment variable not set")
return False
log(f"Database URL: {db_url}")
max_attempts = 30
retry_delay = 2
for attempt in range(1, max_attempts + 1):
log(f"Attempt {attempt}/{max_attempts} to connect to database...")
try:
if db_url.startswith('postgresql'):
# Parse connection string using urlparse for proper handling
# Handle both postgresql:// and postgresql+psycopg2:// schemes
if db_url.startswith('postgresql+psycopg2://'):
parsed_url = urlparse(db_url.replace('postgresql+psycopg2://', 'postgresql://'))
else:
parsed_url = urlparse(db_url)
# Extract connection parameters
user = parsed_url.username or 'timetracker'
password = parsed_url.password or 'timetracker'
host = parsed_url.hostname or 'db'
port = parsed_url.port or 5432
# Remove leading slash from path to get database name
database = parsed_url.path.lstrip('/') or 'timetracker'
conn = psycopg2.connect(
host=host,
port=port,
database=database,
user=user,
password=password,
connect_timeout=5
)
conn.close()
log("✓ PostgreSQL database is available")
return True
elif db_url.startswith('sqlite://'):
db_file = db_url.replace('sqlite://', '')
if os.path.exists(db_file) or os.access(os.path.dirname(db_file), os.W_OK):
log("✓ SQLite database is available")
return True
else:
log("SQLite file not accessible")
else:
log(f"Unknown database URL format: {db_url}")
except Exception as e:
log(f"Database connection failed: {e}")
if attempt < max_attempts:
log(f"Waiting {retry_delay} seconds before next attempt...")
time.sleep(retry_delay)
log("✗ Database is not available after maximum retries")
return False
def run_migrations():
"""Run database migrations"""
log("Checking migrations...")
try:
# Check if migrations directory exists
if os.path.exists("/app/migrations"):
log("Migrations directory exists, checking status...")
# Try to apply any pending migrations
result = subprocess.run(['flask', 'db', 'upgrade'],
capture_output=True, text=True, timeout=120)
if result.returncode == 0:
log("✓ Migrations applied successfully")
# Verify all columns from models exist and fix if missing
log("Verifying complete database schema against models...")
fix_result = subprocess.run(
['python', '/app/scripts/verify_and_fix_schema.py'],
capture_output=True,
text=True,
timeout=180
)
if fix_result.returncode == 0:
# Print output to show what was fixed
if fix_result.stdout:
for line in fix_result.stdout.strip().split('\n'):
if line.strip() and not line.startswith('='):
log(line)
log("✓ Database schema verified and fixed")
else:
log(f"⚠ Schema verification had issues: {fix_result.stderr}")
# Fallback to the simpler fix script
log("Attempting fallback column fix...")
fallback_result = subprocess.run(
['python', '/app/scripts/fix_missing_columns.py'],
capture_output=True,
text=True,
timeout=60
)
if fallback_result.returncode == 0:
log("✓ Fallback fix completed")
return True
else:
log(f"⚠ Migration application failed: {result.stderr}")
# Try to fix missing columns even if migration failed
log("Attempting to fix missing columns...")
fix_result = subprocess.run(
['python', '/app/scripts/verify_and_fix_schema.py'],
capture_output=True,
text=True,
timeout=180
)
if fix_result.returncode == 0:
if fix_result.stdout:
for line in fix_result.stdout.strip().split('\n'):
if line.strip() and not line.startswith('='):
log(line)
log("✓ Missing columns fixed")
else:
# Fallback to simpler script
log("Trying fallback fix...")
fallback_result = subprocess.run(
['python', '/app/scripts/fix_missing_columns.py'],
capture_output=True,
text=True,
timeout=60
)
if fallback_result.returncode == 0:
log("✓ Fallback fix completed")
return False
else:
log("No migrations directory found, initializing...")
# Initialize migrations
result = subprocess.run(['flask', 'db', 'init'],
capture_output=True, text=True, timeout=60)
if result.returncode == 0:
log("✓ Migrations initialized")
# Create initial migration
result = subprocess.run(['flask', 'db', 'migrate', '-m', 'Initial schema'],
capture_output=True, text=True, timeout=60)
if result.returncode == 0:
log("✓ Initial migration created")
# Apply migration
result = subprocess.run(['flask', 'db', 'upgrade'],
capture_output=True, text=True, timeout=60)
if result.returncode == 0:
log("✓ Initial migration applied")
return True
else:
log(f"⚠ Initial migration application failed: {result.stderr}")
return False
else:
log(f"⚠ Initial migration creation failed: {result.stderr}")
return False
else:
log(f"⚠ Migration initialization failed: {result.stderr}")
return False
except subprocess.TimeoutExpired:
log("⚠ Migration operation timed out")
return False
except Exception as e:
log(f"⚠ Migration error: {e}")
return False
def main():
"""Main entrypoint function"""
log("=== TimeTracker Docker Entrypoint ===")
# Set environment variables
os.environ.setdefault('FLASK_APP', '/app/app.py')
# Wait for database
if not wait_for_database():
log("✗ Failed to connect to database")
sys.exit(1)
# Run migrations
if not run_migrations():
log("⚠ Migration issues detected, continuing anyway")
log("=== Startup Complete ===")
log("Starting TimeTracker application...")
# Execute the command passed to the container
if len(sys.argv) > 1:
try:
os.execv(sys.argv[1], sys.argv[1:])
except Exception as e:
log(f"✗ Failed to execute command: {e}")
sys.exit(1)
else:
# Default command
try:
os.execv('/usr/bin/python', ['python', '/app/start.py'])
except Exception as e:
log(f"✗ Failed to execute default command: {e}")
sys.exit(1)
if __name__ == "__main__":
main()