diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e7b9c..379421c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.10] - 2026-05-24 + +### Fixed + +- **Ombi webhook race condition fix** — Introduced a 2000ms delay in the webhook background worker for Ombi events before fetching new requests. This resolves a race condition where Ombi triggers the webhook before its database has committed the new request, which previously caused the request to be missing from the subsequent API fetch. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42). +- **Ombi webhook statistics in status panel** — Integrated Ombi webhook metrics into the admin status panel. The backend now retrieves the Ombi webhook configuration status and aggregates its events received and polls skipped metrics. The frontend displays these metrics (consistently formatted as `O:`) under the Webhooks status card. + +--- + ## [1.7.9] - 2026-05-23 ### Fixed diff --git a/client/src/ui/statusPanel.js b/client/src/ui/statusPanel.js index 57e58c0..bd81b09 100644 --- a/client/src/ui/statusPanel.js +++ b/client/src/ui/statusPanel.js @@ -118,18 +118,22 @@ export function renderStatusPanel(data, panel) { const wh = data.webhooks; const sonarrEnabled = wh.sonarr?.enabled ? '●' : '○'; const radarrEnabled = wh.radarr?.enabled ? '●' : '○'; + const ombiEnabled = wh.ombi?.enabled ? '●' : '○'; const sonarrEvents = wh.sonarr?.eventsReceived || 0; const radarrEvents = wh.radarr?.eventsReceived || 0; + const ombiEvents = wh.ombi?.eventsReceived || 0; const sonarrPolls = wh.sonarr?.pollsSkipped || 0; const radarrPolls = wh.radarr?.pollsSkipped || 0; + const ombiPolls = wh.ombi?.pollsSkipped || 0; html += `
Webhooks
Sonarr${sonarrEnabled} ${wh.sonarr?.enabled ? 'Enabled' : 'Disabled'}
Radarr${radarrEnabled} ${wh.radarr?.enabled ? 'Enabled' : 'Disabled'}
-
EventsS:${sonarrEvents} R:${radarrEvents}
-
Polls skippedS:${sonarrPolls} R:${radarrPolls}
+
Ombi${ombiEnabled} ${wh.ombi?.enabled ? 'Enabled' : 'Disabled'}
+
EventsS:${sonarrEvents} R:${radarrEvents} O:${ombiEvents}
+
Polls skippedS:${sonarrPolls} R:${radarrPolls} O:${ombiPolls}
`; } diff --git a/package-lock.json b/package-lock.json index d20f148..800ad9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sofarr", - "version": "1.7.9", + "version": "1.7.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sofarr", - "version": "1.7.9", + "version": "1.7.10", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/package.json b/package.json index adb8225..6d0eec4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sofarr", - "version": "1.7.9", + "version": "1.7.10", "description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish", "main": "server/index.js", "scripts": { diff --git a/server/routes/status.js b/server/routes/status.js index d826ffa..a6fb68f 100644 --- a/server/routes/status.js +++ b/server/routes/status.js @@ -4,9 +4,9 @@ const router = express.Router(); const requireAuth = require('../middleware/requireAuth'); const cache = require('../utils/cache'); const { getLastPollTimings, POLLING_ENABLED } = require('../utils/poller'); -const { getSonarrInstances, getRadarrInstances } = require('../utils/config'); +const { getSonarrInstances, getRadarrInstances, getOmbiInstances } = require('../utils/config'); const { getGlobalWebhookMetrics } = require('../utils/cache'); -const { checkWebhookConfigured, aggregateMetrics } = require('../services/WebhookStatus'); +const { checkWebhookConfigured, checkOmbiWebhookConfigured, aggregateMetrics } = require('../services/WebhookStatus'); /** * @openapi @@ -121,6 +121,7 @@ router.get('/', requireAuth, async (req, res) => { // Check webhook configuration for each service const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); + const ombiInstances = getOmbiInstances(); const sonarrWebhookConfigured = sonarrInstances.length > 0 ? await checkWebhookConfigured(sonarrInstances[0], 'Sonarr') @@ -128,15 +129,21 @@ router.get('/', requireAuth, async (req, res) => { const radarrWebhookConfigured = radarrInstances.length > 0 ? await checkWebhookConfigured(radarrInstances[0], 'Radarr') : false; + const ombiWebhookConfigured = ombiInstances.length > 0 + ? await checkOmbiWebhookConfigured(ombiInstances[0]) + : false; - // Find Sonarr and Radarr metrics from instances + // Find Sonarr, Radarr, and Ombi metrics from instances const sonarrMetrics = {}; const radarrMetrics = {}; + const ombiMetrics = {}; for (const [url, metrics] of Object.entries(webhookMetrics.instances || {})) { if (url.includes('sonarr')) { sonarrMetrics[url] = metrics; } else if (url.includes('radarr')) { radarrMetrics[url] = metrics; + } else if (url.includes('ombi') || (ombiInstances.length > 0 && url === ombiInstances[0].url)) { + ombiMetrics[url] = metrics; } } @@ -156,7 +163,8 @@ router.get('/', requireAuth, async (req, res) => { cache: cacheStats, webhooks: { sonarr: aggregateMetrics(sonarrMetrics, sonarrWebhookConfigured), - radarr: aggregateMetrics(radarrMetrics, radarrWebhookConfigured) + radarr: aggregateMetrics(radarrMetrics, radarrWebhookConfigured), + ombi: aggregateMetrics(ombiMetrics, ombiWebhookConfigured) } }); } catch (err) { diff --git a/server/routes/webhook.js b/server/routes/webhook.js index bccc8f5..3ceb170 100644 --- a/server/routes/webhook.js +++ b/server/routes/webhook.js @@ -259,6 +259,8 @@ async function processWebhookEvent(serviceType, eventType) { const ombiInstances = getOmbiInstances(); if (affectsOmbi) { + // Add a 2000ms delay to resolve the race condition where Ombi fires the webhook before committing to DB + await new Promise(r => setTimeout(r, 2000)); const ombiRequests = await arrRetrieverRegistry.getOmbiRequests(true); cache.set('poll:ombi-requests', ombiRequests, CACHE_TTL); logToFile(`[Webhook] Refreshed poll:ombi-requests (${ombiRequests.movie?.length || 0} movies, ${ombiRequests.tv?.length || 0} TV shows)`); diff --git a/server/services/WebhookStatus.js b/server/services/WebhookStatus.js index f13f556..3597570 100644 --- a/server/services/WebhookStatus.js +++ b/server/services/WebhookStatus.js @@ -49,7 +49,26 @@ function aggregateMetrics(metricsMap, configured) { }; } +/** + * Check if Sofarr webhook is configured in an Ombi instance. + * @param {Object} instance - The Ombi instance config + * @returns {Promise} true if webhook is configured + */ +async function checkOmbiWebhookConfigured(instance) { + try { + const response = await axios.get(`${instance.url}/api/v1/Settings/notifications/webhook`, { + headers: { 'ApiKey': instance.apiKey }, + timeout: 5000 + }); + return !!(response.data && response.data.enabled); + } catch (err) { + console.log(`[WebhookStatus] Failed to check Ombi webhook config: ${err.message}`); + return false; + } +} + module.exports = { checkWebhookConfigured, + checkOmbiWebhookConfigured, aggregateMetrics };