fix: resolve Ombi webhook race condition and add Ombi webhook status metrics to status panel
CI / Security audit (push) Successful in 1m45s
Build and Push Docker Image / build (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
CI / Swagger Validation & Coverage (push) Has been cancelled

This commit is contained in:
2026-05-24 10:22:47 +01:00
parent 64c872423f
commit 9491948ec9
4 changed files with 39 additions and 6 deletions
+6 -2
View File
@@ -118,18 +118,22 @@ export function renderStatusPanel(data, panel) {
const wh = data.webhooks; const wh = data.webhooks;
const sonarrEnabled = wh.sonarr?.enabled ? '●' : '○'; const sonarrEnabled = wh.sonarr?.enabled ? '●' : '○';
const radarrEnabled = wh.radarr?.enabled ? '●' : '○'; const radarrEnabled = wh.radarr?.enabled ? '●' : '○';
const ombiEnabled = wh.ombi?.enabled ? '●' : '○';
const sonarrEvents = wh.sonarr?.eventsReceived || 0; const sonarrEvents = wh.sonarr?.eventsReceived || 0;
const radarrEvents = wh.radarr?.eventsReceived || 0; const radarrEvents = wh.radarr?.eventsReceived || 0;
const ombiEvents = wh.ombi?.eventsReceived || 0;
const sonarrPolls = wh.sonarr?.pollsSkipped || 0; const sonarrPolls = wh.sonarr?.pollsSkipped || 0;
const radarrPolls = wh.radarr?.pollsSkipped || 0; const radarrPolls = wh.radarr?.pollsSkipped || 0;
const ombiPolls = wh.ombi?.pollsSkipped || 0;
html += ` html += `
<div class="status-card"> <div class="status-card">
<div class="status-card-title">Webhooks</div> <div class="status-card-title">Webhooks</div>
<div class="status-row"><span>Sonarr</span><span>${sonarrEnabled} ${wh.sonarr?.enabled ? 'Enabled' : 'Disabled'}</span></div> <div class="status-row"><span>Sonarr</span><span>${sonarrEnabled} ${wh.sonarr?.enabled ? 'Enabled' : 'Disabled'}</span></div>
<div class="status-row"><span>Radarr</span><span>${radarrEnabled} ${wh.radarr?.enabled ? 'Enabled' : 'Disabled'}</span></div> <div class="status-row"><span>Radarr</span><span>${radarrEnabled} ${wh.radarr?.enabled ? 'Enabled' : 'Disabled'}</span></div>
<div class="status-row status-row-sub"><span>Events</span><span>S:${sonarrEvents} R:${radarrEvents}</span></div> <div class="status-row"><span>Ombi</span><span>${ombiEnabled} ${wh.ombi?.enabled ? 'Enabled' : 'Disabled'}</span></div>
<div class="status-row status-row-sub"><span>Polls skipped</span><span>S:${sonarrPolls} R:${radarrPolls}</span></div> <div class="status-row status-row-sub"><span>Events</span><span>S:${sonarrEvents} R:${radarrEvents} O:${ombiEvents}</span></div>
<div class="status-row status-row-sub"><span>Polls skipped</span><span>S:${sonarrPolls} R:${radarrPolls} O:${ombiPolls}</span></div>
</div>`; </div>`;
} }
+12 -4
View File
@@ -4,9 +4,9 @@ const router = express.Router();
const requireAuth = require('../middleware/requireAuth'); const requireAuth = require('../middleware/requireAuth');
const cache = require('../utils/cache'); const cache = require('../utils/cache');
const { getLastPollTimings, POLLING_ENABLED } = require('../utils/poller'); 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 { getGlobalWebhookMetrics } = require('../utils/cache');
const { checkWebhookConfigured, aggregateMetrics } = require('../services/WebhookStatus'); const { checkWebhookConfigured, checkOmbiWebhookConfigured, aggregateMetrics } = require('../services/WebhookStatus');
/** /**
* @openapi * @openapi
@@ -121,6 +121,7 @@ router.get('/', requireAuth, async (req, res) => {
// Check webhook configuration for each service // Check webhook configuration for each service
const sonarrInstances = getSonarrInstances(); const sonarrInstances = getSonarrInstances();
const radarrInstances = getRadarrInstances(); const radarrInstances = getRadarrInstances();
const ombiInstances = getOmbiInstances();
const sonarrWebhookConfigured = sonarrInstances.length > 0 const sonarrWebhookConfigured = sonarrInstances.length > 0
? await checkWebhookConfigured(sonarrInstances[0], 'Sonarr') ? await checkWebhookConfigured(sonarrInstances[0], 'Sonarr')
@@ -128,15 +129,21 @@ router.get('/', requireAuth, async (req, res) => {
const radarrWebhookConfigured = radarrInstances.length > 0 const radarrWebhookConfigured = radarrInstances.length > 0
? await checkWebhookConfigured(radarrInstances[0], 'Radarr') ? await checkWebhookConfigured(radarrInstances[0], 'Radarr')
: false; : 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 sonarrMetrics = {};
const radarrMetrics = {}; const radarrMetrics = {};
const ombiMetrics = {};
for (const [url, metrics] of Object.entries(webhookMetrics.instances || {})) { for (const [url, metrics] of Object.entries(webhookMetrics.instances || {})) {
if (url.includes('sonarr')) { if (url.includes('sonarr')) {
sonarrMetrics[url] = metrics; sonarrMetrics[url] = metrics;
} else if (url.includes('radarr')) { } else if (url.includes('radarr')) {
radarrMetrics[url] = metrics; 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, cache: cacheStats,
webhooks: { webhooks: {
sonarr: aggregateMetrics(sonarrMetrics, sonarrWebhookConfigured), sonarr: aggregateMetrics(sonarrMetrics, sonarrWebhookConfigured),
radarr: aggregateMetrics(radarrMetrics, radarrWebhookConfigured) radarr: aggregateMetrics(radarrMetrics, radarrWebhookConfigured),
ombi: aggregateMetrics(ombiMetrics, ombiWebhookConfigured)
} }
}); });
} catch (err) { } catch (err) {
+2
View File
@@ -259,6 +259,8 @@ async function processWebhookEvent(serviceType, eventType) {
const ombiInstances = getOmbiInstances(); const ombiInstances = getOmbiInstances();
if (affectsOmbi) { 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); const ombiRequests = await arrRetrieverRegistry.getOmbiRequests(true);
cache.set('poll:ombi-requests', ombiRequests, CACHE_TTL); 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)`); logToFile(`[Webhook] Refreshed poll:ombi-requests (${ombiRequests.movie?.length || 0} movies, ${ombiRequests.tv?.length || 0} TV shows)`);
+19
View File
@@ -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<boolean>} 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 = { module.exports = {
checkWebhookConfigured, checkWebhookConfigured,
checkOmbiWebhookConfigured,
aggregateMetrics aggregateMetrics
}; };