feat: Replace database operations with file-based configuration actions
- Removed database-related code from the UI JavaScript - Added configuration backup functionality to export config as JSON - Added configuration reset functionality to restore defaults - Updated ConfigController with backup and reset endpoints - Enhanced file-based configuration persistence 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c61f308de7
commit
63b33c5fc0
@ -1,3 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -33,18 +38,80 @@ namespace TransmissionRssManager.Api.Controllers
|
|||||||
host = config.Transmission.Host,
|
host = config.Transmission.Host,
|
||||||
port = config.Transmission.Port,
|
port = config.Transmission.Port,
|
||||||
useHttps = config.Transmission.UseHttps,
|
useHttps = config.Transmission.UseHttps,
|
||||||
hasCredentials = !string.IsNullOrEmpty(config.Transmission.Username)
|
hasCredentials = !string.IsNullOrEmpty(config.Transmission.Username),
|
||||||
|
username = config.Transmission.Username
|
||||||
},
|
},
|
||||||
|
transmissionInstances = config.TransmissionInstances?.Select(i => new
|
||||||
|
{
|
||||||
|
id = i.Key,
|
||||||
|
name = i.Value.Host,
|
||||||
|
host = i.Value.Host,
|
||||||
|
port = i.Value.Port,
|
||||||
|
useHttps = i.Value.UseHttps,
|
||||||
|
hasCredentials = !string.IsNullOrEmpty(i.Value.Username),
|
||||||
|
username = i.Value.Username
|
||||||
|
}),
|
||||||
autoDownloadEnabled = config.AutoDownloadEnabled,
|
autoDownloadEnabled = config.AutoDownloadEnabled,
|
||||||
checkIntervalMinutes = config.CheckIntervalMinutes,
|
checkIntervalMinutes = config.CheckIntervalMinutes,
|
||||||
downloadDirectory = config.DownloadDirectory,
|
downloadDirectory = config.DownloadDirectory,
|
||||||
mediaLibraryPath = config.MediaLibraryPath,
|
mediaLibraryPath = config.MediaLibraryPath,
|
||||||
postProcessing = config.PostProcessing
|
postProcessing = config.PostProcessing,
|
||||||
|
enableDetailedLogging = config.EnableDetailedLogging,
|
||||||
|
userPreferences = config.UserPreferences
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(sanitizedConfig);
|
return Ok(sanitizedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("defaults")]
|
||||||
|
public IActionResult GetDefaultConfig()
|
||||||
|
{
|
||||||
|
// Return default configuration settings
|
||||||
|
var defaultConfig = new
|
||||||
|
{
|
||||||
|
transmission = new
|
||||||
|
{
|
||||||
|
host = "localhost",
|
||||||
|
port = 9091,
|
||||||
|
username = "",
|
||||||
|
useHttps = false
|
||||||
|
},
|
||||||
|
autoDownloadEnabled = true,
|
||||||
|
checkIntervalMinutes = 30,
|
||||||
|
downloadDirectory = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"),
|
||||||
|
mediaLibraryPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Media"),
|
||||||
|
postProcessing = new
|
||||||
|
{
|
||||||
|
enabled = false,
|
||||||
|
extractArchives = true,
|
||||||
|
organizeMedia = true,
|
||||||
|
minimumSeedRatio = 1,
|
||||||
|
mediaExtensions = new[] { ".mp4", ".mkv", ".avi" },
|
||||||
|
autoOrganizeByMediaType = true,
|
||||||
|
renameFiles = false,
|
||||||
|
compressCompletedFiles = false,
|
||||||
|
deleteCompletedAfterDays = 0
|
||||||
|
},
|
||||||
|
enableDetailedLogging = false,
|
||||||
|
userPreferences = new
|
||||||
|
{
|
||||||
|
enableDarkMode = false,
|
||||||
|
autoRefreshUIEnabled = true,
|
||||||
|
autoRefreshIntervalSeconds = 30,
|
||||||
|
notificationsEnabled = true,
|
||||||
|
notificationEvents = new[] { "torrent-added", "torrent-completed", "torrent-error" },
|
||||||
|
defaultView = "dashboard",
|
||||||
|
confirmBeforeDelete = true,
|
||||||
|
maxItemsPerPage = 25,
|
||||||
|
dateTimeFormat = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
showCompletedTorrents = true,
|
||||||
|
keepHistoryDays = 30
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(defaultConfig);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<IActionResult> UpdateConfig([FromBody] AppConfig config)
|
public async Task<IActionResult> UpdateConfig([FromBody] AppConfig config)
|
||||||
{
|
{
|
||||||
@ -59,5 +126,93 @@ namespace TransmissionRssManager.Api.Controllers
|
|||||||
await _configService.SaveConfigurationAsync(config);
|
await _configService.SaveConfigurationAsync(config);
|
||||||
return Ok(new { success = true });
|
return Ok(new { success = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("backup")]
|
||||||
|
public IActionResult BackupConfig()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get the current config
|
||||||
|
var config = _configService.GetConfiguration();
|
||||||
|
|
||||||
|
// Serialize to JSON with indentation
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
var json = JsonSerializer.Serialize(config, options);
|
||||||
|
|
||||||
|
// Create a memory stream from the JSON
|
||||||
|
var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||||
|
|
||||||
|
// Set the content disposition and type
|
||||||
|
var fileName = $"transmission-rss-config-backup-{DateTime.Now:yyyy-MM-dd}.json";
|
||||||
|
return File(stream, "application/json", fileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error creating configuration backup");
|
||||||
|
return StatusCode(500, "Error creating configuration backup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reset")]
|
||||||
|
public async Task<IActionResult> ResetConfig()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create a default config
|
||||||
|
var defaultConfig = new AppConfig
|
||||||
|
{
|
||||||
|
Transmission = new TransmissionConfig
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 9091,
|
||||||
|
Username = "",
|
||||||
|
Password = "",
|
||||||
|
UseHttps = false
|
||||||
|
},
|
||||||
|
AutoDownloadEnabled = true,
|
||||||
|
CheckIntervalMinutes = 30,
|
||||||
|
DownloadDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"),
|
||||||
|
MediaLibraryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Media"),
|
||||||
|
PostProcessing = new PostProcessingConfig
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
ExtractArchives = true,
|
||||||
|
OrganizeMedia = true,
|
||||||
|
MinimumSeedRatio = 1.0f,
|
||||||
|
MediaExtensions = new[] { ".mp4", ".mkv", ".avi", ".mov", ".wmv", ".m4v", ".mpg", ".mpeg", ".flv", ".webm" },
|
||||||
|
AutoOrganizeByMediaType = true,
|
||||||
|
RenameFiles = false,
|
||||||
|
CompressCompletedFiles = false,
|
||||||
|
DeleteCompletedAfterDays = 0
|
||||||
|
},
|
||||||
|
UserPreferences = new UserPreferencesConfig
|
||||||
|
{
|
||||||
|
EnableDarkMode = true,
|
||||||
|
AutoRefreshUIEnabled = true,
|
||||||
|
AutoRefreshIntervalSeconds = 30,
|
||||||
|
NotificationsEnabled = true,
|
||||||
|
NotificationEvents = new[] { "torrent-added", "torrent-completed", "torrent-error" },
|
||||||
|
DefaultView = "dashboard",
|
||||||
|
ConfirmBeforeDelete = true,
|
||||||
|
MaxItemsPerPage = 25,
|
||||||
|
DateTimeFormat = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
ShowCompletedTorrents = true,
|
||||||
|
KeepHistoryDays = 30
|
||||||
|
},
|
||||||
|
Feeds = new List<RssFeed>(),
|
||||||
|
EnableDetailedLogging = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the default config
|
||||||
|
await _configService.SaveConfigurationAsync(defaultConfig);
|
||||||
|
|
||||||
|
return Ok(new { success = true });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error resetting configuration");
|
||||||
|
return StatusCode(500, "Error resetting configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,6 +8,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Load initial dashboard data
|
// Load initial dashboard data
|
||||||
loadDashboardData();
|
loadDashboardData();
|
||||||
|
|
||||||
|
// Set up dark mode based on user preference
|
||||||
|
initDarkMode();
|
||||||
|
|
||||||
|
// Set up auto refresh if enabled
|
||||||
|
initAutoRefresh();
|
||||||
|
|
||||||
// Initialize Bootstrap tooltips
|
// Initialize Bootstrap tooltips
|
||||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
tooltips.forEach(tooltip => new bootstrap.Tooltip(tooltip));
|
tooltips.forEach(tooltip => new bootstrap.Tooltip(tooltip));
|
||||||
@ -86,15 +92,65 @@ function initEventListeners() {
|
|||||||
document.getElementById('btn-refresh-torrents').addEventListener('click', loadTorrents);
|
document.getElementById('btn-refresh-torrents').addEventListener('click', loadTorrents);
|
||||||
document.getElementById('save-torrent-btn').addEventListener('click', saveTorrent);
|
document.getElementById('save-torrent-btn').addEventListener('click', saveTorrent);
|
||||||
|
|
||||||
|
// Logs page
|
||||||
|
document.getElementById('btn-refresh-logs').addEventListener('click', refreshLogs);
|
||||||
|
document.getElementById('btn-clear-logs').addEventListener('click', clearLogs);
|
||||||
|
document.getElementById('btn-apply-log-filters').addEventListener('click', applyLogFilters);
|
||||||
|
document.getElementById('btn-reset-log-filters').addEventListener('click', resetLogFilters);
|
||||||
|
document.getElementById('btn-export-logs').addEventListener('click', exportLogs);
|
||||||
|
|
||||||
// Settings page
|
// Settings page
|
||||||
document.getElementById('settings-form').addEventListener('submit', saveSettings);
|
document.getElementById('settings-form').addEventListener('submit', saveSettings);
|
||||||
|
document.getElementById('dark-mode-toggle').addEventListener('click', toggleDarkMode);
|
||||||
|
document.getElementById('btn-reset-settings').addEventListener('click', resetSettings);
|
||||||
|
|
||||||
|
// Configuration operations
|
||||||
|
document.getElementById('btn-backup-config').addEventListener('click', backupConfig);
|
||||||
|
document.getElementById('btn-reset-config').addEventListener('click', resetConfig);
|
||||||
|
|
||||||
|
// Additional Transmission Instances
|
||||||
|
document.getElementById('add-transmission-instance').addEventListener('click', addTransmissionInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
function loadDashboardData() {
|
function loadDashboardData() {
|
||||||
loadSystemStatus();
|
// Fetch dashboard statistics
|
||||||
loadRecentMatches();
|
fetch('/api/dashboard/stats')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(stats => {
|
||||||
|
document.getElementById('active-downloads').textContent = stats.activeDownloads;
|
||||||
|
document.getElementById('seeding-torrents').textContent = stats.seedingTorrents;
|
||||||
|
document.getElementById('active-feeds').textContent = stats.activeFeeds;
|
||||||
|
document.getElementById('completed-today').textContent = stats.completedToday;
|
||||||
|
|
||||||
|
document.getElementById('added-today').textContent = stats.addedToday;
|
||||||
|
document.getElementById('feeds-count').textContent = stats.feedsCount;
|
||||||
|
document.getElementById('matched-count').textContent = stats.matchedCount;
|
||||||
|
|
||||||
|
// Format download/upload speeds
|
||||||
|
const downloadSpeed = formatBytes(stats.downloadSpeed) + '/s';
|
||||||
|
const uploadSpeed = formatBytes(stats.uploadSpeed) + '/s';
|
||||||
|
document.getElementById('download-speed').textContent = downloadSpeed;
|
||||||
|
document.getElementById('upload-speed').textContent = uploadSpeed;
|
||||||
|
document.getElementById('current-speed').textContent = `↓${downloadSpeed} ↑${uploadSpeed}`;
|
||||||
|
|
||||||
|
// Set progress bars (max 100%)
|
||||||
|
const maxSpeed = Math.max(stats.downloadSpeed, stats.uploadSpeed, 1);
|
||||||
|
const dlPercent = Math.min(Math.round((stats.downloadSpeed / maxSpeed) * 100), 100);
|
||||||
|
const ulPercent = Math.min(Math.round((stats.uploadSpeed / maxSpeed) * 100), 100);
|
||||||
|
document.getElementById('download-speed-bar').style.width = `${dlPercent}%`;
|
||||||
|
document.getElementById('upload-speed-bar').style.width = `${ulPercent}%`;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading dashboard stats:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load chart data
|
||||||
|
loadDownloadHistoryChart();
|
||||||
|
|
||||||
|
// Load other dashboard components
|
||||||
loadActiveTorrents();
|
loadActiveTorrents();
|
||||||
|
loadRecentMatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSystemStatus() {
|
function loadSystemStatus() {
|
||||||
@ -914,3 +970,542 @@ function formatDate(date) {
|
|||||||
function padZero(num) {
|
function padZero(num) {
|
||||||
return num.toString().padStart(2, '0');
|
return num.toString().padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dark Mode Functions
|
||||||
|
function initDarkMode() {
|
||||||
|
// Check local storage preference or system preference
|
||||||
|
const darkModePreference = localStorage.getItem('darkMode');
|
||||||
|
|
||||||
|
if (darkModePreference === 'true' ||
|
||||||
|
(darkModePreference === null && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
enableDarkMode();
|
||||||
|
} else {
|
||||||
|
disableDarkMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDarkMode() {
|
||||||
|
if (document.body.classList.contains('dark-mode')) {
|
||||||
|
disableDarkMode();
|
||||||
|
} else {
|
||||||
|
enableDarkMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableDarkMode() {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
document.getElementById('dark-mode-toggle').innerHTML = '<i class="bi bi-sun-fill"></i>';
|
||||||
|
localStorage.setItem('darkMode', 'true');
|
||||||
|
|
||||||
|
// Also update user preferences if on settings page
|
||||||
|
const darkModeCheckbox = document.getElementById('enable-dark-mode');
|
||||||
|
if (darkModeCheckbox) {
|
||||||
|
darkModeCheckbox.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableDarkMode() {
|
||||||
|
document.body.classList.remove('dark-mode');
|
||||||
|
document.getElementById('dark-mode-toggle').innerHTML = '<i class="bi bi-moon-fill"></i>';
|
||||||
|
localStorage.setItem('darkMode', 'false');
|
||||||
|
|
||||||
|
// Also update user preferences if on settings page
|
||||||
|
const darkModeCheckbox = document.getElementById('enable-dark-mode');
|
||||||
|
if (darkModeCheckbox) {
|
||||||
|
darkModeCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh
|
||||||
|
function initAutoRefresh() {
|
||||||
|
// Get auto-refresh settings from local storage or use defaults
|
||||||
|
const autoRefresh = localStorage.getItem('autoRefresh') !== 'false';
|
||||||
|
const refreshInterval = parseInt(localStorage.getItem('refreshInterval')) || 30;
|
||||||
|
|
||||||
|
if (autoRefresh) {
|
||||||
|
startAutoRefresh(refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoRefresh(intervalSeconds) {
|
||||||
|
// Clear any existing interval
|
||||||
|
if (window.refreshTimer) {
|
||||||
|
clearInterval(window.refreshTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new interval
|
||||||
|
window.refreshTimer = setInterval(() => {
|
||||||
|
const currentPage = window.location.hash.substring(1) || 'dashboard';
|
||||||
|
loadPageData(currentPage);
|
||||||
|
}, intervalSeconds * 1000);
|
||||||
|
|
||||||
|
localStorage.setItem('autoRefresh', 'true');
|
||||||
|
localStorage.setItem('refreshInterval', intervalSeconds.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (window.refreshTimer) {
|
||||||
|
clearInterval(window.refreshTimer);
|
||||||
|
window.refreshTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('autoRefresh', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart functions
|
||||||
|
function loadDownloadHistoryChart() {
|
||||||
|
fetch('/api/dashboard/history')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(history => {
|
||||||
|
const ctx = document.getElementById('download-history-chart').getContext('2d');
|
||||||
|
|
||||||
|
// Extract dates and count values
|
||||||
|
const labels = history.map(point => {
|
||||||
|
const date = new Date(point.date);
|
||||||
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const countData = history.map(point => point.count);
|
||||||
|
const sizeData = history.map(point => point.totalSize / (1024 * 1024 * 1024)); // Convert to GB
|
||||||
|
|
||||||
|
// Create or update chart
|
||||||
|
if (window.downloadHistoryChart) {
|
||||||
|
window.downloadHistoryChart.data.labels = labels;
|
||||||
|
window.downloadHistoryChart.data.datasets[0].data = countData;
|
||||||
|
window.downloadHistoryChart.data.datasets[1].data = sizeData;
|
||||||
|
window.downloadHistoryChart.update();
|
||||||
|
} else {
|
||||||
|
window.downloadHistoryChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Number of Downloads',
|
||||||
|
data: countData,
|
||||||
|
backgroundColor: 'rgba(13, 110, 253, 0.5)',
|
||||||
|
borderColor: 'rgba(13, 110, 253, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
yAxisID: 'y'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Total Size (GB)',
|
||||||
|
data: sizeData,
|
||||||
|
type: 'line',
|
||||||
|
borderColor: 'rgba(25, 135, 84, 1)',
|
||||||
|
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
yAxisID: 'y1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
if (label) {
|
||||||
|
label += ': ';
|
||||||
|
}
|
||||||
|
if (context.datasetIndex === 0) {
|
||||||
|
label += context.parsed.y;
|
||||||
|
} else {
|
||||||
|
label += context.parsed.y.toFixed(2) + ' GB';
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Number of Downloads'
|
||||||
|
},
|
||||||
|
beginAtZero: true
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Total Size (GB)'
|
||||||
|
},
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading download history chart:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs Management
|
||||||
|
function refreshLogs() {
|
||||||
|
const logFilters = getLogFilters();
|
||||||
|
loadLogs(logFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogFilters() {
|
||||||
|
return {
|
||||||
|
level: document.getElementById('log-level').value,
|
||||||
|
search: document.getElementById('log-search').value,
|
||||||
|
dateRange: document.getElementById('log-date-range').value,
|
||||||
|
skip: 0,
|
||||||
|
take: parseInt(document.getElementById('items-per-page')?.value || 25)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLogs(filters) {
|
||||||
|
const tbody = document.getElementById('logs-table-body');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4">Loading logs...</td></tr>';
|
||||||
|
|
||||||
|
// Build query string
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (filters.level && filters.level !== 'All') {
|
||||||
|
query.append('Level', filters.level);
|
||||||
|
}
|
||||||
|
if (filters.search) {
|
||||||
|
query.append('Search', filters.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range
|
||||||
|
const now = new Date();
|
||||||
|
let startDate = null;
|
||||||
|
|
||||||
|
switch (filters.dateRange) {
|
||||||
|
case 'today':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
|
||||||
|
break;
|
||||||
|
case 'yesterday':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
query.append('StartDate', startDate.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
query.append('Skip', filters.skip.toString());
|
||||||
|
query.append('Take', filters.take.toString());
|
||||||
|
|
||||||
|
fetch(`/api/logs?${query.toString()}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(logs => {
|
||||||
|
if (logs.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4">No logs found</td></tr>';
|
||||||
|
document.getElementById('log-count').textContent = '0 entries';
|
||||||
|
document.getElementById('logs-pagination-info').textContent = 'Showing 0 of 0 entries';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
logs.forEach(log => {
|
||||||
|
const timestamp = new Date(log.timestamp);
|
||||||
|
const levelClass = getLevelClass(log.level);
|
||||||
|
|
||||||
|
html += `<tr>
|
||||||
|
<td class="text-nowrap">${formatDate(timestamp)}</td>
|
||||||
|
<td><span class="badge ${levelClass}">${log.level}</span></td>
|
||||||
|
<td>${log.message}</td>
|
||||||
|
<td>${log.context || ''}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
document.getElementById('log-count').textContent = `${logs.length} entries`;
|
||||||
|
document.getElementById('logs-pagination-info').textContent = `Showing ${logs.length} entries`;
|
||||||
|
|
||||||
|
// Update pagination (simplified for now)
|
||||||
|
updateLogPagination(filters, logs.length);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading logs:', error);
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4">Error loading logs</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLogPagination(filters, count) {
|
||||||
|
const pagination = document.getElementById('logs-pagination');
|
||||||
|
|
||||||
|
// Simplified pagination - just first page for now
|
||||||
|
pagination.innerHTML = `
|
||||||
|
<li class="page-item active">
|
||||||
|
<a class="page-link" href="#">1</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLevelClass(level) {
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case 'debug':
|
||||||
|
return 'bg-secondary';
|
||||||
|
case 'information':
|
||||||
|
return 'bg-info';
|
||||||
|
case 'warning':
|
||||||
|
return 'bg-warning';
|
||||||
|
case 'error':
|
||||||
|
return 'bg-danger';
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-dark';
|
||||||
|
default:
|
||||||
|
return 'bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLogFilters() {
|
||||||
|
const filters = getLogFilters();
|
||||||
|
loadLogs(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLogFilters() {
|
||||||
|
document.getElementById('log-level').value = 'All';
|
||||||
|
document.getElementById('log-search').value = '';
|
||||||
|
document.getElementById('log-date-range').value = 'week';
|
||||||
|
loadLogs(getLogFilters());
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogs() {
|
||||||
|
if (!confirm('Are you sure you want to clear all logs? This action cannot be undone.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/logs/clear', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to clear logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh logs
|
||||||
|
loadLogs(getLogFilters());
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error clearing logs:', error);
|
||||||
|
alert('Error clearing logs');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportLogs() {
|
||||||
|
const filters = getLogFilters();
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
if (filters.level && filters.level !== 'All') {
|
||||||
|
query.append('Level', filters.level);
|
||||||
|
}
|
||||||
|
if (filters.search) {
|
||||||
|
query.append('Search', filters.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create download link
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = `/api/logs/export?${query.toString()}`;
|
||||||
|
link.download = `transmission-rss-logs-${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format file sizes
|
||||||
|
function formatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration Operations
|
||||||
|
function backupConfig() {
|
||||||
|
if (!confirm('This will create a backup of your configuration files. Do you want to continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/config/backup', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to backup configuration');
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
// Create download link for the backup file
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = url;
|
||||||
|
a.download = `transmission-rss-config-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
alert('Configuration backup created successfully.');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error backing up configuration:', error);
|
||||||
|
alert('Error creating configuration backup.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfig() {
|
||||||
|
if (!confirm('WARNING: This will reset your configuration to default settings. All your feeds, rules, and user preferences will be lost. This cannot be undone. Are you absolutely sure?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('FINAL WARNING: All feeds, rules, and settings will be permanently deleted. Type "RESET" to confirm.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmation = prompt('Type "RESET" to confirm configuration reset:');
|
||||||
|
if (confirmation !== 'RESET') {
|
||||||
|
alert('Configuration reset cancelled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/config/reset', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to reset configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Configuration has been reset to defaults. The application will now reload.');
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error resetting configuration:', error);
|
||||||
|
alert('Error resetting configuration.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transmission Instance Management
|
||||||
|
function addTransmissionInstance() {
|
||||||
|
const instancesList = document.getElementById('transmission-instances-list');
|
||||||
|
const instanceCount = document.querySelectorAll('.transmission-instance').length;
|
||||||
|
const newInstanceIndex = instanceCount + 1;
|
||||||
|
|
||||||
|
const instanceHtml = `
|
||||||
|
<div class="transmission-instance card mb-3" id="transmission-instance-${newInstanceIndex}">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-3">
|
||||||
|
<h5 class="card-title">Instance #${newInstanceIndex}</h5>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="removeTransmissionInstance(${newInstanceIndex})">
|
||||||
|
<i class="bi bi-trash me-1"></i>Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" name="transmissionInstances[${newInstanceIndex}].name" placeholder="Secondary Server">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Host</label>
|
||||||
|
<input type="text" class="form-control" name="transmissionInstances[${newInstanceIndex}].host">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Port</label>
|
||||||
|
<input type="number" class="form-control" name="transmissionInstances[${newInstanceIndex}].port" value="9091">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-control" name="transmissionInstances[${newInstanceIndex}].username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" name="transmissionInstances[${newInstanceIndex}].password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check form-switch mt-4">
|
||||||
|
<input class="form-check-input" type="checkbox" name="transmissionInstances[${newInstanceIndex}].useHttps">
|
||||||
|
<label class="form-check-label">Use HTTPS</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// If the "no instances" message is showing, remove it
|
||||||
|
if (instancesList.querySelector('.text-center.text-muted')) {
|
||||||
|
instancesList.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new instance
|
||||||
|
instancesList.insertAdjacentHTML('beforeend', instanceHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTransmissionInstance(index) {
|
||||||
|
const instance = document.getElementById(`transmission-instance-${index}`);
|
||||||
|
if (instance) {
|
||||||
|
instance.remove();
|
||||||
|
|
||||||
|
// If there are no instances left, show the "no instances" message
|
||||||
|
const instancesList = document.getElementById('transmission-instances-list');
|
||||||
|
if (instancesList.children.length === 0) {
|
||||||
|
instancesList.innerHTML = '<div class="text-center text-muted py-3">No additional instances configured</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSettings() {
|
||||||
|
if (!confirm('This will reset all settings to their default values. Are you sure?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load default settings
|
||||||
|
fetch('/api/config/defaults')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(defaults => {
|
||||||
|
// Apply defaults to form
|
||||||
|
loadSettingsIntoForm(defaults);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading default settings:', error);
|
||||||
|
alert('Error loading default settings');
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user