diff --git a/app.py b/app.py index ac90b7c..d9b5582 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,8 @@ from werkzeug.security import check_password_hash, generate_password_hash import os from dotenv import load_dotenv from routes.timer import timer +from routes.test import test +from routes.home import home # Load environment variables from .env file in non-production environments if os.environ.get('FLASK_ENV') != 'production': @@ -24,7 +26,10 @@ login_manager.init_app(app) login_manager.login_view = "login" jinja_partials.register_extensions(app) init_app(app) + app.register_blueprint(timer, url_prefix='/timer') +app.register_blueprint(test, url_prefix='/test') +app.register_blueprint(home, url_prefix='/home') class User(UserMixin): def __init__(self, id, username, password_hash, created_at): diff --git a/lib/mithril.py b/lib/mithril.py new file mode 100644 index 0000000..1b50527 --- /dev/null +++ b/lib/mithril.py @@ -0,0 +1,83 @@ +from flask import render_template, jsonify, request, session +from functools import wraps +import time + +class MithrilError(Exception): + def __init__(self, message, code=400): + self.message = message + self.code = code + +class Mithril: + _shared_data = {} + + @classmethod + def share(cls, key, value): + """Add shared data that will be available to all components""" + cls._shared_data[key] = value + + @staticmethod + def render(component_name, props=None): + """Render a Mithril component""" + instance = Mithril(component_name, props or {}) + return instance() + + @classmethod + def error_handler(cls, error_handler): + """Decorator to register a global error handler""" + cls._error_handler = error_handler + return error_handler + + def __init__(self, component_name, props=None): + self.component_name = component_name + self.props = props or {} + self.version = int(time.time()) # Simple asset versioning + + def _prepare_response_data(self): + """Prepare the data to be sent to the frontend""" + return { + 'component': self.component_name, + 'props': self.props, + 'shared': self._shared_data, + 'version': self.version, + 'errors': session.get('errors', {}), # Flash-style errors + 'flash': session.get('flash', []) # Flash messages + } + + def __call__(self): + try: + response_data = self._prepare_response_data() + + # Clear flash data after preparing response + session.pop('errors', None) + session.pop('flash', None) + + if request.headers.get('X-Mithril-Request'): + return jsonify(response_data) + + return render_template('mithril_loader.html', **response_data) + + except Exception as e: + if hasattr(self, '_error_handler'): + return self._error_handler(e) + raise + + @classmethod + def form(cls, route): + """Decorator for handling form submissions""" + def decorator(f): + @wraps(f) + def wrapped(*args, **kwargs): + try: + if request.headers.get('X-Mithril-Request'): + form_data = request.get_json() + else: + form_data = request.form + + return f(form_data, *args, **kwargs) + except MithrilError as e: + session['errors'] = {'message': e.message} + if request.headers.get('X-Mithril-Request'): + return jsonify({'errors': {'message': e.message}}), e.code + return cls.redirect_back() + return wrapped + return decorator \ No newline at end of file diff --git a/routes/home.py b/routes/home.py new file mode 100644 index 0000000..290d134 --- /dev/null +++ b/routes/home.py @@ -0,0 +1,133 @@ +from flask import Blueprint, render_template, request +from flask_login import login_required, current_user +from extensions import db, htmx, environment +from jinja2_fragments import render_block + +home = Blueprint('home', __name__) + + +@home.route('/') +@login_required +def index(): + # Fetch user statistics + stats = db.execute(""" + WITH timer_stats AS ( + SELECT + COUNT(*) as total_timer_functions, + COUNT(*) FILTER (WHERE enabled = true) as active_timer_functions, + (SELECT COUNT(*) FROM timer_function_invocations tfi + JOIN timer_functions tf ON tf.id = tfi.timer_function_id + WHERE tf.user_id = %s) as timer_invocations, + (SELECT COUNT(*) FROM timer_function_invocations tfi + JOIN timer_functions tf ON tf.id = tfi.timer_function_id + WHERE tf.user_id = %s AND tfi.status = 'SUCCESS') as timer_successful_invocations, + MAX(last_run) as last_timer_invocation + FROM timer_functions + WHERE user_id = %s + ), + http_stats AS ( + SELECT + COUNT(*) as total_http_functions, + COUNT(*) FILTER (WHERE is_public = true) as public_http_functions, + SUM(invoked_count) as http_invocations, + (SELECT COUNT(*) FROM http_function_invocations hfi + JOIN http_functions hf ON hf.id = hfi.http_function_id + WHERE hf.user_id = %s AND hfi.status = 'SUCCESS') as http_successful_invocations, + (SELECT MAX(invocation_time) + FROM http_function_invocations hfi + JOIN http_functions hf ON hf.id = hfi.http_function_id + WHERE hf.user_id = %s) as last_http_invocation + FROM http_functions + WHERE user_id = %s + ) + SELECT + *, + CASE + WHEN timer_invocations > 0 THEN + (timer_successful_invocations * 100.0 / timer_invocations)::numeric(5,1) + ELSE 0.0 + END as timer_success_rate, + CASE + WHEN http_invocations > 0 THEN + (http_successful_invocations * 100.0 / http_invocations)::numeric(5,1) + ELSE 0.0 + END as http_success_rate + FROM timer_stats, http_stats + """, [current_user.id, current_user.id, current_user.id, current_user.id, current_user.id, current_user.id], one=True) + + # Get 24-hour distribution + hour_distribution = db.execute(""" + WITH all_invocations AS ( + SELECT date_trunc('hour', tfi.invocation_time) as hour_bucket + FROM timer_function_invocations tfi + JOIN timer_functions tf ON tf.id = tfi.timer_function_id + WHERE tf.user_id = %s + AND tfi.invocation_time > NOW() - INTERVAL '24 hours' + UNION ALL + SELECT date_trunc('hour', hfi.invocation_time) as hour_bucket + FROM http_function_invocations hfi + JOIN http_functions hf ON hf.id = hfi.http_function_id + WHERE hf.user_id = %s + AND hfi.invocation_time > NOW() - INTERVAL '24 hours' + ) + SELECT + EXTRACT(HOUR FROM hour_bucket) as hour, + COUNT(*) as count + FROM all_invocations + GROUP BY hour + ORDER BY hour + """, [current_user.id, current_user.id]) + + # Get 7-day success rate trend + success_trend = db.execute(""" + WITH daily_stats AS ( + WITH timer_daily AS ( + SELECT + date_trunc('day', tfi.invocation_time) as day, + COUNT(*) as total, + COUNT(*) FILTER (WHERE tfi.status = 'SUCCESS') as successes + FROM timer_function_invocations tfi + JOIN timer_functions tf ON tf.id = tfi.timer_function_id + WHERE tf.user_id = %s + AND tfi.invocation_time > NOW() - INTERVAL '7 days' + GROUP BY day + ), + http_daily AS ( + SELECT + date_trunc('day', hfi.invocation_time) as day, + COUNT(*) as total, + COUNT(*) FILTER (WHERE hfi.status = 'SUCCESS') as successes + FROM http_function_invocations hfi + JOIN http_functions hf ON hf.id = hfi.http_function_id + WHERE hf.user_id = %s + AND hfi.invocation_time > NOW() - INTERVAL '7 days' + GROUP BY day + ) + SELECT + COALESCE(t.day, h.day) as day, + COALESCE(t.total, 0) + COALESCE(h.total, 0) as total, + COALESCE(t.successes, 0) + COALESCE(h.successes, 0) as successes + FROM timer_daily t + FULL OUTER JOIN http_daily h ON t.day = h.day + ) + SELECT + to_char(day, 'Dy') as day_name, + CASE + WHEN total > 0 THEN + (successes * 100.0 / total)::float + ELSE 0.0 + END as success_rate + FROM daily_stats + ORDER BY day DESC + LIMIT 7 + """, [current_user.id, current_user.id]) + + if htmx: + return render_block(environment, 'dashboard/home.html', 'page', + stats=stats, + hour_distribution=hour_distribution, + success_trend=success_trend) + return render_template('dashboard/home.html', + stats=stats, + hour_distribution=hour_distribution, + success_trend=success_trend) \ No newline at end of file diff --git a/routes/test.py b/routes/test.py new file mode 100644 index 0000000..a3689e1 --- /dev/null +++ b/routes/test.py @@ -0,0 +1,48 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +from jinja2_fragments import render_block +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import current_user, login_required +from extensions import db, htmx, environment +from datetime import datetime, timezone, timedelta +from lib.mithril import Mithril +import json + +test = Blueprint('test', __name__) + +@test.route('/mithril/') +@login_required +def mithril(function_id): + # Fetch the timer function to verify ownership + timer_function = db.execute(""" + SELECT id, name, code, version_number + FROM timer_functions + WHERE id = %s AND user_id = %s + """, [function_id, current_user.id], one=True) + + if not timer_function: + flash('Timer function not found', 'error') + return redirect(url_for('timer.overview')) + + # Fetch all versions + versions = db.execute(""" + SELECT version_number, script, versioned_at + FROM timer_function_versions + WHERE timer_function_id = %s + ORDER BY version_number DESC + """, [function_id]) + + # Convert datetime objects to ISO format strings + for version in versions: + version['versioned_at'] = version['versioned_at'].isoformat() if version['versioned_at'] else None + + args = { + 'user_id': current_user.id, + 'function_id': function_id, + 'timer_function': timer_function, + 'versions': versions + } + + return Mithril.render('DiffView', args) + + #return render_template('mithril_loader.html', args=args) + diff --git a/routes/timer.py b/routes/timer.py index 26e686a..afd6f7d 100644 --- a/routes/timer.py +++ b/routes/timer.py @@ -153,7 +153,7 @@ def overview(): timer_functions = db.execute(""" SELECT id, name, code, environment, trigger_type, frequency_minutes, run_date, next_run, - last_run, enabled + last_run, enabled, invocation_count FROM timer_functions WHERE user_id = %s ORDER BY id DESC @@ -237,7 +237,7 @@ def edit(function_id): timer_function = db.execute(""" SELECT id, name, code, environment, version_number, trigger_type, frequency_minutes, run_date, next_run, - last_run, enabled + last_run, enabled, invocation_count FROM timer_functions WHERE id = %s AND user_id = %s """, [function_id, current_user.id], one=True) @@ -369,7 +369,7 @@ def toggle(function_id): timer_functions = db.execute(""" SELECT id, name, code, environment, trigger_type, frequency_minutes, run_date, next_run, - last_run, enabled + last_run, enabled, invocation_count FROM timer_functions WHERE user_id = %s ORDER BY id DESC diff --git a/templates/dashboard.html b/templates/dashboard.html index 50a7382..9e79e62 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -23,8 +23,8 @@