ed4237debb
Docs Check / Markdown lint (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m1s
CI / Security audit (push) Successful in 2m48s
Docs Check / Mermaid diagram parse check (push) Successful in 3m8s
CI / Tests & coverage (push) Failing after 3m33s
CI / Swagger Validation & Coverage (push) Successful in 3m34s
Build and Push Docker Image / build (push) Successful in 4m36s
- Add OmbiRetriever extending ArrRetriever for PALDRA compliance - Add OmbiClient for low-level Ombi API communication - Add getOmbiInstances() to config.js following multi-instance pattern - Register Ombi in PALDRA registry with Ombi-specific methods - Add external ID matching (TMDB/TVDB/IMDB) to Ombi requests - Update DownloadMatcher to be async and enrich downloads with Ombi links - Add getOmbiLink/getOmbiSearchLink helpers to DownloadAssembler - Implement new service icon layout (Ombi + Sonarr/Radarr icons) - Add CSS styling for service icons - Update dashboard routes to include Ombi configuration - Extend OpenAPI with Ombi tag and NormalizedDownload properties - Update documentation (README, ARCHITECTURE, SECURITY, CHANGELOG) - Add Ombi configuration to .env.sample
284 lines
9.2 KiB
JavaScript
284 lines
9.2 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
import { state, HISTORY_REFRESH_MS } from '../state.js';
|
|
import { loadHistory as apiLoadHistory } from '../api.js';
|
|
import { saveHistoryDays, saveIgnoreAvailable } from '../utils/storage.js';
|
|
import { formatDate, formatEpisodeInfo, escapeHtml } from '../utils/format.js';
|
|
import { renderTagBadges } from './downloads.js';
|
|
|
|
function createServiceIcons(item) {
|
|
const container = document.createElement('span');
|
|
container.className = 'service-icons-container';
|
|
|
|
// Add Ombi icon for all users if ombiLink exists
|
|
if (item.ombiLink) {
|
|
const ombiIcon = document.createElement('img');
|
|
ombiIcon.className = 'service-icon ombi';
|
|
ombiIcon.src = '/images/ombi.svg';
|
|
ombiIcon.alt = 'Ombi';
|
|
ombiIcon.title = item.ombiTooltip || 'Ombi';
|
|
const ombiLink = document.createElement('a');
|
|
ombiLink.href = item.ombiLink;
|
|
ombiLink.target = '_blank';
|
|
ombiLink.appendChild(ombiIcon);
|
|
container.appendChild(ombiLink);
|
|
}
|
|
|
|
// Add Sonarr/Radarr icon for admin users if arrLink exists
|
|
if (state.isAdmin && item.arrLink) {
|
|
const arrIcon = document.createElement('img');
|
|
if (item.arrType === 'sonarr') {
|
|
arrIcon.className = 'service-icon sonarr';
|
|
arrIcon.src = '/images/sonarr.svg';
|
|
arrIcon.alt = 'Sonarr';
|
|
} else if (item.arrType === 'radarr') {
|
|
arrIcon.className = 'service-icon radarr';
|
|
arrIcon.src = '/images/radarr.svg';
|
|
arrIcon.alt = 'Radarr';
|
|
}
|
|
arrIcon.title = item.arrType === 'sonarr' ? 'Sonarr' : 'Radarr';
|
|
const arrLink = document.createElement('a');
|
|
arrLink.href = item.arrLink;
|
|
arrLink.target = '_blank';
|
|
arrLink.appendChild(arrIcon);
|
|
container.appendChild(arrLink);
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
export function initHistoryControls() {
|
|
const daysInput = document.getElementById('history-days');
|
|
const refreshBtn = document.getElementById('history-refresh-btn');
|
|
const ignoreToggle = document.getElementById('ignore-available-toggle');
|
|
if (daysInput) {
|
|
daysInput.addEventListener('change', () => {
|
|
const v = parseInt(daysInput.value, 10);
|
|
if (v > 0 && v <= 90) {
|
|
historyDays = v;
|
|
saveHistoryDays(v);
|
|
loadHistory(true);
|
|
}
|
|
});
|
|
}
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', () => loadHistory(true));
|
|
}
|
|
if (ignoreToggle) {
|
|
ignoreToggle.checked = state.ignoreAvailable;
|
|
ignoreToggle.addEventListener('change', () => {
|
|
state.ignoreAvailable = ignoreToggle.checked;
|
|
saveIgnoreAvailable(state.ignoreAvailable);
|
|
renderHistory(state.lastHistoryItems);
|
|
});
|
|
}
|
|
|
|
// Listen for history reload events from other modules
|
|
document.addEventListener('historyReload', () => {
|
|
loadHistory(true);
|
|
});
|
|
}
|
|
|
|
export function startHistoryRefresh() {
|
|
stopHistoryRefresh();
|
|
state.historyRefreshHandle = setInterval(() => loadHistory(), HISTORY_REFRESH_MS);
|
|
}
|
|
|
|
export function stopHistoryRefresh() {
|
|
if (state.historyRefreshHandle) {
|
|
clearInterval(state.historyRefreshHandle);
|
|
state.historyRefreshHandle = null;
|
|
}
|
|
}
|
|
|
|
export function clearHistory() {
|
|
state.lastHistoryItems = [];
|
|
document.getElementById('history-list').innerHTML = '';
|
|
document.getElementById('no-history').classList.add('hidden');
|
|
document.getElementById('history-error').classList.add('hidden');
|
|
}
|
|
|
|
export async function loadHistory(forceRefresh = false) {
|
|
const listEl = document.getElementById('history-list');
|
|
const loadingEl = document.getElementById('history-loading');
|
|
const errorEl = document.getElementById('history-error');
|
|
const noHistoryEl = document.getElementById('no-history');
|
|
|
|
loadingEl.classList.remove('hidden');
|
|
errorEl.classList.add('hidden');
|
|
noHistoryEl.classList.add('hidden');
|
|
|
|
try {
|
|
const result = await apiLoadHistory(forceRefresh);
|
|
loadingEl.classList.add('hidden');
|
|
if (result.success) {
|
|
state.lastHistoryItems = result.history;
|
|
renderHistory(state.lastHistoryItems);
|
|
} else {
|
|
errorEl.textContent = result.error || 'Failed to load history.';
|
|
errorEl.classList.remove('hidden');
|
|
}
|
|
} catch (err) {
|
|
loadingEl.classList.add('hidden');
|
|
errorEl.textContent = 'Failed to load history.';
|
|
errorEl.classList.remove('hidden');
|
|
console.error('[History] Load error:', err);
|
|
}
|
|
}
|
|
|
|
export function renderHistory(items) {
|
|
const listEl = document.getElementById('history-list');
|
|
const noHistoryEl = document.getElementById('no-history');
|
|
listEl.innerHTML = '';
|
|
const visible = state.ignoreAvailable
|
|
? items.filter(item => !(item.outcome === 'failed' && item.availableForUpgrade))
|
|
: items;
|
|
if (!visible.length) {
|
|
noHistoryEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
noHistoryEl.classList.add('hidden');
|
|
visible.forEach(item => listEl.appendChild(createHistoryCard(item)));
|
|
}
|
|
|
|
export function createHistoryCard(item) {
|
|
const card = document.createElement('div');
|
|
card.className = `history-card ${item.type} ${item.outcome}`;
|
|
|
|
if (item.coverArt) {
|
|
const coverDiv = document.createElement('div');
|
|
coverDiv.className = 'history-cover';
|
|
const img = document.createElement('img');
|
|
img.src = '/api/dashboard/cover-art?url=' + encodeURIComponent(item.coverArt);
|
|
img.alt = item.movieName || item.seriesName || item.title;
|
|
img.loading = 'lazy';
|
|
coverDiv.appendChild(img);
|
|
card.appendChild(coverDiv);
|
|
}
|
|
|
|
const info = document.createElement('div');
|
|
info.className = 'history-info';
|
|
|
|
// Header row: type badge + outcome badge
|
|
const header = document.createElement('div');
|
|
header.className = 'history-card-header';
|
|
|
|
const typeBadge = document.createElement('span');
|
|
typeBadge.className = `history-type-badge ${item.type}`;
|
|
typeBadge.textContent = item.type === 'series' ? '📺 Series' : '🎬 Movie';
|
|
header.appendChild(typeBadge);
|
|
|
|
const outcomeBadge = document.createElement('span');
|
|
outcomeBadge.className = `history-outcome-badge ${item.outcome}`;
|
|
outcomeBadge.textContent = item.outcome === 'imported' ? '✓ Imported' : '✗ Failed';
|
|
header.appendChild(outcomeBadge);
|
|
|
|
if (item.availableForUpgrade) {
|
|
const upgradeBadge = document.createElement('span');
|
|
upgradeBadge.className = 'history-upgrade-badge';
|
|
upgradeBadge.title = 'A previous version of this item is available. An upgrade download has failed.';
|
|
upgradeBadge.textContent = '⬆ Available';
|
|
header.appendChild(upgradeBadge);
|
|
}
|
|
|
|
if (item.instanceName) {
|
|
const instBadge = document.createElement('span');
|
|
instBadge.className = 'history-instance-badge';
|
|
instBadge.textContent = item.instanceName;
|
|
header.appendChild(instBadge);
|
|
}
|
|
|
|
const badges = renderTagBadges(item.tagBadges, state.showAll, item.matchedUserTag);
|
|
header.appendChild(badges);
|
|
|
|
info.appendChild(header);
|
|
|
|
// Title
|
|
const title = document.createElement('h3');
|
|
title.className = 'history-title';
|
|
title.textContent = item.title;
|
|
info.appendChild(title);
|
|
|
|
// Series/movie name with service icons
|
|
if (item.seriesName) {
|
|
const p = document.createElement('p');
|
|
p.className = 'history-media-name';
|
|
|
|
// Add service icons
|
|
const serviceIcons = createServiceIcons(item);
|
|
if (serviceIcons.hasChildNodes()) {
|
|
p.appendChild(serviceIcons);
|
|
p.appendChild(document.createTextNode(' '));
|
|
}
|
|
|
|
// Series name is now plain text for all users (no link)
|
|
const seriesText = document.createElement('span');
|
|
seriesText.textContent = 'Series: ' + item.seriesName;
|
|
p.appendChild(seriesText);
|
|
|
|
info.appendChild(p);
|
|
const epEl = formatEpisodeInfo(item.episodes);
|
|
if (epEl) info.appendChild(epEl);
|
|
}
|
|
if (item.movieName) {
|
|
const p = document.createElement('p');
|
|
p.className = 'history-media-name';
|
|
|
|
// Add service icons
|
|
const serviceIcons = createServiceIcons(item);
|
|
if (serviceIcons.hasChildNodes()) {
|
|
p.appendChild(serviceIcons);
|
|
p.appendChild(document.createTextNode(' '));
|
|
}
|
|
|
|
// Movie name is now plain text for all users (no link)
|
|
const movieText = document.createElement('span');
|
|
movieText.textContent = 'Movie: ' + item.movieName;
|
|
p.appendChild(movieText);
|
|
|
|
info.appendChild(p);
|
|
}
|
|
|
|
// Detail pills
|
|
const details = document.createElement('div');
|
|
details.className = 'history-details';
|
|
|
|
if (item.completedAt) {
|
|
details.appendChild(createDetailItem('Completed', formatDate(item.completedAt)));
|
|
}
|
|
if (item.quality) {
|
|
details.appendChild(createDetailItem('Quality', item.quality));
|
|
}
|
|
|
|
// Failed imports: show failure message
|
|
if (item.outcome === 'failed' && item.failureMessage) {
|
|
const failItem = document.createElement('div');
|
|
failItem.className = 'history-failure-message';
|
|
failItem.textContent = item.failureMessage;
|
|
details.appendChild(failItem);
|
|
}
|
|
|
|
info.appendChild(details);
|
|
card.appendChild(info);
|
|
return card;
|
|
}
|
|
|
|
function createDetailItem(label, value) {
|
|
const item = document.createElement('div');
|
|
item.className = 'detail-item';
|
|
item.dataset.label = label;
|
|
|
|
const labelSpan = document.createElement('span');
|
|
labelSpan.className = 'detail-label';
|
|
labelSpan.textContent = label;
|
|
|
|
const valueSpan = document.createElement('span');
|
|
valueSpan.className = 'detail-value';
|
|
valueSpan.textContent = value;
|
|
|
|
item.appendChild(labelSpan);
|
|
item.appendChild(valueSpan);
|
|
|
|
return item;
|
|
}
|