Skip to content
Merged

Dev #464

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions frontend/static/js/new-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* Main JavaScript file for handling UI interactions and API communication
*/

/**
* Huntarr - New UI Implementation
* Main JavaScript file for handling UI interactions and API communication
*/

let huntarrUI = {
// Current state
eventSources: {},
Expand Down Expand Up @@ -2064,6 +2069,38 @@ let huntarrUI = {
});
},

// Format large numbers with appropriate suffixes (K, MIL, BIL, TRI)
formatLargeNumber: function(num) {
if (num < 1000) {
// 0-999: Display as is
return num.toString();
} else if (num < 10000) {
// 1,000-9,999: Display with single decimal and K (e.g., 5.2K)
return (num / 1000).toFixed(1) + 'K';
} else if (num < 100000) {
// 10,000-99,999: Display with single decimal and K (e.g., 75.4K)
return (num / 1000).toFixed(1) + 'K';
} else if (num < 1000000) {
// 100,000-999,999: Display with K (no decimal) (e.g., 982K)
return Math.floor(num / 1000) + 'K';
} else if (num < 10000000) {
// 1,000,000-9,999,999: Display with single decimal and MIL (e.g., 9.7 MIL)
return (num / 1000000).toFixed(1) + ' MIL';
} else if (num < 100000000) {
// 10,000,000-99,999,999: Display with single decimal and MIL (e.g., 99.7 MIL)
return (num / 1000000).toFixed(1) + ' MIL';
} else if (num < 1000000000) {
// 100,000,000-999,999,999: Display with MIL (no decimal)
return Math.floor(num / 1000000) + ' MIL';
} else if (num < 1000000000000) {
// 1B - 999B: Display with single decimal and BIL
return (num / 1000000000).toFixed(1) + ' BIL';
} else {
// 1T+: Display with TRI
return (num / 1000000000000).toFixed(1) + ' TRI';
}
},

