feat(webhooks): display webhook statistics (events received, polls skipped, last event) in status panel
All checks were successful
All checks were successful
This commit is contained in:
@@ -459,3 +459,41 @@ body {
|
||||
.trigger-value.inactive {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.webhook-stats {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.webhook-stats-title {
|
||||
color: #999;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.webhook-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.webhook-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.webhook-stat-label {
|
||||
color: #999;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.webhook-stat-value {
|
||||
color: #333;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ function App() {
|
||||
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 [sonarrWebhook, setSonarrWebhook] = useState({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false }, stats: null });
|
||||
const [radarrWebhook, setRadarrWebhook] = useState({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false }, stats: null });
|
||||
const [webhookMetrics, setWebhookMetrics] = useState(null);
|
||||
const [webhookLoading, setWebhookLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,43 +73,82 @@ function App() {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const formatTimeAgo = (timestamp) => {
|
||||
if (!timestamp) return 'Never';
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return `${Math.floor(hours / 24)}d ago`;
|
||||
};
|
||||
|
||||
const fetchWebhookMetrics = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/dashboard/webhook-metrics');
|
||||
setWebhookMetrics(response.data);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
// Not fatal — stats just won't display
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchWebhookStatus = async () => {
|
||||
try {
|
||||
// Fetch metrics in parallel with notification status
|
||||
const metricsPromise = fetchWebhookMetrics();
|
||||
|
||||
// Fetch Sonarr notifications
|
||||
let sonarrEnabled = false;
|
||||
let sonarrTriggers = { onGrab: false, onDownload: false, onImport: false, onUpgrade: false };
|
||||
try {
|
||||
const sonarrResponse = await axios.get('/api/sonarr/notifications');
|
||||
const sonarrSofarr = sonarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
setSonarrWebhook({
|
||||
enabled: !!sonarrSofarr,
|
||||
triggers: sonarrSofarr ? {
|
||||
sonarrEnabled = !!sonarrSofarr;
|
||||
if (sonarrSofarr) {
|
||||
sonarrTriggers = {
|
||||
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
|
||||
let radarrEnabled = false;
|
||||
let radarrTriggers = { onGrab: false, onDownload: false, onImport: false, onUpgrade: false };
|
||||
try {
|
||||
const radarrResponse = await axios.get('/api/radarr/notifications');
|
||||
const radarrSofarr = radarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
setRadarrWebhook({
|
||||
enabled: !!radarrSofarr,
|
||||
triggers: radarrSofarr ? {
|
||||
radarrEnabled = !!radarrSofarr;
|
||||
if (radarrSofarr) {
|
||||
radarrTriggers = {
|
||||
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 } });
|
||||
}
|
||||
|
||||
const metrics = await metricsPromise;
|
||||
|
||||
// Attach per-instance stats from global metrics.
|
||||
// The instances object is keyed by instance URL; we pick the first
|
||||
// sonarr/radarr entry by matching env-configured URLs.
|
||||
const instanceEntries = metrics ? Object.entries(metrics.instances || {}) : [];
|
||||
const sonarrStats = instanceEntries.find(([url]) => url.includes('sonarr'))?.[1] || null;
|
||||
const radarrStats = instanceEntries.find(([url]) => url.includes('radarr'))?.[1] || null;
|
||||
|
||||
setSonarrWebhook({ enabled: sonarrEnabled, triggers: sonarrTriggers, stats: sonarrStats });
|
||||
setRadarrWebhook({ enabled: radarrEnabled, triggers: radarrTriggers, stats: radarrStats });
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch webhook status:', err);
|
||||
}
|
||||
@@ -147,6 +187,7 @@ function App() {
|
||||
const sonarrSofarr = sonarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
if (sonarrSofarr) {
|
||||
await axios.post('/api/sonarr/notifications/test', { id: sonarrSofarr.id });
|
||||
await fetchWebhookStatus();
|
||||
alert('Sonarr webhook test sent successfully!');
|
||||
} else {
|
||||
alert('Sofarr webhook not configured for Sonarr.');
|
||||
@@ -166,6 +207,7 @@ function App() {
|
||||
const radarrSofarr = radarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
if (radarrSofarr) {
|
||||
await axios.post('/api/radarr/notifications/test', { id: radarrSofarr.id });
|
||||
await fetchWebhookStatus();
|
||||
alert('Radarr webhook test sent successfully!');
|
||||
} else {
|
||||
alert('Sofarr webhook not configured for Radarr.');
|
||||
@@ -342,6 +384,25 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sonarrWebhook.stats && (
|
||||
<div className="webhook-stats">
|
||||
<div className="webhook-stats-title">Statistics</div>
|
||||
<div className="webhook-stats-grid">
|
||||
<div className="webhook-stat">
|
||||
<span className="webhook-stat-label">Events Received</span>
|
||||
<span className="webhook-stat-value">{sonarrWebhook.stats.eventsReceived ?? 0}</span>
|
||||
</div>
|
||||
<div className="webhook-stat">
|
||||
<span className="webhook-stat-label">Polls Skipped</span>
|
||||
<span className="webhook-stat-value">{sonarrWebhook.stats.pollsSkipped ?? 0}</span>
|
||||
</div>
|
||||
<div className="webhook-stat">
|
||||
<span className="webhook-stat-label">Last Event</span>
|
||||
<span className="webhook-stat-value">{formatTimeAgo(sonarrWebhook.stats.lastWebhookTimestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="webhook-instance">
|
||||
<h3>Radarr</h3>
|
||||
@@ -388,6 +449,25 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{radarrWebhook.stats && (
|
||||
<div className="webhook-stats">
|
||||
<div className="webhook-stats-title">Statistics</div>
|
||||
<div className="webhook-stats-grid">
|
||||
<div className="webhook-stat">
|
||||
<span className="webhook-stat-label">Events Received</span>
|
||||
<span className="webhook-stat-value">{radarrWebhook.stats.eventsReceived ?? 0}</span>
|
||||
</div>
|
||||
<div className="webhook-stat">
|
||||
<span className="webhook-stat-label">Polls Skipped</span>
|
||||
<span className="webhook-stat-value">{radarrWebhook.stats.pollsSkipped ?? 0}</span>
|
||||
</div>
|
||||
<div className="webhook-stat">
|
||||
<span className="webhook-stat-label">Last Event</span>
|
||||
<span className="webhook-stat-value">{formatTimeAgo(radarrWebhook.stats.lastWebhookTimestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user