feat: initialize timezone from environment variable and enhance documentation for timezone settings

This commit is contained in:
Admin9705
2025-06-08 08:51:44 -04:00
parent 0e99dbafef
commit b28ab0ad34
15 changed files with 306 additions and 75 deletions

View File

@@ -57,9 +57,11 @@
<ul>
<li><a href="#system-settings">System Settings</a>
<ul>
<li><a href="#timezone">Timezone</a></li>
<li><a href="#check-for-updates">Check for Updates</a></li>
<li><a href="#debug-mode">Debug Mode</a></li>
<li><a href="#display-resources">Display Resources</a></li>
<li><a href="#low-usage-mode">Low Usage Mode</a></li>
</ul>
</li>
<li><a href="#notifications">Notifications</a>
@@ -92,6 +94,7 @@
<li><a href="#cmd-wait-attempts">CMD Wait Attempts</a></li>
<li><a href="#max-dl-queue-size">Max DL Queue Size</a></li>
<li><a href="#log-refresh-interval">Log Refresh Interval</a></li>
<li><a href="#base-url">Base URL</a></li>
</ul>
</li>
</ul>
@@ -101,6 +104,33 @@
<h2>System Settings</h2>
<p>These settings control basic functionality and appearance of your Huntarr.io instance.</p>
<h3 id="timezone"><a href="#timezone" class="info-icon"><i class="fas fa-info-circle"></i></a> Timezone</h3>
<p>Set your timezone for accurate time display in logs, scheduling, and all time-related features throughout Huntarr.</p>
<p>This setting controls how timestamps are displayed in the following areas:</p>
<ul>
<li><strong>Log timestamps:</strong> All log entries will show the correct local time</li>
<li><strong>Scheduling displays:</strong> Schedule times will be shown in your local timezone</li>
<li><strong>Stateful management:</strong> Reset times and state information will use your timezone</li>
<li><strong>Cycle tracking:</strong> Cycle start/end times will be in your local time</li>
</ul>
<p>When running in Docker, Huntarr will automatically detect your timezone from the <code>TZ</code> environment variable if set. You can override this setting through the web interface at any time.</p>
<p><strong>Supported timezones include:</strong></p>
<ul>
<li>UTC (Coordinated Universal Time)</li>
<li>North American timezones (Eastern, Central, Mountain, Pacific, Hawaii)</li>
<li>Canadian timezones (Eastern and Pacific Canada)</li>
<li>European timezones (UK, Central Europe, Germany, Netherlands, Italy, Spain)</li>
<li>Asian timezones (Japan, China, India)</li>
<li>Australian and Pacific timezones (Sydney, Perth, Auckland)</li>
</ul>
<p><strong>Docker Example:</strong> Set <code>TZ=Pacific/Honolulu</code> in your docker-compose.yml environment variables for Hawaii time.</p>
<p>Changes to this setting take effect immediately and will update all logging and display times throughout the application.</p>
<h3 id="check-for-updates"><a href="#check-for-updates" class="info-icon"><i class="fas fa-info-circle"></i></a> Check for Updates</h3>
<p>When enabled, Huntarr will automatically check for new versions and notify you when updates are available.</p>
@@ -121,6 +151,27 @@
<p>The Resources section displays helpful links like documentation, GitHub repository, and community forums. You may want to hide this once you're familiar with Huntarr to optimize screen space.</p>
<p>New users should keep this enabled as it provides quick access to documentation and support resources. More experienced users might prefer to hide this section to focus on the core functionality of Huntarr.</p>
<h3 id="low-usage-mode"><a href="#low-usage-mode" class="info-icon"><i class="fas fa-info-circle"></i></a> Low Usage Mode</h3>
<p>Reduces CPU and GPU usage by disabling animations and visual effects, making Huntarr more suitable for older devices or systems with limited resources.</p>
<p>When enabled, this setting will:</p>
<ul>
<li><strong>Disable animations:</strong> Removes smooth transitions and loading animations</li>
<li><strong>Reduce visual effects:</strong> Simplifies the user interface to use fewer system resources</li>
<li><strong>Optimize rendering:</strong> Uses more efficient rendering techniques for slower devices</li>
<li><strong>Lower CPU usage:</strong> Reduces background processing for UI elements</li>
</ul>
<p>This mode is particularly useful for:</p>
<ul>
<li>Older computers or single-board computers like Raspberry Pi</li>
<li>Systems with limited RAM or processing power</li>
<li>Remote access scenarios where bandwidth is limited</li>
<li>Users who prefer a more responsive, simplified interface</li>
</ul>
<p>The setting takes effect immediately and doesn't require a restart. You can toggle it on and off as needed depending on your current usage requirements.</p>
</section>
<section id="notifications">
@@ -353,6 +404,45 @@
<li><strong>30 seconds:</strong> Default, good balance for most users</li>
<li><strong>60 seconds:</strong> For systems with limited resources or when logs aren't frequently needed</li>
</ul>
<h3 id="base-url"><a href="#base-url" class="info-icon"><i class="fas fa-info-circle"></i></a> Base URL</h3>
<p>Base URL path for reverse proxy configurations (e.g., '/huntarr'). Leave empty for root path deployment.</p>
<p>This setting is essential when running Huntarr behind a reverse proxy server like Nginx, Apache, or Cloudflare Tunnel where you want to host Huntarr at a subpath rather than the root domain.</p>
<p><strong>Example configurations:</strong></p>
<ul>
<li><strong>Root path:</strong> Leave empty to access Huntarr at <code>https://yourdomain.com/</code></li>
<li><strong>Subpath:</strong> Set to <code>/huntarr</code> to access at <code>https://yourdomain.com/huntarr/</code></li>
<li><strong>Multiple services:</strong> Set to <code>/media/huntarr</code> for <code>https://yourdomain.com/media/huntarr/</code></li>
</ul>
<p><strong>Reverse proxy configuration examples:</strong></p>
<p><strong>Nginx:</strong></p>
<pre><code>location /huntarr {
proxy_pass http://huntarr:9705;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}</code></pre>
<p><strong>Cloudflare Tunnel:</strong></p>
<pre><code>ingress:
- hostname: yourdomain.com
path: /huntarr
service: http://huntarr:9705</code></pre>
<p><strong>Important notes:</strong></p>
<ul>
<li>Always include the leading slash (e.g., <code>/huntarr</code> not <code>huntarr</code>)</li>
<li>Do not include trailing slashes</li>
<li>Requires container restart to take effect</li>
<li>Ensure your reverse proxy is configured to forward requests to this path</li>
</ul>
<p>Credit to <a href="https://github.com/scr4tchy" target="_blank">scr4tchy</a> for implementing this feature.</p>
</section>
<div class="section-nav">

View File

@@ -328,7 +328,7 @@ const SettingsForms = {
container.innerHTML = instancesHtml + searchSettingsHtml;
// Add event listeners for the instance management
SettingsForms.setupInstanceManagement(container, 'radarr', settings.instances.length);
this.setupInstanceManagement(container, 'radarr', settings.instances.length);
// Set up event listeners for the skip_future_releases checkbox
const skipFutureCheckbox = container.querySelector('#radarr_skip_future_releases');
@@ -1160,7 +1160,7 @@ const SettingsForms = {
<p class="setting-help" style="margin-left: -3ch !important;">Show or hide the Resources section on the home page</p>
</div>
<div class="setting-item">
<label for="low_usage_mode"><a href="#" class="info-icon" title="Learn more about Low Usage Mode" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Low Usage Mode:</label>
<label for="low_usage_mode"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#low-usage-mode" class="info-icon" title="Learn more about Low Usage Mode" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Low Usage Mode:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
<input type="checkbox" id="low_usage_mode" ${settings.low_usage_mode === true ? 'checked' : ''}>
<span class="toggle-slider" style="position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#3d4353; border-radius:20px; transition:0.4s;"></span>
@@ -1168,13 +1168,14 @@ const SettingsForms = {
<p class="setting-help" style="margin-left: -3ch !important;">Disables animations to reduce CPU/GPU usage on older devices</p>
</div>
<div class="setting-item">
<label for="timezone"><a href="#" class="info-icon" title="Set your timezone for accurate time display" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Timezone:</label>
<label for="timezone"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#timezone" class="info-icon" title="Set your timezone for accurate time display" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>Timezone:</label>
<select id="timezone" name="timezone" style="width: 300px; padding: 8px 12px; border-radius: 6px; cursor: pointer; border: 1px solid rgba(255, 255, 255, 0.1); background-color: #1f2937; color: #d1d5db;">
<option value="UTC" ${settings.timezone === 'UTC' || !settings.timezone ? 'selected' : ''}>UTC (Coordinated Universal Time)</option>
<option value="America/New_York" ${settings.timezone === 'America/New_York' ? 'selected' : ''}>Eastern Time (America/New_York)</option>
<option value="America/Chicago" ${settings.timezone === 'America/Chicago' ? 'selected' : ''}>Central Time (America/Chicago)</option>
<option value="America/Denver" ${settings.timezone === 'America/Denver' ? 'selected' : ''}>Mountain Time (America/Denver)</option>
<option value="America/Los_Angeles" ${settings.timezone === 'America/Los_Angeles' ? 'selected' : ''}>Pacific Time (America/Los_Angeles)</option>
<option value="Pacific/Honolulu" ${settings.timezone === 'Pacific/Honolulu' ? 'selected' : ''}>Hawaii Time (Pacific/Honolulu)</option>
<option value="America/Toronto" ${settings.timezone === 'America/Toronto' ? 'selected' : ''}>Eastern Canada (America/Toronto)</option>
<option value="America/Vancouver" ${settings.timezone === 'America/Vancouver' ? 'selected' : ''}>Pacific Canada (America/Vancouver)</option>
<option value="Europe/London" ${settings.timezone === 'Europe/London' ? 'selected' : ''}>UK Time (Europe/London)</option>
@@ -1215,7 +1216,7 @@ const SettingsForms = {
</div>
</div>
<div class="setting-item">
<label for="stateful_management_hours"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#stateful-management" class="info-icon" title="Learn more about state reset intervals" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>State Reset (Hours):</label>
<label for="stateful_management_hours"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#state-reset-hours" class="info-icon" title="Learn more about state reset intervals" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>State Reset (Hours):</label>
<input type="number" id="stateful_management_hours" min="1" value="${settings.stateful_management_hours || 168}" style="width: 50% !important; max-width: 200px !important; box-sizing: border-box !important; margin: 0 !important; padding: 8px 12px !important; border-radius: 4px !important; display: block !important; text-align: left !important;">
<p class="setting-help" style="margin-left: -3ch !important;">Hours before resetting processed media state (<span id="stateful_management_days">${((settings.stateful_management_hours || 168) / 24).toFixed(1)} days</span>)</p>
<p class="setting-help reset-help" style="margin-left: -3ch !important;">Reset clears all processed media IDs to allow reprocessing</p>

View File

@@ -116,6 +116,14 @@ try:
# Initialize main logger
huntarr_logger = setup_main_logger()
# Initialize timezone from TZ environment variable
try:
from primary.settings_manager import initialize_timezone_from_env
initialize_timezone_from_env()
huntarr_logger.info("Timezone initialization completed.")
except Exception as e:
huntarr_logger.warning(f"Failed to initialize timezone from environment: {e}")
# Initialize clean logging for frontend consumption
setup_clean_logging()
huntarr_logger.info("Clean logging system initialized for frontend consumption.")

View File

@@ -47,15 +47,8 @@ hourly_cap_scheduler_thread = None
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
# Import timezone handling
try:
user_tz = pytz.timezone(timezone_name)
return user_tz
except pytz.UnknownTimeZoneError:
return pytz.UTC
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception:
return pytz.UTC

View File

@@ -205,19 +205,8 @@ def _save_cycle_data(data: Dict[str, Any]) -> None:
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
from src.primary import settings_manager
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
# Import timezone handling
import pytz
try:
user_tz = pytz.timezone(timezone_name)
print(f"[CycleTracker] Using user timezone: {timezone_name}")
return user_tz
except pytz.UnknownTimeZoneError:
print(f"[CycleTracker] Unknown timezone '{timezone_name}', falling back to UTC")
return pytz.UTC
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception as e:
print(f"[CycleTracker] Error getting user timezone: {e}, using UTC")
import pytz
@@ -346,7 +335,7 @@ def update_sleep_json(app_type: str, next_cycle_time: datetime.datetime, cyclelo
# Determine cyclelock value
if cyclelock is None:
# If not explicitly set, preserve existing value or default to True (cycle starting)
existing_cyclelock = sleep_data.get(app_type, {}).get('cyclelock', True)
existing_cyclelock = sleep_data.get(app_type, {})
cyclelock = existing_cyclelock
# Update the app's data - store times in user's timezone format

View File

@@ -45,16 +45,8 @@ scheduler_thread = None
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
from src.primary import settings_manager
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
import pytz
try:
user_tz = pytz.timezone(timezone_name)
return user_tz
except pytz.UnknownTimeZoneError:
return pytz.UTC
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception:
import pytz
return pytz.UTC

View File

@@ -200,6 +200,15 @@ def save_settings(app_name: str, settings_data: Dict[str, Any]) -> bool:
# Clear cache for this app to ensure fresh reads
clear_cache(app_name)
# If general settings were saved, also clear timezone cache
if app_name == 'general':
try:
from src.primary.utils.timezone_utils import clear_timezone_cache
clear_timezone_cache()
settings_logger.debug("Timezone cache cleared after general settings save")
except Exception as e:
settings_logger.warning(f"Failed to clear timezone cache: {e}")
return True
except Exception as e:
settings_logger.error(f"Error saving settings for {app_name} to {settings_file}: {e}")
@@ -286,6 +295,47 @@ def apply_timezone(timezone: str) -> bool:
settings_logger.error(f"Error setting timezone: {str(e)}")
return False
def initialize_timezone_from_env():
"""Initialize timezone setting from TZ environment variable if not already set."""
try:
# Get the TZ environment variable
tz_env = os.environ.get('TZ')
if not tz_env:
settings_logger.info("No TZ environment variable found, using default UTC")
return
# Load current general settings
general_settings = load_settings("general")
current_timezone = general_settings.get("timezone")
# If timezone is not set in settings, initialize it from TZ environment variable
if not current_timezone or current_timezone == "UTC":
settings_logger.info(f"Initializing timezone from TZ environment variable: {tz_env}")
# Validate the timezone
try:
import pytz
pytz.timezone(tz_env) # This will raise an exception if invalid
# Update the settings
general_settings["timezone"] = tz_env
save_settings("general", general_settings)
# Apply the timezone to the system
apply_timezone(tz_env)
settings_logger.info(f"Successfully initialized timezone to {tz_env}")
except pytz.UnknownTimeZoneError:
settings_logger.warning(f"Invalid timezone in TZ environment variable: {tz_env}, keeping UTC")
except Exception as e:
settings_logger.error(f"Error validating timezone {tz_env}: {e}")
else:
settings_logger.info(f"Timezone already set in settings: {current_timezone}")
except Exception as e:
settings_logger.error(f"Error initializing timezone from environment: {e}")
# Add a list of known advanced settings for clarity and documentation
ADVANCED_SETTINGS = [
"api_timeout",

View File

@@ -180,11 +180,8 @@ def clear_processed_ids(app_type: str = None) -> None:
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
import pytz
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
user_tz = pytz.timezone(timezone_name)
return user_tz
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception as e:
logger.warning(f"Could not get user timezone, defaulting to UTC: {e}")
import pytz

View File

@@ -357,12 +357,8 @@ def get_state_management_summary(app_type: str, instance_name: str) -> Dict[str,
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
from src.primary.settings_manager import load_settings
import pytz
general_settings = load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
user_tz = pytz.timezone(timezone_name)
return user_tz
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception as e:
stateful_logger.warning(f"Could not get user timezone, defaulting to UTC: {e}")
import pytz

View File

@@ -20,15 +20,8 @@ def get_ip_address():
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
from src.primary import settings_manager
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
import pytz
try:
return pytz.timezone(timezone_name)
except pytz.UnknownTimeZoneError:
return pytz.UTC
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception:
import pytz
return pytz.UTC

View File

@@ -31,14 +31,8 @@ CLEAN_LOG_FILES = {
def _get_user_timezone():
"""Get the user's selected timezone from general settings"""
try:
from src.primary import settings_manager
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
try:
return pytz.timezone(timezone_name)
except pytz.UnknownTimeZoneError:
return pytz.UTC
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception:
return pytz.UTC

View File

@@ -45,16 +45,10 @@ class LocalTimeFormatter(logging.Formatter):
def _get_user_timezone(self):
"""Get the user's selected timezone from general settings"""
try:
from src.primary import settings_manager
general_settings = settings_manager.load_settings("general")
timezone_name = general_settings.get("timezone", "UTC")
import pytz
try:
return pytz.timezone(timezone_name)
except pytz.UnknownTimeZoneError:
return pytz.UTC
from src.primary.utils.timezone_utils import get_user_timezone
return get_user_timezone()
except Exception:
# Final fallback if timezone_utils can't be imported
import pytz
return pytz.UTC
@@ -216,6 +210,33 @@ def update_logging_levels():
print(f"[Logger] Updated all logger levels to {logging.getLevelName(level)}")
def refresh_timezone_formatters():
"""
Force refresh of all logger formatters to use updated timezone settings.
This should be called when the timezone setting changes.
"""
print("[Logger] Refreshing timezone formatters for all loggers")
# Create new formatter with updated timezone handling
log_format = "%(asctime)s - huntarr - %(levelname)s - %(message)s"
new_formatter = LocalTimeFormatter(log_format, datefmt="%Y-%m-%d %H:%M:%S")
# Update main logger handlers
if logger:
for handler in logger.handlers:
handler.setFormatter(new_formatter)
# Update all app logger handlers
for app_name, app_logger in app_loggers.items():
app_type = app_name.split('.')[-1] if '.' in app_name else app_name
app_format = f"%(asctime)s - huntarr.{app_type} - %(levelname)s - %(message)s"
app_formatter = LocalTimeFormatter(app_format, datefmt="%Y-%m-%d %H:%M:%S")
for handler in app_logger.handlers:
handler.setFormatter(app_formatter)
print("[Logger] Timezone formatters refreshed for all loggers")
def debug_log(message: str, data: object = None, app_type: Optional[str] = None) -> None:
"""
Log debug messages with optional data.

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""
Timezone utilities for Huntarr
Centralized timezone handling with proper fallbacks
"""
import os
import pytz
from typing import Union
# Cache for timezone to avoid repeated settings lookups
_timezone_cache = None
_cache_timestamp = 0
_cache_ttl = 5 # 5 seconds cache TTL
def clear_timezone_cache():
"""Clear the timezone cache to force a fresh lookup."""
global _timezone_cache, _cache_timestamp
_timezone_cache = None
_cache_timestamp = 0
def get_user_timezone() -> pytz.BaseTzInfo:
"""
Get the user's selected timezone with proper fallback handling.
Fallback order:
1. User's timezone setting from general settings
2. TZ environment variable
3. UTC as final fallback
Returns:
pytz.BaseTzInfo: The timezone object to use
"""
global _timezone_cache, _cache_timestamp
# Check cache first
import time
current_time = time.time()
if _timezone_cache and (current_time - _cache_timestamp) < _cache_ttl:
return _timezone_cache
try:
# First try to get timezone from user settings
try:
from src.primary import settings_manager
general_settings = settings_manager.load_settings("general", use_cache=False) # Force fresh read
timezone_name = general_settings.get("timezone")
if timezone_name and timezone_name != "UTC":
try:
tz = pytz.timezone(timezone_name)
# Cache the result
_timezone_cache = tz
_cache_timestamp = current_time
return tz
except pytz.UnknownTimeZoneError:
pass # Fall through to TZ environment variable
except Exception:
pass # Fall through to TZ environment variable
# Second try TZ environment variable
tz_env = os.environ.get('TZ')
if tz_env:
try:
tz = pytz.timezone(tz_env)
# Cache the result
_timezone_cache = tz
_cache_timestamp = current_time
return tz
except pytz.UnknownTimeZoneError:
pass # Fall through to UTC
# Final fallback to UTC
tz = pytz.UTC
_timezone_cache = tz
_cache_timestamp = current_time
return tz
except Exception:
# If anything goes wrong, always return UTC
tz = pytz.UTC
_timezone_cache = tz
_cache_timestamp = current_time
return tz
def get_timezone_name() -> str:
"""
Get the timezone name as a string.
Returns:
str: The timezone name (e.g., 'Pacific/Honolulu', 'UTC')
"""
try:
timezone = get_user_timezone()
return str(timezone)
except Exception:
return "UTC"

View File

@@ -619,6 +619,13 @@ def save_general_settings():
timezone_success = settings_manager.apply_timezone(new_timezone)
if timezone_success:
general_logger.info(f"Successfully applied timezone {new_timezone}")
# Refresh all logger formatters to use the new timezone
try:
from src.primary.utils.logger import refresh_timezone_formatters
refresh_timezone_formatters()
general_logger.info("Timezone formatters refreshed for all loggers")
except Exception as e:
general_logger.warning(f"Failed to refresh timezone formatters: {e}")
else:
general_logger.warning(f"Failed to apply timezone {new_timezone}, but settings saved")
except Exception as e:

View File

@@ -1 +1 @@
7.6.2
7.6.3