mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-02-22 02:24:10 -05:00
- Introduced a global function to refresh the scheduling timezone display when settings change. - Implemented logic to clear existing timer intervals before setting a new one for server time updates. - Enhanced the settings form to trigger the timezone refresh upon relevant changes, improving user experience.
1029 lines
38 KiB
JavaScript
1029 lines
38 KiB
JavaScript
/**
|
|
* Scheduling functionality for Huntarr
|
|
* Implements a SABnzbd-style scheduler for controlling Arr application behavior
|
|
*/
|
|
|
|
// Define the schedules object in the global window scope to prevent redeclaration errors
|
|
// This ensures the variable is only declared once no matter how many times the script loads
|
|
window.huntarrSchedules = window.huntarrSchedules || {
|
|
global: [],
|
|
sonarr: [],
|
|
radarr: [],
|
|
lidarr: [],
|
|
readarr: []
|
|
};
|
|
|
|
// Use an immediately invoked function expression to create a new scope
|
|
(function() {
|
|
// Reference the global schedules object
|
|
const schedules = window.huntarrSchedules;
|
|
|
|
/**
|
|
* Capitalize the first letter of a string
|
|
* @param {string} string - The string to capitalize
|
|
* @returns {string} - The capitalized string
|
|
*/
|
|
function capitalizeFirst(string) {
|
|
if (!string) return '';
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
}
|
|
|
|
// Initialize when document is loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize the scheduler
|
|
initScheduler();
|
|
|
|
// Load existing schedules
|
|
loadSchedules();
|
|
|
|
// Set up event listeners
|
|
setupEventListeners();
|
|
|
|
// Load app instances dynamically
|
|
loadAppInstances();
|
|
|
|
// Load server timezone and display current time
|
|
loadServerTimezone();
|
|
});
|
|
|
|
/**
|
|
* Initialize the scheduler functionality
|
|
*/
|
|
function initScheduler() {
|
|
console.debug('Initializing scheduler'); // DEBUG level per user preference
|
|
|
|
// Load and render the schedules
|
|
loadSchedules();
|
|
|
|
// Initialize the app dropdown
|
|
loadAppInstances();
|
|
|
|
// Set up event listeners
|
|
setupEventListeners();
|
|
|
|
// Initialize time inputs with server current time (will be updated once timezone loads)
|
|
initializeTimeInputs();
|
|
|
|
// Make sure schedule containers are visible
|
|
setTimeout(() => {
|
|
// Ensure schedule table container is visible
|
|
const tableContainer = document.getElementById('schedule-table-container');
|
|
if (tableContainer) {
|
|
tableContainer.style.display = 'block';
|
|
console.debug('Schedule table container visibility ensured');
|
|
}
|
|
|
|
// Ensure current schedules panel is visible
|
|
const schedulePanel = document.querySelector('.scheduler-panel:nth-child(2)');
|
|
if (schedulePanel) {
|
|
schedulePanel.style.display = 'block';
|
|
console.debug('Current schedules panel visibility ensured');
|
|
}
|
|
}, 200);
|
|
|
|
// Check if we're on the scheduling section
|
|
if (window.location.hash === '#scheduling') {
|
|
// Make sure nav item is active
|
|
const schedulingNav = document.getElementById('schedulingNav');
|
|
if (schedulingNav) schedulingNav.classList.add('active');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up event listeners for the scheduler UI
|
|
*/
|
|
function setupEventListeners() {
|
|
// Only set up the event handlers during initialization, not on every page render
|
|
// Use a closure to ensure event listeners are registered only once
|
|
if (!window.huntarrSchedulerInitialized) {
|
|
// Add Schedule button (edit functionality removed for simplicity)
|
|
const addScheduleButton = document.getElementById('addScheduleButton');
|
|
if (addScheduleButton) {
|
|
addScheduleButton.addEventListener('click', function() {
|
|
// Always treat as a new schedule - edit functionality removed
|
|
addSchedule();
|
|
});
|
|
}
|
|
|
|
// Document level listener to catch delete actions regardless of when items are added
|
|
document.addEventListener('click', function(e) {
|
|
// Only react to delete buttons
|
|
const deleteButton = e.target.closest('.delete-schedule');
|
|
if (deleteButton) {
|
|
// Prevent default and bubbling to avoid multiple handlers
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const scheduleId = deleteButton.dataset.id;
|
|
const appType = deleteButton.dataset.appType || 'global';
|
|
|
|
// One single confirmation dialog
|
|
if (confirm('Are you sure you want to delete this schedule?')) {
|
|
deleteSchedule(scheduleId, appType);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Flag to prevent duplicate initialization
|
|
window.huntarrSchedulerInitialized = true;
|
|
console.debug('Scheduler event handlers initialized once');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch app instances from the pre-generated list.json file
|
|
* @returns {Promise<Object>} - Object containing app instances
|
|
*/
|
|
async function fetchAppInstances() {
|
|
console.debug('Fetching app instances from list.json for scheduler dropdown'); // DEBUG level per user preference
|
|
|
|
// Define the app types we support (for fallback)
|
|
const appTypes = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr', 'bazarr'];
|
|
const instances = {};
|
|
|
|
// Initialize all app types with empty arrays
|
|
appTypes.forEach(appType => {
|
|
instances[appType] = [];
|
|
});
|
|
|
|
try {
|
|
// Add a cache-busting parameter to ensure we get fresh data
|
|
const cacheBuster = new Date().getTime();
|
|
const listUrl = `/api/scheduling/list?nocache=${cacheBuster}`;
|
|
|
|
console.debug(`Loading app instances from ${listUrl}`);
|
|
const response = await HuntarrUtils.fetchWithTimeout(listUrl);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.debug('Successfully loaded app instances from list.json');
|
|
|
|
// Process each app type from the list.json file
|
|
for (const appType of appTypes) {
|
|
if (data[appType] && Array.isArray(data[appType]) && data[appType].length > 0) {
|
|
instances[appType] = data[appType];
|
|
console.debug(`Added ${instances[appType].length} ${appType} instances from list.json`);
|
|
} else {
|
|
// Add a fallback default instance if none found
|
|
console.debug(`No ${appType} instances found in list.json, adding default fallback`);
|
|
instances[appType] = [
|
|
{ id: '0', name: `${capitalizeFirst(appType)} Default` }
|
|
];
|
|
}
|
|
}
|
|
} else {
|
|
console.warn(`Error fetching list.json: ${response.status} ${response.statusText}`);
|
|
// Add fallback defaults for all app types
|
|
useDefaultInstances(instances, appTypes);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Error fetching app instances from list.json:', error);
|
|
// Add fallback defaults for all app types
|
|
useDefaultInstances(instances, appTypes);
|
|
}
|
|
|
|
console.debug('Final instances object:', instances);
|
|
return instances;
|
|
}
|
|
|
|
/**
|
|
* Add default instances for all app types as a fallback
|
|
* @param {Object} instances - The instances object to populate
|
|
* @param {Array} appTypes - Array of app types to create defaults for
|
|
*/
|
|
function useDefaultInstances(instances, appTypes) {
|
|
console.debug('Using default instances for all app types');
|
|
appTypes.forEach(appType => {
|
|
instances[appType] = [
|
|
{ id: '0', name: `${capitalizeFirst(appType)} Default` }
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load standard apps for the scheduler
|
|
*/
|
|
function loadAppInstances() {
|
|
console.debug('Loading standard apps for scheduler dropdown'); // DEBUG level per user preference
|
|
|
|
const scheduleApp = document.getElementById('scheduleApp');
|
|
if (!scheduleApp) {
|
|
console.error('Schedule app dropdown not found in DOM');
|
|
return;
|
|
}
|
|
|
|
// Clear existing options
|
|
scheduleApp.innerHTML = '';
|
|
|
|
// Define the standard apps list
|
|
const standardApps = [
|
|
{ value: 'global', text: 'All Apps (Global)' },
|
|
{ value: 'sonarr-all', text: 'Sonarr' },
|
|
{ value: 'radarr-all', text: 'Radarr' },
|
|
{ value: 'lidarr-all', text: 'Lidarr' },
|
|
{ value: 'readarr-all', text: 'Readarr' },
|
|
{ value: 'whisparr-v2', text: 'Whisparr V2' },
|
|
{ value: 'whisparr-v3', text: 'Whisparr V3' }
|
|
];
|
|
|
|
// Add each app to the dropdown
|
|
standardApps.forEach(app => {
|
|
const option = document.createElement('option');
|
|
option.value = app.value;
|
|
option.textContent = app.text;
|
|
scheduleApp.appendChild(option);
|
|
});
|
|
|
|
console.debug('Standard apps loaded for scheduler');
|
|
}
|
|
|
|
/**
|
|
* Format app instances data to a consistent structure
|
|
* @param {Object} data Raw app instances data
|
|
* @returns {Object} Formatted app instances data
|
|
*/
|
|
function formatAppInstances(data) {
|
|
const formatted = {};
|
|
|
|
// Check if data is in the expected format
|
|
if (typeof data !== 'object') {
|
|
throw new Error('Invalid app instances data format');
|
|
}
|
|
|
|
// Process different potential formats
|
|
if (Array.isArray(data)) {
|
|
// Handle array format - group by app type
|
|
data.forEach(instance => {
|
|
if (!instance.type) return;
|
|
|
|
const appType = instance.type.toLowerCase();
|
|
if (!formatted[appType]) {
|
|
formatted[appType] = [];
|
|
}
|
|
|
|
formatted[appType].push({
|
|
id: instance.id || formatted[appType].length + 1,
|
|
name: instance.name || `${capitalizeFirst(appType)} Instance ${instance.id || formatted[appType].length + 1}`
|
|
});
|
|
});
|
|
} else {
|
|
// Handle object format with app types as keys
|
|
Object.keys(data).forEach(appType => {
|
|
const normalizedType = appType.toLowerCase();
|
|
|
|
if (Array.isArray(data[appType])) {
|
|
formatted[normalizedType] = data[appType].map((instance, index) => {
|
|
// Handle if instance is just a string or object
|
|
if (typeof instance === 'string') {
|
|
return {
|
|
id: (index + 1).toString(),
|
|
name: instance
|
|
};
|
|
} else if (typeof instance === 'object') {
|
|
return {
|
|
id: instance.id || (index + 1).toString(),
|
|
name: instance.name || `${capitalizeFirst(normalizedType)} Instance ${instance.id || index + 1}`
|
|
};
|
|
}
|
|
}).filter(Boolean); // Remove any undefined entries
|
|
}
|
|
// If days is already in our format
|
|
else if (typeof data[appType] === 'object' && data[appType] !== null) {
|
|
// Handle object with instance IDs as keys
|
|
formatted[normalizedType] = Object.keys(data[appType]).map((id) => {
|
|
const instance = data[appType][id];
|
|
return {
|
|
id: id,
|
|
name: instance.name || `${capitalizeFirst(normalizedType)} Instance ${id}`
|
|
};
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
/**
|
|
* Load schedules from server JSON files via API
|
|
*/
|
|
function loadSchedules() {
|
|
console.debug('Loading schedules from server'); // DEBUG level per user preference
|
|
|
|
// Make API call to get schedules
|
|
HuntarrUtils.fetchWithTimeout('./api/scheduler/load')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load schedules');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.debug('Loaded schedules from server:', data);
|
|
|
|
// Process the data to ensure it's in the correct format
|
|
Object.keys(schedules).forEach(key => {
|
|
if (Array.isArray(data[key])) {
|
|
// Process each schedule to ensure correct format
|
|
schedules[key] = data[key].map(schedule => {
|
|
// Make sure we have a proper time object
|
|
let timeObj = schedule.time;
|
|
if (typeof schedule.time === 'string') {
|
|
// Convert string time (HH:MM) to time object {hour: HH, minute: MM}
|
|
const [hour, minute] = schedule.time.split(':').map(Number);
|
|
timeObj = { hour, minute };
|
|
} else if (!schedule.time) {
|
|
timeObj = { hour: 0, minute: 0 };
|
|
}
|
|
|
|
return {
|
|
id: schedule.id || String(Date.now() + Math.random() * 1000),
|
|
time: timeObj,
|
|
days: Array.isArray(schedule.days) ? schedule.days : [],
|
|
action: schedule.action || 'pause',
|
|
app: schedule.app || 'global',
|
|
appType: schedule.appType || key, // Preserve the appType
|
|
enabled: schedule.enabled !== false
|
|
};
|
|
});
|
|
} else {
|
|
schedules[key] = [];
|
|
}
|
|
});
|
|
|
|
console.debug('Processed schedules for rendering:', schedules);
|
|
renderSchedules();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading schedules:', error);
|
|
// Initialize empty schedule structure if load fails
|
|
Object.keys(schedules).forEach(key => {
|
|
schedules[key] = [];
|
|
});
|
|
renderSchedules();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse days from API format to our internal format
|
|
*/
|
|
function parseDays(daysData) {
|
|
// Default all days to false
|
|
const days = {
|
|
monday: false,
|
|
tuesday: false,
|
|
wednesday: false,
|
|
thursday: false,
|
|
friday: false,
|
|
saturday: false,
|
|
sunday: false
|
|
};
|
|
|
|
// If days is an array of day names
|
|
if (Array.isArray(daysData)) {
|
|
daysData.forEach(day => {
|
|
// Convert day names to our format (e.g., 'Mon' -> 'monday')
|
|
const dayLower = day.toLowerCase();
|
|
if (dayLower.startsWith('mon')) days.monday = true;
|
|
else if (dayLower.startsWith('tue')) days.tuesday = true;
|
|
else if (dayLower.startsWith('wed')) days.wednesday = true;
|
|
else if (dayLower.startsWith('thu')) days.thursday = true;
|
|
else if (dayLower.startsWith('fri')) days.friday = true;
|
|
else if (dayLower.startsWith('sat')) days.saturday = true;
|
|
else if (dayLower.startsWith('sun')) days.sunday = true;
|
|
});
|
|
}
|
|
// If days is already in our format
|
|
else if (daysData && typeof daysData === 'object') {
|
|
Object.assign(days, daysData);
|
|
}
|
|
|
|
return days;
|
|
}
|
|
|
|
/**
|
|
* Save schedules to server via API
|
|
*/
|
|
function saveSchedules() {
|
|
console.debug('Saving schedules to server');
|
|
|
|
try {
|
|
// Before saving, ensure that schedules include appType information
|
|
// This ensures consistent data structure between saves and loads
|
|
const schedulesCopy = {};
|
|
|
|
// Initialize with empty arrays for each app type
|
|
Object.keys(schedules).forEach(key => {
|
|
schedulesCopy[key] = [];
|
|
});
|
|
|
|
// Process each schedule and ensure proper formatting before saving
|
|
Object.entries(schedules).forEach(([appType, appSchedules]) => {
|
|
if (Array.isArray(appSchedules)) {
|
|
schedulesCopy[appType] = appSchedules.map(schedule => {
|
|
// Clean up the schedule object to ensure it has all required fields
|
|
// Convert days from object format to array format if needed
|
|
let daysArray = [];
|
|
if (schedule.days) {
|
|
if (typeof schedule.days === 'object' && !Array.isArray(schedule.days)) {
|
|
// Convert from object format {monday: true, tuesday: false} to array ['monday']
|
|
Object.entries(schedule.days).forEach(([day, selected]) => {
|
|
if (selected === true) {
|
|
daysArray.push(day);
|
|
}
|
|
});
|
|
} else if (Array.isArray(schedule.days)) {
|
|
// Already an array, just use it
|
|
daysArray = schedule.days;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: schedule.id,
|
|
time: schedule.time,
|
|
days: daysArray, // Use the array format
|
|
action: schedule.action,
|
|
app: schedule.app || 'global',
|
|
enabled: schedule.enabled !== false,
|
|
appType: appType // Store appType as a property for reference when loading
|
|
};
|
|
});
|
|
}
|
|
});
|
|
|
|
console.debug('Saving processed schedules:', schedulesCopy);
|
|
|
|
// Make API call to save schedules
|
|
HuntarrUtils.fetchWithTimeout('./api/scheduler/save', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(schedulesCopy)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to save schedules');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.debug('Schedules saved successfully');
|
|
// Show success toast notification
|
|
if (window.huntarrUI && typeof window.huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification('Schedules saved successfully!', 'success');
|
|
} else {
|
|
alert('Schedules saved successfully!'); // Fallback
|
|
}
|
|
|
|
// Update our schedules object with the cleaned version
|
|
Object.keys(schedules).forEach(key => {
|
|
schedules[key] = schedulesCopy[key];
|
|
});
|
|
} else {
|
|
console.error('Failed to save schedules:', data.message);
|
|
|
|
// Show error message
|
|
const errorMessage = document.createElement('div');
|
|
errorMessage.classList.add('save-error-message');
|
|
errorMessage.textContent = `Failed to save: ${data.message}`;
|
|
document.querySelector('.scheduler-container').appendChild(errorMessage);
|
|
|
|
// Remove message after 3 seconds
|
|
setTimeout(() => {
|
|
if (errorMessage.parentNode) {
|
|
errorMessage.parentNode.removeChild(errorMessage);
|
|
}
|
|
}, 3000);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error saving schedules:', error);
|
|
|
|
// Show error message
|
|
const errorMessage = document.createElement('div');
|
|
errorMessage.classList.add('save-error-message');
|
|
errorMessage.textContent = 'Failed to save schedules!';
|
|
document.querySelector('.scheduler-container').appendChild(errorMessage);
|
|
|
|
// Remove message after 3 seconds
|
|
setTimeout(() => {
|
|
if (errorMessage.parentNode) {
|
|
errorMessage.parentNode.removeChild(errorMessage);
|
|
}
|
|
}, 3000);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in save function:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get formatted schedules for rendering
|
|
* This combines schedules from all app types into a single flat array
|
|
*/
|
|
function getFormattedSchedules() {
|
|
const formattedSchedules = [];
|
|
|
|
// Flatten all app type schedules into a single array
|
|
Object.entries(schedules).forEach(([appType, appSchedules]) => {
|
|
if (Array.isArray(appSchedules)) {
|
|
appSchedules.forEach(schedule => {
|
|
// Ensure we have the correct appType for UI operations
|
|
formattedSchedules.push({
|
|
...schedule,
|
|
appType: schedule.appType || appType // Use existing appType if present, otherwise use the key
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
console.debug('Formatted schedules for display:', formattedSchedules);
|
|
return formattedSchedules;
|
|
}
|
|
|
|
/**
|
|
* Render schedules in the UI
|
|
*/
|
|
function renderSchedules() {
|
|
// Find the schedules container and message element
|
|
const schedulesContainer = document.getElementById('schedulesContainer');
|
|
const noSchedulesMessage = document.getElementById('noSchedulesMessage');
|
|
|
|
// If elements don't exist, try to find their parent and make them visible
|
|
if (!schedulesContainer || !noSchedulesMessage) {
|
|
// Look for the parent panel that contains schedules
|
|
const schedulePanel = document.querySelector('.scheduler-panel:nth-child(2)');
|
|
if (schedulePanel) {
|
|
// Ensure the panel content is visible
|
|
const panelContent = schedulePanel.querySelector('.panel-content');
|
|
if (panelContent) {
|
|
panelContent.style.display = 'block';
|
|
|
|
// Try again to get the elements after making panel visible
|
|
setTimeout(() => renderSchedules(), 100);
|
|
return;
|
|
}
|
|
}
|
|
console.warn('Schedule container elements not found, cannot render schedules');
|
|
return;
|
|
}
|
|
|
|
// Make sure container's parent elements are visible
|
|
const parentPanel = schedulesContainer.closest('.scheduler-panel');
|
|
if (parentPanel) {
|
|
parentPanel.style.display = 'block';
|
|
}
|
|
|
|
// Clear current schedules
|
|
schedulesContainer.innerHTML = '';
|
|
|
|
// Get all schedules in a flat array
|
|
const allSchedules = getFormattedSchedules();
|
|
|
|
// Count total schedules
|
|
const totalSchedules = allSchedules.length;
|
|
|
|
// Show message if no schedules
|
|
if (totalSchedules === 0) {
|
|
schedulesContainer.style.display = 'none';
|
|
noSchedulesMessage.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// Show schedules and hide message
|
|
schedulesContainer.style.display = 'block';
|
|
noSchedulesMessage.style.display = 'none';
|
|
|
|
// Sort schedules by time for easier viewing
|
|
allSchedules.sort((a, b) => {
|
|
if (!a.time) return 1;
|
|
if (!b.time) return -1;
|
|
|
|
const aTime = `${String(a.time.hour).padStart(2, '0')}:${String(a.time.minute).padStart(2, '0')}`;
|
|
const bTime = `${String(b.time.hour).padStart(2, '0')}:${String(b.time.minute).padStart(2, '0')}`;
|
|
return aTime.localeCompare(bTime);
|
|
});
|
|
|
|
// Add each schedule to the UI
|
|
allSchedules.forEach(schedule => {
|
|
const scheduleItem = document.createElement('div');
|
|
scheduleItem.className = 'schedule-item';
|
|
|
|
// Format time
|
|
const formattedTime = `${String(schedule.time.hour).padStart(2, '0')}:${String(schedule.time.minute).padStart(2, '0')}`;
|
|
|
|
// Format days
|
|
let daysText = '';
|
|
if (Array.isArray(schedule.days)) {
|
|
// API format - array of day names
|
|
if (schedule.days.length === 7) {
|
|
daysText = 'Daily';
|
|
} else if (schedule.days.length === 0) {
|
|
daysText = 'None';
|
|
} else {
|
|
// Format day names nicely (e.g., 'Mon, Wed, Fri')
|
|
daysText = schedule.days.map(day => {
|
|
// Capitalize first letter and take first 3 characters
|
|
return day.substring(0, 1).toUpperCase() + day.substring(1, 3);
|
|
}).join(', ');
|
|
}
|
|
} else if (typeof schedule.days === 'object') {
|
|
// Internal format - object with day properties
|
|
const allDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
|
const selectedDays = allDays.filter(day => schedule.days[day]);
|
|
|
|
if (selectedDays.length === 7) {
|
|
daysText = 'Daily';
|
|
} else if (selectedDays.length === 0) {
|
|
daysText = 'None';
|
|
} else {
|
|
// Format day names nicely (e.g., 'Mon, Wed, Fri')
|
|
daysText = selectedDays.map(day => day.substring(0, 1).toUpperCase() + day.substring(1, 3)).join(', ');
|
|
}
|
|
}
|
|
|
|
// Format action name
|
|
let actionText = schedule.action || '';
|
|
if (actionText === 'resume' || actionText === 'enable') {
|
|
actionText = 'Enable';
|
|
} else if (actionText === 'pause' || actionText === 'disable') {
|
|
actionText = 'Disable';
|
|
} else if (actionText.startsWith('api-')) {
|
|
const limit = actionText.split('-')[1];
|
|
actionText = `API Limits ${limit}`;
|
|
}
|
|
|
|
// Format app name
|
|
let appText = 'All Apps';
|
|
if (schedule.app && schedule.app !== 'global') {
|
|
// Format app name nicely using the actual instance name
|
|
const [app, instanceId] = schedule.app.split('-');
|
|
if (instanceId === 'all') {
|
|
appText = `All ${capitalizeFirst(app)} Instances`;
|
|
} else if (app === 'whisparr' && instanceId === 'v2') {
|
|
appText = 'Whisparr V2';
|
|
} else if (app === 'whisparr' && instanceId === 'v3') {
|
|
appText = 'Whisparr V3';
|
|
} else {
|
|
appText = `${capitalizeFirst(app)} Instance ${instanceId}`;
|
|
}
|
|
}
|
|
|
|
// Build the schedule item HTML (checkbox removed but layout preserved)
|
|
scheduleItem.innerHTML = `
|
|
<div class="schedule-item-checkbox"></div>
|
|
<div class="schedule-item-time">${formattedTime}</div>
|
|
<div class="schedule-item-days">${daysText}</div>
|
|
<div class="schedule-item-action">${actionText}</div>
|
|
<div class="schedule-item-app">${appText}</div>
|
|
<div class="schedule-item-actions">
|
|
<button class="icon-button delete-schedule" data-id="${schedule.id}" data-app-type="${schedule.appType}"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
`;
|
|
|
|
// Checkbox removed but empty div kept for layout preservation
|
|
|
|
// Add event listeners for edit and delete buttons
|
|
const editButton = scheduleItem.querySelector('.edit-schedule');
|
|
if (editButton) {
|
|
editButton.addEventListener('click', function() {
|
|
editSchedule(this.getAttribute('data-id'), this.getAttribute('data-app-type'));
|
|
});
|
|
}
|
|
|
|
// No individual delete button handlers - all handled by the document level listener
|
|
|
|
// Add to container
|
|
schedulesContainer.appendChild(scheduleItem);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if we're on a mobile device
|
|
*/
|
|
function isMobileDevice() {
|
|
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
}
|
|
|
|
// Global variable to store current timer interval
|
|
let serverTimeInterval = null;
|
|
|
|
/**
|
|
* Update timezone display for mobile/desktop
|
|
*/
|
|
function updateTimezoneDisplay(serverTimezone) {
|
|
const timezoneLabel = document.querySelector('.timezone-label');
|
|
const timezoneSpan = document.getElementById('serverTimezone');
|
|
const timezoneSeparator = document.querySelector('.timezone-separator');
|
|
|
|
if (timezoneLabel && timezoneSpan) {
|
|
if (isMobileDevice()) {
|
|
// Hide the "Server Time:" label on mobile
|
|
timezoneLabel.style.display = 'none';
|
|
// Hide the timezone name on mobile, show only clock and time
|
|
timezoneSpan.style.display = 'none';
|
|
// Hide the separator pipe on mobile
|
|
if (timezoneSeparator) {
|
|
timezoneSeparator.style.display = 'none';
|
|
}
|
|
} else {
|
|
// Show the label and timezone on desktop
|
|
timezoneLabel.style.display = 'inline';
|
|
timezoneSpan.style.display = 'inline';
|
|
// Show the separator pipe on desktop
|
|
if (timezoneSeparator) {
|
|
timezoneSeparator.style.display = 'inline';
|
|
}
|
|
|
|
// Format timezone for display
|
|
const displayTimezone = serverTimezone.replace('_', ' ');
|
|
timezoneSpan.textContent = displayTimezone;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load server timezone from API and update display
|
|
*/
|
|
function loadServerTimezone() {
|
|
console.debug('Loading server timezone from settings API');
|
|
|
|
HuntarrUtils.fetchWithTimeout('./api/settings')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const serverTimezone = data.general?.timezone || 'UTC';
|
|
console.debug('Server timezone loaded:', serverTimezone);
|
|
|
|
// Clear any existing timer
|
|
if (serverTimeInterval) {
|
|
clearInterval(serverTimeInterval);
|
|
serverTimeInterval = null;
|
|
}
|
|
|
|
// Update timezone display with mobile handling
|
|
updateTimezoneDisplay(serverTimezone);
|
|
|
|
// Update current time in server timezone
|
|
updateServerTime(serverTimezone);
|
|
|
|
// Update time inputs to show server current time
|
|
updateTimeInputsWithServerTime(serverTimezone);
|
|
|
|
// Set up new timer with current timezone
|
|
serverTimeInterval = setInterval(() => updateServerTime(serverTimezone), 60000);
|
|
|
|
// Handle window resize to adjust mobile/desktop display
|
|
window.addEventListener('resize', () => {
|
|
updateTimezoneDisplay(serverTimezone);
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Failed to load server timezone:', error);
|
|
updateTimezoneDisplay('UTC');
|
|
updateServerTime('UTC');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refresh timezone display and timer (called when timezone settings change)
|
|
*/
|
|
function refreshSchedulingTimezone() {
|
|
console.debug('Refreshing scheduling timezone due to settings change');
|
|
loadServerTimezone();
|
|
}
|
|
|
|
/**
|
|
* Update the displayed current server time
|
|
*/
|
|
function updateServerTime(timezone) {
|
|
const currentTimeSpan = document.getElementById('serverCurrentTime');
|
|
if (!currentTimeSpan) return;
|
|
|
|
try {
|
|
const now = new Date();
|
|
const timeString = now.toLocaleTimeString('en-US', {
|
|
timeZone: timezone,
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
currentTimeSpan.textContent = timeString;
|
|
} catch (error) {
|
|
console.error('Error formatting server time:', error);
|
|
currentTimeSpan.textContent = '--:--';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update time inputs with server current time
|
|
*/
|
|
function updateTimeInputsWithServerTime(timezone) {
|
|
const hourSelect = document.getElementById('scheduleHour');
|
|
const minuteSelect = document.getElementById('scheduleMinute');
|
|
|
|
if (hourSelect && minuteSelect) {
|
|
try {
|
|
const now = new Date();
|
|
const serverTime = new Date(now.toLocaleString('en-US', { timeZone: timezone }));
|
|
const hour = serverTime.getHours();
|
|
const minute = serverTime.getMinutes();
|
|
|
|
hourSelect.value = hour;
|
|
minuteSelect.value = minute;
|
|
} catch (error) {
|
|
console.error('Error updating time inputs with server time:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize time inputs with server current time (will be updated once timezone loads)
|
|
*/
|
|
function initializeTimeInputs() {
|
|
const now = new Date();
|
|
const hour = now.getHours();
|
|
const minute = Math.floor(now.getMinutes() / 5) * 5; // Round to nearest 5 minutes
|
|
|
|
const hourSelect = document.getElementById('scheduleHour');
|
|
const minuteSelect = document.getElementById('scheduleMinute');
|
|
|
|
if (hourSelect && minuteSelect) {
|
|
hourSelect.value = hour;
|
|
minuteSelect.value = minute;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a new schedule
|
|
*/
|
|
function addSchedule() {
|
|
// Get form values
|
|
const hour = parseInt(document.getElementById('scheduleHour').value);
|
|
const minute = parseInt(document.getElementById('scheduleMinute').value);
|
|
const action = document.getElementById('scheduleAction').value;
|
|
|
|
// Get selected days
|
|
const days = {
|
|
monday: document.getElementById('day-monday').checked,
|
|
tuesday: document.getElementById('day-tuesday').checked,
|
|
wednesday: document.getElementById('day-wednesday').checked,
|
|
thursday: document.getElementById('day-thursday').checked,
|
|
friday: document.getElementById('day-friday').checked,
|
|
saturday: document.getElementById('day-saturday').checked,
|
|
sunday: document.getElementById('day-sunday').checked
|
|
};
|
|
|
|
// Calculate if any day was selected
|
|
const anyDaySelected = Object.values(days).some(dayIsSelected => dayIsSelected === true);
|
|
|
|
// Validate form inputs (basic validation)
|
|
if (isNaN(hour) || isNaN(minute)) {
|
|
if (window.huntarrUI && typeof window.huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification('Please enter a valid hour and minute.', 'error');
|
|
} else {
|
|
alert('Please enter a valid hour and minute.'); // Fallback
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!anyDaySelected) {
|
|
// Validation is now handled by button state management - no need for warnings
|
|
console.debug('No day selected - this should be prevented by disabled button state');
|
|
return;
|
|
}
|
|
|
|
const app = document.getElementById('scheduleApp').value;
|
|
|
|
// Convert days from object format to array format
|
|
const daysArray = [];
|
|
Object.entries(days).forEach(([day, selected]) => {
|
|
if (selected === true) {
|
|
daysArray.push(day);
|
|
}
|
|
});
|
|
|
|
// Create new schedule object
|
|
const newSchedule = {
|
|
id: Date.now().toString(), // Unique ID for the new schedule
|
|
time: { hour, minute },
|
|
days: daysArray, // Store days as array
|
|
action,
|
|
app,
|
|
enabled: true
|
|
};
|
|
|
|
// Determine app type for this schedule
|
|
let appType = 'global';
|
|
if (app && app !== 'global') {
|
|
const appParts = app.split('-');
|
|
appType = appParts[0] || 'global';
|
|
}
|
|
|
|
// Make sure the array exists for this app type
|
|
if (!schedules[appType]) {
|
|
schedules[appType] = [];
|
|
}
|
|
|
|
// Add to appropriate schedules array
|
|
schedules[appType].push(newSchedule);
|
|
|
|
// Auto-save schedules after adding
|
|
saveSchedules();
|
|
|
|
// Log at DEBUG level
|
|
console.debug(`Added new schedule to ${appType}:`, newSchedule);
|
|
|
|
// Update UI
|
|
renderSchedules();
|
|
|
|
// Don't reset day checkboxes - let user add multiple schedules with same days if needed
|
|
// resetDayCheckboxes(); // Removed to prevent automatic unchecking
|
|
}
|
|
|
|
/**
|
|
* Format days from internal format to API format (array of day names)
|
|
*/
|
|
function formatDaysForAPI(days) {
|
|
const apiDays = [];
|
|
|
|
if (days.monday) apiDays.push('Mon');
|
|
if (days.tuesday) apiDays.push('Tue');
|
|
if (days.wednesday) apiDays.push('Wed');
|
|
if (days.thursday) apiDays.push('Thu');
|
|
if (days.friday) apiDays.push('Fri');
|
|
if (days.saturday) apiDays.push('Sat');
|
|
if (days.sunday) apiDays.push('Sun');
|
|
|
|
return apiDays;
|
|
}
|
|
|
|
/**
|
|
* Delete a schedule (no confirmations to prevent multiple dialog issues)
|
|
*/
|
|
function deleteSchedule(scheduleId, appType = 'global') {
|
|
console.debug(`Deleting schedule ID: ${scheduleId} from ${appType}`); // DEBUG level per user preference
|
|
|
|
// Ensure the app type array exists
|
|
if (!schedules[appType]) {
|
|
schedules[appType] = [];
|
|
return;
|
|
}
|
|
|
|
// Find the schedule index
|
|
const scheduleIndex = schedules[appType].findIndex(s => s.id === scheduleId);
|
|
if (scheduleIndex === -1) return;
|
|
|
|
// Remove from array
|
|
schedules[appType].splice(scheduleIndex, 1);
|
|
|
|
// Auto-save schedules after deletion
|
|
saveSchedules();
|
|
|
|
// Update UI
|
|
renderSchedules();
|
|
|
|
console.debug(`Successfully deleted schedule ID: ${scheduleId} from ${appType}`); // DEBUG level per user preference
|
|
}
|
|
|
|
/**
|
|
* Toggle schedule functionality removed
|
|
* This function is kept as a stub in case other code references it
|
|
*/
|
|
function toggleScheduleEnabled(scheduleId, appType = 'global', enabled) {
|
|
// Function kept as a stub but functionality removed
|
|
console.debug('Toggle schedule enabled called, but functionality removed');
|
|
}
|
|
|
|
/**
|
|
* Reset day checkboxes to unchecked
|
|
*/
|
|
function resetDayCheckboxes() {
|
|
document.getElementById('day-monday').checked = false;
|
|
document.getElementById('day-tuesday').checked = false;
|
|
document.getElementById('day-wednesday').checked = false;
|
|
document.getElementById('day-thursday').checked = false;
|
|
document.getElementById('day-friday').checked = false;
|
|
document.getElementById('day-saturday').checked = false;
|
|
document.getElementById('day-sunday').checked = false;
|
|
|
|
// Also reset the Daily checkbox and its visual state
|
|
const dailyInput = document.getElementById('day-daily');
|
|
const dailyCheckboxDiv = document.querySelector('.daily-checkbox');
|
|
|
|
if (dailyInput) {
|
|
dailyInput.checked = false;
|
|
}
|
|
|
|
if (dailyCheckboxDiv) {
|
|
dailyCheckboxDiv.classList.remove('checked');
|
|
}
|
|
}
|
|
|
|
// Expose scheduling timezone refresh function globally
|
|
window.refreshSchedulingTimezone = refreshSchedulingTimezone;
|
|
|
|
// Close the IIFE that wraps the script
|
|
})();
|