Add home dashboard and Mithril rendering support
- Create new home route with comprehensive dashboard statistics - Implement Mithril rendering support with new `mithril_loader.html` template - Add new routes for home and test pages in `app.py` - Create `lib/mithril.py` with Mithril rendering and error handling utilities - Update dashboard template to use new home route - Add detailed dashboard view with timer and HTTP function statistics
This commit is contained in:
5
app.py
5
app.py
@@ -11,6 +11,8 @@ from werkzeug.security import check_password_hash, generate_password_hash
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from routes.timer import timer
|
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
|
# Load environment variables from .env file in non-production environments
|
||||||
if os.environ.get('FLASK_ENV') != 'production':
|
if os.environ.get('FLASK_ENV') != 'production':
|
||||||
@@ -24,7 +26,10 @@ login_manager.init_app(app)
|
|||||||
login_manager.login_view = "login"
|
login_manager.login_view = "login"
|
||||||
jinja_partials.register_extensions(app)
|
jinja_partials.register_extensions(app)
|
||||||
init_app(app)
|
init_app(app)
|
||||||
|
|
||||||
app.register_blueprint(timer, url_prefix='/timer')
|
app.register_blueprint(timer, url_prefix='/timer')
|
||||||
|
app.register_blueprint(test, url_prefix='/test')
|
||||||
|
app.register_blueprint(home, url_prefix='/home')
|
||||||
|
|
||||||
class User(UserMixin):
|
class User(UserMixin):
|
||||||
def __init__(self, id, username, password_hash, created_at):
|
def __init__(self, id, username, password_hash, created_at):
|
||||||
|
|||||||
83
lib/mithril.py
Normal file
83
lib/mithril.py
Normal file
@@ -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
|
||||||
133
routes/home.py
Normal file
133
routes/home.py
Normal file
@@ -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)
|
||||||
48
routes/test.py
Normal file
48
routes/test.py
Normal file
@@ -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/<int:function_id>')
|
||||||
|
@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)
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ def overview():
|
|||||||
timer_functions = db.execute("""
|
timer_functions = db.execute("""
|
||||||
SELECT id, name, code, environment, trigger_type,
|
SELECT id, name, code, environment, trigger_type,
|
||||||
frequency_minutes, run_date, next_run,
|
frequency_minutes, run_date, next_run,
|
||||||
last_run, enabled
|
last_run, enabled, invocation_count
|
||||||
FROM timer_functions
|
FROM timer_functions
|
||||||
WHERE user_id = %s
|
WHERE user_id = %s
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
@@ -237,7 +237,7 @@ def edit(function_id):
|
|||||||
timer_function = db.execute("""
|
timer_function = db.execute("""
|
||||||
SELECT id, name, code, environment, version_number, trigger_type,
|
SELECT id, name, code, environment, version_number, trigger_type,
|
||||||
frequency_minutes, run_date, next_run,
|
frequency_minutes, run_date, next_run,
|
||||||
last_run, enabled
|
last_run, enabled, invocation_count
|
||||||
FROM timer_functions
|
FROM timer_functions
|
||||||
WHERE id = %s AND user_id = %s
|
WHERE id = %s AND user_id = %s
|
||||||
""", [function_id, current_user.id], one=True)
|
""", [function_id, current_user.id], one=True)
|
||||||
@@ -369,7 +369,7 @@ def toggle(function_id):
|
|||||||
timer_functions = db.execute("""
|
timer_functions = db.execute("""
|
||||||
SELECT id, name, code, environment, trigger_type,
|
SELECT id, name, code, environment, trigger_type,
|
||||||
frequency_minutes, run_date, next_run,
|
frequency_minutes, run_date, next_run,
|
||||||
last_run, enabled
|
last_run, enabled, invocation_count
|
||||||
FROM timer_functions
|
FROM timer_functions
|
||||||
WHERE user_id = %s
|
WHERE user_id = %s
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
<div class="flex-1 overflow-auto py-2" data-id="11">
|
<div class="flex-1 overflow-auto py-2" data-id="11">
|
||||||
<nav class="grid items-start px-4 text-sm font-medium" data-id="12"><a
|
<nav class="grid items-start px-4 text-sm font-medium" data-id="12"><a
|
||||||
class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50 cursor-pointer"
|
class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50 cursor-pointer"
|
||||||
href="{{ url_for('home') }}"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
href="{{ url_for('home.index') }}"><svg xmlns="http://www.w3.org/2000/svg" width="24"
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" data-id="14">
|
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" data-id="14">
|
||||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||||
|
|||||||
282
templates/dashboard/home.html
Normal file
282
templates/dashboard/home.html
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
{% extends 'dashboard.html' %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Dashboard Overview</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timer Functions Stats -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 mb-4">Timer Functions</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<!-- Total Timer Functions Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-blue-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Total Timer Functions</h2>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.total_timer_functions }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Timer Functions Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-green-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Active Timer Functions</h2>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.active_timer_functions }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timer Function Invocations Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-purple-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-purple-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Timer Invocations</h2>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.timer_invocations }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timer Success Rate Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-emerald-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-emerald-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Success Rate</h2>
|
||||||
|
<div class="flex items-baseline">
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.timer_success_rate }}%</p>
|
||||||
|
<p class="ml-2 text-sm text-gray-500">({{ stats.timer_successful_invocations }}/{{
|
||||||
|
stats.timer_invocations }})</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HTTP Functions Stats -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 mb-4">HTTP Functions</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<!-- Total HTTP Functions Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-indigo-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Total HTTP Functions</h2>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.total_http_functions }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Public HTTP Functions Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-yellow-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-yellow-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Public HTTP Functions</h2>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.public_http_functions }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HTTP Function Invocations Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-red-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">HTTP Invocations</h2>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.http_invocations }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HTTP Success Rate Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-full bg-emerald-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-emerald-600" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h2 class="text-sm font-medium text-gray-500">Success Rate</h2>
|
||||||
|
<div class="flex items-baseline">
|
||||||
|
<p class="text-2xl font-semibold text-gray-900">{{ stats.http_success_rate }}%</p>
|
||||||
|
<p class="ml-2 text-sm text-gray-500">({{ stats.http_successful_invocations }}/{{
|
||||||
|
stats.http_invocations }})</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add this after the stats cards and before the Last Activity section -->
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
|
<!-- 24-Hour Distribution -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">24-Hour Invocation Distribution</h3>
|
||||||
|
<div class="relative h-64">
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 400 200" preserveAspectRatio="none">
|
||||||
|
<!-- Y-axis -->
|
||||||
|
<line x1="40" y1="20" x2="40" y2="180" stroke="#E5E7EB" stroke-width="1" />
|
||||||
|
<!-- X-axis -->
|
||||||
|
<line x1="40" y1="180" x2="380" y2="180" stroke="#E5E7EB" stroke-width="1" />
|
||||||
|
|
||||||
|
{% set max_count = namespace(value=1) %}
|
||||||
|
{% for hour in hour_distribution %}
|
||||||
|
{% if hour.count > max_count.value %}
|
||||||
|
{% set max_count.value = hour.count %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Hour bars -->
|
||||||
|
{% for hour in hour_distribution %}
|
||||||
|
{% set x = 40 + (hour.hour * 14) %}
|
||||||
|
{% set height = (hour.count / max_count.value) * 160 %}
|
||||||
|
{% set y = 180 - height %}
|
||||||
|
<rect x="{{ x }}" y="{{ y }}" width="12" height="{{ height }}" fill="#60A5FA" opacity="0.8">
|
||||||
|
<title>{{ hour.hour }}:00 - {{ hour.count }} invocations</title>
|
||||||
|
</rect>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Hour labels (every 6 hours) -->
|
||||||
|
{% for h in [0, 6, 12, 18, 23] %}
|
||||||
|
<text x="{{ 40 + (h * 14) }}" y="195" text-anchor="middle" class="text-xs text-gray-500">{{ h
|
||||||
|
}}h</text>
|
||||||
|
{% endfor %}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 7-Day Success Rate Trend -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">7-Day Success Rate Trend</h3>
|
||||||
|
<div class="relative h-64">
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 400 200" preserveAspectRatio="none">
|
||||||
|
<!-- Y-axis -->
|
||||||
|
<line x1="40" y1="20" x2="40" y2="180" stroke="#E5E7EB" stroke-width="1" />
|
||||||
|
<!-- X-axis -->
|
||||||
|
<line x1="40" y1="180" x2="380" y2="180" stroke="#E5E7EB" stroke-width="1" />
|
||||||
|
|
||||||
|
<!-- Success rate line -->
|
||||||
|
{% set path = namespace(d='M') %}
|
||||||
|
{% for day in success_trend %}
|
||||||
|
{% set x = 380 - (loop.index0 * 48) %}
|
||||||
|
{% set y = 180 - (day.success_rate * 1.6) %}
|
||||||
|
{% if loop.first %}
|
||||||
|
{% set path.d = path.d ~ x ~ "," ~ y %}
|
||||||
|
{% else %}
|
||||||
|
{% set path.d = path.d ~ " L" ~ x ~ "," ~ y %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<path d="{{ path.d }}" stroke="#10B981" stroke-width="2" fill="none" />
|
||||||
|
|
||||||
|
<!-- Data points -->
|
||||||
|
{% for day in success_trend %}
|
||||||
|
{% set x = 380 - (loop.index0 * 48) %}
|
||||||
|
{% set y = 180 - (day.success_rate * 1.6) %}
|
||||||
|
<circle cx="{{ x }}" cy="{{ y }}" r="4" fill="#10B981">
|
||||||
|
<title>{{ day.day_name }}: {{ day.success_rate }}% success rate</title>
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Day labels -->
|
||||||
|
<text x="{{ x }}" y="195" text-anchor="middle" class="text-xs text-gray-500">{{ day.day_name
|
||||||
|
}}</text>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Y-axis labels -->
|
||||||
|
{% for percent in [0, 25, 50, 75, 100] %}
|
||||||
|
<text x="35" y="{{ 180 - (percent * 1.6) }}" text-anchor="end" class="text-xs text-gray-500">{{
|
||||||
|
percent }}%</text>
|
||||||
|
{% endfor %}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Activity -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-medium text-gray-900">Last Activity</h2>
|
||||||
|
<div class="mt-1 space-y-1">
|
||||||
|
{% if stats.last_timer_invocation %}
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Last Timer Invocation: {{ stats.last_timer_invocation.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if stats.last_http_invocation %}
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Last HTTP Invocation: {{ stats.last_http_invocation.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-x-4">
|
||||||
|
<a href="{{ url_for('timer.overview') }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200">
|
||||||
|
View Timer Functions
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dashboard_http_functions') }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors duration-200">
|
||||||
|
View HTTP Functions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -36,6 +36,10 @@
|
|||||||
hx-get="{{ url_for('timer.edit', function_id=function.id) }}" hx-target="#container"
|
hx-get="{{ url_for('timer.edit', function_id=function.id) }}" hx-target="#container"
|
||||||
hx-swap="innerHTML" hx-push-url="true">
|
hx-swap="innerHTML" hx-push-url="true">
|
||||||
<span class="font-medium text-gray-900">{{ function.name }}</span>
|
<span class="font-medium text-gray-900">{{ function.name }}</span>
|
||||||
|
<span
|
||||||
|
class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||||
|
{{ function.invocation_count }}
|
||||||
|
</span>
|
||||||
{% if function.last_run %}
|
{% if function.last_run %}
|
||||||
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||||
Last run: {{ function.last_run.strftime('%Y-%m-%d %H:%M') }}
|
Last run: {{ function.last_run.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
|||||||
11
templates/mithril_loader.html
Normal file
11
templates/mithril_loader.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="mithril-loader"></div>
|
||||||
|
<script>
|
||||||
|
m.mount(document.getElementById('mithril-loader'), {
|
||||||
|
view: () => m(DiffView, {{ props | tojson | safe }})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user