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:
DJP 2026-01-08 14:50:04 -05:00
commit facacc94d4
21 changed files with 3655 additions and 0 deletions

20
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.