refactor: simplify AI monitoring dashboard with PLN costs
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Replace complex dashboard (11 stat cards, token stats, model breakdown,
recent logs, advanced filters) with clean 3-card PLN cost view,
usage by type, user ranking, company ranking, and daily history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-11 04:22:27 +01:00
parent 42e42c5fa0
commit c9d133cf90
2 changed files with 214 additions and 880 deletions

View File

@ -147,62 +147,30 @@ def admin_analytics_export():
@role_required(SystemRole.OFFICE_MANAGER)
def admin_ai_usage():
"""Admin dashboard for AI (Gemini) API usage monitoring"""
from datetime import datetime, timedelta
# Get filter params
period = request.args.get('period', 'month') # day, week, month, all, custom
filter_user_id = request.args.get('user_id', type=int)
filter_company_id = request.args.get('company_id', type=int)
filter_model = request.args.get('model', '')
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
period = request.args.get('period', 'month')
db = SessionLocal()
try:
now = datetime.now()
today = now.date()
today = date.today()
week_ago = today - timedelta(days=7)
month_ago = today - timedelta(days=30)
day_ago = now - timedelta(hours=24)
# Determine date filter based on period
if period == 'custom' and date_from:
try:
period_start = date.fromisoformat(date_from)
period_end = date.fromisoformat(date_to) if date_to else today
period_label = f'{period_start.strftime("%d.%m")} - {period_end.strftime("%d.%m.%Y")}'
except ValueError:
period_start = month_ago
period_end = today
period_label = 'Ten miesiąc'
period = 'month'
else:
period_end = None
period_labels = {
'day': ('Dzisiaj', today),
'week': ('Ten tydzień', week_ago),
'month': ('Ten miesiąc', month_ago),
'all': ('Od początku', None)
}
period_label, period_start = period_labels.get(period, period_labels['month'])
period_end = None
period_labels = {
'day': ('Dzisiaj', today),
'week': ('Ten tydzień', week_ago),
'month': ('Ten miesiąc', month_ago),
'all': ('Od początku', None)
}
period_label, period_start = period_labels.get(period, period_labels['month'])
# Build base filter to apply to all queries
def apply_filters(query):
# Apply period filter to queries
def apply_period(query):
if period_start:
query = query.filter(func.date(AIUsageLog.created_at) >= period_start)
if period == 'custom' and period_end:
query = query.filter(func.date(AIUsageLog.created_at) <= period_end)
if filter_user_id:
query = query.filter(AIUsageLog.user_id == filter_user_id)
if filter_company_id:
# Filter by company: get users from this company
company_user_ids = [u.id for u in db.query(User.id).filter(User.company_id == filter_company_id).all()]
if company_user_ids:
query = query.filter(AIUsageLog.user_id.in_(company_user_ids))
else:
query = query.filter(AIUsageLog.id == -1) # No results
if filter_model:
query = query.filter(AIUsageLog.model == filter_model)
return query
# Today's stats (always show, unfiltered)
@ -215,11 +183,6 @@ def admin_ai_usage():
func.date(AIUsageLog.created_at) == today
).first()
# Week stats
week_requests = db.query(func.count(AIUsageLog.id)).filter(
func.date(AIUsageLog.created_at) >= week_ago
).scalar() or 0
# Month stats
month_stats = db.query(
func.count(AIUsageLog.id).label('requests'),
@ -234,23 +197,8 @@ def admin_ai_usage():
func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents')
).first()
# Error rate (last 24h)
last_24h_total = db.query(func.count(AIUsageLog.id)).filter(
AIUsageLog.created_at >= day_ago
).scalar() or 0
last_24h_errors = db.query(func.count(AIUsageLog.id)).filter(
AIUsageLog.created_at >= day_ago,
AIUsageLog.success == False
).scalar() or 0
error_rate = (last_24h_errors / last_24h_total * 100) if last_24h_total > 0 else 0
# Average response time (last 24h)
avg_response_time = db.query(func.avg(AIUsageLog.response_time_ms)).filter(
AIUsageLog.created_at >= day_ago,
AIUsageLog.success == True
).scalar() or 0
# PLN conversion rate (approximate, updated manually)
USD_TO_PLN = 4.05
# Usage by type (filtered)
type_query = db.query(
@ -260,7 +208,7 @@ def admin_ai_usage():
func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'),
func.coalesce(func.sum(AIUsageLog.tokens_output), 0).label('tokens_output')
)
type_query = apply_filters(type_query)
type_query = apply_period(type_query)
type_stats = type_query.group_by(AIUsageLog.request_type).order_by(desc('count')).all()
# Calculate percentages for type breakdown
@ -276,37 +224,13 @@ def admin_ai_usage():
for t in type_stats:
label, css_class = type_labels.get(t.request_type, (t.request_type, 'other'))
percentage = (t.count / total_type_count * 100) if total_type_count > 0 else 0
cost_usd = float(t.cost_cents or 0) / 100
usage_by_type.append({
'type': t.request_type,
'type_label': label,
'type_class': css_class,
'count': t.count,
'percentage': round(percentage, 1),
'cost_usd': float(t.cost_cents or 0) / 100,
'tokens': int(t.tokens_input or 0) + int(t.tokens_output or 0)
})
# Model breakdown (filtered)
model_query = db.query(
AIUsageLog.model,
func.count(AIUsageLog.id).label('count'),
func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents'),
func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'),
func.coalesce(func.sum(AIUsageLog.tokens_output), 0).label('tokens_output')
)
model_query = apply_filters(model_query)
model_stats_raw = model_query.group_by(AIUsageLog.model).order_by(desc('count')).all()
total_model_count = sum(m.count for m in model_stats_raw) if model_stats_raw else 0
model_breakdown = []
for m in model_stats_raw:
percentage = (m.count / total_model_count * 100) if total_model_count > 0 else 0
model_breakdown.append({
'model': m.model or 'unknown',
'count': m.count,
'cost_usd': float(m.cost_cents or 0) / 100,
'tokens': int(m.tokens_input or 0) + int(m.tokens_output or 0),
'percentage': round(percentage, 1)
'cost_pln': round(cost_usd * USD_TO_PLN, 2),
})
# User statistics (filtered)
@ -324,7 +248,7 @@ def admin_ai_usage():
).outerjoin(
Company, User.company_id == Company.id
)
user_query = apply_filters(user_query)
user_query = apply_period(user_query)
user_stats = user_query.group_by(
User.id, User.name, User.email, Company.name
).order_by(desc('cost_cents')).all()
@ -332,15 +256,13 @@ def admin_ai_usage():
# Format user stats
user_rankings = []
for u in user_stats:
cost_usd = float(u.cost_cents or 0) / 100
user_rankings.append({
'id': u.id,
'name': u.user_name or u.email,
'email': u.email,
'company': u.company_name or '-',
'requests': u.requests,
'tokens': int(u.tokens_input) + int(u.tokens_output),
'cost_cents': float(u.cost_cents or 0),
'cost_usd': float(u.cost_cents or 0) / 100
'cost_pln': round(cost_usd * USD_TO_PLN, 2),
})
# Company statistics (filtered)
@ -357,7 +279,7 @@ def admin_ai_usage():
).join(
AIUsageLog, AIUsageLog.user_id == User.id
)
company_query = apply_filters(company_query)
company_query = apply_period(company_query)
company_stats = company_query.group_by(
Company.id, Company.name
).order_by(desc('cost_cents')).all()
@ -365,110 +287,43 @@ def admin_ai_usage():
# Format company stats
company_rankings = []
for c in company_stats:
cost_usd = float(c.cost_cents or 0) / 100
company_rankings.append({
'id': c.id,
'name': c.name,
'requests': c.requests,
'unique_users': c.unique_users,
'tokens': int(c.tokens_input) + int(c.tokens_output),
'cost_cents': float(c.cost_cents or 0),
'cost_usd': float(c.cost_cents or 0) / 100
'cost_pln': round(cost_usd * USD_TO_PLN, 2),
})
# Recent logs with user info (filtered)
recent_query = db.query(AIUsageLog)
recent_query = apply_filters(recent_query)
recent_logs = recent_query.order_by(desc(AIUsageLog.created_at)).limit(50).all()
# Enrich recent logs with user names
for log in recent_logs:
label, _ = type_labels.get(log.request_type, (log.request_type, 'other'))
log.type_label = label
if log.user_id:
user = db.query(User).filter_by(id=log.user_id).first()
if user:
log.user_name = user.name or user.email
else:
log.user_name = None
else:
log.user_name = None
# Daily history (last 14 days)
daily_history = db.query(AIUsageDaily).filter(
AIUsageDaily.date >= today - timedelta(days=14)
).order_by(desc(AIUsageDaily.date)).all()
today_cost_usd = float(today_stats.cost_cents or 0) / 100
month_cost_usd = float(month_stats.cost_cents or 0) / 100
all_cost_usd = float(all_time_stats.cost_cents or 0) / 100
stats = {
'today_requests': today_stats.requests or 0,
'today_tokens_input': int(today_stats.tokens_input) or 0,
'today_tokens_output': int(today_stats.tokens_output) or 0,
'today_cost': float(today_stats.cost_cents or 0) / 100,
'week_requests': week_requests,
'today_cost_pln': round(today_cost_usd * USD_TO_PLN, 2),
'month_requests': month_stats.requests or 0,
'month_cost': float(month_stats.cost_cents or 0) / 100,
'month_cost_pln': round(month_cost_usd * USD_TO_PLN, 2),
'all_requests': all_time_stats.requests or 0,
'all_cost': float(all_time_stats.cost_cents or 0) / 100,
'error_rate': error_rate,
'avg_response_time': int(avg_response_time)
}
# Filtered stats summary (period + filters)
filtered_query = db.query(
func.count(AIUsageLog.id).label('requests'),
func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents'),
func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'),
func.coalesce(func.sum(AIUsageLog.tokens_output), 0).label('tokens_output')
)
filtered_query = apply_filters(filtered_query)
filtered_stats = filtered_query.first()
stats['filtered_requests'] = filtered_stats.requests or 0
stats['filtered_cost'] = float(filtered_stats.cost_cents or 0) / 100
stats['filtered_tokens'] = int(filtered_stats.tokens_input or 0) + int(filtered_stats.tokens_output or 0)
# Dropdown options for filters
filter_users = db.query(User.id, User.name, User.email).join(
AIUsageLog, AIUsageLog.user_id == User.id
).group_by(User.id, User.name, User.email).order_by(User.name).all()
filter_companies = db.query(Company.id, Company.name).join(
User, User.company_id == Company.id
).join(
AIUsageLog, AIUsageLog.user_id == User.id
).group_by(Company.id, Company.name).order_by(Company.name).all()
filter_models = db.query(AIUsageLog.model).filter(
AIUsageLog.model.isnot(None)
).distinct().order_by(AIUsageLog.model).all()
filter_models = [m[0] for m in filter_models]
# Current filters for template
has_filters = bool(filter_user_id or filter_company_id or filter_model)
current_filters = {
'user_id': filter_user_id,
'company_id': filter_company_id,
'model': filter_model,
'date_from': date_from,
'date_to': date_to
'all_cost_pln': round(all_cost_usd * USD_TO_PLN, 2),
'usd_to_pln': USD_TO_PLN,
}
return render_template(
'admin/ai_usage_dashboard.html',
stats=stats,
usage_by_type=usage_by_type,
model_breakdown=model_breakdown,
recent_logs=recent_logs,
daily_history=daily_history,
user_rankings=user_rankings,
company_rankings=company_rankings,
current_period=period,
period_label=period_label,
# Filter options
filter_users=filter_users,
filter_companies=filter_companies,
filter_models=filter_models,
current_filters=current_filters,
has_filters=has_filters
)
finally:
db.close()

File diff suppressed because it is too large Load Diff