mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2025-12-31 00:09:58 -06:00
Timer/Editing - Add/edit time-entry UI and flows in templates (`templates/timer/*`) - Extend timer and API routes (`app/routes/timer.py`, `app/routes/api.py`) - Update mobile interactions (`app/static/mobile.js`) Invoices/PDF - Improve invoice model and route handling (`app/models/invoice.py`, `app/routes/invoices.py`) - Enhance PDF generation and fallback logic (`app/utils/pdf_generator*.py`) - Adjust invoice view layout (`templates/invoices/view.html`) Docker/Startup - Refine Docker build and startup paths (`Dockerfile`) - Improve init/entrypoint scripts (`docker/init-database-*.py`, new `docker/entrypoint*.sh`, `docker/entrypoint.py`) - General startup robustness and permissions fixes Docs/UI - Refresh README and Docker docs (setup, troubleshooting, structure) - Minor UI/help updates (`templates/main/help.html`, `templates/projects/create.html`) - Remove obsolete asset (`assets/screenshots/Task_Management.png`) - Add repo hygiene updates (e.g., `.gitattributes`)
182 lines
6.4 KiB
Python
182 lines
6.4 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
|
|
if db_url.startswith('postgresql+psycopg2://'):
|
|
db_url = db_url.replace('postgresql+psycopg2://', '')
|
|
|
|
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'
|
|
|
|
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=60)
|
|
if result.returncode == 0:
|
|
log("✓ Migrations applied successfully")
|
|
return True
|
|
else:
|
|
log(f"⚠ Migration application failed: {result.stderr}")
|
|
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()
|