diff --git a/third_party/astroglobe/lib/Settings/Admin.php b/third_party/astroglobe/lib/Settings/Admin.php index 7f12a0d..dffde05 100644 --- a/third_party/astroglobe/lib/Settings/Admin.php +++ b/third_party/astroglobe/lib/Settings/Admin.php @@ -54,6 +54,8 @@ class Admin implements ISettings { // Get configuration from config.php $serverUrl = $this->config->getSystemValue('mcp_server_url', ''); $apiKeyConfigured = !empty($this->config->getSystemValue('mcp_server_api_key', '')); + $clientSecret = $this->config->getSystemValue('astroglobe_client_secret', ''); + $clientSecretConfigured = !empty($clientSecret); // Check for server connection error if (isset($serverStatus['error'])) { @@ -110,6 +112,7 @@ class Admin implements ISettings { 'vectorSyncStatus' => $vectorSyncStatus, 'serverUrl' => $serverUrl, 'apiKeyConfigured' => $apiKeyConfigured, + 'clientSecretConfigured' => $clientSecretConfigured, 'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false, 'searchSettings' => $searchSettings, ]; diff --git a/third_party/astroglobe/src/adminSettings.js b/third_party/astroglobe/src/adminSettings.js new file mode 100644 index 0000000..639d94b --- /dev/null +++ b/third_party/astroglobe/src/adminSettings.js @@ -0,0 +1,245 @@ +/** + * Admin settings page JavaScript for Astroglobe. + * + * Handles: + * - Loading webhook presets + * - Enabling/disabling webhook presets + * - Search settings form submission + */ + +import { generateUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' + +document.addEventListener('DOMContentLoaded', () => { + // Initialize search settings form + initSearchSettingsForm() + + // Initialize webhook management (only if webhook section exists) + if (document.getElementById('webhook-presets')) { + initWebhookManagement() + } +}) + +/** + * Initialize search settings form handling. + */ +function initSearchSettingsForm() { + const form = document.getElementById('astroglobe-search-settings-form') + if (!form) return + + const scoreThresholdInput = document.getElementById('search-score-threshold') + const scoreThresholdValue = document.getElementById('score-threshold-value') + + // Update score threshold display when slider changes + if (scoreThresholdInput && scoreThresholdValue) { + scoreThresholdInput.addEventListener('input', (e) => { + scoreThresholdValue.textContent = e.target.value + '%' + }) + } + + // Handle form submission + form.addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(form) + const data = { + algorithm: formData.get('algorithm'), + fusion: formData.get('fusion'), + scoreThreshold: parseInt(formData.get('scoreThreshold')), + limit: parseInt(formData.get('limit')), + } + + const statusEl = document.getElementById('search-settings-status') + if (statusEl) { + statusEl.textContent = 'Saving...' + statusEl.className = 'mcp-status-message' + } + + try { + const response = await axios.post( + generateUrl('/apps/astroglobe/api/admin/search-settings'), + data, + { headers: { 'Content-Type': 'application/json' } } + ) + + if (response.data.success) { + if (statusEl) { + statusEl.textContent = '✓ Settings saved' + statusEl.className = 'mcp-status-message success' + setTimeout(() => { + statusEl.textContent = '' + }, 3000) + } + } + } catch (error) { + console.error('Failed to save search settings:', error) + if (statusEl) { + statusEl.textContent = '✗ Failed to save' + statusEl.className = 'mcp-status-message error' + } + } + }) +} + +/** + * Initialize webhook management UI. + */ +async function initWebhookManagement() { + const container = document.getElementById('webhook-presets-container') + if (!container) return + + try { + // Load webhook presets from API + const response = await axios.get( + generateUrl('/apps/astroglobe/api/admin/webhooks/presets') + ) + + if (!response.data.success) { + throw new Error(response.data.error || 'Failed to load presets') + } + + const presets = response.data.presets + renderWebhookPresets(container, presets) + } catch (error) { + console.error('Failed to load webhook presets:', error) + container.innerHTML = ` +
+

Error loading webhook presets:

+

${error.message || 'Unknown error'}

+
+ ` + } +} + +/** + * Render webhook preset cards. + * + * @param {HTMLElement} container Container element + * @param {Object} presets Preset configurations + */ +function renderWebhookPresets(container, presets) { + const presetIds = Object.keys(presets) + + if (presetIds.length === 0) { + container.innerHTML = ` +
+

No webhook presets available. Install supported apps (Notes, Calendar, Tables, Forms) to enable webhooks.

+
+ ` + return + } + + // Create preset cards grid + const grid = document.createElement('div') + grid.className = 'mcp-preset-grid' + + presetIds.forEach(presetId => { + const preset = presets[presetId] + const card = createPresetCard(presetId, preset) + grid.appendChild(card) + }) + + container.innerHTML = '' + container.appendChild(grid) +} + +/** + * Create a webhook preset card. + * + * @param {string} presetId Preset ID + * @param {Object} preset Preset configuration + * @return {HTMLElement} Card element + */ +function createPresetCard(presetId, preset) { + const card = document.createElement('div') + card.className = 'mcp-preset-card' + card.dataset.presetId = presetId + + const statusClass = preset.enabled ? 'enabled' : 'disabled' + const statusText = preset.enabled ? 'Enabled' : 'Disabled' + const buttonText = preset.enabled ? 'Disable' : 'Enable' + const buttonClass = preset.enabled ? 'secondary' : 'primary' + + card.innerHTML = ` +
+

${escapeHtml(preset.name)}

+ ${statusText} +
+

${escapeHtml(preset.description)}

+
+ App: ${escapeHtml(preset.app)} + ${preset.events.length} events +
+
+ +
+ ` + + // Attach event listener to toggle button + const toggleBtn = card.querySelector('.mcp-preset-toggle') + toggleBtn.addEventListener('click', () => togglePreset(presetId, preset.enabled)) + + return card +} + +/** + * Toggle a webhook preset (enable/disable). + * + * @param {string} presetId Preset ID + * @param {boolean} currentlyEnabled Current enabled state + */ +async function togglePreset(presetId, currentlyEnabled) { + const card = document.querySelector(`[data-preset-id="${presetId}"]`) + if (!card) return + + const toggleBtn = card.querySelector('.mcp-preset-toggle') + const originalText = toggleBtn.textContent + + // Disable button during request + toggleBtn.disabled = true + toggleBtn.textContent = currentlyEnabled ? 'Disabling...' : 'Enabling...' + + try { + const action = currentlyEnabled ? 'disable' : 'enable' + const url = generateUrl(`/apps/astroglobe/api/admin/webhooks/presets/${presetId}/${action}`) + + const response = await axios.post(url) + + if (!response.data.success) { + throw new Error(response.data.error || `Failed to ${action} preset`) + } + + // Reload presets to update UI + const container = document.getElementById('webhook-presets-container') + await initWebhookManagement() + + // Show success notification + OC.Notification.showTemporary(response.data.message || `Preset ${action}d successfully`) + } catch (error) { + console.error(`Failed to toggle preset ${presetId}:`, error) + + // Restore button state + toggleBtn.disabled = false + toggleBtn.textContent = originalText + + // Show error notification + OC.Notification.showTemporary( + error.message || 'Failed to toggle webhook preset', + { type: 'error' } + ) + } +} + +/** + * Escape HTML to prevent XSS. + * + * @param {string} text Text to escape + * @return {string} Escaped text + */ +function escapeHtml(text) { + const div = document.createElement('div') + div.textContent = text + return div.innerHTML +} diff --git a/third_party/astroglobe/templates/settings/admin.php b/third_party/astroglobe/templates/settings/admin.php index 07b141a..ee494c8 100644 --- a/third_party/astroglobe/templates/settings/admin.php +++ b/third_party/astroglobe/templates/settings/admin.php @@ -54,6 +54,21 @@ style('astroglobe', 'astroglobe-settings'); + + t('OAuth Client Secret')); ?> + + + + + t('Configured')); ?> + + + + t('Optional - Uses PKCE fallback')); ?> + + + + @@ -69,6 +84,19 @@ style('astroglobe', 'astroglobe-settings');

+ + +
+

t('Optional: Confidential OAuth Client')); ?>

+

t('To use refresh tokens for long-lived sessions, generate a client secret:')); ?>

+
openssl rand -hex 32
+

t('Then add it to your config.php:')); ?>

+
'astroglobe_client_secret' => 'your-generated-secret',
+

+ t('Without a client secret, the system will use PKCE (public client) authentication. Both methods work, but confidential clients provide better security for long-lived sessions.')); ?> +

+
+ @@ -259,6 +287,41 @@ style('astroglobe', 'astroglobe-settings'); + + +
+

t('Webhook Management')); ?>

+

+ t('Configure real-time synchronization for Nextcloud apps using webhooks. Webhooks provide instant updates to the MCP server when content changes.')); ?> +

+ +
+
+ t('Loading webhook presets...')); ?> +
+
+ +
+

t('How Webhooks Work')); ?>

+ +
+ +
+

t('Requirements')); ?>

+ +
+
+ +

t('Capabilities')); ?>

diff --git a/third_party/astroglobe/vite.config.js b/third_party/astroglobe/vite.config.js index eba18ff..078602d 100644 --- a/third_party/astroglobe/vite.config.js +++ b/third_party/astroglobe/vite.config.js @@ -2,6 +2,7 @@ import { createAppConfig } from '@nextcloud/vite-config' export default createAppConfig({ main: 'src/main.js', + adminSettings: 'src/adminSettings.js', }, { inlineCSS: { relativeCSSInjection: true }, })