diff --git a/TODO.md b/TODO.md
index e0ed7ff..7698450 100644
--- a/TODO.md
+++ b/TODO.md
@@ -55,10 +55,10 @@ This document outlines upcoming tasks and improvements for the LeagueLedger appl
## User Management & Profile Features
- [ ] **Profile Management**
- - [ ] Update profile picture functionality
- - [ ] Change username capability
- - [ ] Account deletion process
- - [ ] Profile privacy settings
+ - [x] Update profile picture functionality
+ - [x] Change username capability
+ - [x] Account deletion process
+ - [x] Profile privacy settings
- [ ] Social media integration
## Environment Variables & Configuration
@@ -158,4 +158,10 @@ This document outlines upcoming tasks and improvements for the LeagueLedger appl
- [ ] Create database migration tools
- [ ] Add support for clustering/high availability
- [ ] Implement CDN for static assets
-- [ ] Create backup and disaster recovery procedures
\ No newline at end of file
+- [ ] Create backup and disaster recovery procedures
+
+
+## Layout
+- [ ] Change layout to use Tailwind CSS
+- [ ] Change layout to use https://ui.shadcn.com/
+- [ ] Make mail templates less spam-y
\ No newline at end of file
diff --git a/app/db_init.py b/app/db_init.py
index 8fd13f4..775f0c8 100644
--- a/app/db_init.py
+++ b/app/db_init.py
@@ -8,6 +8,7 @@ from datetime import datetime, timedelta
from sqlalchemy import inspect
from sqlalchemy.orm import Session
from passlib.context import CryptContext
+import asyncio
from .models import User, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event, SystemSettings
from .db import SessionLocal, engine
@@ -26,17 +27,17 @@ def table_has_column(engine, table_name, column_name):
columns = [col['name'] for col in inspector.get_columns(table_name)]
return column_name in columns
-def init_db():
+async def init_db():
"""Initialize the database, applying migrations and seeding data."""
# First, create all tables if they don't exist
from .models import Base
Base.metadata.create_all(bind=engine)
# Then, run any needed migrations
- run_migrations(engine)
+ await asyncio.to_thread(run_migrations, engine)
# Then proceed with seeding if needed
- seed_db()
+ await asyncio.to_thread(seed_db)
# Initialize system settings if needed
- init_system_settings()
+ await asyncio.to_thread(init_system_settings)
def init_system_settings():
"""Initialize the system settings table if it doesn't exist."""
@@ -173,23 +174,24 @@ def seed_db():
memberships = []
membership_data = [
# Quiz Wizards
- (1, 1, True, 160), # Admin user is team admin of Quiz Wizards
- (2, 1, True, 155),
- (3, 1, False, 130),
- (4, 1, False, 90),
- (5, 1, False, 45),
+ (1, 1, True, True, 160), # Admin user is team admin AND captain of Quiz Wizards
+ (2, 1, True, True, 155), # John is also admin AND captain
+ (3, 1, False, False, 130),
+ (4, 1, False, False, 90),
+ (5, 1, False, False, 45),
# Trivia Titans
- (2, 2, True, 150),
- (1, 2, False, 145),
+ (2, 2, True, True, 150), # John is admin AND captain of Trivia Titans
+ (1, 2, False, False, 145),
# Beer Brainiacs
- (3, 3, True, 120),
+ (3, 3, True, True, 120), # Sarah is admin AND captain of Beer Brainiacs
]
- for user_id, team_id, is_admin, days_ago in membership_data:
+ for user_id, team_id, is_admin, is_captain, days_ago in membership_data:
membership_attrs = {
"user_id": user_id,
"team_id": team_id,
- "is_admin": is_admin
+ "is_admin": is_admin,
+ "is_captain": is_captain # Added is_captain field
}
if has_joined_at:
membership_attrs["joined_at"] = datetime.now() - timedelta(days=days_ago)
@@ -366,4 +368,4 @@ def seed_db():
db.close()
if __name__ == "__main__":
- init_db()
+ asyncio.run(init_db())
diff --git a/app/main.py b/app/main.py
index c0af7a3..56ed8db 100644
--- a/app/main.py
+++ b/app/main.py
@@ -11,6 +11,11 @@ from starlette.middleware.authentication import AuthenticationMiddleware
from dotenv import load_dotenv
import logging
import contextlib
+import time
+import asyncio
+import sqlalchemy
+from sqlalchemy.exc import OperationalError, ProgrammingError
+from sqlalchemy import text
from .db import engine, get_db
from . import models
@@ -52,10 +57,54 @@ app.add_middleware(
# Then add SessionMiddleware (last added = first executed)
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)
-# Initialize database on startup
-@app.on_event("startup")
-async def startup_db_client():
- logger.info("Starting database initialization")
+# Function to check database connection
+async def check_db_connection(max_retries=10, initial_retry_delay=1):
+ """
+ Check database connection with retries and exponential backoff
+
+ Args:
+ max_retries: Maximum number of retry attempts
+ initial_retry_delay: Initial delay before first retry in seconds
+
+ Returns:
+ bool: True if connection succeeded, False otherwise
+ """
+ retry_delay = initial_retry_delay
+
+ for attempt in range(1, max_retries + 1):
+ try:
+ # Try a simple query to test the connection
+ with engine.connect() as conn:
+ conn.execute(text("SELECT 1"))
+ logger.info(f"Database connection successful on attempt {attempt}")
+ return True
+ except (OperationalError, ProgrammingError) as e:
+ if "Connection refused" in str(e) or "Can't connect" in str(e):
+ logger.warning(f"Database connection attempt {attempt}/{max_retries} failed: {str(e)}")
+
+ if attempt < max_retries:
+ logger.info(f"Retrying in {retry_delay} seconds...")
+ await asyncio.sleep(retry_delay)
+ # Exponential backoff with a maximum delay of 10 seconds
+ retry_delay = min(retry_delay * 2, 10)
+ else:
+ logger.error(f"Failed to connect to database after {max_retries} attempts")
+ return False
+ else:
+ # For other database errors, log and continue
+ logger.error(f"Database error: {str(e)}")
+ return False
+ except Exception as e:
+ logger.error(f"Unexpected error connecting to database: {str(e)}")
+ return False
+
+ return False
+
+# Initialize database with schema and tables
+async def initialize_database():
+ """
+ Initialize the database schema and seed data
+ """
try:
# Import models to ensure they're registered with Base before initialization
from . import models
@@ -63,10 +112,29 @@ async def startup_db_client():
# Initialize database (applies migrations and seeds data)
init_db()
logger.info("Database initialized and migrated successfully")
-
+ return True
except Exception as e:
logger.error(f"Database initialization error: {str(e)}")
- # We continue even if there was an error, as the application might still work with partial functionality
+ return False
+
+# Initialize database on startup
+@app.on_event("startup")
+async def startup_db_client():
+ logger.info("Starting database initialization")
+
+ # First, ensure database server is available with polling
+ max_retries = 15 # Try up to 15 times (with exponential backoff, this could be ~2 minutes)
+ connected = await check_db_connection(max_retries=max_retries)
+
+ if connected:
+ # Once connected, initialize the database
+ success = await initialize_database()
+ if success:
+ logger.info("Database setup completed successfully")
+ else:
+ logger.warning("Database initialization completed with errors")
+ else:
+ logger.error("Failed to connect to database, application may not function correctly")
# Mount static files
app.mount("/static", StaticFiles(directory="app/static"), name="static")
diff --git a/app/templates/email/email_verification.html b/app/templates/email/email_verification.html
index 67c5a98..a561a8e 100644
--- a/app/templates/email/email_verification.html
+++ b/app/templates/email/email_verification.html
@@ -12,62 +12,92 @@
max-width: 600px;
margin: 0 auto;
padding: 20px;
+ background-color: #f9f9f9;
+ }
+ .container {
+ background-color: #ffffff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ overflow: hidden;
}
.header {
background-color: #2D7738;
padding: 20px;
text-align: center;
color: white;
- border-radius: 5px 5px 0 0;
}
.content {
- padding: 20px;
- border: 1px solid #ddd;
- border-top: none;
- border-radius: 0 0 5px 5px;
+ padding: 30px;
+ background-color: #ffffff;
+ }
+ .button-container {
+ margin: 25px 0;
+ text-align: center;
}
.button {
display: inline-block;
background-color: #2D7738;
color: white;
text-decoration: none;
- padding: 10px 20px;
+ padding: 12px 25px;
border-radius: 5px;
- margin: 20px 0;
+ font-weight: 500;
+ }
+ .link-help {
+ margin-top: 20px;
+ padding: 15px;
+ background-color: #f5f5f5;
+ border-radius: 5px;
+ font-size: 14px;
}
.footer {
- margin-top: 20px;
- font-size: 12px;
+ padding: 20px;
text-align: center;
+ font-size: 12px;
color: #777;
+ border-top: 1px solid #eaeaea;
}
-
-
-
-
Hello {{ username }},
+
+
-
Thank you for registering with LeagueLedger! To complete your registration, please verify your email address by clicking the button below:
+
+
Hello {{ username }},
+
+
Thank you for creating an account with LeagueLedger, your pub quiz team tracking platform. We're excited to have you join our community!
+
+
To ensure account security and complete your registration, please verify your email address by clicking the button below:
+
+
+
+
+
+
This verification step helps us keep your account secure and allows you to:
+
+ - Create or join teams
+ - Track your quiz night scores
+ - Earn points and achievements
+ - Climb the leaderboards
+
+
+
If you did not create an account with LeagueLedger, please disregard this email.
+
+
Best regards,
The LeagueLedger Team
+
-
- Verify Email
-
-
-
Or copy and paste this link into your browser:
-
{{ verification_link }}
-
-
If you did not create an account with us, please ignore this email.
-
-
Best regards,
The LeagueLedger Team
-
-
-