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
This commit is contained in:
2026-05-21 20:59:06 +01:00
parent 884fb5285f
commit 1dccda529a
13 changed files with 1186 additions and 54 deletions
+150
View File
@@ -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();
}