rough new admin panel, more changes pending

This commit is contained in:
2025-06-28 04:45:36 -05:00
parent adbb3250ad
commit 72e060d783
33 changed files with 1550 additions and 24 deletions

View File

@ -1,11 +1,334 @@
from flask import Blueprint, render_template
# plugins/admin/routes.py
from flask import (
Blueprint, render_template, redirect, url_for,
flash, request, jsonify
)
from flask_login import login_required, current_user
from flask_wtf.csrf import generate_csrf
from sqlalchemy import func, desc
from datetime import datetime, timedelta
bp = Blueprint('admin', __name__, template_folder='templates')
from app import db
from plugins.auth.models import User
from plugins.growlog.models import GrowLog
from plugins.plant.models import Plant
from plugins.admin.models import AnalyticsEvent
from .forms import UserForm
@bp.route('/admin')
bp = Blueprint('admin', __name__, url_prefix='/admin', template_folder='templates')
@bp.route('/dashboard')
@login_required
def admin_dashboard():
def dashboard():
if current_user.role != 'admin':
return "Access denied", 403
return render_template('admin/admin_dashboard.html')
now = datetime.utcnow()
week_ago = now - timedelta(days=7)
month_ago= now - timedelta(days=30)
# ─── Overview metrics ────────────────────────────────────────────── #
# Users
total_users = User.query.count()
active_users = User.query.filter(
User.is_deleted == False,
User.is_banned == False,
(User.suspended_until == None) | (User.suspended_until <= now)
).count()
suspended_users = User.query.filter(User.suspended_until > now).count()
banned_users = User.query.filter_by(is_banned=True).count()
deleted_users = User.query.filter_by(is_deleted=True).count()
new_7d_users = User.query.filter(User.created_at >= week_ago).count()
# Growlogs
total_logs = GrowLog.query.count()
logs_7d = GrowLog.query.filter(GrowLog.created_at >= week_ago).count()
# Signups last 30 days
signup_dates = [(month_ago + timedelta(days=i)).date() for i in range(31)]
signup_counts = [
User.query.filter(func.date(User.created_at)==d).count()
for d in signup_dates
]
chart_dates = [d.strftime('%Y-%m-%d') for d in signup_dates]
# Status distribution
role_counts = {
'User': User.query.filter_by(role='user').count(),
'Admin': User.query.filter_by(role='admin').count()
}
# ─── Analytics aggregates ─────────────────────────────────────────── #
ev_q = AnalyticsEvent.query.filter(AnalyticsEvent.timestamp >= week_ago)
total_ev = ev_q.count() or 1
error_ev = ev_q.filter(AnalyticsEvent.status_code >= 500).count()
error_pct = round(error_ev / total_ev * 100, 1)
top_routes = [
{'path': path, 'count': cnt}
for path, cnt in db.session.query(
AnalyticsEvent.path,
func.count(AnalyticsEvent.id)
)
.filter(AnalyticsEvent.timestamp >= week_ago)
.group_by(AnalyticsEvent.path)
.order_by(desc(func.count(AnalyticsEvent.id)))
.limit(10)
.all()
]
# Browser share
browsers = {}
ua_counts = (
db.session.query(
func.substr(AnalyticsEvent.user_agent, 1, 200).label('ua'),
func.count(AnalyticsEvent.id).label('cnt')
)
.filter(AnalyticsEvent.user_agent.isnot(None))
.filter(AnalyticsEvent.timestamp >= week_ago)
.group_by(func.substr(AnalyticsEvent.user_agent, 1, 200))
.all()
)
for ua, cnt in ua_counts:
b = 'Other'
if 'Chrome' in ua: b = 'Chrome'
elif 'Firefox' in ua: b = 'Firefox'
elif 'Safari' in ua and 'Chrome' not in ua: b = 'Safari'
elif 'Edge' in ua: b = 'Edge'
browsers[b] = browsers.get(b, 0) + cnt
referrers = dict(
db.session.query(
AnalyticsEvent.referer, func.count(AnalyticsEvent.id)
)
.filter(AnalyticsEvent.referer.isnot(None))
.filter(AnalyticsEvent.timestamp >= week_ago)
.group_by(AnalyticsEvent.referer)
.order_by(desc(func.count(AnalyticsEvent.id)))
.limit(5)
.all()
)
# ─── Stats metrics ────────────────────────────────────────────────── #
# Plant totals
total_plants = Plant.query.count()
active_plants = Plant.query.filter_by(is_active=True).count()
# Top 5 popular plant types
popular_plants = db.session.query(
Plant.plant_type,
func.count(Plant.id).label('count')
).filter(Plant.is_active==True) \
.group_by(Plant.plant_type) \
.order_by(desc('count')) \
.limit(5) \
.all()
# ─── Render template ──────────────────────────────────────────────── #
return render_template(
'admin/dashboard.html',
# Overview
total_users=total_users,
active_users=active_users,
suspended_users=suspended_users,
banned_users=banned_users,
deleted_users=deleted_users,
new_7d_users=new_7d_users,
total_logs=total_logs,
logs_7d=logs_7d,
chart_dates=chart_dates,
signup_counts=signup_counts,
role_counts=role_counts,
# Analytics
error_pct=error_pct,
top_routes=top_routes,
browsers=browsers,
referrers=referrers,
# Stats
total_plants=total_plants,
active_plants=active_plants,
popular_plants=popular_plants,
)
@bp.route('/users')
@login_required
def list_users():
if current_user.role != 'admin':
return "Access denied", 403
# --- parse query parameters ---
page = request.args.get('page', 1, type=int)
show_deleted = request.args.get('show_deleted', '0') == '1'
q = request.args.get('q', '', type=str).strip()
# --- build base query ---
query = User.query
if not show_deleted:
query = query.filter_by(is_deleted=False)
if q:
like = f"%{q}%"
query = query.filter(User.email.ilike(like))
# --- paginate the results ---
pagination = query.order_by(User.created_at.desc()) \
.paginate(page=page, per_page=25, error_out=False)
users = pagination.items
# --- AJAX response (JSON) ---
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
token = generate_csrf()
users_data = []
for u in users:
if u.is_banned:
status = 'banned'
until = None
elif u.suspended_until and u.suspended_until > datetime.utcnow():
status = 'suspended'
until = u.suspended_until.strftime('%Y-%m-%d')
else:
status = 'active'
until = None
users_data.append({
'id': u.id,
'email': u.email,
'role': u.role,
'is_verified': u.is_verified,
'excluded_from_analytics': u.excluded_from_analytics,
'status': status,
'suspended_until': until,
'is_deleted': u.is_deleted,
'created_at': u.created_at.strftime('%Y-%m-%d')
})
return jsonify({
'users': users_data,
'csrf_token': token
})
# --- normal HTML response ---
return render_template(
'admin/users/list.html',
users=users,
pagination=pagination,
show_deleted=show_deleted,
q=q
)
@bp.route('/users/create', methods=['GET', 'POST'])
@login_required
def create_user():
if current_user.role != 'admin':
return "Access denied", 403
form = UserForm()
if form.validate_on_submit():
u = User(
email=form.email.data,
role=form.role.data,
is_verified=form.is_verified.data,
excluded_from_analytics=form.excluded_from_analytics.data
)
# ban/suspend logic here if you need it...
if form.password.data:
u.set_password(form.password.data)
db.session.add(u)
db.session.commit()
flash('User created.', 'success')
return redirect(url_for('admin.list_users'))
return render_template('admin/users/form.html', form=form, action='Create')
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_user(user_id):
if current_user.role != 'admin':
return "Access denied", 403
u = User.query.get_or_404(user_id)
form = UserForm(obj=u)
# Pre-populate on GET
if request.method == 'GET':
if u.is_banned:
form.ban_type.data = 'perm'
elif u.suspended_until and u.suspended_until > datetime.utcnow():
form.ban_type.data = 'temp'
days = (u.suspended_until - datetime.utcnow()).days
form.suspend_days.data = days if days > 0 else 1
else:
form.ban_type.data = 'active'
if form.validate_on_submit():
u.email = form.email.data
u.role = form.role.data
u.is_verified = form.is_verified.data
u.excluded_from_analytics = form.excluded_from_analytics.data
# Ban/Suspend logic
if form.ban_type.data == 'perm':
u.is_banned = True
u.suspended_until = None
elif form.ban_type.data == 'temp':
u.is_banned = False
days = form.suspend_days.data or 7
u.suspended_until = datetime.utcnow() + timedelta(days=days)
else:
u.is_banned = False
u.suspended_until = None
if form.password.data:
u.set_password(form.password.data)
db.session.commit()
flash('User updated.', 'success')
return redirect(url_for('admin.list_users',
page=request.args.get('page'),
show_deleted=request.args.get('show_deleted'),
q=request.args.get('q')))
return render_template(
'admin/users/form.html',
form=form,
action='Edit'
)
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
@login_required
def delete_user(user_id):
if current_user.role != 'admin':
return "Access denied", 403
u = User.query.get_or_404(user_id)
if u.id == current_user.id:
flash("Cannot delete yourself.", 'warning')
else:
u.is_deleted = True
db.session.commit()
flash('User deleted (soft).', 'success')
return redirect(url_for('admin.list_users',
page=request.args.get('page'),
show_deleted=request.args.get('show_deleted'),
q=request.args.get('q')))
@bp.route('/users/<int:user_id>/undelete', methods=['POST'])
@login_required
def undelete_user(user_id):
if current_user.role != 'admin':
return "Access denied", 403
u = User.query.get_or_404(user_id)
u.is_deleted = False
db.session.commit()
flash('User restored.', 'success')
return redirect(url_for('admin.list_users',
page=request.args.get('page'),
show_deleted=request.args.get('show_deleted'),
q=request.args.get('q')))