Files
TimeTracker/docker/entrypoint.py
Dries Peeters b880191c16 feat: add time-entry editing; improve invoices/PDF; harden Docker startup
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`)
2025-09-03 09:48:19 +02:00

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()