commit facacc94d4a5dc05ca9b17d33e643c5c11628aba Author: DJP Date: Thu Jan 8 14:50:04 2026 -0500 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) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d26a51a --- /dev/null +++ b/.env.example @@ -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 diff --git a/.env.example copy b/.env.example copy new file mode 100644 index 0000000..d26a51a --- /dev/null +++ b/.env.example copy @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16a8674 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AuthMiddleware.php b/AuthMiddleware.php new file mode 100644 index 0000000..bfa7e0a --- /dev/null +++ b/AuthMiddleware.php @@ -0,0 +1,409 @@ +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 = '') { + ?> + + + + + + Sign In - VEO3 Usage Report + + + + + + + + + + + + + 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; + } +} diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..1d7eccf --- /dev/null +++ b/QUICKSTART.md @@ -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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..939a573 --- /dev/null +++ b/README.md @@ -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) diff --git a/SSO-SETUP.md b/SSO-SETUP.md new file mode 100644 index 0000000..1210dc0 --- /dev/null +++ b/SSO-SETUP.md @@ -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 + + Require all denied + + + + Require all denied + + + + Require all denied + +``` + +#### 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 +requireAuth(); + +// User is authenticated, $user contains: +// - name: Display name +// - preferred_username: Email address +?> +``` + +## User Logout + +Add a logout link: + +```html +Logout + + +``` + +## 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 diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..4e40467 --- /dev/null +++ b/auth.php @@ -0,0 +1,118 @@ + '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' + ]); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a03a9f7 --- /dev/null +++ b/composer.json @@ -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 + } +} diff --git a/config.php b/config.php new file mode 100644 index 0000000..dff8311 --- /dev/null +++ b/config.php @@ -0,0 +1,30 @@ + /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 "" diff --git a/env_loader.php b/env_loader.php new file mode 100644 index 0000000..b1c4598 --- /dev/null +++ b/env_loader.php @@ -0,0 +1,35 @@ + 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); +?> + + + + + + AI Tools Usage Report + + + + +
+
+
+

AI Tools Usage Report

+

Data from to

+
+ +
+ +
+
+

Total Requests

+
+
All AI tool requests
+
+
+

Total Cost

+
$
+
All tools combined
+
+
+

Unique Users

+
+
Active accounts
+
+
+

Avg Requests/User

+
+
Mean usage per account
+
+
+ +
+
+

Last 24 Hours

+
requests
+
$
+
+
+

Last 7 Days

+
requests
+
$
+
+
+

Last 30 Days

+
requests
+
$
+
+
+

Avg Prompt Length

+
+
Characters
+
+
+ +
+

Tool Usage Breakdown

+

+ Usage distribution across all AI tools +

+ + + + + + + + + + + $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); + } + } + ?> + + + + + + + + +
ToolRequestsPercentageTop User
% + + () + + - + +
+
+ +
+

Daily Usage - Last 30 Days

+
+
+ $count): + $height = ($count / $maxDaily) * 100; + ?> +
+
+
+ requests +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + $count): + $dateObj = new DateTime($date); + $dayOfWeek = $dateObj->format('l'); + $barWidth = ($count / $maxDailyRecent) * 100; + ?> + + + + + + + + +
DateDay of WeekRequestsActivity Level
+
+
;">
+
+
+
+
+ +
+

Monthly Usage Breakdown

+ + + + + + + + + + $count): + $percentage = ($count / $totalRequests) * 100; + $barWidth = ($count / $maxMonthly) * 100; + ?> + + + + + + + +
MonthRequestsDistribution
(%) +
+
;">
+
+
+
+ +
+

šŸ“… Last 24 Hours - Top 25 Users

+ 0): ?> +

+ requests from users + Cost: $ +

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalPrompts24h) * 100; + $barWidth = ($count / $maxCount24h) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserRequestsPercentageActivity
# + + + % +
+
;">
+
+
+ +

No activity in the last 24 hours

+ +
+ +
+

šŸ“Š Last 7 Days - Top 25 Users

+ 0): ?> +

