feat: Add user/company rankings and period filters to AI dashboard
- Add period filter buttons (day/week/month/all) - Add user ranking table with name, company, requests, tokens, cost - Add company ranking table with unique users and costs - Show user names in recent logs - Add all-time statistics - Rankings filtered by selected period Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3bbe2a36dd
commit
3e77ffd206
135
app.py
135
app.py
@ -5727,10 +5727,13 @@ def admin_ai_usage():
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
from database import AIUsageLog, AIUsageDaily
|
||||
from sqlalchemy import func, desc
|
||||
from database import AIUsageLog, AIUsageDaily, User, Company
|
||||
from sqlalchemy import func, desc, case
|
||||
from datetime import timedelta
|
||||
|
||||
# Get period filter from query params
|
||||
period = request.args.get('period', 'month') # day, week, month, all
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
now = datetime.now()
|
||||
@ -5739,7 +5742,22 @@ def admin_ai_usage():
|
||||
month_ago = today - timedelta(days=30)
|
||||
day_ago = now - timedelta(hours=24)
|
||||
|
||||
# Today's stats
|
||||
# Determine date filter based on period
|
||||
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'])
|
||||
|
||||
# Base query filter for period
|
||||
def period_filter(query):
|
||||
if period_start:
|
||||
return query.filter(func.date(AIUsageLog.created_at) >= period_start)
|
||||
return query
|
||||
|
||||
# Today's stats (always show)
|
||||
today_stats = db.query(
|
||||
func.count(AIUsageLog.id).label('requests'),
|
||||
func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'),
|
||||
@ -5762,6 +5780,12 @@ def admin_ai_usage():
|
||||
func.date(AIUsageLog.created_at) >= month_ago
|
||||
).first()
|
||||
|
||||
# All-time stats
|
||||
all_time_stats = db.query(
|
||||
func.count(AIUsageLog.id).label('requests'),
|
||||
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
|
||||
@ -5780,13 +5804,13 @@ def admin_ai_usage():
|
||||
AIUsageLog.success == True
|
||||
).scalar() or 0
|
||||
|
||||
# Usage by type (last 30 days)
|
||||
type_stats = db.query(
|
||||
# Usage by type (filtered by period)
|
||||
type_query = db.query(
|
||||
AIUsageLog.request_type,
|
||||
func.count(AIUsageLog.id).label('count')
|
||||
).filter(
|
||||
func.date(AIUsageLog.created_at) >= month_ago
|
||||
).group_by(AIUsageLog.request_type).order_by(desc('count')).all()
|
||||
)
|
||||
type_query = period_filter(type_query)
|
||||
type_stats = type_query.group_by(AIUsageLog.request_type).order_by(desc('count')).all()
|
||||
|
||||
# Calculate percentages for type breakdown
|
||||
total_type_count = sum(t.count for t in type_stats) if type_stats else 0
|
||||
@ -5809,11 +5833,94 @@ def admin_ai_usage():
|
||||
'percentage': round(percentage, 1)
|
||||
})
|
||||
|
||||
# Recent logs
|
||||
recent_logs = db.query(AIUsageLog).order_by(desc(AIUsageLog.created_at)).limit(20).all()
|
||||
# ========================================
|
||||
# USER STATISTICS (filtered by period)
|
||||
# ========================================
|
||||
user_query = db.query(
|
||||
User.id,
|
||||
User.first_name,
|
||||
User.last_name,
|
||||
User.email,
|
||||
Company.name.label('company_name'),
|
||||
func.count(AIUsageLog.id).label('requests'),
|
||||
func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'),
|
||||
func.coalesce(func.sum(AIUsageLog.tokens_output), 0).label('tokens_output'),
|
||||
func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents')
|
||||
).join(
|
||||
AIUsageLog, AIUsageLog.user_id == User.id
|
||||
).outerjoin(
|
||||
Company, User.company_id == Company.id
|
||||
)
|
||||
user_query = period_filter(user_query)
|
||||
user_stats = user_query.group_by(
|
||||
User.id, User.first_name, User.last_name, User.email, Company.name
|
||||
).order_by(desc('cost_cents')).limit(20).all()
|
||||
|
||||
# Format user stats
|
||||
user_rankings = []
|
||||
for u in user_stats:
|
||||
user_rankings.append({
|
||||
'id': u.id,
|
||||
'name': f"{u.first_name or ''} {u.last_name or ''}".strip() 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
|
||||
})
|
||||
|
||||
# ========================================
|
||||
# COMPANY STATISTICS (filtered by period)
|
||||
# ========================================
|
||||
company_query = db.query(
|
||||
Company.id,
|
||||
Company.name,
|
||||
func.count(AIUsageLog.id).label('requests'),
|
||||
func.count(func.distinct(AIUsageLog.user_id)).label('unique_users'),
|
||||
func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'),
|
||||
func.coalesce(func.sum(AIUsageLog.tokens_output), 0).label('tokens_output'),
|
||||
func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents')
|
||||
).join(
|
||||
User, User.company_id == Company.id
|
||||
).join(
|
||||
AIUsageLog, AIUsageLog.user_id == User.id
|
||||
)
|
||||
company_query = period_filter(company_query)
|
||||
company_stats = company_query.group_by(
|
||||
Company.id, Company.name
|
||||
).order_by(desc('cost_cents')).limit(20).all()
|
||||
|
||||
# Format company stats
|
||||
company_rankings = []
|
||||
for c in company_stats:
|
||||
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
|
||||
})
|
||||
|
||||
# Recent logs with user info
|
||||
recent_logs = db.query(AIUsageLog).options(
|
||||
db.query(AIUsageLog).with_entities(AIUsageLog.user_id)
|
||||
).order_by(desc(AIUsageLog.created_at)).limit(20).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 = f"{user.first_name or ''} {user.last_name or ''}".strip() or user.email
|
||||
else:
|
||||
log.user_name = None
|
||||
else:
|
||||
log.user_name = None
|
||||
|
||||
# Daily history (last 14 days)
|
||||
daily_history = db.query(AIUsageDaily).filter(
|
||||
@ -5828,6 +5935,8 @@ def admin_ai_usage():
|
||||
'week_requests': week_requests,
|
||||
'month_requests': month_stats.requests or 0,
|
||||
'month_cost': float(month_stats.cost_cents or 0) / 100,
|
||||
'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)
|
||||
}
|
||||
@ -5837,7 +5946,11 @@ def admin_ai_usage():
|
||||
stats=stats,
|
||||
usage_by_type=usage_by_type,
|
||||
recent_logs=recent_logs,
|
||||
daily_history=daily_history
|
||||
daily_history=daily_history,
|
||||
user_rankings=user_rankings,
|
||||
company_rankings=company_rankings,
|
||||
current_period=period,
|
||||
period_label=period_label
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -15,9 +15,39 @@
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
/* Period filter buttons */
|
||||
.period-filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.period-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.period-btn:hover {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.period-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
@ -46,8 +76,12 @@
|
||||
border-left: 4px solid var(--error);
|
||||
}
|
||||
|
||||
.stat-card.info {
|
||||
border-left: 4px solid #6366f1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@ -56,6 +90,7 @@
|
||||
.stat-value.success { color: var(--success); }
|
||||
.stat-value.warning { color: var(--warning); }
|
||||
.stat-value.error { color: var(--error); }
|
||||
.stat-value.info { color: #6366f1; }
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
@ -92,6 +127,15 @@
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.section h2 .badge {
|
||||
font-size: var(--font-size-xs);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Usage breakdown bars */
|
||||
.usage-breakdown {
|
||||
display: flex;
|
||||
@ -106,7 +150,7 @@
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
width: 150px;
|
||||
width: 120px;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@ -140,36 +184,62 @@
|
||||
.usage-bar.other { background: #6b7280; }
|
||||
|
||||
.usage-count {
|
||||
width: 80px;
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Daily history table */
|
||||
.daily-table {
|
||||
/* Ranking tables */
|
||||
.ranking-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.daily-table th,
|
||||
.daily-table td {
|
||||
.ranking-table th,
|
||||
.ranking-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.daily-table th {
|
||||
.ranking-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.daily-table tr:hover {
|
||||
.ranking-table tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.ranking-position {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ranking-position.top-1 { color: #f59e0b; }
|
||||
.ranking-position.top-2 { color: #94a3b8; }
|
||||
.ranking-position.top-3 { color: #b45309; }
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.user-company {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.cost-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
@ -190,9 +260,26 @@
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Cost bar visualization */
|
||||
.cost-bar-container {
|
||||
width: 100px;
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cost-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #10b981, #f59e0b, #ef4444);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Recent logs */
|
||||
.log-list {
|
||||
list-style: none;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
@ -201,12 +288,19 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
@ -221,9 +315,16 @@
|
||||
.log-type.image_analysis { background: #ede9fe; color: #5b21b6; }
|
||||
.log-type.other { background: #f3f4f6; color: #374151; }
|
||||
|
||||
.log-user {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
@ -234,15 +335,15 @@
|
||||
|
||||
.log-time {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: var(--error);
|
||||
font-size: var(--font-size-xs);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* No data state */
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl);
|
||||
@ -261,12 +362,39 @@
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
/* Daily history table */
|
||||
.daily-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.daily-table th,
|
||||
.daily-table td {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.daily-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.daily-table tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sections-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.usage-label {
|
||||
width: 100px;
|
||||
width: 80px;
|
||||
}
|
||||
.ranking-table {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -281,21 +409,35 @@
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Powrót</a>
|
||||
</div>
|
||||
|
||||
<!-- Period Filters -->
|
||||
<div class="period-filters">
|
||||
<span class="text-muted" style="padding: var(--spacing-sm) 0;">Okres:</span>
|
||||
<a href="{{ url_for('admin_ai_usage', period='day') }}" class="period-btn {% if current_period == 'day' %}active{% endif %}">Dzisiaj</a>
|
||||
<a href="{{ url_for('admin_ai_usage', period='week') }}" class="period-btn {% if current_period == 'week' %}active{% endif %}">Tydzień</a>
|
||||
<a href="{{ url_for('admin_ai_usage', period='month') }}" class="period-btn {% if current_period == 'month' %}active{% endif %}">Miesiąc</a>
|
||||
<a href="{{ url_for('admin_ai_usage', period='all') }}" class="period-btn {% if current_period == 'all' %}active{% endif %}">Od początku</a>
|
||||
</div>
|
||||
|
||||
<!-- Main Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-value primary">{{ stats.today_requests }}</div>
|
||||
<div class="stat-label">Dzisiaj</div>
|
||||
<div class="stat-sublabel">zapytań do AI</div>
|
||||
<div class="stat-sublabel">zapytań</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.week_requests }}</div>
|
||||
<div class="stat-label">Ten tydzień</div>
|
||||
<div class="stat-label">Tydzień</div>
|
||||
<div class="stat-sublabel">zapytań</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.month_requests }}</div>
|
||||
<div class="stat-label">Ten miesiąc</div>
|
||||
<div class="stat-label">Miesiąc</div>
|
||||
<div class="stat-sublabel">zapytań</div>
|
||||
</div>
|
||||
<div class="stat-card info">
|
||||
<div class="stat-value info">{{ stats.all_requests }}</div>
|
||||
<div class="stat-label">Od początku</div>
|
||||
<div class="stat-sublabel">zapytań</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
@ -308,10 +450,15 @@
|
||||
<div class="stat-label">Koszt miesiąc</div>
|
||||
<div class="stat-sublabel">USD</div>
|
||||
</div>
|
||||
<div class="stat-card info">
|
||||
<div class="stat-value info">${{ "%.4f"|format(stats.all_cost) }}</div>
|
||||
<div class="stat-label">Koszt całkowity</div>
|
||||
<div class="stat-sublabel">USD</div>
|
||||
</div>
|
||||
<div class="stat-card {% if stats.error_rate > 10 %}error{% elif stats.error_rate > 5 %}warning{% else %}success{% endif %}">
|
||||
<div class="stat-value {% if stats.error_rate > 10 %}error{% elif stats.error_rate > 5 %}warning{% else %}success{% endif %}">{{ "%.1f"|format(stats.error_rate) }}%</div>
|
||||
<div class="stat-label">Błędy</div>
|
||||
<div class="stat-sublabel">ostatnie 24h</div>
|
||||
<div class="stat-sublabel">24h</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -329,15 +476,114 @@
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.avg_response_time }}ms</div>
|
||||
<div class="stat-label">Avg czas odpowiedzi</div>
|
||||
<div class="stat-sublabel">ostatnie 24h</div>
|
||||
<div class="stat-label">Avg czas</div>
|
||||
<div class="stat-sublabel">24h</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User and Company Rankings -->
|
||||
<div class="sections-grid">
|
||||
<!-- User Rankings -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="icon">👤</span> Ranking użytkowników
|
||||
<span class="badge">{{ period_label }}</span>
|
||||
</h2>
|
||||
{% if user_rankings %}
|
||||
<table class="ranking-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Użytkownik</th>
|
||||
<th>Zapytania</th>
|
||||
<th>Tokeny</th>
|
||||
<th>Koszt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in user_rankings %}
|
||||
<tr>
|
||||
<td class="ranking-position {% if loop.index == 1 %}top-1{% elif loop.index == 2 %}top-2{% elif loop.index == 3 %}top-3{% endif %}">
|
||||
{{ loop.index }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
<span class="user-company">{{ user.company }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ user.requests }}</td>
|
||||
<td>{{ "{:,}".format(user.tokens) }}</td>
|
||||
<td>
|
||||
<span class="cost-badge {% if user.cost_cents > 100 %}high{% elif user.cost_cents > 10 %}medium{% endif %}">
|
||||
${{ "%.4f"|format(user.cost_usd) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="icon">👤</div>
|
||||
<p>Brak danych o użytkownikach</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Company Rankings -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="icon">🏢</span> Ranking firm
|
||||
<span class="badge">{{ period_label }}</span>
|
||||
</h2>
|
||||
{% if company_rankings %}
|
||||
<table class="ranking-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Firma</th>
|
||||
<th>Użytkownicy</th>
|
||||
<th>Zapytania</th>
|
||||
<th>Koszt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for company in company_rankings %}
|
||||
<tr>
|
||||
<td class="ranking-position {% if loop.index == 1 %}top-1{% elif loop.index == 2 %}top-2{% elif loop.index == 3 %}top-3{% endif %}">
|
||||
{{ loop.index }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="user-name">{{ company.name }}</span>
|
||||
</td>
|
||||
<td>{{ company.unique_users }}</td>
|
||||
<td>{{ company.requests }}</td>
|
||||
<td>
|
||||
<span class="cost-badge {% if company.cost_cents > 100 %}high{% elif company.cost_cents > 10 %}medium{% endif %}">
|
||||
${{ "%.4f"|format(company.cost_usd) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="icon">🏢</div>
|
||||
<p>Brak danych o firmach</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sections-grid">
|
||||
<!-- Usage by Type -->
|
||||
<div class="section">
|
||||
<h2><span class="icon">📊</span> Wykorzystanie wg typu</h2>
|
||||
<h2>
|
||||
<span class="icon">📊</span> Wykorzystanie wg typu
|
||||
<span class="badge">{{ period_label }}</span>
|
||||
</h2>
|
||||
{% if usage_by_type %}
|
||||
<div class="usage-breakdown">
|
||||
{% for item in usage_by_type %}
|
||||
@ -367,15 +613,18 @@
|
||||
<ul class="log-list">
|
||||
{% for log in recent_logs %}
|
||||
<li class="log-item">
|
||||
<div>
|
||||
<div class="log-info">
|
||||
<span class="log-type {{ log.request_type }}">{{ log.type_label }}</span>
|
||||
{% if log.user_name %}
|
||||
<span class="log-user">{{ log.user_name }}</span>
|
||||
{% endif %}
|
||||
{% if not log.success %}
|
||||
<div class="log-error">{{ log.error_message[:50] }}...</div>
|
||||
<span class="log-error">{{ log.error_message[:30] }}...</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="log-meta">
|
||||
<span class="log-tokens">{{ log.tokens_input }}+{{ log.tokens_output }}</span>
|
||||
<span class="log-time">{{ log.created_at.strftime('%H:%M:%S') }}</span>
|
||||
<span class="log-tokens">{{ log.tokens_input or 0 }}+{{ log.tokens_output or 0 }}</span>
|
||||
<span class="log-time">{{ log.created_at.strftime('%d.%m %H:%M') }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user