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
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:
parent
42e42c5fa0
commit
c9d133cf90
@ -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
Loading…
Reference in New Issue
Block a user