+ requests from users + Cost: $ +

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalPrompts7d) * 100; + $barWidth = ($count / $maxCount7d) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserRequestsPercentageActivity
# + + + % +
+
;">
+
+
+ +

No activity in the last 7 days

+ +
+ +
+

šŸ“ˆ Last 30 Days - Top 25 Users

+ 0): ?> +

+ requests from users + Cost: $ +

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalPrompts30d) * 100; + $barWidth = ($count / $maxCount30d) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserRequestsPercentageActivity
# + + + % +
+
;">
+
+
+ +

No activity in the last 30 days

+ +
+ +
+

All Time - Top 20 Users

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalRequests) * 100; + $barWidth = ($count / $maxCount) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserPromptsPercentageActivity
# + + + % +
+
;">
+
+
+
+ +
+

+ All Users Summary ( users) + ā–¼ +

+ +
+
+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..de4a9a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.31.0 +python-dotenv>=1.0.0 +apscheduler>=3.10.0 +pytz>=2023.3 diff --git a/veo3-report.service b/veo3-report.service new file mode 100644 index 0000000..573f26b --- /dev/null +++ b/veo3-report.service @@ -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 diff --git a/veo3_report.py b/veo3_report.py new file mode 100644 index 0000000..e337bf9 --- /dev/null +++ b/veo3_report.py @@ -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"

No activity in {period}

" + + html = '' + html += ''' + + + + + + + + + + + ''' + + 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 = 'šŸ„‡ #1' + elif rank == 2: + badge = '🄈 #2' + elif rank == 3: + badge = 'šŸ„‰ #3' + + html += f''' + + + + + + + + ''' + + html += '
RankUserPromptsPercentageActivity
#{rank}{user}{badge}{count:,}{percentage:.2f}% +
+
' + 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''' +
+

+ {emoji} {title} +

+

No activity recorded for this period.

+
+ ''' + + # Generate tool breakdown table + tool_breakdown_html = '

Tool Usage Breakdown

' + tool_breakdown_html += '' + tool_breakdown_html += '' + + 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'' + + tool_breakdown_html += '
ToolRequestsCostPercentage
{tool_name}{count:,}${tool_cost:,.2f}{percentage:.1f}%
' + + return f''' +
+

+ {emoji} {title} +

+

+ {data['start_date']} to {data['end_date']} +

+ +
+
+

Total Requests

+
{data['total_requests']:,}
+
+
+

Total Cost

+
${data['total_cost']:,.2f}
+
All tools combined
+
+
+

Unique Users

+
{data['unique_users']:,}
+
+
+

Avg/User

+
{data['avg_requests_per_user']:.1f}
+
+
+ + {tool_breakdown_html} + +

Top 25 Users

+ {generate_user_table(data['top_users'], data['total_requests'], data['period_name'])} +
+ ''' + + # Build full HTML + html = f''' + + + + + + + + + +
+
+

AI Tools Usage Report

+

Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+
+ + {generate_period_section(daily_data, 'Last 24 Hours', 'šŸ“…')} + {generate_period_section(weekly_data, 'Last 7 Days', 'šŸ“Š')} + {generate_period_section(monthly_data, 'Last 30 Days', 'šŸ“ˆ')} + +
+

+ This is an automated report. For questions, please contact your administrator. +

+
+
+ + + ''' + + 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() diff --git a/veo3_scheduler.py b/veo3_scheduler.py new file mode 100755 index 0000000..87e2935 --- /dev/null +++ b/veo3_scheduler.py @@ -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() diff --git a/webhook_caller.php b/webhook_caller.php new file mode 100644 index 0000000..b177157 --- /dev/null +++ b/webhook_caller.php @@ -0,0 +1,205 @@ +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')); +?> + + + + + + Webhook Caller + + + +
+

Webhook Caller

+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ Error: +
+
+ + + +
+
+ Success! Response received and saved to +
+

Response:

+
+
+
+ +

Formatted JSON:

+
+
+
+
+ + + diff --git a/webhook_caller.textClipping b/webhook_caller.textClipping new file mode 100644 index 0000000..e84edfd Binary files /dev/null and b/webhook_caller.textClipping differ