mirror of
https://github.com/plexguide/Huntarr.git
synced 2026-01-25 20:58:48 -06:00
a301
This commit is contained in:
@@ -873,7 +873,6 @@ input:checked + .toggle-slider:before {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
width: fit-content;
|
||||
@@ -1115,7 +1114,6 @@ input:checked + .toggle-slider:before {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
@@ -1276,4 +1274,171 @@ input:checked + .toggle-slider:before {
|
||||
|
||||
#reset_stateful_btn i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Apps Section */
|
||||
/* Use the existing log dropdown styles for app section. No custom CSS needed for the dropdown itself. */
|
||||
|
||||
/* App settings content styling */
|
||||
.settings-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.app-apps-panel {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-apps-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Instance panel styling */
|
||||
.instance-panel {
|
||||
background-color: var(--bg-secondary, #2c2c2c);
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid var(--border-color, #3c3c3c);
|
||||
}
|
||||
|
||||
.instance-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
gap: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color, #3c3c3c);
|
||||
}
|
||||
|
||||
.instance-name {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
background-color: var(--bg-tertiary, #252525);
|
||||
border: 1px solid var(--border-color, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary, white);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 400;
|
||||
color: var(--text-primary, #f0f0f0);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-field input {
|
||||
padding: 8px;
|
||||
background-color: var(--bg-tertiary, #252525);
|
||||
border: 1px solid var(--border-color, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary, white);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.add-instance-btn {
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.add-instance-btn:hover {
|
||||
background-color: var(--accent-hover, #0069d9);
|
||||
}
|
||||
|
||||
.remove-instance-btn {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-instance-btn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.test-connection-btn {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 13px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.test-connection-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* Match styling with existing settings UI */
|
||||
#appsSection .section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color, #3c3c3c);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
#appsSection .settings-group {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
background-color: var(--bg-secondary, #252525);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#appsSection .settings-group-header {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color, #363636);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #f0f0f0);
|
||||
}
|
||||
|
||||
.loading-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: var(--text-primary, #f0f0f0);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.error-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: #dc3545;
|
||||
gap: 10px;
|
||||
}
|
||||
308
frontend/static/js/apps.js
Normal file
308
frontend/static/js/apps.js
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* Huntarr - Apps Module
|
||||
* Handles displaying and managing app settings for media server applications
|
||||
*/
|
||||
|
||||
const appsModule = {
|
||||
// State
|
||||
currentApp: 'sonarr',
|
||||
isLoading: false,
|
||||
settingsChanged: false,
|
||||
originalSettings: {},
|
||||
|
||||
// DOM elements
|
||||
elements: {},
|
||||
|
||||
// Initialize the apps module
|
||||
init: function() {
|
||||
this.cacheElements();
|
||||
this.setupEventListeners();
|
||||
|
||||
// Initial load if apps is active section
|
||||
if (huntarrUI && huntarrUI.currentSection === 'apps') {
|
||||
this.loadApps();
|
||||
}
|
||||
},
|
||||
|
||||
// Cache DOM elements
|
||||
cacheElements: function() {
|
||||
this.elements = {
|
||||
// Apps dropdown
|
||||
appsOptions: document.querySelectorAll('#appsSection .log-option'),
|
||||
currentAppsApp: document.getElementById('current-apps-app'),
|
||||
appsDropdownBtn: document.querySelector('#appsSection .log-dropdown-btn'),
|
||||
appsDropdownContent: document.querySelector('#appsSection .log-dropdown-content'),
|
||||
|
||||
// Apps panels
|
||||
appAppsPanels: document.querySelectorAll('.app-apps-panel'),
|
||||
|
||||
// Controls
|
||||
saveAppsButton: document.getElementById('saveAppsButton')
|
||||
};
|
||||
},
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners: function() {
|
||||
// App selection
|
||||
if (this.elements.appsOptions) {
|
||||
this.elements.appsOptions.forEach(option => {
|
||||
option.addEventListener('click', e => this.handleAppsAppChange(e));
|
||||
});
|
||||
}
|
||||
|
||||
// Dropdown toggle
|
||||
if (this.elements.appsDropdownBtn) {
|
||||
this.elements.appsDropdownBtn.addEventListener('click', () => {
|
||||
this.elements.appsDropdownContent.classList.toggle('show');
|
||||
|
||||
// Close all other dropdowns
|
||||
document.querySelectorAll('.log-dropdown-content.show').forEach(dropdown => {
|
||||
if (dropdown !== this.elements.appsDropdownContent) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.matches('#appsSection .log-dropdown-btn') &&
|
||||
!e.target.closest('#appsSection .log-dropdown-btn')) {
|
||||
if (this.elements.appsDropdownContent && this.elements.appsDropdownContent.classList.contains('show')) {
|
||||
this.elements.appsDropdownContent.classList.remove('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save button
|
||||
if (this.elements.saveAppsButton) {
|
||||
this.elements.saveAppsButton.addEventListener('click', () => this.saveApps());
|
||||
}
|
||||
},
|
||||
|
||||
// Load apps data when section becomes active
|
||||
loadApps: function() {
|
||||
console.log('[Apps] Loading apps data for ' + this.currentApp);
|
||||
|
||||
// Disable save button until changes are made
|
||||
if (this.elements.saveAppsButton) {
|
||||
this.elements.saveAppsButton.disabled = true;
|
||||
}
|
||||
this.settingsChanged = false;
|
||||
|
||||
// Get all settings to populate forms
|
||||
fetch('/api/settings')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Loaded settings:', data);
|
||||
|
||||
// Store original settings for comparison
|
||||
this.originalSettings = data;
|
||||
|
||||
// Ensure current app panel is visible
|
||||
this.showAppPanel(this.currentApp);
|
||||
|
||||
// Populate each app's settings form
|
||||
this.populateAllAppPanels(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading settings:', error);
|
||||
const appPanel = document.getElementById(this.currentApp + 'Apps');
|
||||
if (appPanel) {
|
||||
appPanel.innerHTML = '<div class="error-panel"><i class="fas fa-exclamation-triangle"></i> Failed to load app settings. Please try again.</div>';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Populate all app panels with settings
|
||||
populateAllAppPanels: function(data) {
|
||||
// Clear existing panels
|
||||
this.elements.appAppsPanels.forEach(panel => {
|
||||
panel.innerHTML = '';
|
||||
});
|
||||
|
||||
// Populate each app panel
|
||||
if (data.sonarr) this.populateAppPanel('sonarr', data.sonarr);
|
||||
if (data.radarr) this.populateAppPanel('radarr', data.radarr);
|
||||
if (data.lidarr) this.populateAppPanel('lidarr', data.lidarr);
|
||||
if (data.readarr) this.populateAppPanel('readarr', data.readarr);
|
||||
if (data.whisparr) this.populateAppPanel('whisparr', data.whisparr);
|
||||
if (data.swaparr) this.populateAppPanel('swaparr', data.swaparr);
|
||||
},
|
||||
|
||||
// Populate a specific app panel with settings
|
||||
populateAppPanel: function(app, appSettings) {
|
||||
const appPanel = document.getElementById(app + 'Apps');
|
||||
if (!appPanel) return;
|
||||
|
||||
// Create settings container
|
||||
const settingsContainer = document.createElement('div');
|
||||
settingsContainer.className = 'settings-group';
|
||||
|
||||
// Create settings form
|
||||
const settingsForm = document.createElement('div');
|
||||
settingsForm.id = app + 'SettingsForm';
|
||||
settingsForm.className = 'settings-form';
|
||||
|
||||
// Add to container and panel
|
||||
settingsContainer.appendChild(settingsForm);
|
||||
appPanel.appendChild(settingsContainer);
|
||||
|
||||
// Generate the form using SettingsForms module
|
||||
if (typeof SettingsForms !== 'undefined') {
|
||||
const formFunction = SettingsForms[`generate${app.charAt(0).toUpperCase()}${app.slice(1)}Form`];
|
||||
if (typeof formFunction === 'function') {
|
||||
formFunction(settingsForm, appSettings);
|
||||
|
||||
// Update duration displays for this app
|
||||
if (typeof SettingsForms.updateDurationDisplay === 'function') {
|
||||
SettingsForms.updateDurationDisplay();
|
||||
}
|
||||
|
||||
// Add change listener to detect modifications
|
||||
this.addFormChangeListeners(settingsForm);
|
||||
} else {
|
||||
console.warn(`Form generation function not found for ${app}`);
|
||||
settingsForm.innerHTML = `<div class="settings-message">Settings for ${app.charAt(0).toUpperCase() + app.slice(1)} are not available.</div>`;
|
||||
}
|
||||
} else {
|
||||
console.error('SettingsForms module not found');
|
||||
settingsForm.innerHTML = '<div class="error-panel">Unable to generate settings form. Please reload the page.</div>';
|
||||
}
|
||||
},
|
||||
|
||||
// Add change event listeners to form elements
|
||||
addFormChangeListeners: function(form) {
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', () => this.markAppsAsChanged());
|
||||
// For text inputs, also listen for input event
|
||||
if (input.type === 'text' || input.type === 'password' || input.type === 'number' || input.tagName.toLowerCase() === 'textarea') {
|
||||
input.addEventListener('input', () => this.markAppsAsChanged());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Show specific app panel and hide others
|
||||
showAppPanel: function(app) {
|
||||
// Hide all app panels
|
||||
this.elements.appAppsPanels.forEach(panel => {
|
||||
panel.classList.remove('active');
|
||||
panel.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show the selected app's panel
|
||||
const selectedPanel = document.getElementById(app + 'Apps');
|
||||
if (selectedPanel) {
|
||||
selectedPanel.classList.add('active');
|
||||
selectedPanel.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
// Handle app selection changes
|
||||
handleAppsAppChange: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const selectedApp = e.target.getAttribute('data-app');
|
||||
if (!selectedApp || selectedApp === this.currentApp) return;
|
||||
|
||||
// Check if there are unsaved changes
|
||||
if (this.settingsChanged) {
|
||||
const confirmSwitch = confirm('You have unsaved changes. Do you want to continue without saving?');
|
||||
if (!confirmSwitch) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI
|
||||
this.elements.appsOptions.forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
e.target.classList.add('active');
|
||||
|
||||
// Update the current app text with proper capitalization
|
||||
let displayName = selectedApp.charAt(0).toUpperCase() + selectedApp.slice(1);
|
||||
this.elements.currentAppsApp.textContent = displayName;
|
||||
|
||||
// Close the dropdown
|
||||
this.elements.appsDropdownContent.classList.remove('show');
|
||||
|
||||
// Show the selected app's panel
|
||||
this.showAppPanel(selectedApp);
|
||||
|
||||
this.currentApp = selectedApp;
|
||||
console.log(`[Apps] Switched app to: ${this.currentApp}`);
|
||||
|
||||
// Reset changed state
|
||||
this.settingsChanged = false;
|
||||
this.elements.saveAppsButton.disabled = true;
|
||||
},
|
||||
|
||||
// Mark apps as changed
|
||||
markAppsAsChanged: function() {
|
||||
this.settingsChanged = true;
|
||||
if (this.elements.saveAppsButton) {
|
||||
this.elements.saveAppsButton.disabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Save apps settings
|
||||
saveApps: function() {
|
||||
console.log('[Apps] Saving app settings for ' + this.currentApp);
|
||||
|
||||
// Gather settings from all app forms
|
||||
const allSettings = {};
|
||||
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'swaparr'];
|
||||
|
||||
// Loop through each app and collect settings
|
||||
apps.forEach(app => {
|
||||
const appPanel = document.getElementById(app + 'Apps');
|
||||
if (!appPanel) return;
|
||||
|
||||
const appForm = appPanel.querySelector('.settings-form');
|
||||
if (!appForm) return;
|
||||
|
||||
// Get settings using SettingsForms
|
||||
if (typeof SettingsForms !== 'undefined' && typeof SettingsForms.getFormSettings === 'function') {
|
||||
const appSettings = SettingsForms.getFormSettings(appForm);
|
||||
if (appSettings) {
|
||||
allSettings[app] = appSettings;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Send settings update request
|
||||
fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [this.currentApp]: allSettings[this.currentApp] })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Settings saved:', data);
|
||||
|
||||
// Disable save button
|
||||
this.settingsChanged = false;
|
||||
if (this.elements.saveAppsButton) {
|
||||
this.elements.saveAppsButton.disabled = true;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
alert('Settings saved successfully!');
|
||||
|
||||
// Update original settings
|
||||
this.originalSettings = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving settings:', error);
|
||||
alert('Error saving settings. Please try again.');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when document is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
appsModule.init();
|
||||
});
|
||||
@@ -419,6 +419,18 @@ let huntarrUI = {
|
||||
this.currentSection = 'history';
|
||||
// Disconnect logs if switching away from logs
|
||||
this.disconnectAllEventSources();
|
||||
} else if (section === 'apps' && document.getElementById('appsSection')) {
|
||||
document.getElementById('appsSection').classList.add('active');
|
||||
if (document.getElementById('appsNav')) document.getElementById('appsNav').classList.add('active');
|
||||
newTitle = 'Apps';
|
||||
this.currentSection = 'apps';
|
||||
// Disconnect logs if switching away from logs
|
||||
this.disconnectAllEventSources();
|
||||
|
||||
// Load apps if the apps module exists
|
||||
if (typeof appsModule !== 'undefined') {
|
||||
appsModule.loadApps();
|
||||
}
|
||||
} else if (section === 'settings' && this.elements.settingsSection) {
|
||||
this.elements.settingsSection.classList.add('active');
|
||||
if (this.elements.settingsNav) this.elements.settingsNav.classList.add('active');
|
||||
|
||||
37
frontend/templates/components/apps_section.html
Normal file
37
frontend/templates/components/apps_section.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<section id="appsSection" class="content-section">
|
||||
<div class="section-header">
|
||||
<h2>App Settings</h2>
|
||||
<button id="saveAppsButton" class="save-button" disabled>
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="log-dropdown-container">
|
||||
<div class="log-dropdown">
|
||||
<button class="log-dropdown-btn">
|
||||
<span id="current-apps-app">Sonarr</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="log-dropdown-content">
|
||||
<a href="#" class="log-option active" data-app="sonarr">Sonarr</a>
|
||||
<a href="#" class="log-option" data-app="radarr">Radarr</a>
|
||||
<a href="#" class="log-option" data-app="lidarr">Lidarr</a>
|
||||
<a href="#" class="log-option" data-app="readarr">Readarr</a>
|
||||
<a href="#" class="log-option" data-app="whisparr">Whisparr</a>
|
||||
<a href="#" class="log-option" data-app="swaparr">Swaparr</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="settings-content">
|
||||
<!-- App forms will be loaded here dynamically -->
|
||||
<div id="sonarrApps" class="app-apps-panel active" style="display: block;"></div>
|
||||
<div id="radarrApps" class="app-apps-panel"></div>
|
||||
<div id="lidarrApps" class="app-apps-panel"></div>
|
||||
<div id="readarrApps" class="app-apps-panel"></div>
|
||||
<div id="whisparrApps" class="app-apps-panel"></div>
|
||||
<div id="swaparrApps" class="app-apps-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,23 +1,6 @@
|
||||
<section id="settingsSection" class="content-section">
|
||||
<div class="section-header">
|
||||
<!-- Replace settings tabs with dropdown -->
|
||||
<div class="settings-dropdown-container">
|
||||
<div class="settings-dropdown">
|
||||
<button class="settings-dropdown-btn">
|
||||
<span id="current-settings-app">General</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="settings-dropdown-content">
|
||||
<a href="#" class="settings-option active" data-app="general">General</a>
|
||||
<a href="#" class="settings-option" data-app="sonarr">Sonarr</a>
|
||||
<a href="#" class="settings-option" data-app="radarr">Radarr</a>
|
||||
<a href="#" class="settings-option" data-app="lidarr">Lidarr</a>
|
||||
<a href="#" class="settings-option" data-app="readarr">Readarr</a>
|
||||
<a href="#" class="settings-option" data-app="whisparr">Whisparr</a>
|
||||
<a href="#" class="settings-option" data-app="swaparr">Swaparr</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>General Settings</h2>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button id="saveSettingsButton" class="save-button" disabled>
|
||||
@@ -28,12 +11,6 @@
|
||||
|
||||
<div class="settings-form">
|
||||
<div id="generalSettings" class="app-settings-panel active" style="display: block;"></div>
|
||||
<div id="sonarrSettings" class="app-settings-panel"></div>
|
||||
<div id="radarrSettings" class="app-settings-panel"></div>
|
||||
<div id="lidarrSettings" class="app-settings-panel"></div>
|
||||
<div id="readarrSettings" class="app-settings-panel"></div>
|
||||
<div id="whisparrSettings" class="app-settings-panel"></div>
|
||||
<div id="swaparrSettings" class="app-settings-panel"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<i class="fas fa-history"></i>
|
||||
<span>History</span>
|
||||
</a>
|
||||
<a href="#apps" class="nav-item" id="appsNav">
|
||||
<i class="fas fa-tools"></i>
|
||||
<span>Apps</span>
|
||||
</a>
|
||||
<a href="#settings" class="nav-item" id="settingsNav">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Settings</span>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
<!-- History Section -->
|
||||
{% include 'components/history_section.html' %}
|
||||
|
||||
<!-- Apps Section -->
|
||||
{% include 'components/apps_section.html' %}
|
||||
|
||||
<!-- Settings Section -->
|
||||
{% include 'components/settings_section.html' %}
|
||||
|
||||
@@ -36,6 +39,8 @@
|
||||
<script src="/static/js/settings_forms.js"></script>
|
||||
<!-- Load history script -->
|
||||
<script src="/static/js/history.js"></script>
|
||||
<!-- Load apps script -->
|
||||
<script src="/static/js/apps.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -346,96 +346,75 @@ def logs_stream():
|
||||
response.headers['X-Accel-Buffering'] = 'no' # Disable nginx buffering if using nginx
|
||||
return response
|
||||
|
||||
@app.route('/api/settings', methods=['GET', 'POST'])
|
||||
@app.route('/api/settings', methods=['GET'])
|
||||
def api_settings():
|
||||
if request.method == 'GET':
|
||||
# Return all settings using the new manager function
|
||||
all_settings = settings_manager.get_all_settings() # Corrected function name
|
||||
return jsonify(all_settings)
|
||||
|
||||
elif request.method == 'POST':
|
||||
data = request.json
|
||||
web_logger = get_logger("web_server")
|
||||
web_logger.debug(f"Received settings save request: {data}")
|
||||
|
||||
# Expecting data format like: { "sonarr": { "api_url": "...", ... } }
|
||||
if not isinstance(data, dict) or len(data) != 1:
|
||||
return jsonify({"success": False, "error": "Invalid payload format. Expected {'app_name': {settings...}}"}), 400
|
||||
|
||||
app_name = list(data.keys())[0]
|
||||
settings_data = data[app_name]
|
||||
|
||||
if app_name not in settings_manager.KNOWN_APP_TYPES: # Corrected attribute name
|
||||
# Allow saving settings for potentially unknown apps if needed, or return error
|
||||
# For now, let's restrict to known types
|
||||
return jsonify({"success": False, "error": f"Unknown application type: {app_name}"}), 400
|
||||
|
||||
# Save the settings using the manager
|
||||
success = settings_manager.save_settings(app_name, settings_data) # Corrected function name
|
||||
|
||||
if success:
|
||||
# ---> ADDED: Update stateful expiration if general settings were saved <---
|
||||
if app_name == 'general':
|
||||
try:
|
||||
new_hours = int(settings_data.get('stateful_management_hours'))
|
||||
if new_hours > 0:
|
||||
web_logger.info(f"General settings saved, updating stateful expiration to {new_hours} hours.")
|
||||
update_lock_expiration(hours=new_hours)
|
||||
else:
|
||||
web_logger.warning("Invalid stateful_management_hours value received, not updating expiration.")
|
||||
except (ValueError, TypeError) as e:
|
||||
web_logger.error(f"Could not update stateful expiration after saving general settings: {e}")
|
||||
except Exception as e:
|
||||
web_logger.error(f"Unexpected error updating stateful expiration: {e}")
|
||||
# ---> END ADDED SECTION <---
|
||||
|
||||
# Return the full updated configuration
|
||||
all_settings = settings_manager.get_all_settings() # Corrected: Use get_all_settings
|
||||
return jsonify(all_settings) # Return the full config object
|
||||
else:
|
||||
return jsonify({"success": False, "error": f"Failed to save settings for {app_name}"}), 500
|
||||
|
||||
@app.route('/api/settings/general', methods=['POST'])
|
||||
def save_general_settings():
|
||||
general_logger = get_logger("web_server")
|
||||
general_logger.info("Received request to save general settings.")
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'general' not in data:
|
||||
general_logger.error("Invalid payload received for saving general settings.")
|
||||
return jsonify({"success": False, "error": "Invalid payload"}), 400
|
||||
|
||||
general_settings_data = data['general']
|
||||
general_logger.debug(f"Saving general settings data: {general_settings_data}")
|
||||
|
||||
# Save the entire general settings dictionary
|
||||
success = settings_manager.save_settings('general', general_settings_data)
|
||||
|
||||
# Make sure we have data
|
||||
if not request.is_json:
|
||||
return jsonify({"success": False, "error": "Expected JSON data"}), 400
|
||||
|
||||
data = request.json
|
||||
|
||||
# Save general settings
|
||||
success = settings_manager.save_settings('general', data)
|
||||
|
||||
if success:
|
||||
# Update expiration timing from general settings if applicable
|
||||
try:
|
||||
new_hours = int(data.get('stateful_management_hours'))
|
||||
if new_hours > 0:
|
||||
general_logger.info(f"Updating stateful expiration to {new_hours} hours.")
|
||||
update_lock_expiration(hours=new_hours)
|
||||
except (ValueError, TypeError, KeyError):
|
||||
# Don't update if the value wasn't provided or is invalid
|
||||
pass
|
||||
except Exception as e:
|
||||
general_logger.error(f"Error updating expiration timing: {e}")
|
||||
|
||||
# Return all settings
|
||||
return jsonify(settings_manager.get_all_settings())
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Failed to save general settings"}), 500
|
||||
|
||||
@app.route('/api/settings/<app_name>', methods=['GET', 'POST'])
|
||||
def handle_app_settings(app_name):
|
||||
web_logger = get_logger("web_server")
|
||||
|
||||
# Validate app_name
|
||||
if app_name not in settings_manager.KNOWN_APP_TYPES:
|
||||
return jsonify({"success": False, "error": f"Unknown application type: {app_name}"}), 400
|
||||
|
||||
if request.method == 'GET':
|
||||
# Return settings for the specific app
|
||||
app_settings = settings_manager.load_settings(app_name)
|
||||
return jsonify(app_settings)
|
||||
|
||||
elif request.method == 'POST':
|
||||
# Make sure we have data
|
||||
if not request.is_json:
|
||||
return jsonify({"success": False, "error": "Expected JSON data"}), 400
|
||||
|
||||
data = request.json
|
||||
web_logger.debug(f"Received {app_name} settings save request: {data}")
|
||||
|
||||
# Save the app settings
|
||||
success = settings_manager.save_settings(app_name, data)
|
||||
|
||||
if success:
|
||||
# ---> ADDED: Update stateful expiration if general settings were saved <---
|
||||
try:
|
||||
new_hours = int(general_settings_data.get('stateful_management_hours'))
|
||||
if new_hours > 0:
|
||||
general_logger.info(f"General settings saved, updating stateful expiration to {new_hours} hours.")
|
||||
update_lock_expiration(hours=new_hours)
|
||||
else:
|
||||
general_logger.warning("Invalid stateful_management_hours value received, not updating expiration.")
|
||||
except (ValueError, TypeError) as e:
|
||||
general_logger.error(f"Could not update stateful expiration after saving general settings: {e}")
|
||||
except Exception as e:
|
||||
general_logger.error(f"Unexpected error updating stateful expiration: {e}")
|
||||
# ---> END ADDED SECTION <---
|
||||
|
||||
general_logger.info("General settings saved successfully.")
|
||||
# Return the full updated config
|
||||
full_config = settings_manager.load_settings()
|
||||
return jsonify(full_config)
|
||||
web_logger.info(f"Successfully saved {app_name} settings")
|
||||
return jsonify({"success": True})
|
||||
else:
|
||||
general_logger.error("Failed to save general settings via settings_manager.")
|
||||
return jsonify({"success": False, "error": "Failed to save settings"}), 500
|
||||
except Exception as e:
|
||||
general_logger.error(f"Error saving general settings: {e}", exc_info=True)
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
web_logger.error(f"Failed to save {app_name} settings")
|
||||
return jsonify({"success": False, "error": f"Failed to save {app_name} settings"}), 500
|
||||
|
||||
@app.route('/api/settings/theme', methods=['GET', 'POST'])
|
||||
def api_theme():
|
||||
|
||||
Reference in New Issue
Block a user