Compare commits

..

8 Commits

Author SHA1 Message Date
Peter Stockings
b4121eada7 Add database connection pooling 2026-02-04 00:03:03 +11:00
Peter Stockings
a6a71f3139 Only load graphs when they come into view 2026-02-03 23:52:59 +11:00
Peter Stockings
9998616946 Add defer to hyperscript 2026-02-03 23:52:21 +11:00
Peter Stockings
c20f2e2f85 Added safety checks to the graph regression logic in utils.py. This stops those "illegal value" server warnings and makes the math more efficient for small datasets 2026-02-03 23:51:52 +11:00
Peter Stockings
ec8d7f6825 Add asset caching 2026-02-03 23:36:58 +11:00
Peter Stockings
2e79ad1b8b Remove more unused js and css 2026-02-03 23:36:30 +11:00
Peter Stockings
d223bdeebc Add compression 2026-02-03 23:25:13 +11:00
Peter Stockings
9a2ce6754a Remove unused js libs 2026-02-03 23:24:49 +11:00
14 changed files with 47 additions and 2224 deletions

4
app.py
View File

@@ -22,12 +22,16 @@ from flask_htmx import HTMX
import minify_html import minify_html
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from flask_compress import Compress
# Load environment variables from .env file in non-production environments # Load environment variables from .env file in non-production environments
if os.environ.get('FLASK_ENV') != 'production': if os.environ.get('FLASK_ENV') != 'production':
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__)
app.config['COMPRESS_REGISTER'] = True
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # 1 year
Compress(app)
app.config.from_pyfile('config.py') app.config.from_pyfile('config.py')
app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f') app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
jinja_partials.register_extensions(app) jinja_partials.register_extensions(app)

29
db.py
View File

@@ -1,10 +1,11 @@
import os import os
import psycopg2 import psycopg2
from psycopg2 import pool
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from datetime import datetime from datetime import datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from urllib.parse import urlparse from urllib.parse import urlparse
from flask import g from flask import g, current_app
from features.exercises import Exercises from features.exercises import Exercises
from features.people_graphs import PeopleGraphs from features.people_graphs import PeopleGraphs
from features.person_overview import PersonOverview from features.person_overview import PersonOverview
@@ -16,6 +17,8 @@ from utils import get_exercise_graph_model
class DataBase(): class DataBase():
_pool = None
def __init__(self, app=None): def __init__(self, app=None):
self.stats = Stats(self.execute) self.stats = Stats(self.execute)
self.exercises = Exercises(self.execute) self.exercises = Exercises(self.execute)
@@ -25,29 +28,29 @@ class DataBase():
self.schema = Schema(self.execute) self.schema = Schema(self.execute)
self.activityRequest = Activity(self.execute) self.activityRequest = Activity(self.execute)
db_url = urlparse(os.environ['DATABASE_URL']) if not os.environ.get('DATABASE_URL'):
# if db_url is null then throw error
if not db_url:
raise Exception("No DATABASE_URL environment variable set") raise Exception("No DATABASE_URL environment variable set")
def getDB(self): if DataBase._pool is None:
db = getattr(g, 'database', None)
if db is None:
db_url = urlparse(os.environ['DATABASE_URL']) db_url = urlparse(os.environ['DATABASE_URL'])
g.database = psycopg2.connect( DataBase._pool = pool.ThreadedConnectionPool(
1, 20, # minconn, maxconn
database=db_url.path[1:], database=db_url.path[1:],
user=db_url.username, user=db_url.username,
password=db_url.password, password=db_url.password,
host=db_url.hostname, host=db_url.hostname,
port=db_url.port port=db_url.port
) )
db = g.database
return db
def close_connection(exception): def getDB(self):
db = getattr(g, 'database', None) if 'database' not in g:
g.database = self._pool.getconn()
return g.database
def close_connection(self, exception=None):
db = g.pop('database', None)
if db is not None: if db is not None:
db.close() self._pool.putconn(db)
def execute(self, query, args=(), one=False, commit=False): def execute(self, query, args=(), one=False, commit=False):
conn = self.getDB() conn = self.getDB()

