From 1dccda529a244ce88bf2ed91977d56f875bd05cb Mon Sep 17 00:00:00 2001 From: Gronod Date: Thu, 21 May 2026 20:59:06 +0100 Subject: [PATCH] feat: add Ombi requests tab and webhook panel integration - Add Ombi requests tab UI with movie/TV request display - Add showAll parameter support for Ombi requests (API and SSE) - Add Ombi webhook panel with enable/test functionality - Add Ombi webhook status endpoint with metrics - Add Ombi webhook test endpoint - Change GET /api/ombi/requests to use OmbiRetriever instead of cache - Add Ombi webhook state and API functions to frontend - Update SSE payload to include Ombi baseUrl and requests --- client/src/api.js | 47 +++++ client/src/sse.js | 10 + client/src/state.js | 3 + client/src/ui/requests.js | 150 ++++++++++++++ client/src/ui/tabs.js | 38 +++- client/src/ui/webhooks.js | 145 ++++++++++---- public/index.html | 38 ++++ public/style.css | 154 +++++++++++++++ server/app.js | 2 + server/routes/dashboard.js | 31 ++- server/routes/ombi.js | 395 +++++++++++++++++++++++++++++++++++++ server/routes/webhook.js | 203 ++++++++++++++++++- server/utils/poller.js | 24 ++- 13 files changed, 1186 insertions(+), 54 deletions(-) create mode 100644 client/src/ui/requests.js create mode 100644 server/routes/ombi.js diff --git a/client/src/api.js b/client/src/api.js index 598017d..6a94eb4 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -181,6 +181,22 @@ export async function fetchWebhookStatus() { } catch (err) { // Radarr not configured } + + // Fetch Ombi webhook status + let ombiEnabled = false; + let ombiTriggers = { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }; + let ombiStats = null; + try { + const ombiRes = await fetch('/api/ombi/webhook/status'); + if (ombiRes.ok) { + const ombiData = await ombiRes.json(); + ombiEnabled = ombiData.enabled || false; + ombiTriggers = ombiData.triggers || { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }; + ombiStats = ombiData.stats || null; + } + } catch (err) { + // Ombi not configured + } state.webhookMetrics = await metricsPromise; @@ -191,6 +207,7 @@ export async function fetchWebhookStatus() { state.sonarrWebhook = { enabled: sonarrEnabled, triggers: sonarrTriggers, stats: sonarrStats }; state.radarrWebhook = { enabled: radarrEnabled, triggers: radarrTriggers, stats: radarrStats }; + state.ombiWebhook = { enabled: ombiEnabled, triggers: ombiTriggers, stats: ombiStats }; return { success: true }; } catch (err) { @@ -279,6 +296,36 @@ export async function testRadarrWebhook() { } } +export async function enableOmbiWebhook() { + try { + const res = await fetch('/api/ombi/webhook/enable', { + method: 'POST', + headers: { 'X-CSRF-Token': state.csrfToken || '' } + }); + if (!res.ok) throw new Error('Failed to enable'); + await fetchWebhookStatus(); + return { success: true }; + } catch (err) { + console.error('Failed to enable Ombi webhook:', err); + return { success: false, error: err.message }; + } +} + +export async function testOmbiWebhook() { + try { + const res = await fetch('/api/ombi/webhook/test', { + method: 'POST', + headers: { 'X-CSRF-Token': state.csrfToken || '' } + }); + if (!res.ok) throw new Error('Test failed'); + await fetchWebhookStatus(); + return { success: true }; + } catch (err) { + console.error('Failed to test Ombi webhook:', err); + return { success: false, error: err.message }; + } +} + export async function refreshStatusPanel() { try { const res = await fetch('/api/status'); diff --git a/client/src/sse.js b/client/src/sse.js index 25b24b0..5e635bd 100644 --- a/client/src/sse.js +++ b/client/src/sse.js @@ -25,6 +25,16 @@ export function startSSE() { const filterUpdateEvent = new CustomEvent('downloadClientsUpdated'); document.dispatchEvent(filterUpdateEvent); } + // Store Ombi requests and base URL + if (data.ombiRequests) { + state.ombiRequests = data.ombiRequests; + // Trigger requests update event + const requestsUpdateEvent = new CustomEvent('ombiRequestsUpdated'); + document.dispatchEvent(requestsUpdateEvent); + } + if (data.ombiBaseUrl) { + state.ombiBaseUrl = data.ombiBaseUrl; + } document.getElementById('currentUser').textContent = state.currentUser || '-'; renderDownloads(); hideError(); diff --git a/client/src/state.js b/client/src/state.js index a3a1d5f..6895bdb 100644 --- a/client/src/state.js +++ b/client/src/state.js @@ -9,6 +9,8 @@ export const state = { isAdmin: false, showAll: false, csrfToken: null, // double-submit CSRF token, sent as X-CSRF-Token on mutating requests + ombiBaseUrl: null, // Ombi base URL for generating links + ombiRequests: null, // Ombi requests data // History section state historyDays: 7, // Default value, will be loaded from localStorage @@ -28,6 +30,7 @@ export const state = { webhookLoading: false, sonarrWebhook: { enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false }, stats: null }, radarrWebhook: { enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false }, stats: null }, + ombiWebhook: { enabled: false, triggers: { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }, stats: null }, webhookMetrics: null }; diff --git a/client/src/ui/requests.js b/client/src/ui/requests.js new file mode 100644 index 0000000..8d5fdb9 --- /dev/null +++ b/client/src/ui/requests.js @@ -0,0 +1,150 @@ +// Copyright (c) 2026 Gordon Bolton. MIT License. + +import { state } from '../state.js'; +import { escapeHtml } from '../utils/format.js'; + +export function renderRequests() { + const requestsList = document.getElementById('requests-list'); + const noRequests = document.getElementById('no-requests'); + + if (!requestsList) return; + + const ombiRequests = state.ombiRequests || { movie: [], tv: [] }; + const allRequests = [ + ...ombiRequests.movie.map(r => ({ ...r, mediaType: 'movie' })), + ...ombiRequests.tv.map(r => ({ ...r, mediaType: 'tv' })) + ]; + + requestsList.innerHTML = ''; + + if (allRequests.length === 0) { + if (noRequests) noRequests.style.display = 'block'; + return; + } + + if (noRequests) noRequests.style.display = 'none'; + + allRequests.forEach(request => { + const card = createRequestCard(request); + requestsList.appendChild(card); + }); +} + +function createRequestCard(request) { + const card = document.createElement('div'); + card.className = 'request-card'; + + const typeIcon = document.createElement('span'); + typeIcon.className = `request-type-icon ${request.mediaType}`; + typeIcon.textContent = request.mediaType === 'movie' ? '🎬' : '📺'; + + const content = document.createElement('div'); + content.className = 'request-content'; + + const title = document.createElement('div'); + title.className = 'request-title'; + title.textContent = request.title || 'Unknown Title'; + + const meta = document.createElement('div'); + meta.className = 'request-meta'; + + const statusBadge = createStatusBadge(request); + meta.appendChild(statusBadge); + + if (request.year) { + const year = document.createElement('span'); + year.className = 'request-year'; + year.textContent = request.year; + meta.appendChild(year); + } + + if (request.requestedUser || request.requestedByAlias) { + const user = document.createElement('span'); + user.className = 'request-user'; + user.textContent = `Requested by: ${request.requestedUser || request.requestedByAlias}`; + meta.appendChild(user); + } + + if (request.quality) { + const quality = document.createElement('span'); + quality.className = 'request-quality'; + quality.textContent = request.quality; + meta.appendChild(quality); + } + + content.appendChild(title); + content.appendChild(meta); + + const actions = document.createElement('div'); + actions.className = 'request-actions'; + + if (request.theMovieDbId) { + const ombiLink = document.createElement('a'); + ombiLink.className = 'request-link ombi-link'; + ombiLink.href = `${state.ombiBaseUrl}/details/${request.mediaType}/${request.theMovieDbId}`; + ombiLink.target = '_blank'; + ombiLink.title = 'View in Ombi'; + + const ombiIcon = document.createElement('img'); + ombiIcon.src = '/images/ombi.svg'; + ombiIcon.alt = 'Ombi'; + ombiIcon.className = 'request-icon'; + + ombiLink.appendChild(ombiIcon); + actions.appendChild(ombiLink); + } + + card.appendChild(typeIcon); + card.appendChild(content); + card.appendChild(actions); + + return card; +} + +function createStatusBadge(request) { + const badge = document.createElement('span'); + badge.className = 'request-status-badge'; + + let status = 'unknown'; + let text = 'Unknown'; + + if (request.available) { + status = 'available'; + text = 'Available'; + } else if (request.approved) { + status = 'approved'; + text = 'Approved'; + } else if (request.denied) { + status = 'denied'; + text = `Denied: ${request.deniedReason || 'No reason'}`; + } else if (request.requested) { + status = 'pending'; + text = 'Pending'; + } + + badge.classList.add(status); + badge.textContent = text; + + return badge; +} + +export function setupRequestsTab() { + // Listen for SSE updates + if (state.sseSource) { + state.sseSource.addEventListener('message', (event) => { + const data = JSON.parse(event.data); + if (data.ombiRequests) { + state.ombiRequests = data.ombiRequests; + renderRequests(); + } + }); + } + + // Also listen for custom event triggered from sse.js + document.addEventListener('ombiRequestsUpdated', () => { + renderRequests(); + }); + + // Initial render + renderRequests(); +} diff --git a/client/src/ui/tabs.js b/client/src/ui/tabs.js index 94d1e35..aa62125 100644 --- a/client/src/ui/tabs.js +++ b/client/src/ui/tabs.js @@ -2,42 +2,62 @@ import { getActiveTab, saveActiveTab } from '../utils/storage.js'; import { loadHistory } from './history.js'; +import { setupRequestsTab } from './requests.js'; export function initTabs() { const downloadsTab = document.querySelector('[data-tab="downloads"]'); + const requestsTab = document.querySelector('[data-tab="requests"]'); const historyTab = document.querySelector('[data-tab="history"]'); if (!downloadsTab || !historyTab) return; // Load saved tab const savedTab = getActiveTab(); - if (savedTab === 'history') { + if (savedTab === 'requests') { + activateTab('requests'); + } else if (savedTab === 'history') { activateTab('history'); } else { activateTab('downloads'); } downloadsTab.addEventListener('click', () => activateTab('downloads')); + if (requestsTab) { + requestsTab.addEventListener('click', () => activateTab('requests')); + } historyTab.addEventListener('click', () => activateTab('history')); } export function activateTab(tab) { const downloadsTab = document.querySelector('[data-tab="downloads"]'); + const requestsTab = document.querySelector('[data-tab="requests"]'); const historyTab = document.querySelector('[data-tab="history"]'); const downloadsSection = document.getElementById('tab-downloads'); + const requestsSection = document.getElementById('tab-requests'); const historySection = document.getElementById('tab-history'); + // Remove active class from all tabs + if (downloadsTab) downloadsTab.classList.remove('active'); + if (requestsTab) requestsTab.classList.remove('active'); + if (historyTab) historyTab.classList.remove('active'); + + // Hide all sections + if (downloadsSection) downloadsSection.classList.add('hidden'); + if (requestsSection) requestsSection.classList.add('hidden'); + if (historySection) historySection.classList.add('hidden'); + if (tab === 'downloads') { - downloadsTab.classList.add('active'); - historyTab.classList.remove('active'); - downloadsSection.classList.remove('hidden'); - historySection.classList.add('hidden'); + if (downloadsTab) downloadsTab.classList.add('active'); + if (downloadsSection) downloadsSection.classList.remove('hidden'); saveActiveTab('downloads'); + } else if (tab === 'requests') { + if (requestsTab) requestsTab.classList.add('active'); + if (requestsSection) requestsSection.classList.remove('hidden'); + saveActiveTab('requests'); + setupRequestsTab(); } else if (tab === 'history') { - historyTab.classList.add('active'); - downloadsTab.classList.remove('active'); - historySection.classList.remove('hidden'); - downloadsSection.classList.add('hidden'); + if (historyTab) historyTab.classList.add('active'); + if (historySection) historySection.classList.remove('hidden'); saveActiveTab('history'); loadHistory(); } diff --git a/client/src/ui/webhooks.js b/client/src/ui/webhooks.js index 55f720e..9da0a12 100644 --- a/client/src/ui/webhooks.js +++ b/client/src/ui/webhooks.js @@ -1,7 +1,7 @@ // Copyright (c) 2026 Gordon Bolton. MIT License. import { state } from '../state.js'; -import { fetchWebhookStatus as apiFetchWebhookStatus, enableSonarrWebhook as apiEnableSonarrWebhook, enableRadarrWebhook as apiEnableRadarrWebhook, testSonarrWebhook as apiTestSonarrWebhook, testRadarrWebhook as apiTestRadarrWebhook } from '../api.js'; +import { fetchWebhookStatus as apiFetchWebhookStatus, enableSonarrWebhook as apiEnableSonarrWebhook, enableRadarrWebhook as apiEnableRadarrWebhook, enableOmbiWebhook as apiEnableOmbiWebhook, testSonarrWebhook as apiTestSonarrWebhook, testRadarrWebhook as apiTestRadarrWebhook, testOmbiWebhook as apiTestOmbiWebhook } from '../api.js'; import { formatTimeAgo } from '../utils/format.js'; export function initWebhooks() { @@ -13,8 +13,10 @@ export function initWebhooks() { document.getElementById('webhooks-header').addEventListener('click', toggleWebhookSection); document.getElementById('enable-sonarr-webhook').addEventListener('click', enableSonarrWebhook); document.getElementById('enable-radarr-webhook').addEventListener('click', enableRadarrWebhook); + document.getElementById('enable-ombi-webhook').addEventListener('click', enableOmbiWebhook); document.getElementById('test-sonarr-webhook').addEventListener('click', testSonarrWebhook); document.getElementById('test-radarr-webhook').addEventListener('click', testRadarrWebhook); + document.getElementById('test-ombi-webhook').addEventListener('click', testOmbiWebhook); } export function toggleWebhookSection() { @@ -58,9 +60,9 @@ export function renderWebhookStatus() { const sonarrTriggers = document.getElementById('sonarr-triggers'); const sonarrStats = document.getElementById('sonarr-stats'); - sonarrStatus.textContent = sonarrWebhook.enabled ? '● Enabled' : '○ Disabled'; - sonarrStatus.className = 'status-indicator ' + (sonarrWebhook.enabled ? 'enabled' : 'disabled'); - if (sonarrWebhook.enabled) { + sonarrStatus.textContent = state.sonarrWebhook.enabled ? '● Enabled' : '○ Disabled'; + sonarrStatus.className = 'status-indicator ' + (state.sonarrWebhook.enabled ? 'enabled' : 'disabled'); + if (state.sonarrWebhook.enabled) { sonarrEnableBtn.classList.add('hidden'); sonarrTestBtn.classList.remove('hidden'); sonarrTriggers.classList.remove('hidden'); @@ -70,22 +72,22 @@ export function renderWebhookStatus() { sonarrTriggers.classList.add('hidden'); } - if (sonarrWebhook.enabled) { - document.getElementById('sonarr-onGrab').textContent = sonarrWebhook.triggers.onGrab ? '✓' : '✗'; - document.getElementById('sonarr-onGrab').className = 'trigger-value ' + (sonarrWebhook.triggers.onGrab ? 'active' : 'inactive'); - document.getElementById('sonarr-onDownload').textContent = sonarrWebhook.triggers.onDownload ? '✓' : '✗'; - document.getElementById('sonarr-onDownload').className = 'trigger-value ' + (sonarrWebhook.triggers.onDownload ? 'active' : 'inactive'); - document.getElementById('sonarr-onImport').textContent = sonarrWebhook.triggers.onImport ? '✓' : '✗'; - document.getElementById('sonarr-onImport').className = 'trigger-value ' + (sonarrWebhook.triggers.onImport ? 'active' : 'inactive'); - document.getElementById('sonarr-onUpgrade').textContent = sonarrWebhook.triggers.onUpgrade ? '✓' : '✗'; - document.getElementById('sonarr-onUpgrade').className = 'trigger-value ' + (sonarrWebhook.triggers.onUpgrade ? 'active' : 'inactive'); + if (state.sonarrWebhook.enabled) { + document.getElementById('sonarr-onGrab').textContent = state.sonarrWebhook.triggers.onGrab ? '✓' : '✗'; + document.getElementById('sonarr-onGrab').className = 'trigger-value ' + (state.sonarrWebhook.triggers.onGrab ? 'active' : 'inactive'); + document.getElementById('sonarr-onDownload').textContent = state.sonarrWebhook.triggers.onDownload ? '✓' : '✗'; + document.getElementById('sonarr-onDownload').className = 'trigger-value ' + (state.sonarrWebhook.triggers.onDownload ? 'active' : 'inactive'); + document.getElementById('sonarr-onImport').textContent = state.sonarrWebhook.triggers.onImport ? '✓' : '✗'; + document.getElementById('sonarr-onImport').className = 'trigger-value ' + (state.sonarrWebhook.triggers.onImport ? 'active' : 'inactive'); + document.getElementById('sonarr-onUpgrade').textContent = state.sonarrWebhook.triggers.onUpgrade ? '✓' : '✗'; + document.getElementById('sonarr-onUpgrade').className = 'trigger-value ' + (state.sonarrWebhook.triggers.onUpgrade ? 'active' : 'inactive'); } - if (sonarrWebhook.stats) { + if (state.sonarrWebhook.stats) { sonarrStats.classList.remove('hidden'); - document.getElementById('sonarr-events').textContent = sonarrWebhook.stats.eventsReceived ?? 0; - document.getElementById('sonarr-polls').textContent = sonarrWebhook.stats.pollsSkipped ?? 0; - document.getElementById('sonarr-last').textContent = formatTimeAgo(sonarrWebhook.stats.lastWebhookTimestamp); + document.getElementById('sonarr-events').textContent = state.sonarrWebhook.stats.eventsReceived ?? 0; + document.getElementById('sonarr-polls').textContent = state.sonarrWebhook.stats.pollsSkipped ?? 0; + document.getElementById('sonarr-last').textContent = formatTimeAgo(state.sonarrWebhook.stats.lastWebhookTimestamp); } else { sonarrStats.classList.add('hidden'); } @@ -97,9 +99,9 @@ export function renderWebhookStatus() { const radarrTriggers = document.getElementById('radarr-triggers'); const radarrStats = document.getElementById('radarr-stats'); - radarrStatus.textContent = radarrWebhook.enabled ? '● Enabled' : '○ Disabled'; - radarrStatus.className = 'status-indicator ' + (radarrWebhook.enabled ? 'enabled' : 'disabled'); - if (radarrWebhook.enabled) { + radarrStatus.textContent = state.radarrWebhook.enabled ? '● Enabled' : '○ Disabled'; + radarrStatus.className = 'status-indicator ' + (state.radarrWebhook.enabled ? 'enabled' : 'disabled'); + if (state.radarrWebhook.enabled) { radarrEnableBtn.classList.add('hidden'); radarrTestBtn.classList.remove('hidden'); radarrTriggers.classList.remove('hidden'); @@ -109,25 +111,66 @@ export function renderWebhookStatus() { radarrTriggers.classList.add('hidden'); } - if (radarrWebhook.enabled) { - document.getElementById('radarr-onGrab').textContent = radarrWebhook.triggers.onGrab ? '✓' : '✗'; - document.getElementById('radarr-onGrab').className = 'trigger-value ' + (radarrWebhook.triggers.onGrab ? 'active' : 'inactive'); - document.getElementById('radarr-onDownload').textContent = radarrWebhook.triggers.onDownload ? '✓' : '✗'; - document.getElementById('radarr-onDownload').className = 'trigger-value ' + (radarrWebhook.triggers.onDownload ? 'active' : 'inactive'); - document.getElementById('radarr-onImport').textContent = radarrWebhook.triggers.onImport ? '✓' : '✗'; - document.getElementById('radarr-onImport').className = 'trigger-value ' + (radarrWebhook.triggers.onImport ? 'active' : 'inactive'); - document.getElementById('radarr-onUpgrade').textContent = radarrWebhook.triggers.onUpgrade ? '✓' : '✗'; - document.getElementById('radarr-onUpgrade').className = 'trigger-value ' + (radarrWebhook.triggers.onUpgrade ? 'active' : 'inactive'); + if (state.radarrWebhook.enabled) { + document.getElementById('radarr-onGrab').textContent = state.radarrWebhook.triggers.onGrab ? '✓' : '✗'; + document.getElementById('radarr-onGrab').className = 'trigger-value ' + (state.radarrWebhook.triggers.onGrab ? 'active' : 'inactive'); + document.getElementById('radarr-onDownload').textContent = state.radarrWebhook.triggers.onDownload ? '✓' : '✗'; + document.getElementById('radarr-onDownload').className = 'trigger-value ' + (state.radarrWebhook.triggers.onDownload ? 'active' : 'inactive'); + document.getElementById('radarr-onImport').textContent = state.radarrWebhook.triggers.onImport ? '✓' : '✗'; + document.getElementById('radarr-onImport').className = 'trigger-value ' + (state.radarrWebhook.triggers.onImport ? 'active' : 'inactive'); + document.getElementById('radarr-onUpgrade').textContent = state.radarrWebhook.triggers.onUpgrade ? '✓' : '✗'; + document.getElementById('radarr-onUpgrade').className = 'trigger-value ' + (state.radarrWebhook.triggers.onUpgrade ? 'active' : 'inactive'); } - if (radarrWebhook.stats) { + if (state.radarrWebhook.stats) { radarrStats.classList.remove('hidden'); - document.getElementById('radarr-events').textContent = radarrWebhook.stats.eventsReceived ?? 0; - document.getElementById('radarr-polls').textContent = radarrWebhook.stats.pollsSkipped ?? 0; - document.getElementById('radarr-last').textContent = formatTimeAgo(radarrWebhook.stats.lastWebhookTimestamp); + document.getElementById('radarr-events').textContent = state.radarrWebhook.stats.eventsReceived ?? 0; + document.getElementById('radarr-polls').textContent = state.radarrWebhook.stats.pollsSkipped ?? 0; + document.getElementById('radarr-last').textContent = formatTimeAgo(state.radarrWebhook.stats.lastWebhookTimestamp); } else { radarrStats.classList.add('hidden'); } + + // Ombi + const ombiStatus = document.getElementById('ombi-status'); + const ombiEnableBtn = document.getElementById('enable-ombi-webhook'); + const ombiTestBtn = document.getElementById('test-ombi-webhook'); + const ombiTriggers = document.getElementById('ombi-triggers'); + const ombiStats = document.getElementById('ombi-stats'); + + ombiStatus.textContent = state.ombiWebhook.enabled ? '● Enabled' : '○ Disabled'; + ombiStatus.className = 'status-indicator ' + (state.ombiWebhook.enabled ? 'enabled' : 'disabled'); + if (state.ombiWebhook.enabled) { + ombiEnableBtn.classList.add('hidden'); + ombiTestBtn.classList.remove('hidden'); + ombiTriggers.classList.remove('hidden'); + } else { + ombiEnableBtn.classList.remove('hidden'); + ombiTestBtn.classList.add('hidden'); + ombiTriggers.classList.add('hidden'); + } + + if (state.ombiWebhook.enabled) { + document.getElementById('ombi-requestAvailable').textContent = state.ombiWebhook.triggers.requestAvailable ? '✓' : '✗'; + document.getElementById('ombi-requestAvailable').className = 'trigger-value ' + (state.ombiWebhook.triggers.requestAvailable ? 'active' : 'inactive'); + document.getElementById('ombi-requestApproved').textContent = state.ombiWebhook.triggers.requestApproved ? '✓' : '✗'; + document.getElementById('ombi-requestApproved').className = 'trigger-value ' + (state.ombiWebhook.triggers.requestApproved ? 'active' : 'inactive'); + document.getElementById('ombi-requestDeclined').textContent = state.ombiWebhook.triggers.requestDeclined ? '✓' : '✗'; + document.getElementById('ombi-requestDeclined').className = 'trigger-value ' + (state.ombiWebhook.triggers.requestDeclined ? 'active' : 'inactive'); + document.getElementById('ombi-requestPending').textContent = state.ombiWebhook.triggers.requestPending ? '✓' : '✗'; + document.getElementById('ombi-requestPending').className = 'trigger-value ' + (state.ombiWebhook.triggers.requestPending ? 'active' : 'inactive'); + document.getElementById('ombi-requestProcessing').textContent = state.ombiWebhook.triggers.requestProcessing ? '✓' : '✗'; + document.getElementById('ombi-requestProcessing').className = 'trigger-value ' + (state.ombiWebhook.triggers.requestProcessing ? 'active' : 'inactive'); + } + + if (state.ombiWebhook.stats) { + ombiStats.classList.remove('hidden'); + document.getElementById('ombi-events').textContent = state.ombiWebhook.stats.eventsReceived ?? 0; + document.getElementById('ombi-polls').textContent = state.ombiWebhook.stats.pollsSkipped ?? 0; + document.getElementById('ombi-last').textContent = formatTimeAgo(state.ombiWebhook.stats.lastWebhookTimestamp); + } else { + ombiStats.classList.add('hidden'); + } } export async function enableSonarrWebhook() { @@ -198,12 +241,48 @@ export async function testRadarrWebhook() { } } +export async function enableOmbiWebhook() { + setWebhookLoading(true); + try { + const result = await apiEnableOmbiWebhook(); + if (!result.success) { + console.error('Failed to enable Ombi webhook:', result.error); + alert('Failed to enable Ombi webhook. Check console for details.'); + } + } catch (err) { + console.error('Failed to enable Ombi webhook:', err); + alert('Failed to enable Ombi webhook. Check console for details.'); + } finally { + setWebhookLoading(false); + } +} + +export async function testOmbiWebhook() { + setWebhookLoading(true); + try { + const result = await apiTestOmbiWebhook(); + if (result.success) { + alert('Ombi webhook test sent successfully!'); + } else { + console.error('Failed to test Ombi webhook:', result.error); + alert('Failed to test Ombi webhook. Check console for details.'); + } + } catch (err) { + console.error('Failed to test Ombi webhook:', err); + alert('Failed to test Ombi webhook. Check console for details.'); + } finally { + setWebhookLoading(false); + } +} + export function setWebhookLoading(loading) { state.webhookLoading = loading; document.getElementById('enable-sonarr-webhook').disabled = loading; document.getElementById('enable-radarr-webhook').disabled = loading; + document.getElementById('enable-ombi-webhook').disabled = loading; document.getElementById('test-sonarr-webhook').disabled = loading; document.getElementById('test-radarr-webhook').disabled = loading; + document.getElementById('test-ombi-webhook').disabled = loading; const loadingEl = document.getElementById('webhook-loading'); if (loading) { loadingEl.classList.remove('hidden'); diff --git a/public/index.html b/public/index.html index 7356847..4939009 100644 --- a/public/index.html +++ b/public/index.html @@ -129,6 +129,31 @@ + + +
+

Ombi

+
+ ○ Disabled + + +
+ + +
@@ -139,6 +164,7 @@
+
@@ -172,6 +198,18 @@
+ +