diff --git a/analyticshub.zip b/analyticshub.zip index 6e7b673..1f31beb 100644 Binary files a/analyticshub.zip and b/analyticshub.zip differ diff --git a/analyticshub/appinfo/Application.php b/analyticshub/appinfo/Application.php index 0c6cb6f..223746c 100644 --- a/analyticshub/appinfo/Application.php +++ b/analyticshub/appinfo/Application.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace OCA\AnalyticsHub\AppInfo; use OCP\AppFramework\App; +use OCP\Util; /** * Application class for Mini-CMO Analytics Hub @@ -13,10 +14,13 @@ class Application extends App { public const APP_NAME = 'analyticshub'; public const APP_ID = 'analyticshub'; + public const APP_VERSION = '1.0.0'; public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); - // No resources to load for simple test + // Load scripts and styles for admin page + Util::addStyle(self::APP_ID, 'admin'); + Util::addScript(self::APP_ID, 'admin'); } } diff --git a/analyticshub/appinfo/routes.php b/analyticshub/appinfo/routes.php index ba62553..a521479 100644 --- a/analyticshub/appinfo/routes.php +++ b/analyticshub/appinfo/routes.php @@ -10,12 +10,24 @@ namespace OCA\AnalyticsHub; return [ 'routes' => [ - // Admin route - use root path + // Admin routes [ 'name' => 'page#index', 'url' => '/', 'verb' => 'GET', 'requirements' => [], ], + [ + 'name' => 'page#save', + 'url' => '/save', + 'verb' => 'POST', + 'requirements' => [], + ], + [ + 'name' => 'page#load', + 'url' => '/load', + 'verb' => 'GET', + 'requirements' => [], + ], ], ]; diff --git a/analyticshub/css/admin.css b/analyticshub/css/admin.css index c64b9e6..4554762 100644 --- a/analyticshub/css/admin.css +++ b/analyticshub/css/admin.css @@ -1,60 +1,176 @@ -#analytics-hub-settings { - max-width: 800px; +.analytics-hub-settings { + max-width: 900px; margin: 0 auto; - padding: 20px; + padding: 30px; } .analytics-hub-settings__section { - margin-bottom: 30px; + margin-bottom: 35px; + padding: 25px; + border: 1px solid #e0e0e6; + border-radius: 8px; + background: #ffffff; +} + +.analytics-hub-settings__section h3 { + color: #0066cc; + font-size: 20px; + font-weight: 600; + margin-bottom: 20px; + margin-top: 0; } .analytics-hub-settings__field { - margin-bottom: 20px; + margin-bottom: 25px; } .analytics-hub-settings__field label { display: block; font-weight: 600; - margin-bottom: 8px; + margin-bottom: 10px; + color: #333; + font-size: 15px; } -.analytics-hub-settings__field input { +.analytics-hub-settings__required { + color: #dc3545; + margin-left: 3px; +} + +.analytics-hub-settings__optional { + color: #6c757d; + margin-left: 3px; + font-weight: normal; + font-size: 13px; +} + +.analytics-hub-settings__field input[type="text"], +.analytics-hub-settings__field input[type="password"] { width: 100%; - padding: 10px; + padding: 12px 15px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; box-sizing: border-box; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.analytics-hub-settings__field input[type="text"]:focus, +.analytics-hub-settings__field input[type="password"]:focus { + outline: none; + border-color: #0066cc; + box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); } .analytics-hub-settings__hint { font-size: 13px; color: #666; margin-top: 8px; - line-height: 1.4; + line-height: 1.5; +} + +.analytics-hub-settings__success { + padding: 20px; + background: #d4edda; + border-left: 4px solid #28a745; + border-radius: 4px; + margin-bottom: 25px; +} + +.analytics-hub-settings__success h3 { + color: #28a745; + margin: 0 0 10px 0; +} + +.analytics-hub-settings__warning { + padding: 20px; + background: #fff3cd; + border-left: 4px solid #dc3545; + border-radius: 4px; + margin-bottom: 25px; +} + +.analytics-hub-settings__warning h3 { + color: #dc3545; + margin: 0 0 10px 0; } .analytics-hub-settings__actions { display: flex; - gap: 10px; - margin-top: 20px; + gap: 15px; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #e0e0e6; } .analytics-hub-settings__actions button { - padding: 12px 24px; + padding: 12px 30px; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + transition: all 0.2s; +} + +.analytics-hub-settings__actions button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.analytics-hub-settings__actions button:disabled { + opacity: 0.6; + cursor: not-allowed; } .analytics-hub-settings__actions .primary { - background-color: #0078d4; + background-color: #0066cc; color: white; } +.analytics-hub-settings__actions .primary:hover:not(:disabled) { + background-color: #0052a3; +} + .analytics-hub-settings__actions .secondary { background-color: #6c757d; color: white; } + +.analytics-hub-settings__actions .secondary:hover:not(:disabled) { + background-color: #5a6268; +} + +.analytics-hub-settings__status { + padding: 20px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e0e0e6; +} + +.analytics-hub-settings__status h3 { + color: #0066cc; + font-size: 18px; + margin-bottom: 15px; +} + +.analytics-hub-settings__status p { + margin-bottom: 8px; + color: #333; + font-size: 14px; +} + +.analytics-hub-settings__status strong { + color: #0066cc; +} + +.analytics-hub-settings__success { + background: #d4edda; + border-left-color: #28a745; +} + +.analytics-hub-settings__success h3 { + color: #28a745; +} diff --git a/analyticshub/js/admin.js b/analyticshub/js/admin.js index 66ec288..5651040 100644 --- a/analyticshub/js/admin.js +++ b/analyticshub/js/admin.js @@ -2,65 +2,136 @@ 'use strict'; const saveButton = document.getElementById('analytics-hub-save'); - const testButton = document.getElementById('analytics-hub-test'); + const cancelButton = document.getElementById('analytics-hub-cancel'); + const form = document.getElementById('analytics-hub-form'); if (saveButton) { - saveButton.addEventListener('click', function() { - // Collect form data - const data = { - google_client_id: document.getElementById('google_client_id').value, - google_client_secret: document.getElementById('google_client_secret').value, - google_refresh_token: document.getElementById('google_refresh_token').value, - anthropic_api_key: document.getElementById('anthropic_api_key').value - }; - - // Send save request - fetch(OC.generateUrl('/apps/analyticshub/admin/save'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'requesttoken': OC.requestToken - }, - body: JSON.stringify(data) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - OC.Notification.showTemporary(t('analyticshub', 'Configuration saved successfully')); - } else { - OC.Notification.showTemporary(t('analyticshub', 'Error: ' + data.error)); - } - }) - .catch(error => { - OC.Notification.showTemporary(t('analyticshub', 'Error saving configuration')); - }); + saveButton.addEventListener('click', function(e) { + e.preventDefault(); + saveConfiguration(); }); } - if (testButton) { - testButton.addEventListener('click', function() { - // Test connections - fetch(OC.generateUrl('/apps/analyticshub/admin/status'), { - method: 'GET', - headers: { - 'requesttoken': OC.requestToken - } - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - const status = data.data; - const message = [ - 'App Status: ' + status.status, - 'Google Analytics: ' + status.google_analytics, - 'LLM Service: ' + status.llm_service - ].join('\n'); - alert(message); - } - }) - .catch(error => { - OC.Notification.showTemporary(t('analyticshub', 'Error getting status')); - }); + if (cancelButton) { + cancelButton.addEventListener('click', function(e) { + e.preventDefault(); + loadConfiguration(); }); } + + function saveConfiguration() { + // Get form data + const formData = new FormData(form); + const data = { + google_client_id: document.getElementById('google_client_id').value, + google_client_secret: document.getElementById('google_client_secret').value, + google_refresh_token: document.getElementById('google_refresh_token').value, + anthropic_api_key: document.getElementById('anthropic_api_key').value + }; + + // Validate required fields + if (!data.google_client_id) { + showNotification('Error', 'Google Client ID is required'); + return; + } + + if (!data.google_client_secret) { + showNotification('Error', 'Google Client Secret is required'); + return; + } + + if (!data.anthropic_api_key) { + showNotification('Error', 'Anthropic API Key is required'); + return; + } + + // Send save request + saveButton.textContent = 'Saving...'; + saveButton.disabled = true; + + const url = OC.generateUrl('/apps/analyticshub/admin/save'); + + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'requesttoken': OC.requestToken + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotification('Success', 'Configuration saved successfully'); + if (data.data && data.data.is_configured) { + updateStatus(true); + } + } else { + showNotification('Error', data.error || 'Failed to save configuration'); + } + }) + .catch(error => { + showNotification('Error', 'Failed to save configuration: ' + error.message); + }) + .finally(() => { + saveButton.textContent = 'Save Configuration'; + saveButton.disabled = false; + }); + } + + function loadConfiguration() { + const url = OC.generateUrl('/apps/analyticshub/admin/load'); + + fetch(url, { + method: 'GET', + headers: { + 'requesttoken': OC.requestToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success && data.data) { + document.getElementById('google_client_id').value = data.data.google_client_id || ''; + document.getElementById('google_client_secret').value = ''; + document.getElementById('google_refresh_token').value = ''; + document.getElementById('anthropic_api_key').value = ''; + updateStatus(!!data.data.is_configured); + showNotification('Success', 'Configuration loaded'); + } else { + showNotification('Error', data.error || 'Failed to load configuration'); + } + }) + .catch(error => { + showNotification('Error', 'Failed to load configuration: ' + error.message); + }); + } + + function updateStatus(isConfigured) { + const statusDiv = document.querySelector('.analytics-hub-settings__status'); + if (!statusDiv) return; + + const statusMessage = isConfigured ? '✅ Configured' : '❌ Not configured'; + + // Update status indicators in the status section + const statusElements = statusDiv.querySelectorAll('p strong'); + statusElements.forEach(el => { + const text = el.textContent; + if (text.includes('Google Client ID')) { + el.parentElement.innerHTML = 'Google Client ID: ' + statusMessage; + } + if (text.includes('Anthropic API Key')) { + el.parentElement.innerHTML = 'Anthropic API Key: ' + statusMessage; + } + }); + } + + function showNotification(title, message) { + // Try Nextcloud's notification system first + if (typeof OC !== 'undefined' && OC.Notification) { + OC.Notification.showTemporary(t('analyticshub', title) + ': ' + message); + } else { + // Fallback to alert + alert(title + ': ' + message); + } + } })(); diff --git a/analyticshub/lib/Controller/PageController.php b/analyticshub/lib/Controller/PageController.php index 7c5afc3..880d0b1 100644 --- a/analyticshub/lib/Controller/PageController.php +++ b/analyticshub/lib/Controller/PageController.php @@ -6,6 +6,10 @@ namespace OCA\AnalyticsHub\Controller; use OCP\IRequest; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IConfig; +use OCA\AnalyticsHub\AppInfo\Application; /** * Admin Settings Controller @@ -17,50 +21,124 @@ class PageController extends Controller { protected $appName; protected $request; + private IConfig $config; - public function __construct(string $appName, IRequest $request) { + public function __construct(string $appName, IRequest $request, IConfig $config) { parent::__construct($appName, $request); $this->appName = $appName; $this->request = $request; + $this->config = $config; } /** - * Index page - simple render without TemplateResponse + * Index page - render admin UI * * @NoAdminRequired * @NoCSRFRequired */ - public function index(): void { - echo ''; - echo ''; - echo '
'; - echo ''; - echo 'App Name: ' . htmlspecialchars($this->appName) . '
'; - echo 'Request Path: ' . htmlspecialchars($this->request->getPathInfo()) . '
'; - echo 'Status: Controller properly initialized with protected property visibility.
'; - echo '✅ Routing test successful!
'; - echo 'The app is now working correctly. You can:
'; - echo 'AI-powered Google Analytics 4 reporting with automated daily reports.
-Status:
-Version:
+t('AI-powered Google Analytics 4 reporting with automated daily reports.')); ?>
-- After OAuth consent, paste only the refresh token here. - The access token is refreshed automatically each run. -
-t('App is configured and ready to use.')); ?>
- Enter your Anthropic API key for AI-powered report generation. - Model: claude-sonnet-4-5-20250929 (cost-effective) - Cost: ~$0.015 per report (3K tokens) -
-t('App is not yet configured. Please enter your credentials below.')); ?>
t('App Name')); ?>:
+t('Version')); ?>:
+t('Google Client ID')); ?>:
+t('Anthropic API Key')); ?>: config->getAppValue('OCA\AnalyticsHub', 'anthropic_api_key', '')) ? '✅ Configured' : '❌ Not configured'; ?>