- Integrated MSAL authentication for web pages - Added AuthMiddleware.php for SSO orchestration - Added JWTValidator.php for token validation - Protected report.php and webhook_caller.php - Firebase PHP-JWT for token verification - SSO can be disabled for local development - Complete SSO setup documentation - Environment-based configuration
838 lines
31 KiB
PHP
838 lines
31 KiB
PHP
<?php
|
|
// Configuration
|
|
$responseFile = 'webhook_response.json';
|
|
$costPerVideo = 3.20; // Veo 3.1 Standard: $3.20 per 8-second video
|
|
|
|
// SSO Authentication
|
|
require_once __DIR__ . '/AuthMiddleware.php';
|
|
$auth = new AuthMiddleware();
|
|
$user = $auth->requireAuth(); // This will redirect to login if not authenticated
|
|
|
|
// Load and parse JSON
|
|
if (!file_exists($responseFile)) {
|
|
die("Error: Response file not found. Please run webhook_caller.php first.");
|
|
}
|
|
|
|
$jsonContent = file_get_contents($responseFile);
|
|
$data = json_decode($jsonContent, true);
|
|
|
|
if (!$data) {
|
|
die("Error: Unable to parse JSON data.");
|
|
}
|
|
|
|
// Initialize arrays for analysis
|
|
$userCounts = [];
|
|
$dailyCounts = [];
|
|
$monthlyCounts = [];
|
|
$promptLengths = [];
|
|
$totalPrompts = count($data);
|
|
|
|
// Period-specific analysis
|
|
$now = new DateTime('now', new DateTimeZone('UTC'));
|
|
$last24Hours = clone $now;
|
|
$last24Hours->sub(new DateInterval('P1D'));
|
|
$last7Days = clone $now;
|
|
$last7Days->sub(new DateInterval('P7D'));
|
|
$last30Days = clone $now;
|
|
$last30Days->sub(new DateInterval('P30D'));
|
|
|
|
$userCounts24h = [];
|
|
$userCounts7d = [];
|
|
$userCounts30d = [];
|
|
$totalPrompts24h = 0;
|
|
$totalPrompts7d = 0;
|
|
$totalPrompts30d = 0;
|
|
|
|
// Process each record
|
|
foreach ($data as $record) {
|
|
$item = $record['data'];
|
|
|
|
// User counts
|
|
$user = $item['USER'];
|
|
if (!isset($userCounts[$user])) {
|
|
$userCounts[$user] = 0;
|
|
}
|
|
$userCounts[$user]++;
|
|
|
|
// Date parsing
|
|
$date = $item['Date'];
|
|
$dateObj = new DateTime($date);
|
|
$dateStr = $dateObj->format('Y-m-d');
|
|
$monthStr = $dateObj->format('Y-m');
|
|
|
|
// Period filtering
|
|
if ($dateObj >= $last24Hours) {
|
|
if (!isset($userCounts24h[$user])) $userCounts24h[$user] = 0;
|
|
$userCounts24h[$user]++;
|
|
$totalPrompts24h++;
|
|
}
|
|
if ($dateObj >= $last7Days) {
|
|
if (!isset($userCounts7d[$user])) $userCounts7d[$user] = 0;
|
|
$userCounts7d[$user]++;
|
|
$totalPrompts7d++;
|
|
}
|
|
if ($dateObj >= $last30Days) {
|
|
if (!isset($userCounts30d[$user])) $userCounts30d[$user] = 0;
|
|
$userCounts30d[$user]++;
|
|
$totalPrompts30d++;
|
|
}
|
|
|
|
// Daily counts
|
|
if (!isset($dailyCounts[$dateStr])) {
|
|
$dailyCounts[$dateStr] = 0;
|
|
}
|
|
$dailyCounts[$dateStr]++;
|
|
|
|
// Monthly counts
|
|
if (!isset($monthlyCounts[$monthStr])) {
|
|
$monthlyCounts[$monthStr] = 0;
|
|
}
|
|
$monthlyCounts[$monthStr]++;
|
|
|
|
// Prompt length analysis
|
|
$promptLength = strlen($item['PROMPT']);
|
|
$promptLengths[] = $promptLength;
|
|
}
|
|
|
|
// Sort period-specific user counts
|
|
arsort($userCounts24h);
|
|
arsort($userCounts7d);
|
|
arsort($userCounts30d);
|
|
|
|
// Get top 25 for each period
|
|
$topUsers24h = array_slice($userCounts24h, 0, 25, true);
|
|
$topUsers7d = array_slice($userCounts7d, 0, 25, true);
|
|
$topUsers30d = array_slice($userCounts30d, 0, 25, true);
|
|
|
|
// Calculate costs
|
|
$totalCost = $totalPrompts * $costPerVideo;
|
|
$cost24h = $totalPrompts24h * $costPerVideo;
|
|
$cost7d = $totalPrompts7d * $costPerVideo;
|
|
$cost30d = $totalPrompts30d * $costPerVideo;
|
|
|
|
// Sort data
|
|
arsort($userCounts);
|
|
ksort($dailyCounts);
|
|
ksort($monthlyCounts);
|
|
|
|
// Calculate statistics
|
|
$uniqueUsers = count($userCounts);
|
|
$avgPromptsPerUser = round($totalPrompts / $uniqueUsers, 2);
|
|
$avgPromptLength = round(array_sum($promptLengths) / count($promptLengths), 0);
|
|
$maxPromptLength = max($promptLengths);
|
|
$minPromptLength = min($promptLengths);
|
|
|
|
// Get date range
|
|
$dates = array_keys($dailyCounts);
|
|
$startDate = reset($dates);
|
|
$endDate = end($dates);
|
|
|
|
// Get top 20 users
|
|
$topUsers = array_slice($userCounts, 0, 20, true);
|
|
|
|
// Get recent daily activity (last 30 days)
|
|
$recentDaily = array_slice($dailyCounts, -30, 30, true);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VEO3 Usage Report</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
|
background-color: #f5f7fa;
|
|
padding: 20px;
|
|
color: #333;
|
|
}
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
color: #2c3e50;
|
|
margin-bottom: 10px;
|
|
font-size: 32px;
|
|
}
|
|
.subtitle {
|
|
color: #7f8c8d;
|
|
margin-bottom: 30px;
|
|
font-size: 14px;
|
|
}
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.stat-card {
|
|
background: white;
|
|
padding: 25px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
border-left: 4px solid #FFC407;
|
|
}
|
|
.stat-card h3 {
|
|
color: #7f8c8d;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.stat-card .value {
|
|
font-size: 36px;
|
|
font-weight: 700;
|
|
color: #2c3e50;
|
|
}
|
|
.stat-card .subvalue {
|
|
font-size: 14px;
|
|
color: #95a5a6;
|
|
margin-top: 5px;
|
|
}
|
|
.section {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
margin-bottom: 30px;
|
|
}
|
|
.section h2 {
|
|
color: #2c3e50;
|
|
margin-bottom: 20px;
|
|
font-size: 22px;
|
|
border-bottom: 2px solid #ecf0f1;
|
|
padding-bottom: 10px;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
th, td {
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ecf0f1;
|
|
}
|
|
th {
|
|
background-color: #f8f9fa;
|
|
color: #2c3e50;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
tr:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
.bar-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.bar {
|
|
height: 24px;
|
|
background: linear-gradient(90deg, #FFC407, #f5b800);
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.chart-container {
|
|
height: 300px;
|
|
position: relative;
|
|
margin-top: 20px;
|
|
}
|
|
.chart {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
height: 100%;
|
|
gap: 4px;
|
|
padding: 20px 0;
|
|
}
|
|
.chart-bar {
|
|
flex: 1;
|
|
background: linear-gradient(180deg, #FFC407, #f5b800);
|
|
border-radius: 4px 4px 0 0;
|
|
position: relative;
|
|
min-width: 2px;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
}
|
|
.chart-bar:hover {
|
|
opacity: 0.8;
|
|
}
|
|
.chart-bar .tooltip {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #2c3e50;
|
|
color: white;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
display: none;
|
|
margin-bottom: 5px;
|
|
z-index: 10;
|
|
}
|
|
.chart-bar:hover .tooltip {
|
|
display: block;
|
|
}
|
|
.chart-labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 10px;
|
|
font-size: 11px;
|
|
color: #7f8c8d;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.badge-gold {
|
|
background-color: #f39c12;
|
|
color: white;
|
|
}
|
|
.badge-silver {
|
|
background-color: #95a5a6;
|
|
color: white;
|
|
}
|
|
.badge-bronze {
|
|
background-color: #cd7f32;
|
|
color: white;
|
|
}
|
|
.rank {
|
|
font-weight: 700;
|
|
color: #FFC407;
|
|
min-width: 30px;
|
|
display: inline-block;
|
|
}
|
|
.daily-table {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
.daily-table table {
|
|
font-size: 13px;
|
|
}
|
|
.daily-table td, .daily-table th {
|
|
padding: 8px 12px;
|
|
}
|
|
.refresh-button {
|
|
background: linear-gradient(135deg, #FFC407, #f5b800);
|
|
color: #2c3e50;
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 8px rgba(255,196,7,0.3);
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.refresh-button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(255,196,7,0.4);
|
|
}
|
|
.refresh-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
.collapsible {
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.collapsible:hover {
|
|
color: #FFC407;
|
|
}
|
|
.collapsible-icon {
|
|
transition: transform 0.3s ease;
|
|
font-size: 20px;
|
|
color: #FFC407;
|
|
}
|
|
.collapsible-icon.collapsed {
|
|
transform: rotate(-90deg);
|
|
}
|
|
.collapsible-content {
|
|
max-height: 600px;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease;
|
|
}
|
|
.collapsible-content.collapsed {
|
|
max-height: 0;
|
|
}
|
|
.header-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 2px solid #2c3e50;
|
|
border-top-color: transparent;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header-actions">
|
|
<div>
|
|
<h1 style="margin: 0;">VEO3 Usage Report</h1>
|
|
<p class="subtitle" style="margin: 5px 0 0 0;">Data from <?php echo $startDate; ?> to <?php echo $endDate; ?></p>
|
|
</div>
|
|
<button id="refreshBtn" class="refresh-button" onclick="refreshData()">
|
|
<span id="refreshIcon">🔄</span>
|
|
<span id="refreshText">Refresh Data</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<h3>Total Prompts</h3>
|
|
<div class="value"><?php echo number_format($totalPrompts); ?></div>
|
|
<div class="subvalue">Video generation requests</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Total Cost</h3>
|
|
<div class="value">$<?php echo number_format($totalCost, 2); ?></div>
|
|
<div class="subvalue">@$<?php echo number_format($costPerVideo, 2); ?> per 8-second video</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Unique Users</h3>
|
|
<div class="value"><?php echo number_format($uniqueUsers); ?></div>
|
|
<div class="subvalue">Active accounts</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Avg Prompts/User</h3>
|
|
<div class="value"><?php echo $avgPromptsPerUser; ?></div>
|
|
<div class="subvalue">Mean usage per account</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid" style="margin-top: 20px;">
|
|
<div class="stat-card">
|
|
<h3>Last 24 Hours</h3>
|
|
<div class="value" style="font-size: 28px;"><?php echo number_format($totalPrompts24h); ?> videos</div>
|
|
<div class="subvalue" style="font-size: 16px; color: #2c3e50; font-weight: 600; margin-top: 5px;">$<?php echo number_format($cost24h, 2); ?></div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Last 7 Days</h3>
|
|
<div class="value" style="font-size: 28px;"><?php echo number_format($totalPrompts7d); ?> videos</div>
|
|
<div class="subvalue" style="font-size: 16px; color: #2c3e50; font-weight: 600; margin-top: 5px;">$<?php echo number_format($cost7d, 2); ?></div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Last 30 Days</h3>
|
|
<div class="value" style="font-size: 28px;"><?php echo number_format($totalPrompts30d); ?> videos</div>
|
|
<div class="subvalue" style="font-size: 16px; color: #2c3e50; font-weight: 600; margin-top: 5px;">$<?php echo number_format($cost30d, 2); ?></div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Avg Prompt Length</h3>
|
|
<div class="value" style="font-size: 28px;"><?php echo number_format($avgPromptLength); ?></div>
|
|
<div class="subvalue">Characters</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Daily Usage - Last 30 Days</h2>
|
|
<div class="chart-container">
|
|
<div class="chart">
|
|
<?php
|
|
$maxDaily = max($recentDaily);
|
|
foreach ($recentDaily as $date => $count):
|
|
$height = ($count / $maxDaily) * 100;
|
|
?>
|
|
<div class="chart-bar" style="height: <?php echo $height; ?>%;">
|
|
<div class="tooltip">
|
|
<strong><?php echo $date; ?></strong><br>
|
|
<?php echo $count; ?> prompts
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="chart-labels">
|
|
<span><?php echo array_key_first($recentDaily); ?></span>
|
|
<span><?php echo array_key_last($recentDaily); ?></span>
|
|
</div>
|
|
</div>
|
|
<div class="daily-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Day of Week</th>
|
|
<th>Prompts</th>
|
|
<th>Activity Level</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$maxDailyRecent = max($recentDaily);
|
|
foreach (array_reverse($recentDaily, true) as $date => $count):
|
|
$dateObj = new DateTime($date);
|
|
$dayOfWeek = $dateObj->format('l');
|
|
$barWidth = ($count / $maxDailyRecent) * 100;
|
|
?>
|
|
<tr>
|
|
<td><strong><?php echo $date; ?></strong></td>
|
|
<td><?php echo $dayOfWeek; ?></td>
|
|
<td><?php echo number_format($count); ?></td>
|
|
<td>
|
|
<div class="bar-cell">
|
|
<div class="bar" style="width: <?php echo $barWidth . "%"; ?>;"></div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Monthly Usage Breakdown</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Month</th>
|
|
<th>Prompts</th>
|
|
<th>Distribution</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$maxMonthly = max($monthlyCounts);
|
|
foreach ($monthlyCounts as $month => $count):
|
|
$percentage = ($count / $totalPrompts) * 100;
|
|
$barWidth = ($count / $maxMonthly) * 100;
|
|
?>
|
|
<tr>
|
|
<td><strong><?php echo date('F Y', strtotime($month . '-01')); ?></strong></td>
|
|
<td><?php echo number_format($count); ?> <span style="color: #95a5a6; font-size: 12px;">(<?php echo number_format($percentage, 1); ?>%)</span></td>
|
|
<td>
|
|
<div class="bar-cell">
|
|
<div class="bar" style="width: <?php echo $barWidth . "%"; ?>;"></div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📅 Last 24 Hours - Top 25 Users</h2>
|
|
<?php if ($totalPrompts24h > 0): ?>
|
|
<p style="color: #7f8c8d; margin-bottom: 15px; font-size: 14px;">
|
|
<?php echo number_format($totalPrompts24h); ?> prompts from <?php echo count($userCounts24h); ?> users
|
|
<strong style="color: #2c3e50; margin-left: 15px;">Cost: $<?php echo number_format($cost24h, 2); ?></strong>
|
|
</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;">Rank</th>
|
|
<th>User</th>
|
|
<th>Prompts</th>
|
|
<th>Percentage</th>
|
|
<th>Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$rank = 1;
|
|
$maxCount24h = !empty($topUsers24h) ? max($topUsers24h) : 1;
|
|
foreach ($topUsers24h as $user => $count):
|
|
$percentage = ($count / $totalPrompts24h) * 100;
|
|
$barWidth = ($count / $maxCount24h) * 100;
|
|
|
|
$badge = '';
|
|
if ($rank === 1) $badge = '<span class="badge badge-gold">🥇 #1</span>';
|
|
elseif ($rank === 2) $badge = '<span class="badge badge-silver">🥈 #2</span>';
|
|
elseif ($rank === 3) $badge = '<span class="badge badge-bronze">🥉 #3</span>';
|
|
?>
|
|
<tr>
|
|
<td><span class="rank">#<?php echo $rank; ?></span></td>
|
|
<td>
|
|
<?php echo htmlspecialchars($user); ?>
|
|
<?php if ($badge) echo ' ' . $badge; ?>
|
|
</td>
|
|
<td><strong><?php echo number_format($count); ?></strong></td>
|
|
<td><?php echo number_format($percentage, 2); ?>%</td>
|
|
<td>
|
|
<div class="bar-cell">
|
|
<div class="bar" style="width: <?php echo $barWidth . "%"; ?>;"></div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
$rank++;
|
|
endforeach;
|
|
?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<p style="color: #95a5a6; padding: 20px; text-align: center;">No activity in the last 24 hours</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📊 Last 7 Days - Top 25 Users</h2>
|
|
<?php if ($totalPrompts7d > 0): ?>
|
|
<p style="color: #7f8c8d; margin-bottom: 15px; font-size: 14px;">
|
|
<?php echo number_format($totalPrompts7d); ?> prompts from <?php echo count($userCounts7d); ?> users
|
|
<strong style="color: #2c3e50; margin-left: 15px;">Cost: $<?php echo number_format($cost7d, 2); ?></strong>
|
|
</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;">Rank</th>
|
|
<th>User</th>
|
|
<th>Prompts</th>
|
|
<th>Percentage</th>
|
|
<th>Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$rank = 1;
|
|
$maxCount7d = !empty($topUsers7d) ? max($topUsers7d) : 1;
|
|
foreach ($topUsers7d as $user => $count):
|
|
$percentage = ($count / $totalPrompts7d) * 100;
|
|
$barWidth = ($count / $maxCount7d) * 100;
|
|
|
|
$badge = '';
|
|
if ($rank === 1) $badge = '<span class="badge badge-gold">🥇 #1</span>';
|
|
elseif ($rank === 2) $badge = '<span class="badge badge-silver">🥈 #2</span>';
|
|
elseif ($rank === 3) $badge = '<span class="badge badge-bronze">🥉 #3</span>';
|
|
?>
|
|
<tr>
|
|
<td><span class="rank">#<?php echo $rank; ?></span></td>
|
|
<td>
|
|
<?php echo htmlspecialchars($user); ?>
|
|
<?php if ($badge) echo ' ' . $badge; ?>
|
|
</td>
|
|
<td><strong><?php echo number_format($count); ?></strong></td>
|
|
<td><?php echo number_format($percentage, 2); ?>%</td>
|
|
<td>
|
|
<div class="bar-cell">
|
|
<div class="bar" style="width: <?php echo $barWidth . "%"; ?>;"></div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
$rank++;
|
|
endforeach;
|
|
?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<p style="color: #95a5a6; padding: 20px; text-align: center;">No activity in the last 7 days</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📈 Last 30 Days - Top 25 Users</h2>
|
|
<?php if ($totalPrompts30d > 0): ?>
|
|
<p style="color: #7f8c8d; margin-bottom: 15px; font-size: 14px;">
|
|
<?php echo number_format($totalPrompts30d); ?> prompts from <?php echo count($userCounts30d); ?> users
|
|
<strong style="color: #2c3e50; margin-left: 15px;">Cost: $<?php echo number_format($cost30d, 2); ?></strong>
|
|
</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;">Rank</th>
|
|
<th>User</th>
|
|
<th>Prompts</th>
|
|
<th>Percentage</th>
|
|
<th>Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$rank = 1;
|
|
$maxCount30d = !empty($topUsers30d) ? max($topUsers30d) : 1;
|
|
foreach ($topUsers30d as $user => $count):
|
|
$percentage = ($count / $totalPrompts30d) * 100;
|
|
$barWidth = ($count / $maxCount30d) * 100;
|
|
|
|
$badge = '';
|
|
if ($rank === 1) $badge = '<span class="badge badge-gold">🥇 #1</span>';
|
|
elseif ($rank === 2) $badge = '<span class="badge badge-silver">🥈 #2</span>';
|
|
elseif ($rank === 3) $badge = '<span class="badge badge-bronze">🥉 #3</span>';
|
|
?>
|
|
<tr>
|
|
<td><span class="rank">#<?php echo $rank; ?></span></td>
|
|
<td>
|
|
<?php echo htmlspecialchars($user); ?>
|
|
<?php if ($badge) echo ' ' . $badge; ?>
|
|
</td>
|
|
<td><strong><?php echo number_format($count); ?></strong></td>
|
|
<td><?php echo number_format($percentage, 2); ?>%</td>
|
|
<td>
|
|
<div class="bar-cell">
|
|
<div class="bar" style="width: <?php echo $barWidth . "%"; ?>;"></div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
$rank++;
|
|
endforeach;
|
|
?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<p style="color: #95a5a6; padding: 20px; text-align: center;">No activity in the last 30 days</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>All Time - Top 20 Users</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;">Rank</th>
|
|
<th>User</th>
|
|
<th>Prompts</th>
|
|
<th>Percentage</th>
|
|
<th>Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$rank = 1;
|
|
$maxCount = max($topUsers);
|
|
foreach ($topUsers as $user => $count):
|
|
$percentage = ($count / $totalPrompts) * 100;
|
|
$barWidth = ($count / $maxCount) * 100;
|
|
|
|
$badge = '';
|
|
if ($rank === 1) $badge = '<span class="badge badge-gold">🥇 #1</span>';
|
|
elseif ($rank === 2) $badge = '<span class="badge badge-silver">🥈 #2</span>';
|
|
elseif ($rank === 3) $badge = '<span class="badge badge-bronze">🥉 #3</span>';
|
|
?>
|
|
<tr>
|
|
<td><span class="rank">#<?php echo $rank; ?></span></td>
|
|
<td>
|
|
<?php echo htmlspecialchars($user); ?>
|
|
<?php if ($badge) echo ' ' . $badge; ?>
|
|
</td>
|
|
<td><strong><?php echo number_format($count); ?></strong></td>
|
|
<td><?php echo number_format($percentage, 2); ?>%</td>
|
|
<td>
|
|
<div class="bar-cell">
|
|
<div class="bar" style="width: <?php echo $barWidth . "%"; ?>;"></div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
$rank++;
|
|
endforeach;
|
|
?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2 class="collapsible" onclick="toggleCollapse()">
|
|
<span>All Users Summary (<?php echo count($userCounts); ?> users)</span>
|
|
<span class="collapsible-icon">▼</span>
|
|
</h2>
|
|
<div class="collapsible-content collapsed" id="allUsersContent">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Total Prompts</th>
|
|
<th>Percentage of Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($userCounts as $user => $count):
|
|
$percentage = ($count / $totalPrompts) * 100;
|
|
?>
|
|
<tr>
|
|
<td><?php echo htmlspecialchars($user); ?></td>
|
|
<td><?php echo number_format($count); ?></td>
|
|
<td><?php echo number_format($percentage, 2); ?>%</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleCollapse() {
|
|
const content = document.getElementById('allUsersContent');
|
|
const icon = document.querySelector('.collapsible-icon');
|
|
|
|
content.classList.toggle('collapsed');
|
|
icon.classList.toggle('collapsed');
|
|
}
|
|
|
|
function refreshData() {
|
|
const btn = document.getElementById('refreshBtn');
|
|
const icon = document.getElementById('refreshIcon');
|
|
const text = document.getElementById('refreshText');
|
|
|
|
// Disable button
|
|
btn.disabled = true;
|
|
icon.innerHTML = '<span class="spinner"></span>';
|
|
text.textContent = 'Refreshing...';
|
|
|
|
// Get default dates (last 30 days)
|
|
const endDate = new Date().toISOString().split('T')[0];
|
|
const startDate = new Date(Date.now() - 30*24*60*60*1000).toISOString().split('T')[0];
|
|
|
|
// Call webhook
|
|
fetch('webhook_caller.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `start_date=${startDate}&end_date=${endDate}&ajax=1`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Reload the page to show new data
|
|
window.location.reload();
|
|
} else {
|
|
alert('Error refreshing data: ' + (data.error || 'Unknown error'));
|
|
btn.disabled = false;
|
|
icon.textContent = '🔄';
|
|
text.textContent = 'Refresh Data';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
alert('Error refreshing data: ' + error);
|
|
btn.disabled = false;
|
|
icon.textContent = '🔄';
|
|
text.textContent = 'Refresh Data';
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|