From 80e8b728788e6f112696ee46055cc70128c5d23b Mon Sep 17 00:00:00 2001 From: Gronod Date: Tue, 19 May 2026 15:52:44 +0100 Subject: [PATCH] feat(webhooks): add simple frontend webhook configuration UI (Phase 4) --- client/src/App.css | 155 ++++++++++++++++++++++++++++++++ client/src/App.jsx | 215 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) diff --git a/client/src/App.css b/client/src/App.css index 23206ae..1831a5c 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -304,3 +304,158 @@ body { grid-template-columns: 1fr; } } + +/* Webhooks Section Styles */ +.webhooks-section { + background: white; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; + overflow: hidden; +} + +.webhooks-header { + padding: 20px 30px; + background: #f8f9fa; + border-bottom: 2px solid #e0e0e0; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: background 0.3s; +} + +.webhooks-header:hover { + background: #f0f1f2; +} + +.webhooks-header h2 { + color: #333; + font-size: 1.3rem; + margin: 0; +} + +.webhooks-toggle { + font-size: 1.2rem; + color: #666; + transition: transform 0.3s; +} + +.webhooks-toggle.expanded { + transform: rotate(180deg); +} + +.webhooks-content { + padding: 20px 30px; +} + +.webhook-instance { + padding: 20px 0; + border-bottom: 1px solid #e0e0e0; +} + +.webhook-instance:last-child { + border-bottom: none; +} + +.webhook-instance h3 { + color: #333; + font-size: 1.1rem; + margin-bottom: 15px; +} + +.webhook-status { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.status-indicator { + font-size: 1rem; + font-weight: 500; + padding: 5px 15px; + border-radius: 20px; +} + +.status-indicator.enabled { + background: #e8f5e9; + color: #4caf50; +} + +.status-indicator.disabled { + background: #f5f5f5; + color: #999; +} + +.enable-webhook-btn { + padding: 8px 16px; + background: #667eea; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.95rem; + transition: background 0.3s; +} + +.enable-webhook-btn:hover { + background: #5568d3; +} + +.enable-webhook-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +.test-webhook-btn { + padding: 8px 16px; + background: #f093fb; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.95rem; + transition: background 0.3s; +} + +.test-webhook-btn:hover { + background: #d97ed8; +} + +.test-webhook-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +.webhook-triggers { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 10px; + padding-top: 15px; + border-top: 1px solid #e0e0e0; +} + +.trigger-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.trigger-label { + color: #666; + font-size: 0.9rem; +} + +.trigger-value { + font-weight: 500; + font-size: 1.1rem; +} + +.trigger-value.active { + color: #4caf50; +} + +.trigger-value.inactive { + color: #999; +} diff --git a/client/src/App.jsx b/client/src/App.jsx index 17eaaf7..aeef72d 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,9 +10,14 @@ function App() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [sessions, setSessions] = useState([]); + const [webhookSectionExpanded, setWebhookSectionExpanded] = useState(false); + const [sonarrWebhook, setSonarrWebhook] = useState({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } }); + const [radarrWebhook, setRadarrWebhook] = useState({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } }); + const [webhookLoading, setWebhookLoading] = useState(false); useEffect(() => { fetchSessions(); + fetchWebhookStatus(); }, []); const fetchSessions = async () => { @@ -67,6 +72,112 @@ function App() { return new Date(dateString).toLocaleString(); }; + const fetchWebhookStatus = async () => { + try { + // Fetch Sonarr notifications + try { + const sonarrResponse = await axios.get('/api/sonarr/notifications'); + const sonarrSofarr = sonarrResponse.data.find(n => n.name === 'Sofarr'); + setSonarrWebhook({ + enabled: !!sonarrSofarr, + triggers: sonarrSofarr ? { + onGrab: sonarrSofarr.onGrab, + onDownload: sonarrSofarr.onDownload, + onImport: sonarrSofarr.onImport, + onUpgrade: sonarrSofarr.onUpgrade + } : { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } + }); + } catch (err) { + // Sonarr not configured or not accessible + setSonarrWebhook({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } }); + } + + // Fetch Radarr notifications + try { + const radarrResponse = await axios.get('/api/radarr/notifications'); + const radarrSofarr = radarrResponse.data.find(n => n.name === 'Sofarr'); + setRadarrWebhook({ + enabled: !!radarrSofarr, + triggers: radarrSofarr ? { + onGrab: radarrSofarr.onGrab, + onDownload: radarrSofarr.onDownload, + onImport: radarrSofarr.onImport, + onUpgrade: radarrSofarr.onUpgrade + } : { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } + }); + } catch (err) { + // Radarr not configured or not accessible + setRadarrWebhook({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } }); + } + } catch (err) { + console.error('Failed to fetch webhook status:', err); + } + }; + + const enableSonarrWebhook = async () => { + setWebhookLoading(true); + try { + await axios.post('/api/sonarr/notifications/sofarr-webhook'); + await fetchWebhookStatus(); + } catch (err) { + console.error('Failed to enable Sonarr webhook:', err); + alert('Failed to enable Sonarr webhook. Check console for details.'); + } finally { + setWebhookLoading(false); + } + }; + + const enableRadarrWebhook = async () => { + setWebhookLoading(true); + try { + await axios.post('/api/radarr/notifications/sofarr-webhook'); + await fetchWebhookStatus(); + } catch (err) { + console.error('Failed to enable Radarr webhook:', err); + alert('Failed to enable Radarr webhook. Check console for details.'); + } finally { + setWebhookLoading(false); + } + }; + + const testSonarrWebhook = async () => { + setWebhookLoading(true); + try { + const sonarrResponse = await axios.get('/api/sonarr/notifications'); + const sonarrSofarr = sonarrResponse.data.find(n => n.name === 'Sofarr'); + if (sonarrSofarr) { + await axios.post('/api/sonarr/notifications/test', { id: sonarrSofarr.id }); + alert('Sonarr webhook test sent successfully!'); + } else { + alert('Sofarr webhook not configured for Sonarr.'); + } + } catch (err) { + console.error('Failed to test Sonarr webhook:', err); + alert('Failed to test Sonarr webhook. Check console for details.'); + } finally { + setWebhookLoading(false); + } + }; + + const testRadarrWebhook = async () => { + setWebhookLoading(true); + try { + const radarrResponse = await axios.get('/api/radarr/notifications'); + const radarrSofarr = radarrResponse.data.find(n => n.name === 'Sofarr'); + if (radarrSofarr) { + await axios.post('/api/radarr/notifications/test', { id: radarrSofarr.id }); + alert('Radarr webhook test sent successfully!'); + } else { + alert('Sofarr webhook not configured for Radarr.'); + } + } catch (err) { + console.error('Failed to test Radarr webhook:', err); + alert('Failed to test Radarr webhook. Check console for details.'); + } finally { + setWebhookLoading(false); + } + }; + return (
@@ -178,6 +289,110 @@ function App() {
)} +
+
setWebhookSectionExpanded(!webhookSectionExpanded)}> +