View File

@@ -17,4 +17,5 @@ Flask-Bcrypt==1.0.1
email-validator==2.2.0 email-validator==2.2.0
requests==2.26.0 requests==2.26.0
polars>=0.20.0 polars>=0.20.0
pyarrow>=14.0.0 pyarrow>=14.0.0
Flask-Compress==1.13

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2029
static/js/mermaid.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,7 @@
<link href="/static/css/style.css" rel="stylesheet"> <link href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/htmx.min.js" defer></script> <script src="/static/js/htmx.min.js" defer></script>
<script src="/static/js/hyperscript.min.js"></script> <script src="/static/js/hyperscript.min.js" defer></script>
</head> </head>
<body> <body>

View File

@@ -130,8 +130,12 @@
<div class="overflow-x-auto rounded-lg"> <div class="overflow-x-auto rounded-lg">
<div class="align-middle inline-block min-w-full"> <div class="align-middle inline-block min-w-full">
<div class="shadow overflow-hidden sm:rounded-lg"> <div class="shadow overflow-hidden sm:rounded-lg">
<div class="w-full mt-2 pb-2 aspect-video"> <div class="w-full mt-2 pb-2 aspect-video"
{{ render_partial('partials/sparkline.html', **exercise.graph) }} hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person.id, exercise_id=exercise.id, min_date=min_date, max_date=max_date) }}"
hx-trigger="intersect once" hx-swap="outerHTML">
<div class="flex items-center justify-center h-full bg-gray-50 rounded-lg">
<div class="text-sm text-gray-400 animate-pulse font-medium">Loading graph...</div>
</div>
</div> </div>
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">

View File

@@ -105,7 +105,13 @@
<div class="mt-4 mb-4 w-full grid grid-cols-1 2xl:grid-cols-2 gap-4"> <div class="mt-4 mb-4 w-full grid grid-cols-1 2xl:grid-cols-2 gap-4">
{% for graph in exercise_progress_graphs %} {% for graph in exercise_progress_graphs %}
{{ render_partial('partials/sparkline.html', **graph.progress_graph) }} <div hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person_id, exercise_id=graph.exercise_id, min_date=min_date, max_date=max_date) }}"
hx-trigger="intersect once" hx-swap="outerHTML">
<div class="flex items-center justify-center h-48 bg-gray-50 rounded-lg">
<div class="text-sm text-gray-400 animate-pulse font-medium">Loading {{ graph.exercise_name }}...
</div>
</div>
</div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -48,18 +48,22 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da
best_fit_points = [] best_fit_points = []
try: try:
if len(relative_positions) > 1: # Ensure there are enough points for polyfit # Filter out NaNs if any (though scaled values shouldn't have them if ranges are correct)
# Fit a polynomial of the given degree mask = ~np.isnan(estimated_1rm_scaled)
coeffs = np.polyfit(relative_positions, estimated_1rm_scaled, degree) x_fit = relative_positions[mask]
y_fit = estimated_1rm_scaled[mask]
# Ensure we have enough unique X positions for the given degree
if len(np.unique(x_fit)) > degree:
coeffs = np.polyfit(x_fit, y_fit, degree)
poly_fit = np.poly1d(coeffs) poly_fit = np.poly1d(coeffs)
y_best_fit = poly_fit(relative_positions) y_best_fit = poly_fit(relative_positions)
best_fit_points = list(zip(y_best_fit.tolist(), relative_positions.tolist())) best_fit_points = list(zip(y_best_fit.tolist(), relative_positions.tolist()))
else: else:
raise ValueError("Not enough data points for polyfit") best_fit_points = []
except (np.linalg.LinAlgError, ValueError) as e: except (np.linalg.LinAlgError, ValueError, TypeError) as e:
# Handle cases where polyfit fails # Handle cases where polyfit fails or input is invalid
best_fit_points = [] best_fit_points = []
m, b = 0, 0
# Prepare data for plots # Prepare data for plots
repetitions_data = { repetitions_data = {