From 9aec5582db25d805fa2b7c9be676c60e819fad97 Mon Sep 17 00:00:00 2001
From: Chris Coutinho
Date: Mon, 15 Dec 2025 21:41:29 +0100
Subject: [PATCH] feat(astrolabe): add webhook management UI to admin settings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add admin interface for configuring real-time webhook sync with
pre-configured presets for common scenarios.
Changes:
- Add webhook presets section to admin settings page
- Shows available presets filtered by installed apps
- Enable/disable presets with one click
- Displays current webhook status
- Add client secret configuration status display
- Shows whether confidential client is configured
- Provides setup instructions for optional client secret
- Add adminSettings.js for webhook management
- Load webhook presets via API
- Enable/disable webhook presets
- Handle search settings form submission
- Update vite.config.js to build adminSettings entry point
- Pass clientSecretConfigured flag to template
UI Features:
- Real-time preset status (enabled/disabled)
- One-click enable/disable for webhook bundles
- App-aware filtering (only shows presets for installed apps)
- Clear instructions for requirements and setup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5
---
third_party/astroglobe/lib/Settings/Admin.php | 3 +
third_party/astroglobe/src/adminSettings.js | 245 ++++++++++++++++++
.../astroglobe/templates/settings/admin.php | 63 +++++
third_party/astroglobe/vite.config.js | 1 +
4 files changed, 312 insertions(+)
create mode 100644 third_party/astroglobe/src/adminSettings.js
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.description)}
+
+ App: ${escapeHtml(preset.app)}
+ ${preset.events.length} events
+
+
+
+ ${buttonText}
+
+
+ `
+
+ // 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('Enable a preset to register webhooks for that app with the MCP server')); ?>
+ t('When content changes in Nextcloud, webhooks notify the MCP server instantly')); ?>
+ t('The MCP server updates its vector index in real-time for semantic search')); ?>
+ t('Disable a preset to stop receiving updates for that app')); ?>
+
+
+
+
+
t('Requirements')); ?>
+
+ t('The webhook_listeners app must be installed and enabled in Nextcloud')); ?>
+ t('The MCP server must be reachable from your Nextcloud instance')); ?>
+ t('You must have authorized Astroglobe with the MCP server (see Personal Settings)')); ?>
+
+
+
+
+
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 },
})