mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-05 03:01:13 -06:00
- Fix AUTH_METHOD=none: Read from Flask app config instead of Config class - Add comprehensive schema verification: Verify all SQLAlchemy models against database and auto-fix missing columns - Improve startup logging: Unified format with timestamps and log levels - Enhanced migration flow: Automatic schema verification after migrations Fixes authentication issue where password field showed even with AUTH_METHOD=none. Ensures all database columns from models exist, preventing missing column errors. Improves startup logging for better debugging and monitoring.
207 lines
6.8 KiB
Python
207 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Improved Python startup script for TimeTracker
|
||
This script ensures proper database initialization order and handles errors gracefully
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
import subprocess
|
||
import traceback
|
||
import psycopg2
|
||
from urllib.parse import urlparse
|
||
|
||
def wait_for_database():
|
||
"""Wait for database to be ready with proper connection testing"""
|
||
# Logging is handled by main()
|
||
|
||
# Get database URL from environment
|
||
db_url = os.getenv('DATABASE_URL', 'postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker')
|
||
|
||
# If using SQLite, ensure the database directory exists and return immediately
|
||
if db_url.startswith('sqlite:'):
|
||
try:
|
||
# Normalize file path from URL
|
||
db_path = None
|
||
prefix_four = 'sqlite:////'
|
||
prefix_three = 'sqlite:///'
|
||
prefix_mem = 'sqlite://'
|
||
if db_url.startswith(prefix_four):
|
||
db_path = '/' + db_url[len(prefix_four):]
|
||
elif db_url.startswith(prefix_three):
|
||
# Relative inside container; keep as-is
|
||
db_path = db_url[len(prefix_three):]
|
||
# If it's a relative path, make sure directory exists
|
||
if not db_path.startswith('/'):
|
||
db_path = '/' + db_path
|
||
elif db_url.startswith(prefix_mem):
|
||
# Could be sqlite:///:memory:
|
||
if db_url.endswith(':memory:'):
|
||
return True
|
||
# Fallback: strip scheme
|
||
db_path = db_url[len(prefix_mem):]
|
||
|
||
if db_path:
|
||
import os as _os
|
||
import sqlite3 as _sqlite3
|
||
dir_path = _os.path.dirname(db_path)
|
||
if dir_path:
|
||
_os.makedirs(dir_path, exist_ok=True)
|
||
# Try to open the database to ensure writability
|
||
conn = _sqlite3.connect(db_path)
|
||
conn.close()
|
||
return True
|
||
except Exception as e:
|
||
print(f"SQLite path/setup check failed: {e}")
|
||
return False
|
||
|
||
# Parse the URL to get connection details (PostgreSQL)
|
||
if db_url.startswith('postgresql+psycopg2://'):
|
||
db_url = db_url.replace('postgresql+psycopg2://', '')
|
||
|
||
# Extract host, port, database, user, password
|
||
if '@' in db_url:
|
||
auth_part, rest = db_url.split('@', 1)
|
||
user, password = auth_part.split(':', 1)
|
||
if ':' in rest:
|
||
host_port, database = rest.rsplit('/', 1)
|
||
if ':' in host_port:
|
||
host, port = host_port.split(':', 1)
|
||
else:
|
||
host, port = host_port, '5432'
|
||
else:
|
||
host, port, database = rest, '5432', 'timetracker'
|
||
else:
|
||
host, port, database, user, password = 'db', '5432', 'timetracker', 'timetracker', 'timetracker'
|
||
|
||
max_attempts = 30
|
||
attempt = 0
|
||
|
||
while attempt < max_attempts:
|
||
try:
|
||
conn = psycopg2.connect(
|
||
host=host,
|
||
port=port,
|
||
database=database,
|
||
user=user,
|
||
password=password,
|
||
connect_timeout=5
|
||
)
|
||
conn.close()
|
||
return True
|
||
except Exception as e:
|
||
attempt += 1
|
||
if attempt < max_attempts:
|
||
time.sleep(2)
|
||
|
||
return False
|
||
|
||
def run_script(script_path, description):
|
||
"""Run a Python script with proper error handling"""
|
||
try:
|
||
result = subprocess.run(
|
||
[sys.executable, script_path],
|
||
check=True,
|
||
capture_output=False, # Let the script output directly
|
||
text=True
|
||
)
|
||
return True
|
||
except subprocess.CalledProcessError as e:
|
||
log(f"{description} failed with exit code {e.returncode}", "ERROR")
|
||
return False
|
||
except Exception as e:
|
||
log(f"Unexpected error running {description}: {e}", "ERROR")
|
||
return False
|
||
|
||
def display_network_info():
|
||
"""Display network information for debugging"""
|
||
print("=== Network Information ===")
|
||
try:
|
||
print(f"Hostname: {os.uname().nodename}")
|
||
except:
|
||
print("Hostname: N/A (Windows)")
|
||
|
||
try:
|
||
import socket
|
||
hostname = socket.gethostname()
|
||
local_ip = socket.gethostbyname(hostname)
|
||
print(f"Local IP: {local_ip}")
|
||
except:
|
||
print("Local IP: N/A")
|
||
|
||
print(f"Environment: {os.environ.get('FLASK_APP', 'N/A')}")
|
||
print(f"Working Directory: {os.getcwd()}")
|
||
print("==========================")
|
||
|
||
def log(message, level="INFO"):
|
||
"""Log message with timestamp and level"""
|
||
from datetime import datetime
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
prefix = {
|
||
"INFO": "ℹ",
|
||
"SUCCESS": "✓",
|
||
"WARNING": "⚠",
|
||
"ERROR": "✗"
|
||
}.get(level, "•")
|
||
print(f"[{timestamp}] {prefix} {message}")
|
||
|
||
def main():
|
||
log("=" * 60, "INFO")
|
||
log("Starting TimeTracker Application", "INFO")
|
||
log("=" * 60, "INFO")
|
||
|
||
# Set environment
|
||
os.environ['FLASK_APP'] = 'app'
|
||
os.chdir('/app')
|
||
|
||
# Wait for database
|
||
log("Waiting for database connection...", "INFO")
|
||
if not wait_for_database():
|
||
log("Database is not available, exiting...", "ERROR")
|
||
sys.exit(1)
|
||
|
||
# Run enhanced database initialization and migration
|
||
log("Running database initialization...", "INFO")
|
||
if not run_script('/app/docker/init-database-enhanced.py', 'Database initialization'):
|
||
log("Database initialization failed, exiting...", "ERROR")
|
||
sys.exit(1)
|
||
|
||
log("Database initialization completed", "SUCCESS")
|
||
|
||
# Ensure default settings and admin user exist (idempotent)
|
||
# Note: Database initialization is already handled by the migration system above
|
||
# The flask init_db command is optional and may not be available in all environments
|
||
try:
|
||
result = subprocess.run(
|
||
['flask', 'init_db'],
|
||
check=False, # Don't fail if command doesn't exist
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30
|
||
)
|
||
if result.returncode != 0 and "No such command" not in (result.stderr or ""):
|
||
log("flask init_db returned non-zero exit code (continuing)", "WARNING")
|
||
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
||
# All errors are non-fatal - database is already initialized
|
||
pass
|
||
|
||
log("=" * 60, "INFO")
|
||
log("Starting application server", "INFO")
|
||
log("=" * 60, "INFO")
|
||
# Start gunicorn with access logs
|
||
os.execv('/usr/local/bin/gunicorn', [
|
||
'gunicorn',
|
||
'--bind', '0.0.0.0:8080',
|
||
'--worker-class', 'eventlet',
|
||
'--workers', '1',
|
||
'--timeout', '120',
|
||
'--access-logfile', '-',
|
||
'--error-logfile', '-',
|
||
'--log-level', 'info',
|
||
'app:create_app()'
|
||
])
|
||
|
||
if __name__ == '__main__':
|
||
main()
|