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 os
from dotenv import load_dotenv
from flask_compress import Compress
# Load environment variables from .env file in non-production environments
if os.environ.get('FLASK_ENV') != 'production':
load_dotenv()
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.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
jinja_partials.register_extensions(app)

29
db.py
View File

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

View File

@@ -18,3 +18,4 @@ email-validator==2.2.0
requests==2.26.0
polars>=0.20.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">
<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>
<body>

View File

@@ -130,8 +130,12 @@
<div class="overflow-x-auto rounded-lg">
<div class="align-middle inline-block min-w-full">
<div class="shadow overflow-hidden sm:rounded-lg">
<div class="w-full mt-2 pb-2 aspect-video">
{{ render_partial('partials/sparkline.html', **exercise.graph) }}
<div class="w-full mt-2 pb-2 aspect-video"
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>
<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">
{% 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 %}
</div>

View File

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