⚡ Webhooks Configuration

+ +
+ {webhookSectionExpanded && ( +
+ {webhookLoading &&
Loading webhook status...
} +
+

Sonarr

+
+ + {sonarrWebhook.enabled ? '● Enabled' : '○ Disabled'} + + {!sonarrWebhook.enabled && ( + + )} + {sonarrWebhook.enabled && ( + + )} +
+ {sonarrWebhook.enabled && ( +
+
+ On Grab + + {sonarrWebhook.triggers.onGrab ? '✓' : '✗'} + +
+
+ On Download + + {sonarrWebhook.triggers.onDownload ? '✓' : '✗'} + +
+
+ On Import + + {sonarrWebhook.triggers.onImport ? '✓' : '✗'} + +
+
+ On Upgrade + + {sonarrWebhook.triggers.onUpgrade ? '✓' : '✗'} + +
+
+ )} +
+
+

Radarr

+
+ + {radarrWebhook.enabled ? '● Enabled' : '○ Disabled'} + + {!radarrWebhook.enabled && ( + + )} + {radarrWebhook.enabled && ( + + )} +
+ {radarrWebhook.enabled && ( +
+
+ On Grab + + {radarrWebhook.triggers.onGrab ? '✓' : '✗'} + +
+
+ On Download + + {radarrWebhook.triggers.onDownload ? '✓' : '✗'} + +
+
+ On Import + + {radarrWebhook.triggers.onImport ? '✓' : '✗'} + +
+
+ On Upgrade + + {radarrWebhook.triggers.onUpgrade ? '✓' : '✗'} + +
+
+ )} +
+
+ )} +
+