animateNumber: function(element, start, end) {
const duration = 1000; // Animation duration in milliseconds
const startTime = performance.now();
Expand All @@ -2076,12 +2113,15 @@ let huntarrUI = {
const easeOutQuad = progress * (2 - progress);

const currentValue = Math.floor(start + (end - start) * easeOutQuad);
element.textContent = currentValue;

// Format number for display
element.textContent = this.formatLargeNumber(currentValue);

if (progress < 1) {
requestAnimationFrame(updateNumber);
} else {
element.textContent = end; // Ensure we end with the exact target number
// Ensure we end with the exact formatted target number
element.textContent = this.formatLargeNumber(end);
}
};

Expand Down
5 changes: 5 additions & 0 deletions frontend/static/js/settings_forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,11 @@ const SettingsForms = {
<input type="number" id="log_refresh_interval_seconds" min="5" value="${settings.log_refresh_interval_seconds !== undefined ? settings.log_refresh_interval_seconds : 30}">
<p class="setting-help" style="margin-left: -3ch !important;">How often Huntarr refreshes logs from apps (seconds)</p>
</div>
<div class="setting-item">
<label for="base_url"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#base-url" class="info-icon" title="Learn more about reverse proxy base URL settings" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>&nbsp;&nbsp;&nbsp;Base URL:</label>
<input type="text" id="base_url" value="${settings.base_url || ''}" placeholder="/huntarr">
<p class="setting-help" style="margin-left: -3ch !important;">Base URL path for reverse proxy (e.g., '/huntarr'). Leave empty for root path. Requires restart.</p>
</div>
</div>
`;

Expand Down
16 changes: 15 additions & 1 deletion frontend/static/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,21 @@ const HuntarrUtils = {
signal: controller.signal
};

return fetch(url, fetchOptions)
// Process URL to handle base URL for reverse proxy subpaths
let processedUrl = url;

// Only process internal API requests (not external URLs)
if (url && typeof url === 'string' && !url.startsWith('http') && !url.startsWith('//')) {
// Handle base URL from window.HUNTARR_BASE_URL if available
const baseUrl = window.HUNTARR_BASE_URL || '';
if (baseUrl && !url.startsWith(baseUrl)) {
// Ensure path starts with a slash
const normalizedPath = url.startsWith('/') ? url : '/' + url;
processedUrl = baseUrl + normalizedPath;
}
}

return fetch(processedUrl, fetchOptions)
.then(response => {
clearTimeout(timeoutId);
return response;
Expand Down
5 changes: 5 additions & 0 deletions frontend/templates/components/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
<link rel="preload" href="/static/logo/256.png" as="image" fetchpriority="high">
<!-- Preload theme script to prevent flashing -->
<script src="/static/js/theme-preload.js"></script>
<!-- Pass base URL configuration to JavaScript -->
<script>
// Make base URL available to frontend JavaScript
window.HUNTARR_BASE_URL = '{{ base_url|default("", true) }}';
</script>
<link rel="stylesheet" href="/static/css/new-style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="icon" href="/static/logo/16.png">
Expand Down
3 changes: 2 additions & 1 deletion src/primary/default_configs/general.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"command_wait_attempts": 600,
"minimum_download_queue_size": -1,
"api_timeout": 120,
"ssl_verify": true
"ssl_verify": true,
"base_url": ""
}
62 changes: 61 additions & 1 deletion src/primary/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,60 @@
if os.path.exists(template_dir):
print(f"Template dir contents: {os.listdir(template_dir)}")

# Get base_url from settings (used for reverse proxy subpath configurations)
def get_base_url():
"""
Get the configured base URL from general settings.
This allows Huntarr to run under a subpath like /huntarr when behind a reverse proxy.

Returns:
str: The configured base URL (e.g., '/huntarr') or empty string if not configured
"""
try:
base_url = settings_manager.get_setting('general', 'base_url', '')
# Ensure base_url always starts with a / if not empty
if base_url and not base_url.startswith('/'):
base_url = f'/{base_url}'
# Remove trailing slash if present
if base_url and base_url != '/' and base_url.endswith('/'):
base_url = base_url.rstrip('/')
return base_url
except Exception as e:
print(f"Error getting base_url from settings: {e}")
return ''

# Define base_url at module level
base_url = ''

# Create Flask app with additional debug logging
app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
print(f"Flask app created with template_folder: {app.template_folder}")
print(f"Flask app created with static_folder: {app.static_folder}")

# Get and apply the base URL setting after app is created
try:
base_url = get_base_url()
if base_url:
print(f"Configuring base URL: {base_url}")
app.config['APPLICATION_ROOT'] = base_url
# Flask 1.x compatibility - needed for proper URL generation
if not hasattr(app, 'wsgi_app') or not hasattr(app.wsgi_app, '__call__'):
print("Warning: Unable to configure WSGI middleware for base URL")
else:
# This ensures static files and other routes respect the base URL
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.exceptions import NotFound
app.wsgi_app = DispatcherMiddleware(
NotFound(), # Default 404 app when accessed without base URL
{base_url: app.wsgi_app} # Main app mounted at base URL
)
print(f"WSGI middleware configured for base URL: {base_url}")
else:
print("Running at root URL path (no base URL)")
except Exception as e:
print(f"Error applying base URL setting: {e}")
base_url = '' # Fallback to empty string on error

# Add debug logging for template rendering
def debug_template_rendering():
"""Additional logging for Flask template rendering"""
Expand Down Expand Up @@ -198,6 +247,12 @@ def get_source_wrapper(environment, template):
# Register the authentication check to run before requests
app.before_request(authenticate_request)

# Add base_url to template context so it can be used in templates
@app.context_processor
def inject_base_url():
"""Add base_url to template context for use in templates"""
return {'base_url': base_url}

# Removed MAIN_PID and signal-related code

# Lock for accessing the log files
Expand All @@ -219,14 +274,19 @@ def get_source_wrapper(environment, template):

ALL_APP_LOG_FILES = list(KNOWN_LOG_FILES.values()) # List of all individual log file paths

# Handle both root path and base URL root path
@app.route('/')
def home():
"""Render the main index page"""
return render_template('index.html')

@app.route('/user')
def user():
# User account screen
"""Render the user account screen"""
return render_template('user.html')

# This section previously contained code for redirecting paths to include the base URL
# It has been removed as Flask's APPLICATION_ROOT setting provides this functionality

# Removed /settings and /logs routes if handled by index.html and JS routing
# Keep /logs if it's the actual SSE endpoint
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.0.2
7.0.3
Loading