mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-10 21:55:07 -06:00
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
233 lines
8.9 KiB
Python
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()
|