Update AI Tools Usage Report System - Multi-Tool Support
Enhanced the VEO3 usage report system to support all AI tool types: - Added support for 6 tool types: VEO3, TEXT2IMAGE, TEXT2VOICE, SPEECH2SPEECH, DOCUMENT_TRANSLATION, VIDEOQUERY - Updated Python report generator (veo3_report.py) with dynamic tool detection - Updated PHP report page (report.php) with tool usage breakdown section - Changed all "Prompts" references to "Requests" for clarity - Updated refresh button to fetch only last 12 weeks (84 days) for better performance - Made system dynamic to handle unknown tool types automatically - Renamed report titles from "VEO3 Usage Report" to "AI Tools Usage Report" Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
facacc94d4
21 changed files with 3655 additions and 0 deletions
20
.env.example
Normal file
20
.env.example
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# VEO3 Report Configuration
|
||||
|
||||
# Webhook URL (default is provided, override if needed)
|
||||
WEBHOOK_URL=https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj
|
||||
|
||||
# SMTP Configuration (Mailgun SMTP)
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-mailgun-smtp-user@your-domain.com
|
||||
SMTP_PASSWORD=your-mailgun-smtp-password
|
||||
SENDER_EMAIL=reports@your-domain.com
|
||||
|
||||
# Email Recipients (comma-separated)
|
||||
REPORT_RECIPIENTS=user1@example.com,user2@example.com
|
||||
|
||||
# SSO Configuration (Microsoft Azure AD / MSAL)
|
||||
# Set SSO_ENABLED=true for production, false for local development without SSO
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-azure-tenant-id
|
||||
SSO_CLIENT_ID=your-azure-client-id
|
||||
20
.env.example copy
Normal file
20
.env.example copy
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# VEO3 Report Configuration
|
||||
|
||||
# Webhook URL (default is provided, override if needed)
|
||||
WEBHOOK_URL=https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj
|
||||
|
||||
# SMTP Configuration (Mailgun SMTP)
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-mailgun-smtp-user@your-domain.com
|
||||
SMTP_PASSWORD=your-mailgun-smtp-password
|
||||
SENDER_EMAIL=reports@your-domain.com
|
||||
|
||||
# Email Recipients (comma-separated)
|
||||
REPORT_RECIPIENTS=user1@example.com,user2@example.com
|
||||
|
||||
# SSO Configuration (Microsoft Azure AD / MSAL)
|
||||
# Set SSO_ENABLED=true for production, false for local development without SSO
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-azure-tenant-id
|
||||
SSO_CLIENT_ID=your-azure-client-id
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Environment configuration
|
||||
.env
|
||||
|
||||
# Generated data files
|
||||
webhook_response.json
|
||||
email_report.html
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
409
AuthMiddleware.php
Normal file
409
AuthMiddleware.php
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
<?php
|
||||
/**
|
||||
* Authentication Middleware for MSAL / Azure AD
|
||||
* Central authentication orchestrator with login UI
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
class AuthMiddleware {
|
||||
private $validator;
|
||||
private $tenantId;
|
||||
private $clientId;
|
||||
private $ssoEnabled;
|
||||
|
||||
public function __construct() {
|
||||
$this->ssoEnabled = SSO_ENABLED;
|
||||
$this->tenantId = SSO_TENANT_ID;
|
||||
$this->clientId = SSO_CLIENT_ID;
|
||||
|
||||
// Only initialize validator if SSO is enabled
|
||||
if ($this->ssoEnabled) {
|
||||
require_once __DIR__ . '/JWTValidator.php';
|
||||
$this->validator = new JWTValidator($this->tenantId, $this->clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SSO is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSSOEnabled() {
|
||||
return $this->ssoEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*
|
||||
* @return array ['authenticated' => bool, 'user' => array|null, 'error' => string|null]
|
||||
*/
|
||||
public function isAuthenticated() {
|
||||
// If SSO is disabled, return authenticated with mock user
|
||||
if (!$this->ssoEnabled) {
|
||||
return [
|
||||
'authenticated' => true,
|
||||
'user' => [
|
||||
'name' => 'Local Developer',
|
||||
'preferred_username' => 'dev@localhost'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Get token from cookie
|
||||
$token = $this->getTokenFromCookie();
|
||||
if (!$token) {
|
||||
return ['authenticated' => false, 'error' => 'No authentication token found'];
|
||||
}
|
||||
|
||||
// Validate token
|
||||
$validation = $this->validator->validateToken($token);
|
||||
if (!$validation['valid']) {
|
||||
return ['authenticated' => false, 'error' => $validation['error']];
|
||||
}
|
||||
|
||||
return ['authenticated' => true, 'user' => $validation['payload']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication - blocks if not authenticated
|
||||
*
|
||||
* @return array User data
|
||||
*/
|
||||
public function requireAuth() {
|
||||
// If SSO is disabled, return mock user
|
||||
if (!$this->ssoEnabled) {
|
||||
return [
|
||||
'name' => 'Local Developer',
|
||||
'preferred_username' => 'dev@localhost'
|
||||
];
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
$auth = $this->isAuthenticated();
|
||||
|
||||
if (!$auth['authenticated']) {
|
||||
$this->handleUnauthorized($auth['error'] ?? 'Authentication required');
|
||||
exit;
|
||||
}
|
||||
|
||||
return $auth['user'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication token (after login)
|
||||
*
|
||||
* @param string $token JWT token from MSAL
|
||||
* @return array ['success' => bool, 'user' => array|null, 'error' => string|null]
|
||||
*/
|
||||
public function setAuthToken($token) {
|
||||
if (!$this->ssoEnabled) {
|
||||
return ['success' => false, 'error' => 'SSO is disabled'];
|
||||
}
|
||||
|
||||
// Validate token
|
||||
$validation = $this->validator->validateToken($token);
|
||||
if (!$validation['valid']) {
|
||||
return ['success' => false, 'error' => $validation['error']];
|
||||
}
|
||||
|
||||
// Set httpOnly cookie with security options
|
||||
$cookieOptions = [
|
||||
'expires' => time() + (24 * 60 * 60), // 24 hours
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => isset($_SERVER['HTTPS']), // Only over HTTPS in production
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
|
||||
setcookie('auth_token', $token, $cookieOptions);
|
||||
|
||||
return ['success' => true, 'user' => $validation['payload']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication token (logout)
|
||||
*/
|
||||
public function clearAuthToken() {
|
||||
setcookie('auth_token', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'httponly' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from cookie
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getTokenFromCookie() {
|
||||
return isset($_COOKIE['auth_token']) ? $_COOKIE['auth_token'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unauthorized access
|
||||
*
|
||||
* @param string $error Error message
|
||||
*/
|
||||
private function handleUnauthorized($error) {
|
||||
if ($this->isAjaxRequest()) {
|
||||
// For AJAX requests, return JSON
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'error' => 'Authentication required',
|
||||
'message' => $error,
|
||||
'requiresAuth' => true
|
||||
]);
|
||||
} else {
|
||||
// For page requests, show login interface
|
||||
$this->showLoginPage($error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is AJAX
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isAjaxRequest() {
|
||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show login page with MSAL integration
|
||||
*
|
||||
* @param string $error Optional error message
|
||||
*/
|
||||
public function showLoginPage($error = '') {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In - VEO3 Usage Report</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #FFC407;
|
||||
border-radius: 12px;
|
||||
padding: 50px 40px;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 50px rgba(255, 196, 7, 0.2);
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #FFC407;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1rem;
|
||||
color: #999;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ff4444;
|
||||
color: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #FFC407 0%, #ff9500 100%);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 18px 40px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 20px rgba(255, 196, 7, 0.4);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #000;
|
||||
border-top: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.info-text {
|
||||
margin-top: 30px;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-logo">📊</div>
|
||||
<h1 class="login-title">VEO3 Usage Report</h1>
|
||||
<p class="login-subtitle">Video Generation Analytics Dashboard</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-message">
|
||||
<?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<button class="btn-login" onclick="signIn()" id="loginBtn">
|
||||
<span id="lockIcon">🔒</span>
|
||||
<span id="loadingIcon" class="loading"></span>
|
||||
<span id="btnText">Sign In with Microsoft</span>
|
||||
</button>
|
||||
|
||||
<div class="info-text">
|
||||
Sign in with your Microsoft account to access usage reports
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: "<?php echo $this->clientId; ?>",
|
||||
authority: "https://login.microsoftonline.com/<?php echo $this->tenantId; ?>",
|
||||
redirectUri: window.location.origin + window.location.pathname
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "sessionStorage",
|
||||
storeAuthStateInCookie: true,
|
||||
}
|
||||
};
|
||||
|
||||
const loginRequest = {
|
||||
scopes: ["openid", "profile", "email"],
|
||||
prompt: "select_account"
|
||||
};
|
||||
|
||||
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
function signIn() {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const lockIcon = document.getElementById('lockIcon');
|
||||
const loadingIcon = document.getElementById('loadingIcon');
|
||||
const btnText = document.getElementById('btnText');
|
||||
|
||||
// Show loading state
|
||||
loginBtn.disabled = true;
|
||||
lockIcon.style.display = 'none';
|
||||
loadingIcon.classList.add('show');
|
||||
btnText.textContent = 'Signing in...';
|
||||
|
||||
myMSALObj.loginPopup(loginRequest)
|
||||
.then(loginResponse => {
|
||||
// Send token to server for validation and cookie setting
|
||||
fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'login',
|
||||
idToken: loginResponse.idToken,
|
||||
accessToken: loginResponse.accessToken
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Authentication failed: ' + (data.error || 'Unknown error'));
|
||||
resetButton();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Authentication error:', error);
|
||||
alert('Authentication failed. Please try again.');
|
||||
resetButton();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Login failed:", error);
|
||||
alert('Login failed: ' + error.message);
|
||||
resetButton();
|
||||
});
|
||||
}
|
||||
|
||||
function resetButton() {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const lockIcon = document.getElementById('lockIcon');
|
||||
const loadingIcon = document.getElementById('loadingIcon');
|
||||
const btnText = document.getElementById('btnText');
|
||||
|
||||
loginBtn.disabled = false;
|
||||
lockIcon.style.display = 'inline';
|
||||
loadingIcon.classList.remove('show');
|
||||
btnText.textContent = 'Sign In with Microsoft';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
318
DEPLOYMENT.md
Normal file
318
DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
# VEO3 Report System - Server Deployment Guide
|
||||
|
||||
This guide covers deploying the VEO3 report system as a systemd service that runs daily at 7:00 PM EST.
|
||||
|
||||
## Overview
|
||||
|
||||
Instead of using cron, this system uses:
|
||||
- **Python Scheduler Daemon** (`veo3_scheduler.py`) - Runs continuously and triggers reports at scheduled times
|
||||
- **systemd Service** - Manages the scheduler, ensures it starts on boot and restarts if it crashes
|
||||
- **APScheduler** - Python library for robust job scheduling with timezone support
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Server Requirements
|
||||
- Linux server (Ubuntu 20.04+ or similar)
|
||||
- Python 3.8 or higher
|
||||
- systemd (standard on most modern Linux distributions)
|
||||
- sudo/root access for initial setup
|
||||
|
||||
### Required Files
|
||||
```
|
||||
veo3_report.py - Report generation script
|
||||
veo3_scheduler.py - Scheduler daemon (NEW)
|
||||
requirements.txt - Python dependencies
|
||||
.env - Configuration file with SMTP credentials
|
||||
veo3-report.service - systemd service definition
|
||||
deploy.sh - Automated deployment script
|
||||
```
|
||||
|
||||
## Quick Deploy
|
||||
|
||||
### Option 1: Automated Deployment (Recommended)
|
||||
|
||||
1. **Upload files to server:**
|
||||
```bash
|
||||
scp veo3_report.py veo3_scheduler.py requirements.txt .env \
|
||||
veo3-report.service deploy.sh \
|
||||
user@your-server:/tmp/veo3-deploy/
|
||||
```
|
||||
|
||||
2. **SSH into server:**
|
||||
```bash
|
||||
ssh user@your-server
|
||||
```
|
||||
|
||||
3. **Run deployment script:**
|
||||
```bash
|
||||
cd /tmp/veo3-deploy
|
||||
chmod +x deploy.sh
|
||||
sudo ./deploy.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
- ✅ Create `/var/www/veo3-report` directory
|
||||
- ✅ Set up Python virtual environment
|
||||
- ✅ Install all dependencies
|
||||
- ✅ Configure systemd service
|
||||
- ✅ Start the service
|
||||
- ✅ Enable auto-start on boot
|
||||
|
||||
### Option 2: Manual Deployment
|
||||
|
||||
1. **Create deployment directory:**
|
||||
```bash
|
||||
sudo mkdir -p /var/www/veo3-report
|
||||
sudo mkdir -p /var/log/veo3-report
|
||||
```
|
||||
|
||||
2. **Copy files:**
|
||||
```bash
|
||||
sudo cp veo3_report.py veo3_scheduler.py requirements.txt .env /var/www/veo3-report/
|
||||
```
|
||||
|
||||
3. **Create virtual environment:**
|
||||
```bash
|
||||
cd /var/www/veo3-report
|
||||
sudo python3 -m venv venv
|
||||
sudo ./venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **Set permissions:**
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/veo3-report
|
||||
sudo chown -R www-data:www-data /var/log/veo3-report
|
||||
sudo chmod 600 /var/www/veo3-report/.env
|
||||
```
|
||||
|
||||
5. **Install systemd service:**
|
||||
```bash
|
||||
sudo cp veo3-report.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable veo3-report
|
||||
sudo systemctl start veo3-report
|
||||
```
|
||||
|
||||
## Service Management
|
||||
|
||||
### Check Service Status
|
||||
```bash
|
||||
sudo systemctl status veo3-report
|
||||
```
|
||||
|
||||
### View Live Logs
|
||||
```bash
|
||||
# Systemd logs
|
||||
sudo journalctl -u veo3-report -f
|
||||
|
||||
# Application logs
|
||||
sudo tail -f /var/www/veo3-report/logs/veo3_scheduler.log
|
||||
```
|
||||
|
||||
### Start/Stop/Restart
|
||||
```bash
|
||||
sudo systemctl start veo3-report
|
||||
sudo systemctl stop veo3-report
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
### Disable Auto-Start
|
||||
```bash
|
||||
sudo systemctl disable veo3-report
|
||||
```
|
||||
|
||||
### Force Immediate Report Run (Testing)
|
||||
```bash
|
||||
# Stop the service
|
||||
sudo systemctl stop veo3-report
|
||||
|
||||
# Run manually to test
|
||||
cd /var/www/veo3-report
|
||||
sudo -u www-data ./venv/bin/python veo3_report.py
|
||||
|
||||
# Restart service
|
||||
sudo systemctl start veo3-report
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Change Schedule Time
|
||||
|
||||
Edit `/var/www/veo3-report/veo3_scheduler.py`:
|
||||
|
||||
```python
|
||||
# Current: 7:00 PM EST
|
||||
self.scheduler.add_job(
|
||||
self.run_report,
|
||||
trigger=CronTrigger(hour=19, minute=0, timezone=EST), # Change hour (24hr format)
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
After editing, restart the service:
|
||||
```bash
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
### Change Timezone
|
||||
|
||||
Edit `veo3_scheduler.py`:
|
||||
```python
|
||||
# Change from EST to another timezone
|
||||
EST = pytz.timezone('America/New_York') # Change to your timezone
|
||||
# Examples:
|
||||
# PST: 'America/Los_Angeles'
|
||||
# CST: 'America/Chicago'
|
||||
# UTC: 'UTC'
|
||||
```
|
||||
|
||||
### Update SMTP Credentials
|
||||
|
||||
Edit `/var/www/veo3-report/.env` and restart:
|
||||
```bash
|
||||
sudo nano /var/www/veo3-report/.env
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
### Change Cost Per Video
|
||||
|
||||
Edit `/var/www/veo3-report/veo3_report.py`:
|
||||
```python
|
||||
COST_PER_VIDEO = 3.20 # Change this value
|
||||
```
|
||||
|
||||
Restart service after changes:
|
||||
```bash
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Check Next Scheduled Run
|
||||
```bash
|
||||
sudo journalctl -u veo3-report --no-pager | grep "Next scheduled run"
|
||||
```
|
||||
|
||||
### View Recent Reports
|
||||
```bash
|
||||
ls -lh /var/www/veo3-report/email_report.html
|
||||
cat /var/www/veo3-report/email_report.html
|
||||
```
|
||||
|
||||
### Check Email Delivery
|
||||
```bash
|
||||
sudo journalctl -u veo3-report --no-pager | grep "Email sent"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service Won't Start
|
||||
```bash
|
||||
# Check for errors
|
||||
sudo journalctl -u veo3-report -n 50
|
||||
|
||||
# Check service file syntax
|
||||
sudo systemd-analyze verify veo3-report.service
|
||||
|
||||
# Check Python dependencies
|
||||
cd /var/www/veo3-report
|
||||
sudo -u www-data ./venv/bin/pip list
|
||||
```
|
||||
|
||||
### Reports Not Sending
|
||||
```bash
|
||||
# Test SMTP credentials
|
||||
cd /var/www/veo3-report
|
||||
sudo -u www-data ./venv/bin/python veo3_report.py
|
||||
|
||||
# Check email logs
|
||||
sudo journalctl -u veo3-report | grep -i smtp
|
||||
sudo journalctl -u veo3-report | grep -i email
|
||||
```
|
||||
|
||||
### Wrong Timezone
|
||||
```bash
|
||||
# Check server timezone
|
||||
timedatectl
|
||||
|
||||
# The service uses EST timezone explicitly in the code,
|
||||
# regardless of server timezone
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
```bash
|
||||
# Fix ownership
|
||||
sudo chown -R www-data:www-data /var/www/veo3-report
|
||||
sudo chown -R www-data:www-data /var/log/veo3-report
|
||||
|
||||
# Fix .env permissions
|
||||
sudo chmod 600 /var/www/veo3-report/.env
|
||||
```
|
||||
|
||||
## Log Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/var/log/veo3-report/service.log` | Service stdout |
|
||||
| `/var/log/veo3-report/service-error.log` | Service stderr |
|
||||
| `/var/www/veo3-report/logs/veo3_scheduler.log` | Scheduler application log |
|
||||
| `journalctl -u veo3-report` | Systemd service log |
|
||||
|
||||
## Updating the System
|
||||
|
||||
1. **Stop the service:**
|
||||
```bash
|
||||
sudo systemctl stop veo3-report
|
||||
```
|
||||
|
||||
2. **Update files:**
|
||||
```bash
|
||||
sudo cp new_veo3_report.py /var/www/veo3-report/veo3_report.py
|
||||
```
|
||||
|
||||
3. **Update dependencies if needed:**
|
||||
```bash
|
||||
cd /var/www/veo3-report
|
||||
sudo ./venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **Restart service:**
|
||||
```bash
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
# Stop and disable service
|
||||
sudo systemctl stop veo3-report
|
||||
sudo systemctl disable veo3-report
|
||||
|
||||
# Remove service file
|
||||
sudo rm /etc/systemd/system/veo3-report.service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Remove application files
|
||||
sudo rm -rf /var/www/veo3-report
|
||||
sudo rm -rf /var/log/veo3-report
|
||||
```
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
**Why systemd service instead of cron?**
|
||||
|
||||
✅ **Better process management** - Service restarts automatically if it crashes
|
||||
✅ **Centralized logging** - All logs in one place via journalctl
|
||||
✅ **Dependency handling** - Waits for network before starting
|
||||
✅ **Status monitoring** - Easy to check if service is running
|
||||
✅ **Resource control** - Can set CPU/memory limits if needed
|
||||
✅ **Boot integration** - Starts automatically on server reboot
|
||||
✅ **Timezone handling** - Explicit timezone support in Python, not relying on server TZ
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `sudo journalctl -u veo3-report -n 100`
|
||||
2. Test manually: `cd /var/www/veo3-report && sudo -u www-data ./venv/bin/python veo3_report.py`
|
||||
3. Review `.env` configuration
|
||||
4. Verify SMTP credentials are correct
|
||||
201
JWTValidator.php
Normal file
201
JWTValidator.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
/**
|
||||
* JWT Token Validator for Azure AD / MSAL
|
||||
* Validates JWT tokens from Microsoft Azure AD using JWKS
|
||||
*/
|
||||
|
||||
// Load Composer autoload for Firebase JWT library
|
||||
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\JWK;
|
||||
|
||||
class JWTValidator {
|
||||
private $tenantId;
|
||||
private $clientId;
|
||||
private $jwksCache = null;
|
||||
private $jwksCacheTime = 0;
|
||||
private $jwksCacheDuration = 3600; // Cache for 1 hour
|
||||
|
||||
public function __construct($tenantId, $clientId) {
|
||||
$this->tenantId = $tenantId;
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a JWT token
|
||||
*
|
||||
* @param string $token The JWT token to validate
|
||||
* @return array ['valid' => bool, 'payload' => array|null, 'error' => string|null]
|
||||
*/
|
||||
public function validateToken($token) {
|
||||
if (empty($token)) {
|
||||
return ['valid' => false, 'error' => 'Token is empty'];
|
||||
}
|
||||
|
||||
try {
|
||||
// Get public keys from Azure AD
|
||||
$jwks = $this->getJWKS();
|
||||
if (!$jwks) {
|
||||
return ['valid' => false, 'error' => 'Could not retrieve public keys from Azure AD'];
|
||||
}
|
||||
|
||||
// Convert JWKS to Key objects
|
||||
$keys = JWK::parseKeySet($jwks);
|
||||
|
||||
// Decode and validate the JWT
|
||||
$decoded = JWT::decode($token, $keys);
|
||||
$payload = (array) $decoded;
|
||||
|
||||
// Validate claims
|
||||
$validation = $this->validateClaims($payload);
|
||||
if (!$validation['valid']) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
return ['valid' => true, 'payload' => $payload];
|
||||
|
||||
} catch (\Firebase\JWT\ExpiredException $e) {
|
||||
return ['valid' => false, 'error' => 'Token has expired'];
|
||||
} catch (\Firebase\JWT\SignatureInvalidException $e) {
|
||||
return ['valid' => false, 'error' => 'Token signature is invalid'];
|
||||
} catch (\Firebase\JWT\BeforeValidException $e) {
|
||||
return ['valid' => false, 'error' => 'Token is not yet valid'];
|
||||
} catch (Exception $e) {
|
||||
return ['valid' => false, 'error' => 'JWT validation failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate JWT claims
|
||||
*
|
||||
* @param array $payload The decoded JWT payload
|
||||
* @return array ['valid' => bool, 'error' => string|null]
|
||||
*/
|
||||
private function validateClaims($payload) {
|
||||
$now = time();
|
||||
|
||||
// Check expiration (exp claim)
|
||||
if (isset($payload['exp']) && $payload['exp'] < $now) {
|
||||
return ['valid' => false, 'error' => 'Token has expired'];
|
||||
}
|
||||
|
||||
// Check not-before (nbf claim)
|
||||
if (isset($payload['nbf']) && $payload['nbf'] > $now) {
|
||||
return ['valid' => false, 'error' => 'Token is not yet valid'];
|
||||
}
|
||||
|
||||
// Validate audience (aud claim) - must be our client ID or Microsoft Graph
|
||||
if (isset($payload['aud'])) {
|
||||
$validAudiences = [
|
||||
$this->clientId,
|
||||
'00000003-0000-0000-c000-000000000000', // Microsoft Graph
|
||||
'https://graph.microsoft.com'
|
||||
];
|
||||
|
||||
if (!in_array($payload['aud'], $validAudiences)) {
|
||||
return ['valid' => false, 'error' => 'Invalid audience: ' . $payload['aud']];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate issuer (iss claim) - must be from Azure AD tenant
|
||||
if (isset($payload['iss'])) {
|
||||
$validIssuers = [
|
||||
"https://login.microsoftonline.com/{$this->tenantId}/v2.0",
|
||||
"https://login.microsoftonline.com/{$this->tenantId}/",
|
||||
"https://sts.windows.net/{$this->tenantId}/",
|
||||
"https://login.microsoftonline.com/common/v2.0"
|
||||
];
|
||||
|
||||
if (!in_array($payload['iss'], $validIssuers)) {
|
||||
return ['valid' => false, 'error' => 'Invalid issuer: ' . $payload['iss']];
|
||||
}
|
||||
}
|
||||
|
||||
return ['valid' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWKS (JSON Web Key Set) from Azure AD
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function getJWKS() {
|
||||
// Return cached JWKS if still valid
|
||||
if ($this->jwksCache && (time() - $this->jwksCacheTime) < $this->jwksCacheDuration) {
|
||||
return $this->jwksCache;
|
||||
}
|
||||
|
||||
// Get OpenID configuration from Azure AD
|
||||
$configUrl = "https://login.microsoftonline.com/{$this->tenantId}/v2.0/.well-known/openid-configuration";
|
||||
|
||||
$config = $this->fetchJson($configUrl);
|
||||
if (!$config || !isset($config['jwks_uri'])) {
|
||||
error_log("Failed to get OpenID configuration from: $configUrl");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch JWKS from jwks_uri
|
||||
$jwks = $this->fetchJson($config['jwks_uri']);
|
||||
|
||||
if (!$jwks || !isset($jwks['keys'])) {
|
||||
error_log("Failed to get JWKS from: " . $config['jwks_uri']);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure all keys have the 'alg' parameter
|
||||
foreach ($jwks['keys'] as &$key) {
|
||||
if (!isset($key['alg'])) {
|
||||
// Default to RS256 for RSA keys
|
||||
if (isset($key['kty']) && $key['kty'] === 'RSA') {
|
||||
$key['alg'] = 'RS256';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the JWKS
|
||||
$this->jwksCache = $jwks;
|
||||
$this->jwksCacheTime = time();
|
||||
|
||||
return $jwks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch JSON from a URL
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|null
|
||||
*/
|
||||
private function fetchJson($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
error_log("CURL Error fetching $url: $curlError");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
error_log("HTTP Error $httpCode fetching $url");
|
||||
return null;
|
||||
}
|
||||
|
||||
$json = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
error_log("JSON decode error fetching $url: " . json_last_error_msg());
|
||||
return null;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
115
QUICKSTART.md
Normal file
115
QUICKSTART.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# VEO3 Report - Quick Start Guide
|
||||
|
||||
## Deploy to Server in 3 Steps
|
||||
|
||||
### Step 1: Prepare Files
|
||||
```bash
|
||||
# On your local machine, ensure .env is configured
|
||||
cp .env.example .env
|
||||
nano .env # Add your SMTP credentials
|
||||
```
|
||||
|
||||
### Step 2: Upload to Server
|
||||
```bash
|
||||
scp veo3_report.py veo3_scheduler.py requirements.txt .env \
|
||||
veo3-report.service deploy.sh \
|
||||
user@your-server.com:/tmp/veo3-deploy/
|
||||
```
|
||||
|
||||
### Step 3: Deploy
|
||||
```bash
|
||||
# SSH into server
|
||||
ssh user@your-server.com
|
||||
|
||||
# Run deployment
|
||||
cd /tmp/veo3-deploy
|
||||
chmod +x deploy.sh
|
||||
sudo ./deploy.sh
|
||||
```
|
||||
|
||||
**Done!** The service is now running and will send reports daily at 7:00 PM EST.
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Check if service is running
|
||||
sudo systemctl status veo3-report
|
||||
|
||||
# View live logs
|
||||
sudo journalctl -u veo3-report -f
|
||||
|
||||
# Restart service
|
||||
sudo systemctl restart veo3-report
|
||||
|
||||
# Test immediately (without waiting for 7pm)
|
||||
sudo systemctl stop veo3-report
|
||||
cd /var/www/veo3-report
|
||||
sudo -u www-data ./venv/bin/python veo3_report.py
|
||||
sudo systemctl start veo3-report
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Files
|
||||
|
||||
| File | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| `.env` | `/var/www/veo3-report/.env` | SMTP credentials & recipients |
|
||||
| `veo3_scheduler.py` | `/var/www/veo3-report/` | Schedule time & timezone |
|
||||
| `veo3_report.py` | `/var/www/veo3-report/` | Cost per video setting |
|
||||
|
||||
---
|
||||
|
||||
## Changing Settings
|
||||
|
||||
### Change Email Recipients
|
||||
```bash
|
||||
sudo nano /var/www/veo3-report/.env
|
||||
# Edit REPORT_RECIPIENTS line
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
### Change Schedule Time
|
||||
```bash
|
||||
sudo nano /var/www/veo3-report/veo3_scheduler.py
|
||||
# Find: CronTrigger(hour=19, minute=0, ...)
|
||||
# Change hour (24-hour format: 19 = 7pm)
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
### Change Cost Per Video
|
||||
```bash
|
||||
sudo nano /var/www/veo3-report/veo3_report.py
|
||||
# Find: COST_PER_VIDEO = 3.20
|
||||
# Change value
|
||||
sudo systemctl restart veo3-report
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Service won't start?**
|
||||
```bash
|
||||
sudo journalctl -u veo3-report -n 50
|
||||
```
|
||||
|
||||
**Emails not sending?**
|
||||
```bash
|
||||
# Test manually
|
||||
cd /var/www/veo3-report
|
||||
sudo -u www-data ./venv/bin/python veo3_report.py
|
||||
# Check output for SMTP errors
|
||||
```
|
||||
|
||||
**Wrong time?**
|
||||
```bash
|
||||
# Service uses EST timezone explicitly in code
|
||||
# Check: sudo journalctl -u veo3-report | grep "Next scheduled run"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For detailed information, see [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
168
README.md
Normal file
168
README.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# VEO3 Usage Report System
|
||||
|
||||
Automated reporting system for VEO3 video generation usage analytics with Microsoft Azure AD Single Sign-On.
|
||||
|
||||
## Features
|
||||
|
||||
- **PHP Web Interface**: Interactive dashboard for viewing usage reports
|
||||
- **SSO Authentication**: Microsoft Azure AD (MSAL) integration for secure access
|
||||
- **Python Automation**: Automated daily/weekly/monthly email reports via SMTP (Mailgun)
|
||||
- **Granular Analytics**:
|
||||
- Last 24 hours activity
|
||||
- Last 7 days trends
|
||||
- Last 30 days overview
|
||||
- Top 25 users per period
|
||||
- **Professional Styling**: Montserrat font, custom yellow (#FFC407) branding
|
||||
|
||||
## Setup
|
||||
|
||||
### PHP Web Reports with SSO
|
||||
|
||||
1. **Install PHP dependencies:**
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
2. **Configure SSO (see [SSO-SETUP.md](SSO-SETUP.md)):**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # Add Azure AD tenant and client IDs
|
||||
```
|
||||
|
||||
3. **For local development (disable SSO):**
|
||||
```bash
|
||||
# In .env
|
||||
SSO_ENABLED=false
|
||||
```
|
||||
|
||||
4. **Start PHP development server:**
|
||||
```bash
|
||||
php -S localhost:8000
|
||||
```
|
||||
|
||||
5. **Access the interfaces:**
|
||||
- `http://localhost:8000/webhook_caller.php` - Fetch data from webhook
|
||||
- `http://localhost:8000/report.php` - View full report
|
||||
|
||||
### Python Email Reports
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Configure environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your SMTP credentials
|
||||
```
|
||||
|
||||
3. Run the report generator:
|
||||
```bash
|
||||
python veo3_report.py
|
||||
```
|
||||
|
||||
## Configuration (.env)
|
||||
|
||||
```bash
|
||||
# Webhook URL
|
||||
WEBHOOK_URL=https://hook.us1.make.celonis.com/...
|
||||
|
||||
# SMTP Settings (Mailgun SMTP)
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-mailgun-smtp-user@your-domain.com
|
||||
SMTP_PASSWORD=your-smtp-password
|
||||
SENDER_EMAIL=reports@your-domain.com
|
||||
|
||||
# Recipients (comma-separated)
|
||||
REPORT_RECIPIENTS=user1@example.com,user2@example.com
|
||||
```
|
||||
|
||||
## Scheduling Automated Reports
|
||||
|
||||
### Production Server Deployment (Recommended)
|
||||
|
||||
For production servers, use the included systemd service instead of cron:
|
||||
|
||||
1. **Deploy to server:**
|
||||
```bash
|
||||
# Upload files to server
|
||||
scp veo3_report.py veo3_scheduler.py requirements.txt .env \
|
||||
veo3-report.service deploy.sh user@your-server:/tmp/veo3-deploy/
|
||||
|
||||
# Run deployment script
|
||||
ssh user@your-server
|
||||
cd /tmp/veo3-deploy
|
||||
chmod +x deploy.sh
|
||||
sudo ./deploy.sh
|
||||
```
|
||||
|
||||
2. **Service will run automatically at 7:00 PM EST daily**
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Auto-restart on failure
|
||||
- ✅ Starts on server boot
|
||||
- ✅ Better logging with journalctl
|
||||
- ✅ No cron dependency
|
||||
- ✅ Easy status monitoring
|
||||
|
||||
See **[DEPLOYMENT.md](DEPLOYMENT.md)** for full deployment guide.
|
||||
|
||||
### Alternative: Manual Scheduling
|
||||
|
||||
#### macOS/Linux (cron)
|
||||
|
||||
```bash
|
||||
# Daily report at 9 AM
|
||||
0 9 * * * cd /path/to/VEO3-REPORT && /usr/bin/python3 veo3_report.py
|
||||
```
|
||||
|
||||
#### Windows (Task Scheduler)
|
||||
|
||||
Create a scheduled task that runs:
|
||||
```
|
||||
python C:\path\to\VEO3-REPORT\veo3_report.py
|
||||
```
|
||||
|
||||
## Report Output
|
||||
|
||||
The Python script:
|
||||
- Fetches data from webhook
|
||||
- Analyzes three time periods (24h, 7d, 30d)
|
||||
- Generates HTML report saved as `email_report.html`
|
||||
- Sends email via SMTP to configured recipients
|
||||
|
||||
## Files
|
||||
|
||||
### Web Interface
|
||||
- `webhook_caller.php` - Web interface for fetching webhook data
|
||||
- `report.php` - Interactive web dashboard with collapsible sections
|
||||
|
||||
### Python Automation
|
||||
- `veo3_report.py` - Automated email report generator
|
||||
- `veo3_scheduler.py` - Scheduler daemon service (runs continuously)
|
||||
- `requirements.txt` - Python dependencies
|
||||
|
||||
### Deployment
|
||||
- `deploy.sh` - Automated server deployment script
|
||||
- `veo3-report.service` - systemd service configuration
|
||||
- `DEPLOYMENT.md` - Complete deployment guide
|
||||
|
||||
### Configuration
|
||||
- `.env.example` - Configuration template
|
||||
- `.env` - Your configuration (not in git)
|
||||
- `config.php` - SSO and session configuration
|
||||
- `env_loader.php` - Environment variable loader
|
||||
|
||||
### SSO Files
|
||||
- `AuthMiddleware.php` - Main authentication class
|
||||
- `JWTValidator.php` - JWT token validator
|
||||
- `auth.php` - Authentication API endpoint
|
||||
- `composer.json` - PHP dependencies
|
||||
- `SSO-SETUP.md` - Complete SSO setup guide
|
||||
|
||||
### Generated Files
|
||||
- `webhook_response.json` - Cached webhook data (auto-generated)
|
||||
- `email_report.html` - Latest email report (auto-generated)
|
||||
- `logs/` - Application logs (auto-generated)
|
||||
318
SSO-SETUP.md
Normal file
318
SSO-SETUP.md
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
# SSO Setup Guide - Microsoft Azure AD (MSAL)
|
||||
|
||||
The VEO3 Report System uses Microsoft Azure AD for Single Sign-On authentication. This protects both the web dashboard and webhook caller pages.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Microsoft Azure AD tenant
|
||||
- Permission to register applications in Azure AD
|
||||
- PHP 7.4 or higher
|
||||
- Composer (PHP package manager)
|
||||
|
||||
## Setup Steps
|
||||
|
||||
### 1. Install PHP Dependencies
|
||||
|
||||
```bash
|
||||
cd /path/to/veo3-report
|
||||
composer install
|
||||
```
|
||||
|
||||
This installs the `firebase/php-jwt` library required for token validation.
|
||||
|
||||
### 2. Register Application in Azure AD
|
||||
|
||||
1. **Login to Azure Portal**: https://portal.azure.com
|
||||
2. **Navigate to**: Azure Active Directory → App registrations → New registration
|
||||
|
||||
3. **Register the application:**
|
||||
- **Name**: VEO3 Usage Report
|
||||
- **Supported account types**: Accounts in this organizational directory only
|
||||
- **Redirect URI**: Web → `https://your-domain.com/report.php` (and add `/webhook_caller.php`)
|
||||
- Click **Register**
|
||||
|
||||
4. **Note these values:**
|
||||
- **Application (client) ID**: Copy this
|
||||
- **Directory (tenant) ID**: Copy this
|
||||
|
||||
5. **Configure Authentication:**
|
||||
- Go to **Authentication** in left menu
|
||||
- Under **Implicit grant and hybrid flows**:
|
||||
- ✅ Check **ID tokens**
|
||||
- ✅ Check **Access tokens**
|
||||
- Click **Save**
|
||||
|
||||
6. **Configure API Permissions:**
|
||||
- Go to **API permissions** in left menu
|
||||
- Should already have **Microsoft Graph** → **User.Read** (delegated)
|
||||
- This is sufficient for SSO
|
||||
|
||||
### 3. Configure Environment Variables
|
||||
|
||||
Edit your `.env` file:
|
||||
|
||||
```bash
|
||||
# SSO Configuration
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-tenant-id-here
|
||||
SSO_CLIENT_ID=your-client-id-here
|
||||
```
|
||||
|
||||
**For Local Development (disable SSO):**
|
||||
```bash
|
||||
SSO_ENABLED=false
|
||||
```
|
||||
|
||||
When SSO is disabled, you'll be logged in as "Local Developer" automatically.
|
||||
|
||||
### 4. Deploy to Server
|
||||
|
||||
If deploying with the automated script:
|
||||
|
||||
```bash
|
||||
# Ensure .env is configured
|
||||
nano .env
|
||||
|
||||
# Run deployment
|
||||
sudo ./deploy.sh
|
||||
```
|
||||
|
||||
The deployment script will:
|
||||
- Copy SSO files
|
||||
- Install Composer dependencies
|
||||
- Set up the systemd service
|
||||
|
||||
### 5. Configure Web Server
|
||||
|
||||
#### Apache (.htaccess)
|
||||
|
||||
```apache
|
||||
# Enable PHP
|
||||
AddHandler application/x-httpd-php .php
|
||||
|
||||
# Security headers
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
|
||||
# Deny access to sensitive files
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<Files "composer.json">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
<Files "composer.lock">
|
||||
Require all denied
|
||||
</Files>
|
||||
```
|
||||
|
||||
#### Nginx
|
||||
|
||||
```nginx
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Deny access to sensitive files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~ composer\.(json|lock)$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
```
|
||||
|
||||
## Testing SSO
|
||||
|
||||
### 1. Test Locally (SSO Disabled)
|
||||
|
||||
```bash
|
||||
# In .env
|
||||
SSO_ENABLED=false
|
||||
|
||||
# Start PHP server
|
||||
php -S localhost:8000
|
||||
|
||||
# Visit http://localhost:8000/report.php
|
||||
# Should show reports without login
|
||||
```
|
||||
|
||||
### 2. Test with SSO Enabled
|
||||
|
||||
```bash
|
||||
# In .env
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-tenant-id
|
||||
SSO_CLIENT_ID=your-client-id
|
||||
|
||||
# Visit https://your-domain.com/report.php
|
||||
# Should redirect to Microsoft login
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **User visits protected page** (report.php or webhook_caller.php)
|
||||
2. **AuthMiddleware checks** for valid authentication cookie
|
||||
3. **If not authenticated:**
|
||||
- Shows Microsoft login page
|
||||
- User authenticates with Azure AD
|
||||
- MSAL returns JWT token
|
||||
- Token sent to `auth.php` for validation
|
||||
- Valid token stored in secure cookie
|
||||
- Page reloads with authenticated session
|
||||
4. **If authenticated:**
|
||||
- JWT token validated on each request
|
||||
- User information available in `$user` variable
|
||||
|
||||
### Security Features
|
||||
|
||||
- ✅ **JWT validation** with Azure AD public keys
|
||||
- ✅ **HttpOnly cookies** (not accessible to JavaScript)
|
||||
- ✅ **Token expiration** checked (24-hour lifetime)
|
||||
- ✅ **Audience validation** (ensures token is for this app)
|
||||
- ✅ **Issuer validation** (ensures token from correct Azure tenant)
|
||||
- ✅ **SameSite cookie** attribute (CSRF protection)
|
||||
- ✅ **Secure cookies** over HTTPS in production
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
veo3-report/
|
||||
├── AuthMiddleware.php # Main authentication class
|
||||
├── JWTValidator.php # JWT token validator
|
||||
├── auth.php # Authentication API endpoint
|
||||
├── config.php # SSO configuration
|
||||
├── env_loader.php # .env file loader
|
||||
├── report.php # Protected: requires auth
|
||||
├── webhook_caller.php # Protected: requires auth
|
||||
└── composer.json # PHP dependencies
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Could not retrieve public keys from Azure AD"
|
||||
|
||||
**Cause**: Network issue or invalid tenant ID
|
||||
**Fix**:
|
||||
- Verify `SSO_TENANT_ID` in `.env`
|
||||
- Check server can reach `login.microsoftonline.com`
|
||||
- Check PHP error logs: `tail -f /var/log/apache2/error.log`
|
||||
|
||||
### "Token signature is invalid"
|
||||
|
||||
**Cause**: Token from wrong tenant or client
|
||||
**Fix**:
|
||||
- Verify `SSO_CLIENT_ID` matches Azure app registration
|
||||
- Clear browser cookies and try again
|
||||
- Ensure redirect URI matches exactly in Azure
|
||||
|
||||
### "Invalid audience"
|
||||
|
||||
**Cause**: Token not intended for this application
|
||||
**Fix**:
|
||||
- Verify `SSO_CLIENT_ID` in `.env`
|
||||
- Check Azure app registration client ID
|
||||
|
||||
### Login popup blocked
|
||||
|
||||
**Cause**: Browser blocking popups
|
||||
**Fix**:
|
||||
- Allow popups for your domain
|
||||
- Try different browser
|
||||
|
||||
### Token expired
|
||||
|
||||
**Cause**: Token older than 24 hours
|
||||
**Fix**:
|
||||
- Clear cookies and login again
|
||||
- Automatic redirect to login should occur
|
||||
|
||||
## Disabling SSO
|
||||
|
||||
To temporarily disable SSO (for maintenance or testing):
|
||||
|
||||
```bash
|
||||
# In .env
|
||||
SSO_ENABLED=false
|
||||
|
||||
# Restart web server/PHP-FPM
|
||||
sudo systemctl restart apache2
|
||||
# or
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
When disabled:
|
||||
- No login required
|
||||
- Mock user "Local Developer" used
|
||||
- Full access to all features
|
||||
|
||||
## Adding More Protected Pages
|
||||
|
||||
To protect additional PHP pages:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// At the top of your PHP file
|
||||
require_once __DIR__ . '/AuthMiddleware.php';
|
||||
$auth = new AuthMiddleware();
|
||||
$user = $auth->requireAuth();
|
||||
|
||||
// User is authenticated, $user contains:
|
||||
// - name: Display name
|
||||
// - preferred_username: Email address
|
||||
?>
|
||||
```
|
||||
|
||||
## User Logout
|
||||
|
||||
Add a logout link:
|
||||
|
||||
```html
|
||||
<a href="javascript:void(0)" onclick="logout()">Logout</a>
|
||||
|
||||
<script>
|
||||
function logout() {
|
||||
fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({action: 'logout'})
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Azure app registered with correct redirect URIs
|
||||
- [ ] `.env` configured with correct tenant/client IDs
|
||||
- [ ] `SSO_ENABLED=true` in `.env`
|
||||
- [ ] Composer dependencies installed
|
||||
- [ ] HTTPS enabled (required for production)
|
||||
- [ ] Security headers configured in web server
|
||||
- [ ] Error logs monitored for auth issues
|
||||
- [ ] Test login/logout flow
|
||||
|
||||
## Support
|
||||
|
||||
For SSO issues:
|
||||
1. Check PHP error logs
|
||||
2. Check browser console for JavaScript errors
|
||||
3. Verify Azure app configuration
|
||||
4. Test with `SSO_ENABLED=false` to isolate issue
|
||||
118
auth.php
Normal file
118
auth.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
/**
|
||||
* Authentication API Endpoint
|
||||
* Handles login, logout, and status requests
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/AuthMiddleware.php';
|
||||
|
||||
$auth = new AuthMiddleware();
|
||||
|
||||
// Get POST data
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['action'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid request - action required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $input['action'];
|
||||
|
||||
// Handle different actions
|
||||
switch ($action) {
|
||||
case 'login':
|
||||
handleLogin($auth, $input);
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
handleLogout($auth);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
handleStatus($auth);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Unknown action: ' . $action]);
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login action
|
||||
*/
|
||||
function handleLogin($auth, $input) {
|
||||
if (!$auth->isSSOEnabled()) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'SSO is disabled']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer ID token for validation, fallback to access token
|
||||
$token = $input['idToken'] ?? $input['accessToken'] ?? null;
|
||||
|
||||
if (!$token) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Authentication token is required']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and set token
|
||||
$result = $auth->setAuthToken($token);
|
||||
|
||||
if ($result['success']) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Authentication successful',
|
||||
'user' => [
|
||||
'name' => $result['user']['name'] ?? 'Unknown',
|
||||
'email' => $result['user']['preferred_username'] ?? $result['user']['upn'] ?? 'Unknown'
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $result['error']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout action
|
||||
*/
|
||||
function handleLogout($auth) {
|
||||
$auth->clearAuthToken();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Logged out successfully'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status check action
|
||||
*/
|
||||
function handleStatus($auth) {
|
||||
$authStatus = $auth->isAuthenticated();
|
||||
|
||||
if ($authStatus['authenticated']) {
|
||||
echo json_encode([
|
||||
'authenticated' => true,
|
||||
'sso_enabled' => $auth->isSSOEnabled(),
|
||||
'user' => [
|
||||
'name' => $authStatus['user']['name'] ?? 'Unknown',
|
||||
'email' => $authStatus['user']['preferred_username'] ?? $authStatus['user']['upn'] ?? 'Unknown'
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'authenticated' => false,
|
||||
'sso_enabled' => $auth->isSSOEnabled(),
|
||||
'error' => $authStatus['error'] ?? 'Not authenticated'
|
||||
]);
|
||||
}
|
||||
}
|
||||
20
composer.json
Normal file
20
composer.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "veo3-report/usage-analytics",
|
||||
"description": "VEO3 Usage Report System with MSAL Authentication",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"firebase/php-jwt": "^6.0"
|
||||
},
|
||||
"require-dev": {},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"JWTValidator.php",
|
||||
"AuthMiddleware.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
30
config.php
Normal file
30
config.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Configuration file for VEO3 Usage Report System
|
||||
*/
|
||||
|
||||
// Load environment variables from .env file
|
||||
if (file_exists(__DIR__ . '/env_loader.php')) {
|
||||
require_once __DIR__ . '/env_loader.php';
|
||||
}
|
||||
|
||||
// MSAL / Azure AD SSO Configuration
|
||||
// Set SSO_ENABLED to false for local development, true for production with SSO
|
||||
if (!defined('SSO_ENABLED')) {
|
||||
define('SSO_ENABLED', getenv('SSO_ENABLED') === 'true');
|
||||
}
|
||||
if (!defined('SSO_TENANT_ID')) {
|
||||
define('SSO_TENANT_ID', getenv('SSO_TENANT_ID') ?: '');
|
||||
}
|
||||
if (!defined('SSO_CLIENT_ID')) {
|
||||
define('SSO_CLIENT_ID', getenv('SSO_CLIENT_ID') ?: '');
|
||||
}
|
||||
|
||||
// Session configuration
|
||||
ini_set('session.gc_maxlifetime', 3600); // 1 hour
|
||||
ini_set('session.cookie_lifetime', 3600);
|
||||
|
||||
// Error reporting (set to 0 in production)
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0); // Disabled for production
|
||||
ini_set('log_errors', 1);
|
||||
114
deploy.sh
Executable file
114
deploy.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
#!/bin/bash
|
||||
# VEO3 Report Deployment Script
|
||||
# This script deploys the VEO3 report system to a Linux server
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Configuration
|
||||
DEPLOY_DIR="/var/www/veo3-report"
|
||||
SERVICE_USER="www-data"
|
||||
SERVICE_NAME="veo3-report"
|
||||
LOG_DIR="/var/log/veo3-report"
|
||||
|
||||
echo "========================================"
|
||||
echo "VEO3 Report System Deployment"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "ERROR: This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create deployment directory
|
||||
echo "Creating deployment directory: $DEPLOY_DIR"
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
# Create log directory
|
||||
echo "Creating log directory: $LOG_DIR"
|
||||
mkdir -p "$LOG_DIR"
|
||||
mkdir -p "$DEPLOY_DIR/logs"
|
||||
|
||||
# Copy files
|
||||
echo "Copying application files..."
|
||||
cp veo3_report.py "$DEPLOY_DIR/"
|
||||
cp veo3_scheduler.py "$DEPLOY_DIR/"
|
||||
cp requirements.txt "$DEPLOY_DIR/"
|
||||
cp .env "$DEPLOY_DIR/"
|
||||
|
||||
# Make scripts executable
|
||||
chmod +x "$DEPLOY_DIR/veo3_scheduler.py"
|
||||
chmod +x "$DEPLOY_DIR/veo3_report.py"
|
||||
|
||||
# Set up Python virtual environment
|
||||
echo "Setting up Python virtual environment..."
|
||||
cd "$DEPLOY_DIR"
|
||||
|
||||
if [ ! -d "venv" ]; then
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing Python dependencies..."
|
||||
"$DEPLOY_DIR/venv/bin/pip" install --upgrade pip
|
||||
"$DEPLOY_DIR/venv/bin/pip" install -r "$DEPLOY_DIR/requirements.txt"
|
||||
|
||||
# Set ownership
|
||||
echo "Setting file ownership..."
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$DEPLOY_DIR"
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$LOG_DIR"
|
||||
|
||||
# Set permissions
|
||||
chmod 600 "$DEPLOY_DIR/.env" # Protect sensitive credentials
|
||||
|
||||
# Install systemd service
|
||||
echo "Installing systemd service..."
|
||||
|
||||
# Update service file with correct paths
|
||||
sed -e "s|/var/www/veo3-report|$DEPLOY_DIR|g" \
|
||||
-e "s|User=www-data|User=$SERVICE_USER|g" \
|
||||
-e "s|Group=www-data|Group=$SERVICE_USER|g" \
|
||||
veo3-report.service > /etc/systemd/system/${SERVICE_NAME}.service
|
||||
|
||||
# Reload systemd
|
||||
echo "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable service (start on boot)
|
||||
echo "Enabling service to start on boot..."
|
||||
systemctl enable ${SERVICE_NAME}.service
|
||||
|
||||
# Start service
|
||||
echo "Starting service..."
|
||||
systemctl start ${SERVICE_NAME}.service
|
||||
|
||||
# Wait a moment for service to start
|
||||
sleep 2
|
||||
|
||||
# Check service status
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Service Status:"
|
||||
echo "========================================"
|
||||
systemctl status ${SERVICE_NAME}.service --no-pager
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Deployment Complete!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Service Commands:"
|
||||
echo " Start: sudo systemctl start ${SERVICE_NAME}"
|
||||
echo " Stop: sudo systemctl stop ${SERVICE_NAME}"
|
||||
echo " Restart: sudo systemctl restart ${SERVICE_NAME}"
|
||||
echo " Status: sudo systemctl status ${SERVICE_NAME}"
|
||||
echo " Logs: sudo journalctl -u ${SERVICE_NAME} -f"
|
||||
echo ""
|
||||
echo "Log Files:"
|
||||
echo " Service: $LOG_DIR/service.log"
|
||||
echo " Errors: $LOG_DIR/service-error.log"
|
||||
echo " Scheduler: $DEPLOY_DIR/logs/veo3_scheduler.log"
|
||||
echo ""
|
||||
echo "The report will run automatically at 7:00 PM EST every day."
|
||||
echo ""
|
||||
35
env_loader.php
Normal file
35
env_loader.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Simple .env file loader
|
||||
* Loads environment variables from .env file into $_ENV and getenv()
|
||||
*/
|
||||
|
||||
$envFile = __DIR__ . '/.env';
|
||||
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Skip comments
|
||||
if (strpos(trim($line), '#') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse KEY=VALUE
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
|
||||
// Remove quotes if present
|
||||
if (preg_match('/^(["\'])(.*)\\1$/', $value, $matches)) {
|
||||
$value = $matches[2];
|
||||
}
|
||||
|
||||
// Set environment variable
|
||||
putenv("$key=$value");
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
946
report.php
Normal file
946
report.php
Normal file
|
|
@ -0,0 +1,946 @@
|
|||
<?php
|
||||
// Configuration
|
||||
$responseFile = 'webhook_response.json';
|
||||
|
||||
// Cost configuration per tool type (adjust as needed)
|
||||
$toolCosts = [
|
||||
'VEO3' => 3.20, // $3.20 per 8-second video
|
||||
'TEXT2IMAGE' => 0.04, // Average cost per image generation
|
||||
'TEXT2VOICE' => 0.30, // Cost per TTS conversion
|
||||
'SPEECH2SPEECH' => 0.50, // Cost per speech-to-speech conversion
|
||||
'DOCUMENT_TRANSLATION' => 0.10, // Cost per document translation
|
||||
'VIDEOQUERY' => 0.25, // Cost per video analysis query
|
||||
];
|
||||
|
||||
// Tool display names
|
||||
$toolNames = [
|
||||
'VEO3' => 'VEO3 Video Generation',
|
||||
'TEXT2IMAGE' => 'Text to Image',
|
||||
'TEXT2VOICE' => 'Text to Voice',
|
||||
'SPEECH2SPEECH' => 'Speech to Speech',
|
||||
'DOCUMENT_TRANSLATION' => 'Document Translation',
|
||||
'VIDEOQUERY' => 'Video Query Analysis',
|
||||
];
|
||||
|
||||
// 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 = [];
|
||||
$toolCounts = [];
|
||||
$toolUserCounts = [];
|
||||
$totalRequests = count($data);
|
||||
$totalCost = 0;
|
||||
|
||||
// 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'];
|
||||
$tool = $item['TOOL'] ?? 'UNKNOWN';
|
||||
|
||||
if (!isset($userCounts[$user])) {
|
||||
$userCounts[$user] = 0;
|
||||
}
|
||||
$userCounts[$user]++;
|
||||
|
||||
// Tool counts
|
||||
if (!isset($toolCounts[$tool])) {
|
||||
$toolCounts[$tool] = 0;
|
||||
}
|
||||
$toolCounts[$tool]++;
|
||||
|
||||
// Tool user counts
|
||||
if (!isset($toolUserCounts[$tool])) {
|
||||
$toolUserCounts[$tool] = [];
|
||||
}
|
||||
if (!isset($toolUserCounts[$tool][$user])) {
|
||||
$toolUserCounts[$tool][$user] = 0;
|
||||
}
|
||||
$toolUserCounts[$tool][$user]++;
|
||||
|
||||
// Calculate cost - use default of 0 for unknown tools
|
||||
$costPerRequest = $toolCosts[$tool] ?? 0;
|
||||
$totalCost += $costPerRequest;
|
||||
|
||||
// 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 (if prompt exists)
|
||||
if (isset($item['PROMPT']) && $item['PROMPT']) {
|
||||
$promptLength = strlen($item['PROMPT']);
|
||||
$promptLengths[] = $promptLength;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort tool counts by usage
|
||||
arsort($toolCounts);
|
||||
|
||||
// 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);
|
||||
|
||||
// Note: totalCost is already calculated in the loop above
|
||||
// Calculate period-specific costs (we'll need to recalculate by tool for accuracy)
|
||||
$cost24h = 0;
|
||||
$cost7d = 0;
|
||||
$cost30d = 0;
|
||||
|
||||
// Recalculate period costs
|
||||
foreach ($data as $record) {
|
||||
$item = $record['data'];
|
||||
$tool = $item['TOOL'] ?? 'UNKNOWN';
|
||||
$costPerRequest = $toolCosts[$tool] ?? 0;
|
||||
$dateObj = new DateTime($item['Date']);
|
||||
|
||||
if ($dateObj >= $last24Hours) $cost24h += $costPerRequest;
|
||||
if ($dateObj >= $last7Days) $cost7d += $costPerRequest;
|
||||
if ($dateObj >= $last30Days) $cost30d += $costPerRequest;
|
||||
}
|
||||
|
||||
// Sort data
|
||||
arsort($userCounts);
|
||||
ksort($dailyCounts);
|
||||
ksort($monthlyCounts);
|
||||
|
||||
// Calculate statistics
|
||||
$uniqueUsers = count($userCounts);
|
||||
$avgRequestsPerUser = round($totalRequests / $uniqueUsers, 2);
|
||||
$avgPromptLength = count($promptLengths) > 0 ? round(array_sum($promptLengths) / count($promptLengths), 0) : 0;
|
||||
$maxPromptLength = count($promptLengths) > 0 ? max($promptLengths) : 0;
|
||||
$minPromptLength = count($promptLengths) > 0 ? min($promptLengths) : 0;
|
||||
|
||||
// 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>AI Tools 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;">AI Tools 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 Requests</h3>
|
||||
<div class="value"><?php echo number_format($totalRequests); ?></div>
|
||||
<div class="subvalue">All AI tool requests</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Total Cost</h3>
|
||||
<div class="value">$<?php echo number_format($totalCost, 2); ?></div>
|
||||
<div class="subvalue">All tools combined</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 Requests/User</h3>
|
||||
<div class="value"><?php echo $avgRequestsPerUser; ?></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); ?> requests</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); ?> requests</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); ?> requests</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>Tool Usage Breakdown</h2>
|
||||
<p style="color: #7f8c8d; margin-bottom: 15px; font-size: 14px;">
|
||||
Usage distribution across all AI tools
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tool</th>
|
||||
<th>Requests</th>
|
||||
<th>Percentage</th>
|
||||
<th>Top User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($toolCounts as $tool => $count):
|
||||
$toolName = $toolNames[$tool] ?? $tool;
|
||||
$percentage = ($count / $totalRequests) * 100;
|
||||
|
||||
// Get top user for this tool
|
||||
$topUser = '';
|
||||
$topUserCount = 0;
|
||||
if (isset($toolUserCounts[$tool])) {
|
||||
arsort($toolUserCounts[$tool]);
|
||||
$topUserData = array_slice($toolUserCounts[$tool], 0, 1, true);
|
||||
if (!empty($topUserData)) {
|
||||
$topUser = array_key_first($topUserData);
|
||||
$topUserCount = reset($topUserData);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?php echo htmlspecialchars($toolName); ?></strong></td>
|
||||
<td><?php echo number_format($count); ?></td>
|
||||
<td><?php echo number_format($percentage, 1); ?>%</td>
|
||||
<td>
|
||||
<?php if ($topUser): ?>
|
||||
<?php echo htmlspecialchars($topUser); ?> (<?php echo $topUserCount; ?>)
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</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; ?> requests
|
||||
</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>Requests</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>Requests</th>
|
||||
<th>Distribution</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$maxMonthly = max($monthlyCounts);
|
||||
foreach ($monthlyCounts as $month => $count):
|
||||
$percentage = ($count / $totalRequests) * 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); ?> requests 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>Requests</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); ?> requests 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>Requests</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); ?> requests 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>Requests</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 / $totalRequests) * 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 Requests</th>
|
||||
<th>Percentage of Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($userCounts as $user => $count):
|
||||
$percentage = ($count / $totalRequests) * 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 12 weeks = 84 days)
|
||||
const endDate = new Date().toISOString().split('T')[0];
|
||||
const startDate = new Date(Date.now() - 84*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>
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
requests>=2.31.0
|
||||
python-dotenv>=1.0.0
|
||||
apscheduler>=3.10.0
|
||||
pytz>=2023.3
|
||||
30
veo3-report.service
Normal file
30
veo3-report.service
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[Unit]
|
||||
Description=VEO3 Report Scheduler Service
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/veo3-report
|
||||
Environment="PATH=/var/www/veo3-report/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/var/www/veo3-report/venv/bin/python3 /var/www/veo3-report/veo3_scheduler.py
|
||||
|
||||
# Restart policy
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Logging
|
||||
StandardOutput=append:/var/log/veo3-report/service.log
|
||||
StandardError=append:/var/log/veo3-report/service-error.log
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/www/veo3-report /var/log/veo3-report
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
418
veo3_report.py
Normal file
418
veo3_report.py
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Tools Usage Report Generator
|
||||
Fetches usage data from webhook and sends email reports via SMTP
|
||||
Supports: VEO3, TEXT2IMAGE, TEXT2VOICE, SPEECH2SPEECH, DOCUMENT_TRANSLATION, VIDEOQUERY
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from collections import defaultdict
|
||||
from dotenv import load_dotenv
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configuration
|
||||
WEBHOOK_URL = os.getenv('WEBHOOK_URL', 'https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj')
|
||||
SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.mailgun.org')
|
||||
SMTP_PORT = int(os.getenv('SMTP_PORT', '587'))
|
||||
SMTP_USER = os.getenv('SMTP_USER')
|
||||
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD')
|
||||
SENDER_EMAIL = os.getenv('SENDER_EMAIL')
|
||||
REPORT_RECIPIENTS = os.getenv('REPORT_RECIPIENTS', '').split(',')
|
||||
|
||||
# Cost configuration per tool type (adjust these as needed)
|
||||
TOOL_COSTS = {
|
||||
'VEO3': 3.20, # $3.20 per 8-second video
|
||||
'TEXT2IMAGE': 0.04, # Average cost per image generation
|
||||
'TEXT2VOICE': 0.30, # Cost per TTS conversion
|
||||
'SPEECH2SPEECH': 0.50, # Cost per speech-to-speech conversion
|
||||
'DOCUMENT_TRANSLATION': 0.10, # Cost per document translation
|
||||
'VIDEOQUERY': 0.25, # Cost per video analysis query
|
||||
}
|
||||
|
||||
# Tool display names
|
||||
TOOL_NAMES = {
|
||||
'VEO3': 'VEO3 Video Generation',
|
||||
'TEXT2IMAGE': 'Text to Image',
|
||||
'TEXT2VOICE': 'Text to Voice',
|
||||
'SPEECH2SPEECH': 'Speech to Speech',
|
||||
'DOCUMENT_TRANSLATION': 'Document Translation',
|
||||
'VIDEOQUERY': 'Video Query Analysis',
|
||||
}
|
||||
|
||||
|
||||
def fetch_webhook_data(start_date: str, end_date: str) -> List[Dict]:
|
||||
"""Fetch data from the webhook"""
|
||||
print(f"Fetching data from {start_date} to {end_date}...")
|
||||
|
||||
payload = {
|
||||
'start_date': start_date,
|
||||
'end_date': end_date
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(WEBHOOK_URL, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
print(f"✓ Fetched {len(data)} records")
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"✗ Error fetching data: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def analyze_period_data(data: List[Dict], period_start: datetime, period_end: datetime, period_name: str) -> Dict:
|
||||
"""Analyze data for a specific time period"""
|
||||
user_counts = defaultdict(int)
|
||||
daily_counts = defaultdict(int)
|
||||
tool_counts = defaultdict(int)
|
||||
tool_user_counts = defaultdict(lambda: defaultdict(int))
|
||||
total_requests = 0
|
||||
prompt_lengths = []
|
||||
total_cost = 0
|
||||
|
||||
for record in data:
|
||||
item = record['data']
|
||||
record_date = datetime.fromisoformat(item['Date'].replace('Z', '+00:00'))
|
||||
|
||||
# Filter to period
|
||||
if not (period_start <= record_date <= period_end):
|
||||
continue
|
||||
|
||||
total_requests += 1
|
||||
user = item['USER']
|
||||
tool = item['TOOL']
|
||||
|
||||
user_counts[user] += 1
|
||||
tool_counts[tool] += 1
|
||||
tool_user_counts[tool][user] += 1
|
||||
|
||||
date_str = record_date.strftime('%Y-%m-%d')
|
||||
daily_counts[date_str] += 1
|
||||
|
||||
if 'PROMPT' in item and item['PROMPT']:
|
||||
prompt_lengths.append(len(item['PROMPT']))
|
||||
|
||||
# Calculate cost based on tool type
|
||||
cost_per_request = TOOL_COSTS.get(tool, 0)
|
||||
total_cost += cost_per_request
|
||||
|
||||
# Sort users by count
|
||||
top_users = sorted(user_counts.items(), key=lambda x: x[1], reverse=True)[:25]
|
||||
|
||||
# Sort tools by count
|
||||
tool_breakdown = sorted(tool_counts.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
# Get top users per tool
|
||||
top_users_by_tool = {}
|
||||
for tool, users in tool_user_counts.items():
|
||||
top_users_by_tool[tool] = sorted(users.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
|
||||
# Calculate statistics
|
||||
unique_users = len(user_counts)
|
||||
avg_requests_per_user = total_requests / unique_users if unique_users > 0 else 0
|
||||
avg_prompt_length = sum(prompt_lengths) / len(prompt_lengths) if prompt_lengths else 0
|
||||
|
||||
return {
|
||||
'period_name': period_name,
|
||||
'total_requests': total_requests,
|
||||
'unique_users': unique_users,
|
||||
'avg_requests_per_user': avg_requests_per_user,
|
||||
'avg_prompt_length': avg_prompt_length,
|
||||
'total_cost': total_cost,
|
||||
'top_users': top_users,
|
||||
'tool_breakdown': tool_breakdown,
|
||||
'top_users_by_tool': top_users_by_tool,
|
||||
'daily_counts': dict(sorted(daily_counts.items())),
|
||||
'start_date': period_start.strftime('%Y-%m-%d'),
|
||||
'end_date': period_end.strftime('%Y-%m-%d')
|
||||
}
|
||||
|
||||
|
||||
def generate_html_report(daily_data: Dict, weekly_data: Dict, monthly_data: Dict) -> str:
|
||||
"""Generate HTML email report"""
|
||||
|
||||
def generate_user_table(users: List[Tuple[str, int]], total: int, period: str) -> str:
|
||||
"""Generate HTML table for top users"""
|
||||
if not users:
|
||||
return f"<p>No activity in {period}</p>"
|
||||
|
||||
html = '<table style="width: 100%; border-collapse: collapse; font-size: 13px;">'
|
||||
html += '''
|
||||
<thead>
|
||||
<tr style="background-color: #f8f9fa;">
|
||||
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Rank</th>
|
||||
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">User</th>
|
||||
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Prompts</th>
|
||||
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Percentage</th>
|
||||
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
'''
|
||||
|
||||
max_count = users[0][1] if users else 1
|
||||
|
||||
for rank, (user, count) in enumerate(users, 1):
|
||||
percentage = (count / total * 100) if total > 0 else 0
|
||||
bar_width = (count / max_count * 100) if max_count > 0 else 0
|
||||
|
||||
badge = ''
|
||||
rank_color = '#FFC407'
|
||||
if rank == 1:
|
||||
badge = '<span style="background-color: #f39c12; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-left: 5px;">🥇 #1</span>'
|
||||
elif rank == 2:
|
||||
badge = '<span style="background-color: #95a5a6; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-left: 5px;">🥈 #2</span>'
|
||||
elif rank == 3:
|
||||
badge = '<span style="background-color: #cd7f32; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-left: 5px;">🥉 #3</span>'
|
||||
|
||||
html += f'''
|
||||
<tr style="border-bottom: 1px solid #ecf0f1;">
|
||||
<td style="padding: 12px;"><span style="font-weight: 700; color: {rank_color};">#{rank}</span></td>
|
||||
<td style="padding: 12px;">{user}{badge}</td>
|
||||
<td style="padding: 12px;"><strong>{count:,}</strong></td>
|
||||
<td style="padding: 12px;">{percentage:.2f}%</td>
|
||||
<td style="padding: 12px;">
|
||||
<div style="background: linear-gradient(90deg, #FFC407, #f5b800); height: 24px; width: {bar_width}%; border-radius: 4px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
|
||||
html += '</tbody></table>'
|
||||
return html
|
||||
|
||||
def generate_period_section(data: Dict, title: str, emoji: str) -> str:
|
||||
"""Generate HTML section for a time period"""
|
||||
if data['total_requests'] == 0:
|
||||
return f'''
|
||||
<div style="background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 30px;">
|
||||
<h2 style="color: #2c3e50; margin-bottom: 20px; font-size: 22px; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px;">
|
||||
{emoji} {title}
|
||||
</h2>
|
||||
<p>No activity recorded for this period.</p>
|
||||
</div>
|
||||
'''
|
||||
|
||||
# Generate tool breakdown table
|
||||
tool_breakdown_html = '<div style="margin: 25px 0;"><h3 style="color: #2c3e50; margin-bottom: 15px; font-size: 16px;">Tool Usage Breakdown</h3>'
|
||||
tool_breakdown_html += '<table style="width: 100%; border-collapse: collapse; font-size: 13px;">'
|
||||
tool_breakdown_html += '<thead><tr style="background-color: #f8f9fa;"><th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Tool</th><th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Requests</th><th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Cost</th><th style="padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1;">Percentage</th></tr></thead><tbody>'
|
||||
|
||||
for tool, count in data['tool_breakdown']:
|
||||
tool_name = TOOL_NAMES.get(tool, tool)
|
||||
tool_cost = count * TOOL_COSTS.get(tool, 0)
|
||||
percentage = (count / data['total_requests'] * 100) if data['total_requests'] > 0 else 0
|
||||
tool_breakdown_html += f'<tr style="border-bottom: 1px solid #ecf0f1;"><td style="padding: 12px;"><strong>{tool_name}</strong></td><td style="padding: 12px;">{count:,}</td><td style="padding: 12px;">${tool_cost:,.2f}</td><td style="padding: 12px;">{percentage:.1f}%</td></tr>'
|
||||
|
||||
tool_breakdown_html += '</tbody></table></div>'
|
||||
|
||||
return f'''
|
||||
<div style="background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 30px;">
|
||||
<h2 style="color: #2c3e50; margin-bottom: 20px; font-size: 22px; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px;">
|
||||
{emoji} {title}
|
||||
</h2>
|
||||
<p style="color: #7f8c8d; margin-bottom: 20px; font-size: 14px;">
|
||||
{data['start_date']} to {data['end_date']}
|
||||
</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px;">
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #FFC407; box-shadow: 0 1px 4px rgba(0,0,0,0.08);">
|
||||
<h3 style="color: #7f8c8d; font-size: 12px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px;">Total Requests</h3>
|
||||
<div style="font-size: 28px; font-weight: 700; color: #2c3e50;">{data['total_requests']:,}</div>
|
||||
</div>
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #FFC407; box-shadow: 0 1px 4px rgba(0,0,0,0.08);">
|
||||
<h3 style="color: #7f8c8d; font-size: 12px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px;">Total Cost</h3>
|
||||
<div style="font-size: 28px; font-weight: 700; color: #2c3e50;">${data['total_cost']:,.2f}</div>
|
||||
<div style="font-size: 11px; color: #95a5a6; margin-top: 4px;">All tools combined</div>
|
||||
</div>
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #FFC407; box-shadow: 0 1px 4px rgba(0,0,0,0.08);">
|
||||
<h3 style="color: #7f8c8d; font-size: 12px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px;">Unique Users</h3>
|
||||
<div style="font-size: 28px; font-weight: 700; color: #2c3e50;">{data['unique_users']:,}</div>
|
||||
</div>
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #FFC407; box-shadow: 0 1px 4px rgba(0,0,0,0.08);">
|
||||
<h3 style="color: #7f8c8d; font-size: 12px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px;">Avg/User</h3>
|
||||
<div style="font-size: 28px; font-weight: 700; color: #2c3e50;">{data['avg_requests_per_user']:.1f}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tool_breakdown_html}
|
||||
|
||||
<h3 style="color: #2c3e50; margin: 25px 0 15px 0; font-size: 18px;">Top 25 Users</h3>
|
||||
{generate_user_table(data['top_users'], data['total_requests'], data['period_name'])}
|
||||
</div>
|
||||
'''
|
||||
|
||||
# Build full HTML
|
||||
html = f'''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Montserrat', Arial, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="font-family: 'Montserrat', Arial, sans-serif; background-color: #f5f7fa; margin: 0; padding: 20px;">
|
||||
<div style="max-width: 1200px; margin: 0 auto;">
|
||||
<div style="background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 30px;">
|
||||
<h1 style="color: #2c3e50; margin: 0 0 10px 0; font-size: 32px;">AI Tools Usage Report</h1>
|
||||
<p style="color: #7f8c8d; margin: 0; font-size: 14px;">Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
</div>
|
||||
|
||||
{generate_period_section(daily_data, 'Last 24 Hours', '📅')}
|
||||
{generate_period_section(weekly_data, 'Last 7 Days', '📊')}
|
||||
{generate_period_section(monthly_data, 'Last 30 Days', '📈')}
|
||||
|
||||
<div style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); text-align: center;">
|
||||
<p style="color: #7f8c8d; margin: 0; font-size: 12px;">
|
||||
This is an automated report. For questions, please contact your administrator.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def send_email_via_smtp(subject: str, html_content: str, recipients: List[str]) -> bool:
|
||||
"""Send email via SMTP"""
|
||||
if not SMTP_USER or not SMTP_PASSWORD or not SENDER_EMAIL:
|
||||
print("✗ SMTP credentials not configured")
|
||||
print(f" SMTP_USER: {'✓' if SMTP_USER else '✗'}")
|
||||
print(f" SMTP_PASSWORD: {'✓' if SMTP_PASSWORD else '✗'}")
|
||||
print(f" SENDER_EMAIL: {'✓' if SENDER_EMAIL else '✗'}")
|
||||
return False
|
||||
|
||||
# Filter out empty recipients
|
||||
recipients = [r.strip() for r in recipients if r.strip()]
|
||||
|
||||
if not recipients:
|
||||
print("✗ No valid recipients configured")
|
||||
return False
|
||||
|
||||
print(f"Sending email to {len(recipients)} recipient(s)...")
|
||||
print(f" SMTP Server: {SMTP_SERVER}:{SMTP_PORT}")
|
||||
print(f" From: {SENDER_EMAIL}")
|
||||
print(f" To: {', '.join(recipients)}")
|
||||
|
||||
try:
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = SENDER_EMAIL
|
||||
msg['To'] = ', '.join(recipients)
|
||||
|
||||
# Attach HTML content
|
||||
html_part = MIMEText(html_content, 'html')
|
||||
msg.attach(html_part)
|
||||
|
||||
# Connect to SMTP server and send
|
||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
||||
server.set_debuglevel(0) # Set to 1 for verbose output
|
||||
server.starttls()
|
||||
server.login(SMTP_USER, SMTP_PASSWORD)
|
||||
server.send_message(msg)
|
||||
|
||||
print("✓ Email sent successfully")
|
||||
return True
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
print(f"✗ SMTP Authentication Error: {e}")
|
||||
print(" Check your SMTP_USER and SMTP_PASSWORD")
|
||||
return False
|
||||
except smtplib.SMTPException as e:
|
||||
print(f"✗ SMTP Error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ Error sending email: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main execution function"""
|
||||
print("=" * 60)
|
||||
print("AI TOOLS USAGE REPORT GENERATOR")
|
||||
print("=" * 60)
|
||||
|
||||
# Calculate date ranges (timezone-aware)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Last 24 hours
|
||||
daily_end = now
|
||||
daily_start = now - timedelta(days=1)
|
||||
|
||||
# Last 7 days
|
||||
weekly_end = now
|
||||
weekly_start = now - timedelta(days=7)
|
||||
|
||||
# Last 30 days
|
||||
monthly_end = now
|
||||
monthly_start = now - timedelta(days=30)
|
||||
|
||||
# Fetch data (get last 30 days to cover all periods)
|
||||
data_start = monthly_start.strftime('%Y-%m-%d')
|
||||
data_end = monthly_end.strftime('%Y-%m-%d')
|
||||
|
||||
data = fetch_webhook_data(data_start, data_end)
|
||||
|
||||
if not data:
|
||||
print("✗ No data retrieved, exiting")
|
||||
return
|
||||
|
||||
print("\nAnalyzing data...")
|
||||
|
||||
# Analyze each period
|
||||
daily_report = analyze_period_data(data, daily_start, daily_end, "Last 24 Hours")
|
||||
weekly_report = analyze_period_data(data, weekly_start, weekly_end, "Last 7 Days")
|
||||
monthly_report = analyze_period_data(data, monthly_start, monthly_end, "Last 30 Days")
|
||||
|
||||
print(f"✓ Daily: {daily_report['total_requests']} requests from {daily_report['unique_users']} users")
|
||||
print(f"✓ Weekly: {weekly_report['total_requests']} requests from {weekly_report['unique_users']} users")
|
||||
print(f"✓ Monthly: {monthly_report['total_requests']} requests from {monthly_report['unique_users']} users")
|
||||
|
||||
# Generate HTML report
|
||||
print("\nGenerating HTML report...")
|
||||
html_report = generate_html_report(daily_report, weekly_report, monthly_report)
|
||||
|
||||
# Save HTML to file
|
||||
output_file = 'email_report.html'
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html_report)
|
||||
print(f"✓ Report saved to {output_file}")
|
||||
|
||||
# Send email if configured
|
||||
if REPORT_RECIPIENTS and REPORT_RECIPIENTS[0]:
|
||||
subject = f"AI Tools Usage Report - {now.strftime('%Y-%m-%d')}"
|
||||
send_email_via_smtp(subject, html_report, REPORT_RECIPIENTS)
|
||||
else:
|
||||
print("\n⚠ No recipients configured. Skipping email send.")
|
||||
print(" Set REPORT_RECIPIENTS in .env file to enable email delivery.")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("REPORT GENERATION COMPLETE")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
135
veo3_scheduler.py
Executable file
135
veo3_scheduler.py
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
VEO3 Report Scheduler Service
|
||||
Runs daily at 7:00 PM EST to generate and email VEO3 usage reports
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import signal
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
import pytz
|
||||
|
||||
# Add the script directory to path so we can import veo3_report
|
||||
script_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(script_dir))
|
||||
|
||||
# Import the report generation function
|
||||
from veo3_report import main as generate_report
|
||||
|
||||
# Configure logging
|
||||
log_dir = script_dir / 'logs'
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_dir / 'veo3_scheduler.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger('VEO3Scheduler')
|
||||
|
||||
# Timezone configuration
|
||||
EST = pytz.timezone('America/New_York')
|
||||
|
||||
|
||||
class VEO3Scheduler:
|
||||
"""Scheduler service for VEO3 reports"""
|
||||
|
||||
def __init__(self):
|
||||
self.scheduler = BlockingScheduler(timezone=EST)
|
||||
self.running = False
|
||||
|
||||
# Register signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
"""Handle shutdown signals gracefully"""
|
||||
logger.info(f"Received signal {signum}. Shutting down gracefully...")
|
||||
self.stop()
|
||||
|
||||
def run_report(self):
|
||||
"""Execute the report generation"""
|
||||
try:
|
||||
logger.info("=" * 70)
|
||||
logger.info("STARTING SCHEDULED REPORT GENERATION")
|
||||
logger.info("=" * 70)
|
||||
|
||||
# Change to script directory to ensure relative paths work
|
||||
os.chdir(script_dir)
|
||||
|
||||
# Run the report generation
|
||||
generate_report()
|
||||
|
||||
logger.info("=" * 70)
|
||||
logger.info("REPORT GENERATION COMPLETED SUCCESSFULLY")
|
||||
logger.info("=" * 70)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating report: {e}", exc_info=True)
|
||||
logger.error("=" * 70)
|
||||
logger.error("REPORT GENERATION FAILED")
|
||||
logger.error("=" * 70)
|
||||
|
||||
def start(self):
|
||||
"""Start the scheduler service"""
|
||||
logger.info("VEO3 Report Scheduler Service Starting...")
|
||||
logger.info(f"Working directory: {script_dir}")
|
||||
logger.info(f"Timezone: {EST}")
|
||||
|
||||
# Schedule daily report at 7:00 PM EST
|
||||
self.scheduler.add_job(
|
||||
self.run_report,
|
||||
trigger=CronTrigger(hour=19, minute=0, timezone=EST),
|
||||
id='daily_report',
|
||||
name='Daily VEO3 Report Generation',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
# Log scheduling confirmation
|
||||
logger.info(f"Scheduled to run daily at 7:00 PM EST")
|
||||
logger.info("Job scheduled successfully")
|
||||
|
||||
# Optional: Run immediately on startup for testing
|
||||
# Uncomment the line below to run a report when the service starts
|
||||
# logger.info("Running initial report...")
|
||||
# self.run_report()
|
||||
|
||||
self.running = True
|
||||
|
||||
try:
|
||||
logger.info("Scheduler service is now running. Press Ctrl+C to stop.")
|
||||
self.scheduler.start()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the scheduler service"""
|
||||
if self.running:
|
||||
logger.info("Stopping scheduler service...")
|
||||
self.scheduler.shutdown(wait=True)
|
||||
self.running = False
|
||||
logger.info("Scheduler service stopped.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
logger.info("=" * 70)
|
||||
logger.info("VEO3 REPORT SCHEDULER SERVICE")
|
||||
logger.info("=" * 70)
|
||||
|
||||
scheduler = VEO3Scheduler()
|
||||
scheduler.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
205
webhook_caller.php
Normal file
205
webhook_caller.php
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
// SSO Authentication
|
||||
require_once __DIR__ . '/AuthMiddleware.php';
|
||||
$auth = new AuthMiddleware();
|
||||
$user = $auth->requireAuth(); // This will redirect to login if not authenticated
|
||||
|
||||
// Configuration
|
||||
$webhookUrl = 'https://hook.us1.make.celonis.com/v7ev5x3j50oy1n1iqd6u6462cg0cz4yy';
|
||||
$responseFile = 'webhook_response.json';
|
||||
|
||||
// Initialize variables
|
||||
$response = null;
|
||||
$error = null;
|
||||
$isAjax = isset($_POST['ajax']) && $_POST['ajax'] == '1';
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_date']) && isset($_POST['end_date'])) {
|
||||
$startDate = $_POST['start_date'];
|
||||
$endDate = $_POST['end_date'];
|
||||
|
||||
// Prepare the data to send
|
||||
$postData = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate
|
||||
];
|
||||
|
||||
// Initialize cURL
|
||||
$ch = curl_init($webhookUrl);
|
||||
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json'
|
||||
]);
|
||||
|
||||
// Execute request
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
// Check for errors
|
||||
if (curl_errno($ch)) {
|
||||
$error = 'cURL Error: ' . curl_error($ch);
|
||||
} elseif ($httpCode !== 200) {
|
||||
$error = "HTTP Error: Received status code $httpCode";
|
||||
} else {
|
||||
// Save response to file
|
||||
file_put_contents($responseFile, $response);
|
||||
$error = null;
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
// If AJAX request, return JSON
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
if ($error) {
|
||||
echo json_encode(['success' => false, 'error' => $error]);
|
||||
} else {
|
||||
echo json_encode(['success' => true, 'message' => 'Data refreshed successfully']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Set default dates (last 30 days)
|
||||
$defaultEndDate = date('Y-m-d');
|
||||
$defaultStartDate = date('Y-m-d', strtotime('-30 days'));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Webhook Caller</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-top: 0;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
input[type="date"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 200px;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.response {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Webhook Caller</h1>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="start_date">Start Date:</label>
|
||||
<input type="date" id="start_date" name="start_date"
|
||||
value="<?php echo htmlspecialchars($_POST['start_date'] ?? $defaultStartDate); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="end_date">End Date:</label>
|
||||
<input type="date" id="end_date" name="end_date"
|
||||
value="<?php echo htmlspecialchars($_POST['end_date'] ?? $defaultEndDate); ?>" required>
|
||||
</div>
|
||||
<button type="submit">Call Webhook</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="container">
|
||||
<div class="error">
|
||||
<strong>Error:</strong> <?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($response && !$error): ?>
|
||||
<div class="container">
|
||||
<div class="success">
|
||||
<strong>Success!</strong> Response received and saved to <code><?php echo htmlspecialchars($responseFile); ?></code>
|
||||
</div>
|
||||
<h2>Response:</h2>
|
||||
<div class="response">
|
||||
<pre><?php echo htmlspecialchars($response); ?></pre>
|
||||
</div>
|
||||
|
||||
<h3>Formatted JSON:</h3>
|
||||
<div class="response">
|
||||
<pre><?php
|
||||
$jsonData = json_decode($response, true);
|
||||
if ($jsonData !== null) {
|
||||
echo htmlspecialchars(json_encode($jsonData, JSON_PRETTY_PRINT));
|
||||
} else {
|
||||
echo "Unable to parse as JSON";
|
||||
}
|
||||
?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
BIN
webhook_caller.textClipping
Normal file
BIN
webhook_caller.textClipping
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue