From 45a1b2f234b41f751f33d546c3944153a7424e9b Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:00:56 -0500 Subject: [PATCH 01/10] feat: optimize TTFB with connection pooling, caching, and performance monitoring --- server.py | 271 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 171 insertions(+), 100 deletions(-) diff --git a/server.py b/server.py index 47d4f30..9889ea9 100644 --- a/server.py +++ b/server.py @@ -1,11 +1,12 @@ import os import time +import logging from threading import Thread import smtplib from email.mime.text import MIMEText -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, g from dotenv import load_dotenv -from database import init_db, get_connection, add_email, remove_email +from database import init_db, get_connection, return_connection, add_email, remove_email load_dotenv() @@ -16,18 +17,14 @@ SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') app = Flask(__name__) -_db_initialized = False +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Cache configuration _newsletter_cache = {} _cache_timestamp = {} CACHE_DURATION = 300 -def ensure_db_initialized(): - """Lazy database initialization - only runs on first database access""" - global _db_initialized - if not _db_initialized: - init_db() - _db_initialized = True - def get_newsletters_cached(): """Get newsletters with caching to reduce database hits""" current_time = time.time() @@ -36,26 +33,32 @@ def get_newsletters_cached(): current_time - _cache_timestamp.get('newsletters', 0) < CACHE_DURATION): return _newsletter_cache['newsletters'] - ensure_db_initialized() - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "SELECT id, subject, body, sent_at " - "FROM newsletters ORDER BY sent_at DESC" - ) - rows = cursor.fetchall() - cursor.close() - conn.close() - - newsletters = [ - {"id": r[0], "subject": r[1], "body": r[2], "sent_at": r[3]} - for r in rows - ] - - _newsletter_cache['newsletters'] = newsletters - _cache_timestamp['newsletters'] = current_time - - return newsletters + conn = None + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "SELECT id, subject, body, sent_at " + "FROM newsletters ORDER BY sent_at DESC LIMIT 100" + ) + rows = cursor.fetchall() + cursor.close() + + newsletters = [ + {"id": r[0], "subject": r[1], "body": r[2], "sent_at": r[3]} + for r in rows + ] + + _newsletter_cache['newsletters'] = newsletters + _cache_timestamp['newsletters'] = current_time + + return newsletters + except Exception as e: + app.logger.error(f"Database error in get_newsletters_cached: {e}") + return [] + finally: + if conn: + return_connection(conn) def get_newsletter_by_id_cached(newsletter_id): """Get single newsletter with caching""" @@ -66,32 +69,38 @@ def get_newsletter_by_id_cached(newsletter_id): current_time - _cache_timestamp.get(cache_key, 0) < CACHE_DURATION): return _newsletter_cache[cache_key] - ensure_db_initialized() - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "SELECT id, subject, body, sent_at " - "FROM newsletters WHERE id = %s", - (newsletter_id,) - ) - row = cursor.fetchone() - cursor.close() - conn.close() - - if not row: - return None + conn = None + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "SELECT id, subject, body, sent_at " + "FROM newsletters WHERE id = %s", + (newsletter_id,) + ) + row = cursor.fetchone() + cursor.close() - newsletter = { - "id": row[0], - "subject": row[1], - "body": row[2], - "sent_at": row[3] - } - - _newsletter_cache[cache_key] = newsletter - _cache_timestamp[cache_key] = current_time - - return newsletter + if not row: + return None + + newsletter = { + "id": row[0], + "subject": row[1], + "body": row[2], + "sent_at": row[3] + } + + _newsletter_cache[cache_key] = newsletter + _cache_timestamp[cache_key] = current_time + + return newsletter + except Exception as e: + app.logger.error(f"Database error in get_newsletter_by_id_cached: {e}") + return None + finally: + if conn: + return_connection(conn) def clear_newsletter_cache(): """Clear newsletter cache when data is updated""" @@ -103,35 +112,49 @@ def clear_newsletter_cache(): _cache_timestamp.pop(key, None) @app.before_request -def start_timer(): - request._start_time = time.time() +def before_request(): + """Start timing the request and set up request context""" + g.start_time = time.time() @app.after_request -def log_request(response): - elapsed = time.time() - getattr(request, '_start_time', time.time()) - app.logger.info(f"{request.method} {request.path} completed in {elapsed:.3f}s") +def after_request(response): + """Log request timing and performance metrics""" + total_time = time.time() - g.start_time + + # Log slow requests + if total_time > 1.0: + app.logger.warning(f"Slow request: {request.method} {request.path} took {total_time:.3f}s") + elif total_time > 0.5: + app.logger.info(f"Request: {request.method} {request.path} took {total_time:.3f}s") + + # Add performance headers for debugging + response.headers['X-Response-Time'] = f"{total_time:.3f}s" + return response def send_confirmation_email(to_address: str, unsubscribe_link: str): """ Sends the HTML confirmation email to `to_address`. - This runs inside its own SMTP_SSL connection (timeout=10s). + This runs inside its own SMTP_SSL connection with reduced timeout. """ - subject = "Thanks for subscribing!" - html_body = render_template( - "confirmation_email.html", - unsubscribe_link=unsubscribe_link - ) - - msg = MIMEText(html_body, "html", "utf-8") - msg["Subject"] = subject - msg["From"] = SMTP_USER - msg["To"] = to_address - try: - with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) as server: + subject = "Thanks for subscribing!" + html_body = render_template( + "confirmation_email.html", + unsubscribe_link=unsubscribe_link + ) + + msg = MIMEText(html_body, "html", "utf-8") + msg["Subject"] = subject + msg["From"] = SMTP_USER + msg["To"] = to_address + + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=5) as server: server.login(SMTP_USER, SMTP_PASSWORD) server.sendmail(SMTP_USER, [to_address], msg.as_string()) + + app.logger.info(f"Confirmation email sent successfully to {to_address}") + except Exception as e: app.logger.error(f"Failed to send email to {to_address}: {e}") @@ -148,65 +171,113 @@ def index(): @app.route("/subscribe", methods=["POST"]) def subscribe(): - """Subscribe endpoint - lazy loads database only when needed""" + """Subscribe endpoint with optimized database handling""" data = request.get_json() or {} email = data.get("email") + if not email: return jsonify(error="No email provided"), 400 - ensure_db_initialized() + # Validate email format (basic check) + if "@" not in email or "." not in email.split("@")[-1]: + return jsonify(error="Invalid email format"), 400 - if add_email(email): - unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" + try: + if add_email(email): + unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" - Thread( - target=send_confirmation_async, - args=(email, unsubscribe_link), - daemon=True - ).start() + # Start email sending in background thread + Thread( + target=send_confirmation_async, + args=(email, unsubscribe_link), + daemon=True + ).start() - return jsonify(message="Email has been added"), 201 - - return jsonify(error="Email already exists"), 400 + return jsonify(message="Email has been added"), 201 + else: + return jsonify(error="Email already exists"), 400 + + except Exception as e: + app.logger.error(f"Error in subscribe endpoint: {e}") + return jsonify(error="Internal server error"), 500 @app.route("/unsubscribe", methods=["GET"]) def unsubscribe(): - """Unsubscribe endpoint - lazy loads database only when needed""" + """Unsubscribe endpoint with optimized database handling""" email = request.args.get("email") + if not email: return "No email specified.", 400 - ensure_db_initialized() - - if remove_email(email): - return f"The email {email} has been unsubscribed.", 200 - return f"Email {email} was not found or has already been unsubscribed.", 400 + try: + if remove_email(email): + return f"The email {email} has been unsubscribed.", 200 + else: + return f"Email {email} was not found or has already been unsubscribed.", 400 + + except Exception as e: + app.logger.error(f"Error in unsubscribe endpoint: {e}") + return "Internal server error", 500 @app.route("/newsletters", methods=["GET"]) def newsletters(): """ List all newsletters (newest first) with caching for better performance. """ - newsletters = get_newsletters_cached() - return render_template("newsletters.html", newsletters=newsletters) + try: + newsletters = get_newsletters_cached() + return render_template("newsletters.html", newsletters=newsletters) + except Exception as e: + app.logger.error(f"Error in newsletters endpoint: {e}") + return "Internal server error", 500 @app.route("/newsletter/", methods=["GET"]) def newsletter_detail(newsletter_id): """ Show a single newsletter by its ID with caching. """ - newsletter = get_newsletter_by_id_cached(newsletter_id) - - if not newsletter: - return "Newsletter not found.", 404 + try: + newsletter = get_newsletter_by_id_cached(newsletter_id) + + if not newsletter: + return "Newsletter not found.", 404 - return render_template("newsletter_detail.html", newsletter=newsletter) + return render_template("newsletter_detail.html", newsletter=newsletter) + except Exception as e: + app.logger.error(f"Error in newsletter_detail endpoint: {e}") + return "Internal server error", 500 @app.route("/admin/clear-cache", methods=["POST"]) def clear_cache(): """Admin endpoint to clear newsletter cache""" - clear_newsletter_cache() - return jsonify(message="Cache cleared successfully"), 200 + try: + clear_newsletter_cache() + return jsonify(message="Cache cleared successfully"), 200 + except Exception as e: + app.logger.error(f"Error clearing cache: {e}") + return jsonify(error="Failed to clear cache"), 500 + +@app.route("/health", methods=["GET"]) +def health_check(): + """Health check endpoint for monitoring""" + return jsonify(status="healthy", timestamp=time.time()), 200 + +# Error handlers +@app.errorhandler(404) +def not_found(error): + return jsonify(error="Not found"), 404 + +@app.errorhandler(500) +def internal_error(error): + return jsonify(error="Internal server error"), 500 + +# Initialize database at startup +try: + init_db() + app.logger.info("Database initialized successfully") +except Exception as e: + app.logger.error(f"Failed to initialize database: {e}") + raise if __name__ == "__main__": app.run(host="0.0.0.0", debug=True) \ No newline at end of file From 66011bcd0f3651da6c1674d11f1b3e532be58dec Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:01:08 -0500 Subject: [PATCH 02/10] feat: implement connection pooling and automatic schema migration for performance --- database.py | 246 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 201 insertions(+), 45 deletions(-) diff --git a/database.py b/database.py index 0569f18..d8059fe 100644 --- a/database.py +++ b/database.py @@ -1,66 +1,222 @@ import os import psycopg2 -from psycopg2 import IntegrityError +from psycopg2 import pool, IntegrityError from dotenv import load_dotenv +import logging load_dotenv() +# Global connection pool +_connection_pool = None + +def get_connection_pool(): + """Initialize and return the connection pool""" + global _connection_pool + if _connection_pool is None: + try: + _connection_pool = psycopg2.pool.ThreadedConnectionPool( + minconn=2, + maxconn=20, + host=os.getenv("PG_HOST"), + port=os.getenv("PG_PORT", 5432), + dbname=os.getenv("PG_DATABASE"), + user=os.getenv("PG_USER"), + password=os.getenv("PG_PASSWORD"), + connect_timeout=5 + ) + logging.info("Database connection pool created successfully") + except Exception as e: + logging.error(f"Error creating connection pool: {e}") + raise + return _connection_pool + def get_connection(): - """Return a database connection.""" - return psycopg2.connect( - host=os.getenv("PG_HOST"), - port=os.getenv("PG_PORT"), - dbname=os.getenv("PG_DATABASE"), - user=os.getenv("PG_USER"), - password=os.getenv("PG_PASSWORD"), - connect_timeout=10 - ) + """Get a connection from the pool""" + try: + pool = get_connection_pool() + conn = pool.getconn() + if conn.closed: + # Connection is closed, remove it and get a new one + pool.putconn(conn, close=True) + conn = pool.getconn() + return conn + except Exception as e: + logging.error(f"Error getting connection from pool: {e}") + raise + +def return_connection(conn): + """Return a connection to the pool""" + try: + pool = get_connection_pool() + pool.putconn(conn) + except Exception as e: + logging.error(f"Error returning connection to pool: {e}") + +def close_all_connections(): + """Close all connections in the pool""" + global _connection_pool + if _connection_pool: + _connection_pool.closeall() + _connection_pool = None + logging.info("All database connections closed") + +def column_exists(cursor, table_name, column_name): + """Check if a column exists in a table""" + cursor.execute(""" + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = %s AND column_name = %s + ) + """, (table_name, column_name)) + return cursor.fetchone()[0] + +def index_exists(cursor, index_name): + """Check if an index exists""" + cursor.execute(""" + SELECT EXISTS ( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = %s AND n.nspname = 'public' + ) + """, (index_name,)) + return cursor.fetchone()[0] def init_db(): - conn = get_connection() - cursor = conn.cursor() - cursor.execute(""" - CREATE TABLE IF NOT EXISTS subscribers ( - id SERIAL PRIMARY KEY, - email TEXT UNIQUE NOT NULL - ) - """) - - cursor.execute(""" - CREATE TABLE IF NOT EXISTS newsletters( - id SERIAL PRIMARY KEY, - subject TEXT NOT NULL, - body TEXT NOT NULL, - sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """) + """Initialize database tables and indexes""" + conn = None + try: + conn = get_connection() + cursor = conn.cursor() - conn.commit() - cursor.close() - conn.close() + # Create subscribers table + cursor.execute(""" + CREATE TABLE IF NOT EXISTS subscribers ( + id SERIAL PRIMARY KEY, + email TEXT UNIQUE NOT NULL + ) + """) + + # Add created_at column if it doesn't exist + if not column_exists(cursor, 'subscribers', 'created_at'): + cursor.execute(""" + ALTER TABLE subscribers + ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + """) + logging.info("Added created_at column to subscribers table") + + # Create newsletters table + cursor.execute(""" + CREATE TABLE IF NOT EXISTS newsletters( + id SERIAL PRIMARY KEY, + subject TEXT NOT NULL, + body TEXT NOT NULL, + sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Create indexes only if they don't exist + if not index_exists(cursor, 'idx_newsletters_sent_at'): + cursor.execute("CREATE INDEX idx_newsletters_sent_at ON newsletters(sent_at DESC)") + logging.info("Created index idx_newsletters_sent_at") + + if not index_exists(cursor, 'idx_subscribers_email'): + cursor.execute("CREATE INDEX idx_subscribers_email ON subscribers(email)") + logging.info("Created index idx_subscribers_email") + + if not index_exists(cursor, 'idx_subscribers_created_at'): + cursor.execute("CREATE INDEX idx_subscribers_created_at ON subscribers(created_at DESC)") + logging.info("Created index idx_subscribers_created_at") + + conn.commit() + cursor.close() + logging.info("Database tables and indexes initialized successfully") + + except Exception as e: + logging.error(f"Error initializing database: {e}") + if conn: + conn.rollback() + raise + finally: + if conn: + return_connection(conn) def add_email(email): + """Add email to subscribers with connection pooling""" + conn = None try: - with get_connection() as conn: - with conn.cursor() as cursor: - cursor.execute("INSERT INTO subscribers (email) VALUES (%s)", (email,)) - conn.commit() + conn = get_connection() + cursor = conn.cursor() + cursor.execute("INSERT INTO subscribers (email) VALUES (%s)", (email,)) + conn.commit() + cursor.close() + logging.info(f"Email added successfully: {email}") return True + except IntegrityError: + # Email already exists + if conn: + conn.rollback() + logging.info(f"Email already exists: {email}") return False - except psycopg2.OperationalError as e: - print(f"Error: {e}") + + except Exception as e: + if conn: + conn.rollback() + logging.error(f"Error adding email {email}: {e}") return False + + finally: + if conn: + return_connection(conn) def remove_email(email): + """Remove email from subscribers with connection pooling""" + conn = None try: - with get_connection() as conn: - with conn.cursor() as cursor: - cursor.execute("DELETE FROM subscribers WHERE email = %s", (email,)) - conn.commit() - if cursor.rowcount > 0: - return True - return False + conn = get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM subscribers WHERE email = %s", (email,)) + conn.commit() + rows_affected = cursor.rowcount + cursor.close() + + if rows_affected > 0: + logging.info(f"Email removed successfully: {email}") + return True + else: + logging.info(f"Email not found for removal: {email}") + return False + except Exception as e: - print(f"Error removing email: {e}") - return False \ No newline at end of file + if conn: + conn.rollback() + logging.error(f"Error removing email {email}: {e}") + return False + + finally: + if conn: + return_connection(conn) + +def get_subscriber_count(): + """Get total number of subscribers""" + conn = None + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM subscribers") + count = cursor.fetchone()[0] + cursor.close() + return count + + except Exception as e: + logging.error(f"Error getting subscriber count: {e}") + return 0 + + finally: + if conn: + return_connection(conn) + +# Cleanup function for graceful shutdown +import atexit +atexit.register(close_all_connections) \ No newline at end of file From e52d0c61ca585388f085dbd16a0b925c7f52da89 Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:01:20 -0500 Subject: [PATCH 03/10] perf: optimize production deployment with tuned gunicorn settings and health checks --- Dockerfile | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index cc5893a..76785dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,52 @@ FROM python:3.11-slim-buster -RUN apt-get update && apt-get install -y build-essential +# Install system dependencies +RUN apt-get update && \ + apt-get install -y build-essential && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +# Set working directory WORKDIR /rideaware_landing +# Copy requirements first for better caching COPY requirements.txt . +# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt +# Copy application code COPY . . +# Set environment variables ENV FLASK_APP=server.py +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/rideaware_landing +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash app && \ + chown -R app:app /rideaware_landing +USER app + +# Expose port EXPOSE 5000 -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "server:app"] \ No newline at end of file +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +# Run with optimized Gunicorn settings +CMD ["gunicorn", \ + "--bind", "0.0.0.0:5000", \ + "--workers", "4", \ + "--worker-class", "sync", \ + "--worker-connections", "1000", \ + "--max-requests", "1000", \ + "--max-requests-jitter", "50", \ + "--preload", \ + "--timeout", "30", \ + "--keep-alive", "2", \ + "--access-logfile", "-", \ + "--error-logfile", "-", \ + "--log-level", "info", \ + "server:app"] \ No newline at end of file From 8dd316daf10df00dce4d5247d91959e6d8e2e716 Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:04:52 -0500 Subject: [PATCH 04/10] fix: update base image from deprecated buster to bullseye for security and availability --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 76785dd..9cdddc7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.11-slim-buster +FROM python:3.11-slim-bullseye # Install system dependencies RUN apt-get update && \ - apt-get install -y build-essential && \ + apt-get install -y build-essential curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* From 7713059ffc7ef307000215934c1a610b9a2d959a Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:18:14 -0500 Subject: [PATCH 05/10] styles: removed moving on the phone mockup --- static/css/styles.css | 1 - 1 file changed, 1 deletion(-) diff --git a/static/css/styles.css b/static/css/styles.css index 2935e31..2022086 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -247,7 +247,6 @@ body { box-shadow: 0 30px 60px rgba(0, 0, 0, 0.3); position: relative; transform: rotate(-5deg); - animation: float 6s ease-in-out infinite; } @keyframes float { From ae3febbcbd03d8d6b8de42be31e230b2604d71db Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:21:47 -0500 Subject: [PATCH 06/10] call back to local styles --- templates/index.html | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/templates/index.html b/templates/index.html index 8232c61..ac5110b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,14 +6,8 @@ RideAware - Smart Cycling Training Platform + - - From 5855619d57b241e58557f481d14d0302e2f151c5 Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 19:42:13 -0500 Subject: [PATCH 07/10] refactor: removed newsletter routes --- server.py | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/server.py b/server.py index 9889ea9..5fa8d53 100644 --- a/server.py +++ b/server.py @@ -219,34 +219,6 @@ def unsubscribe(): app.logger.error(f"Error in unsubscribe endpoint: {e}") return "Internal server error", 500 -@app.route("/newsletters", methods=["GET"]) -def newsletters(): - """ - List all newsletters (newest first) with caching for better performance. - """ - try: - newsletters = get_newsletters_cached() - return render_template("newsletters.html", newsletters=newsletters) - except Exception as e: - app.logger.error(f"Error in newsletters endpoint: {e}") - return "Internal server error", 500 - -@app.route("/newsletter/", methods=["GET"]) -def newsletter_detail(newsletter_id): - """ - Show a single newsletter by its ID with caching. - """ - try: - newsletter = get_newsletter_by_id_cached(newsletter_id) - - if not newsletter: - return "Newsletter not found.", 404 - - return render_template("newsletter_detail.html", newsletter=newsletter) - except Exception as e: - app.logger.error(f"Error in newsletter_detail endpoint: {e}") - return "Internal server error", 500 - @app.route("/admin/clear-cache", methods=["POST"]) def clear_cache(): """Admin endpoint to clear newsletter cache""" @@ -280,4 +252,4 @@ except Exception as e: raise if __name__ == "__main__": - app.run(host="0.0.0.0", debug=True) \ No newline at end of file + app.run(host="0.0.0.0", debug=True) From dbf748ab3fdf03006aa375f83c5d45de07e40832 Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 19:43:13 -0500 Subject: [PATCH 08/10] refactor: removed newsletter routes --- templates/index.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index ac5110b..e444c7d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,10 +14,6 @@ From 4934e7fbecf06e61ffcd199f07c31be814d7a348 Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 20:04:29 -0500 Subject: [PATCH 09/10] fix; bring back newsletters; nginx config broke the site --- server.py | 28 ++++++++++++++++++++++++++++ templates/index.html | 4 ++++ 2 files changed, 32 insertions(+) diff --git a/server.py b/server.py index 5fa8d53..d1a3dd0 100644 --- a/server.py +++ b/server.py @@ -219,6 +219,34 @@ def unsubscribe(): app.logger.error(f"Error in unsubscribe endpoint: {e}") return "Internal server error", 500 +@app.route("/newsletters", methods=["GET"]) +def newsletters(): + """ + List all newsletters (newest first) with caching for better performance. + """ + try: + newsletters = get_newsletters_cached() + return render_template("newsletters.html", newsletters=newsletters) + except Exception as e: + app.logger.error(f"Error in newsletters endpoint: {e}") + return "Internal server error", 500 + +@app.route("/newsletter/", methods=["GET"]) +def newsletter_detail(newsletter_id): + """ + Show a single newsletter by its ID with caching. + """ + try: + newsletter = get_newsletter_by_id_cached(newsletter_id) + + if not newsletter: + return "Newsletter not found.", 404 + + return render_template("newsletter_detail.html", newsletter=newsletter) + except Exception as e: + app.logger.error(f"Error in newsletter_detail endpoint: {e}") + return "Internal server error", 500 + @app.route("/admin/clear-cache", methods=["POST"]) def clear_cache(): """Admin endpoint to clear newsletter cache""" diff --git a/templates/index.html b/templates/index.html index e444c7d..ac5110b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,6 +14,10 @@ From 2a2df9f6e5d62aa5d71081bc8dbe1c21d60c8cb0 Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 22:03:41 -0500 Subject: [PATCH 10/10] style: bring back animiimation on phone --- static/css/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/css/styles.css b/static/css/styles.css index 2022086..2935e31 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -247,6 +247,7 @@ body { box-shadow: 0 30px 60px rgba(0, 0, 0, 0.3); position: relative; transform: rotate(-5deg); + animation: float 6s ease-in-out infinite; } @keyframes float {