feat(admin): added live configuration management, where user can enable/disable and change configurations without editing .env file. Some changes will need an application restart

This commit is contained in:
2025-10-03 00:15:21 +02:00
parent a70a1660f0
commit 4b653ac270
5 changed files with 1728 additions and 27 deletions
+7
View File
@@ -69,6 +69,13 @@
- URL parameters take priority over configuration (e.g., `?open_database=1`) - URL parameters take priority over configuration (e.g., `?open_database=1`)
- Database section expanded by default to maintain original behavior - Database section expanded by default to maintain original behavior
- Smart metadata handling: sub-section expansion automatically expands parent metadata section - Smart metadata handling: sub-section expansion automatically expands parent metadata section
- Add live environment variable configuration management system
- Configuration Management interface in admin panel with live preview and badge system
- Live settings: Can be changed without application restart (menu visibility, table display, pagination, features)
- Static settings: Require restart but can be edited and saved to .env file (authentication, server, database, API keys)
- Advanced badge system showing value status: True/False for booleans, Set/Default/Unset for other values, Changed indicator
- Live API endpoints: `/admin/api/config/update` for immediate changes, `/admin/api/config/update-static` for .env updates
- Form pre-population with current values and automatic page reload after successful live updates
- Add performance optimization - Add performance optimization
- SQLite WAL Mode: - SQLite WAL Mode:
- Increased cache size to 10,000 pages (~40MB) for faster query execution - Increased cache size to 10,000 pages (~40MB) for faster query execution
+309
View File
@@ -0,0 +1,309 @@
import os
import logging
from typing import Any, Dict, Final, List, Optional
from pathlib import Path
from flask import current_app
logger = logging.getLogger(__name__)
# Environment variables that can be changed live without restart
LIVE_CHANGEABLE_VARS: Final[List[str]] = [
'BK_BRICKLINK_LINKS',
'BK_DEFAULT_TABLE_PER_PAGE',
'BK_INDEPENDENT_ACCORDIONS',
'BK_HIDE_ADD_SET',
'BK_HIDE_ADD_BULK_SET',
'BK_HIDE_ADMIN',
'BK_ADMIN_DEFAULT_EXPANDED_SECTIONS',
'BK_HIDE_ALL_INSTRUCTIONS',
'BK_HIDE_ALL_MINIFIGURES',
'BK_HIDE_ALL_PARTS',
'BK_HIDE_ALL_PROBLEMS_PARTS',
'BK_HIDE_ALL_SETS',
'BK_HIDE_ALL_STORAGES',
'BK_HIDE_STATISTICS',
'BK_HIDE_SET_INSTRUCTIONS',
'BK_HIDE_TABLE_DAMAGED_PARTS',
'BK_HIDE_TABLE_MISSING_PARTS',
'BK_HIDE_TABLE_CHECKED_PARTS',
'BK_HIDE_WISHES',
'BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP',
'BK_MINIFIGURES_PAGINATION_SIZE_MOBILE',
'BK_MINIFIGURES_SERVER_SIDE_PAGINATION',
'BK_PARTS_PAGINATION_SIZE_DESKTOP',
'BK_PARTS_PAGINATION_SIZE_MOBILE',
'BK_PARTS_SERVER_SIDE_PAGINATION',
'BK_SETS_SERVER_SIDE_PAGINATION',
'BK_PROBLEMS_PAGINATION_SIZE_DESKTOP',
'BK_PROBLEMS_PAGINATION_SIZE_MOBILE',
'BK_PROBLEMS_SERVER_SIDE_PAGINATION',
'BK_SETS_PAGINATION_SIZE_DESKTOP',
'BK_SETS_PAGINATION_SIZE_MOBILE',
'BK_SETS_CONSOLIDATION',
'BK_RANDOM',
'BK_REBRICKABLE_LINKS',
'BK_SHOW_GRID_FILTERS',
'BK_SHOW_GRID_SORT',
'BK_SKIP_SPARE_PARTS',
'BK_USE_REMOTE_IMAGES',
'BK_PEERON_DOWNLOAD_DELAY',
'BK_PEERON_MIN_IMAGE_SIZE',
'BK_REBRICKABLE_PAGE_SIZE',
'BK_STATISTICS_SHOW_CHARTS',
'BK_STATISTICS_DEFAULT_EXPANDED',
# Default ordering and formatting
'BK_INSTRUCTIONS_ALLOWED_EXTENSIONS',
'BK_MINIFIGURES_DEFAULT_ORDER',
'BK_PARTS_DEFAULT_ORDER',
'BK_SETS_DEFAULT_ORDER',
'BK_PURCHASE_LOCATION_DEFAULT_ORDER',
'BK_STORAGE_DEFAULT_ORDER',
'BK_WISHES_DEFAULT_ORDER',
# URL and Pattern Variables
'BK_BRICKLINK_LINK_PART_PATTERN',
'BK_REBRICKABLE_IMAGE_NIL',
'BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE',
'BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN',
'BK_REBRICKABLE_LINK_PART_PATTERN',
'BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN',
'BK_PEERON_INSTRUCTION_PATTERN',
'BK_PEERON_SCAN_PATTERN',
'BK_PEERON_THUMBNAIL_PATTERN',
'BK_RETIRED_SETS_FILE_URL',
'BK_RETIRED_SETS_PATH',
'BK_THEMES_FILE_URL',
'BK_THEMES_PATH'
]
# Environment variables that require restart
RESTART_REQUIRED_VARS: Final[List[str]] = [
'BK_AUTHENTICATION_PASSWORD',
'BK_AUTHENTICATION_KEY',
'BK_DATABASE_PATH',
'BK_DEBUG',
'BK_DOMAIN_NAME',
'BK_HOST',
'BK_PORT',
'BK_SOCKET_NAMESPACE',
'BK_SOCKET_PATH',
'BK_NO_THREADED_SOCKET',
'BK_TIMEZONE',
'BK_REBRICKABLE_API_KEY',
'BK_INSTRUCTIONS_FOLDER',
'BK_PARTS_FOLDER',
'BK_SETS_FOLDER',
'BK_MINIFIGURES_FOLDER',
'BK_DATABASE_TIMESTAMP_FORMAT',
'BK_FILE_DATETIME_FORMAT',
'BK_PURCHASE_DATE_FORMAT',
'BK_PURCHASE_CURRENCY',
'BK_REBRICKABLE_USER_AGENT',
'BK_USER_AGENT'
]
class ConfigManager:
"""Manages live configuration updates for BrickTracker"""
def __init__(self):
self.env_file_path = Path('.env')
def get_current_config(self) -> Dict[str, Any]:
"""Get current configuration values for live-changeable variables"""
config = {}
for var in LIVE_CHANGEABLE_VARS:
# Get internal config name
internal_name = var.replace('BK_', '')
# Get current value from Flask config
if internal_name in current_app.config:
config[var] = current_app.config[internal_name]
else:
# Fallback to environment variable
config[var] = os.environ.get(var, '')
return config
def get_restart_required_config(self) -> Dict[str, Any]:
"""Get current configuration values for restart-required variables"""
config = {}
for var in RESTART_REQUIRED_VARS:
# Get internal config name
internal_name = var.replace('BK_', '')
# Get current value from Flask config
if internal_name in current_app.config:
config[var] = current_app.config[internal_name]
else:
# Fallback to environment variable
config[var] = os.environ.get(var, '')
return config
def update_config(self, updates: Dict[str, Any]) -> Dict[str, str]:
"""Update configuration values. Returns dict with status for each update"""
results = {}
for var_name, new_value in updates.items():
if var_name not in LIVE_CHANGEABLE_VARS:
results[var_name] = f"Error: {var_name} requires restart to change"
continue
try:
# Update environment variable
os.environ[var_name] = str(new_value)
# Update Flask config
internal_name = var_name.replace('BK_', '')
cast_value = self._cast_value(var_name, new_value)
current_app.config[internal_name] = cast_value
# Update .env file
self._update_env_file(var_name, new_value)
results[var_name] = "Updated successfully"
logger.info(f"Config updated: {var_name}={new_value}")
except Exception as e:
results[var_name] = f"Error: {str(e)}"
logger.error(f"Failed to update {var_name}: {e}")
return results
def _cast_value(self, var_name: str, value: Any) -> Any:
"""Cast value to appropriate type based on variable name"""
# List variables (admin sections) - Check this FIRST before boolean check
if 'sections' in var_name.lower():
if isinstance(value, str):
return [section.strip() for section in value.split(',') if section.strip()]
elif isinstance(value, list):
return value
else:
return []
# Integer variables (pagination sizes, delays, etc.) - Check BEFORE boolean check
if any(keyword in var_name.lower() for keyword in ['_size', '_page', 'delay', 'min_', 'per_page', 'page_size']):
try:
return int(value)
except (ValueError, TypeError):
return 0
# Boolean variables - More specific patterns to avoid conflicts
if any(keyword in var_name.lower() for keyword in ['hide_', 'server_side_pagination', '_links', 'random', 'skip_', 'show_', 'use_', '_consolidation', '_charts', '_expanded']):
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes', 'on')
return bool(value)
# String variables (default)
return str(value)
def _format_env_value(self, value: Any) -> str:
"""Format value for .env file storage"""
if isinstance(value, bool):
return 'true' if value else 'false'
elif isinstance(value, (int, float)):
return str(value)
elif isinstance(value, list):
return ','.join(str(item) for item in value)
elif value is None:
return ''
else:
return str(value)
def _update_env_file(self, var_name: str, value: Any) -> None:
"""Update the .env file with new value"""
if not self.env_file_path.exists():
self.env_file_path.touch()
# Read current .env content
lines = []
if self.env_file_path.exists():
with open(self.env_file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Format value for .env file
env_value = self._format_env_value(value)
# Find and update the line, or add new line
updated = False
# First pass: Look for existing active variable
for i, line in enumerate(lines):
if line.strip().startswith(f"{var_name}="):
lines[i] = f"{var_name}={env_value}\n"
updated = True
break
# Second pass: If not found, look for commented-out variable
if not updated:
for i, line in enumerate(lines):
stripped = line.strip()
# Check for commented-out variable: # BK_VAR= or #BK_VAR=
if stripped.startswith('#') and var_name in stripped:
# Extract the part after #, handling optional space
comment_content = stripped[1:].strip()
if comment_content.startswith(f"{var_name}=") or comment_content.startswith(f"{var_name} ="):
# Uncomment and set new value, preserving any leading whitespace from original line
leading_whitespace = line[:len(line) - len(line.lstrip())]
lines[i] = f"{leading_whitespace}{var_name}={env_value}\n"
updated = True
logger.info(f"Uncommented and updated {var_name} in .env file")
break
# Third pass: If still not found, append to end
if not updated:
lines.append(f"{var_name}={env_value}\n")
logger.info(f"Added new {var_name} to end of .env file")
# Write back to file
with open(self.env_file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
def validate_config(self) -> Dict[str, Any]:
"""Validate current configuration"""
issues = []
warnings = []
# Check if critical variables are set
if not os.environ.get('BK_REBRICKABLE_API_KEY'):
warnings.append("BK_REBRICKABLE_API_KEY not set - some features may not work")
# Check for conflicting settings
if (os.environ.get('BK_PARTS_SERVER_SIDE_PAGINATION', '').lower() == 'false' and
int(os.environ.get('BK_PARTS_PAGINATION_SIZE_DESKTOP', '10')) > 100):
warnings.append("Large pagination size with client-side pagination may cause performance issues")
# Check pagination sizes are reasonable
for var in ['BK_SETS_PAGINATION_SIZE_DESKTOP', 'BK_PARTS_PAGINATION_SIZE_DESKTOP', 'BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP']:
try:
size = int(os.environ.get(var, '10'))
if size < 1:
issues.append(f"{var} must be at least 1")
elif size > 1000:
warnings.append(f"{var} is very large ({size}) - may cause performance issues")
except ValueError:
issues.append(f"{var} must be a valid integer")
return {
'issues': issues,
'warnings': warnings,
'status': 'valid' if not issues else 'has_issues'
}
def get_variable_help(self, var_name: str) -> str:
"""Get help text for a configuration variable"""
help_text = {
'BK_BRICKLINK_LINKS': 'Show BrickLink links throughout the application',
'BK_DEFAULT_TABLE_PER_PAGE': 'Default number of items per page in tables',
'BK_INDEPENDENT_ACCORDIONS': 'Make accordion sections independent (can open multiple)',
'BK_HIDE_ADD_SET': 'Hide the "Add Set" menu entry',
'BK_HIDE_ADD_BULK_SET': 'Hide the "Add Bulk Set" menu entry',
'BK_HIDE_ADMIN': 'Hide the "Admin" menu entry',
'BK_ADMIN_DEFAULT_EXPANDED_SECTIONS': 'Admin sections to expand by default (comma-separated)',
'BK_HIDE_ALL_INSTRUCTIONS': 'Hide the "Instructions" menu entry',
'BK_HIDE_ALL_MINIFIGURES': 'Hide the "Minifigures" menu entry',
'BK_HIDE_ALL_PARTS': 'Hide the "Parts" menu entry',
'BK_HIDE_ALL_PROBLEMS_PARTS': 'Hide the "Problems" menu entry',
'BK_HIDE_ALL_SETS': 'Hide the "Sets" menu entry',
'BK_HIDE_ALL_STORAGES': 'Hide the "Storages" menu entry',
'BK_HIDE_STATISTICS': 'Hide the "Statistics" menu entry',
'BK_HIDE_SET_INSTRUCTIONS': 'Hide instructions section in set details',
'BK_HIDE_TABLE_DAMAGED_PARTS': 'Hide the "Damaged" column in parts tables',
'BK_HIDE_TABLE_MISSING_PARTS': 'Hide the "Missing" column in parts tables',
'BK_HIDE_TABLE_CHECKED_PARTS': 'Hide the "Checked" column in parts tables',
'BK_HIDE_WISHES': 'Hide the "Wishes" menu entry',
'BK_SETS_CONSOLIDATION': 'Enable set consolidation/grouping functionality',
'BK_SHOW_GRID_FILTERS': 'Show filter options on grids by default',
'BK_SHOW_GRID_SORT': 'Show sort options on grids by default',
'BK_SKIP_SPARE_PARTS': 'Skip spare parts when importing sets',
'BK_USE_REMOTE_IMAGES': 'Use remote images from Rebrickable CDN instead of local',
'BK_STATISTICS_SHOW_CHARTS': 'Show collection growth charts on statistics page',
'BK_STATISTICS_DEFAULT_EXPANDED': 'Expand all statistics sections by default'
}
return help_text.get(var_name, 'No help available for this variable')
+169 -1
View File
@@ -1,9 +1,11 @@
import logging import logging
from flask import Blueprint, request, render_template, current_app from flask import Blueprint, request, render_template, current_app, jsonify
from flask_login import login_required from flask_login import login_required
from ...configuration_list import BrickConfigurationList from ...configuration_list import BrickConfigurationList
from ...config_manager import ConfigManager
from ...config import CONFIG
from ..exceptions import exception_handler from ..exceptions import exception_handler
from ...instructions_list import BrickInstructionsList from ...instructions_list import BrickInstructionsList
from ...rebrickable_image import RebrickableImage from ...rebrickable_image import RebrickableImage
@@ -27,6 +29,68 @@ logger = logging.getLogger(__name__)
admin_page = Blueprint('admin', __name__, url_prefix='/admin') admin_page = Blueprint('admin', __name__, url_prefix='/admin')
def get_env_values():
"""Get current environment values, using defaults from config when not set"""
import os
from pathlib import Path
env_values = {}
config_defaults = {}
env_explicit_values = {} # Track which values are explicitly set
# Read .env file if it exists
env_file = Path('.env')
env_from_file = {}
if env_file.exists():
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
env_from_file[key] = value
# Process each config item
for config_item in CONFIG:
env_name = f"BK_{config_item['n']}"
# Store default value (with casting applied)
default_value = config_item.get('d', '')
if 'c' in config_item and default_value is not None:
cast_type = config_item['c']
if cast_type == bool and default_value == '':
default_value = False # Default for booleans is False only if no default specified
elif cast_type == list and isinstance(default_value, str):
default_value = [item.strip() for item in default_value.split(',') if item.strip()]
# For int/other types, keep the original default value
config_defaults[env_name] = default_value
# Check if value is explicitly set in .env file or environment
is_explicitly_set = env_name in env_from_file or env_name in os.environ
env_explicit_values[env_name] = is_explicitly_set
# Get value from .env file, environment, or default
value = env_from_file.get(env_name) or os.environ.get(env_name)
if value is None:
value = default_value
else:
# Apply casting if specified
if 'c' in config_item and value is not None:
cast_type = config_item['c']
if cast_type == bool and isinstance(value, str):
value = value.lower() in ('true', '1', 'yes', 'on')
elif cast_type == int and value != '':
try:
value = int(value)
except (ValueError, TypeError):
value = config_item.get('d', 0)
elif cast_type == list and isinstance(value, str):
value = [item.strip() for item in value.split(',') if item.strip()]
env_values[env_name] = value
return env_values, config_defaults, env_explicit_values
# Admin # Admin
@admin_page.route('/', methods=['GET']) @admin_page.route('/', methods=['GET'])
@login_required @login_required
@@ -138,9 +202,13 @@ def admin() -> str:
open_tag open_tag
) )
env_values, config_defaults, env_explicit_values = get_env_values()
return render_template( return render_template(
'admin.html', 'admin.html',
configuration=BrickConfigurationList.list(), configuration=BrickConfigurationList.list(),
env_values=env_values,
config_defaults=config_defaults,
env_explicit_values=env_explicit_values,
database_counters=database_counters, database_counters=database_counters,
database_error=request.args.get('database_error'), database_error=request.args.get('database_error'),
database_exception=database_exception, database_exception=database_exception,
@@ -176,3 +244,103 @@ def admin() -> str:
tag_error=request.args.get('tag_error'), tag_error=request.args.get('tag_error'),
theme=BrickThemeList(), theme=BrickThemeList(),
) )
# API Endpoints for Configuration Management
@admin_page.route('/api/config/update', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_config() -> str:
"""Update live configuration variables"""
try:
data = request.get_json()
if not data:
return jsonify({
'status': 'error',
'message': 'No JSON data provided'
}), 400
updates = data.get('updates', {})
if not updates:
return jsonify({
'status': 'error',
'message': 'No updates provided'
}), 400
# Use ConfigManager to update live configuration
config_manager = ConfigManager()
results = config_manager.update_config(updates)
# Check if all updates were successful
successful_updates = {k: v for k, v in results.items() if "successfully" in v}
failed_updates = {k: v for k, v in results.items() if "successfully" not in v}
logger.info(f"Configuration update: {len(successful_updates)} successful, {len(failed_updates)} failed")
if failed_updates:
logger.warning(f"Failed updates: {failed_updates}")
return jsonify({
'status': 'success' if not failed_updates else 'partial',
'results': results,
'successful_count': len(successful_updates),
'failed_count': len(failed_updates)
})
except Exception as e:
logger.error(f"Error updating configuration: {e}")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@admin_page.route('/api/config/update-static', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_static_config() -> str:
"""Update static configuration variables (requires restart)"""
try:
data = request.get_json()
if not data:
return jsonify({
'status': 'error',
'message': 'No JSON data provided'
}), 400
updates = data.get('updates', {})
if not updates:
return jsonify({
'status': 'error',
'message': 'No updates provided'
}), 400
# Use ConfigManager to update .env file
config_manager = ConfigManager()
# Update each variable in the .env file
updated_count = 0
for var_name, value in updates.items():
try:
config_manager._update_env_file(var_name, value)
updated_count += 1
logger.info(f"Updated static config: {var_name}")
except Exception as e:
logger.error(f"Failed to update static config {var_name}: {e}")
raise e
logger.info(f"Updated {updated_count} static configuration variables")
return jsonify({
'status': 'success',
'message': f'Successfully updated {updated_count} static configuration variables to .env file',
'updated_count': updated_count
})
except Exception as e:
logger.error(f"Error updating static configuration: {e}")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
+325
View File
@@ -0,0 +1,325 @@
// Admin Configuration Management
// Handles live environment variable configuration interface
// Initialize form values with current configuration
function initializeConfigValues() {
console.log('Initializing config values with:', window.CURRENT_CONFIG);
Object.keys(window.CURRENT_CONFIG).forEach(varName => {
const value = window.CURRENT_CONFIG[varName];
console.log(`Setting ${varName} = ${value}`);
// Handle live settings (checkboxes and inputs)
const liveToggle = document.getElementById(varName);
if (liveToggle && liveToggle.type === 'checkbox') {
liveToggle.checked = value === true;
console.log(`Set checkbox ${varName} to ${value}`);
}
const liveInputs = document.querySelectorAll(`input[data-var="${varName}"]:not(.config-static)`);
liveInputs.forEach(input => {
if (input.type !== 'checkbox') {
input.value = value !== null && value !== undefined ? value : '';
console.log(`Set input ${varName} to ${input.value}`);
}
});
// Handle static settings
const staticToggle = document.getElementById(`static-${varName}`);
if (staticToggle && staticToggle.type === 'checkbox') {
staticToggle.checked = value === true;
console.log(`Set static checkbox ${varName} to ${value}`);
}
const staticInputs = document.querySelectorAll(`input[data-var="${varName}"].config-static`);
staticInputs.forEach(input => {
if (input.type !== 'checkbox') {
input.value = value !== null && value !== undefined ? value : '';
console.log(`Set static input ${varName} to ${input.value}`);
}
});
});
}
// Handle config change events
function handleConfigChange(element) {
const varName = element.dataset.var;
let newValue;
if (element.type === 'checkbox') {
newValue = element.checked;
} else if (element.type === 'number') {
newValue = parseInt(element.value) || 0;
} else {
newValue = element.value;
}
// Update the badge display
updateConfigBadge(varName, newValue);
// Note: Changes are only saved when "Save All Changes" button is clicked
}
// Update badge display
function updateConfigBadge(varName, value) {
const defaultValue = window.DEFAULT_CONFIG[varName];
const isChanged = JSON.stringify(value) !== JSON.stringify(defaultValue);
// Remove existing badges but keep them inline
const existingBadges = document.querySelectorAll(`[data-badge-var="${varName}"]`);
existingBadges.forEach(badge => {
badge.remove();
});
// Find the label where we should insert new badges
const label = document.querySelector(`label[for="${varName}"], label[for="static-${varName}"]`);
if (!label) return;
// Find the description div (with .text-muted class) to insert badges before it
const descriptionDiv = label.querySelector('.text-muted');
// Create value badge based on new logic
let valueBadge;
if (value === true) {
valueBadge = document.createElement('span');
valueBadge.className = 'badge rounded-pill text-bg-success ms-2';
valueBadge.textContent = 'True';
valueBadge.setAttribute('data-badge-var', varName);
valueBadge.setAttribute('data-badge-type', 'value');
} else if (value === false) {
valueBadge = document.createElement('span');
valueBadge.className = 'badge rounded-pill text-bg-danger ms-2';
valueBadge.textContent = 'False';
valueBadge.setAttribute('data-badge-var', varName);
valueBadge.setAttribute('data-badge-type', 'value');
} else if (JSON.stringify(value) === JSON.stringify(defaultValue)) {
valueBadge = document.createElement('span');
valueBadge.className = 'badge rounded-pill text-bg-light text-dark ms-2';
valueBadge.textContent = `Default: ${defaultValue}`;
valueBadge.setAttribute('data-badge-var', varName);
valueBadge.setAttribute('data-badge-type', 'value');
} else {
// For text/number fields that have been changed, show "Default: X"
valueBadge = document.createElement('span');
valueBadge.className = 'badge rounded-pill text-bg-light text-dark ms-2';
valueBadge.textContent = `Default: ${defaultValue}`;
valueBadge.setAttribute('data-badge-var', varName);
valueBadge.setAttribute('data-badge-type', 'value');
}
// Insert badge before the description div (to keep it on same line as title)
if (descriptionDiv) {
label.insertBefore(valueBadge, descriptionDiv);
} else {
label.appendChild(valueBadge);
}
// Add changed badge if needed
if (isChanged) {
const changedBadge = document.createElement('span');
changedBadge.className = 'badge rounded-pill text-bg-warning ms-1';
changedBadge.textContent = 'Changed';
changedBadge.setAttribute('data-badge-var', varName);
changedBadge.setAttribute('data-badge-type', 'changed');
// Insert changed badge after the value badge
if (descriptionDiv) {
label.insertBefore(changedBadge, descriptionDiv);
} else {
label.appendChild(changedBadge);
}
}
}
// Handle static config save
function saveStaticConfig() {
const staticInputs = document.querySelectorAll('.config-static, .config-static-toggle');
const updates = {};
staticInputs.forEach(input => {
const varName = input.dataset.var;
let value;
if (input.type === 'checkbox') {
value = input.checked;
} else {
value = input.value;
}
updates[varName] = value;
});
console.log('Saving static config:', updates);
// Send to backend via fetch API
fetch('/admin/api/config/update-static', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ updates: updates })
})
.then(response => response.json())
.then(data => {
const statusContainer = document.getElementById('config-status');
if (statusContainer) {
if (data.status === 'success') {
statusContainer.innerHTML = '<div class="alert alert-success"><i class="ri-check-line"></i> Static configuration saved to .env file!</div>';
setTimeout(() => {
statusContainer.innerHTML = '';
}, 3000);
} else {
statusContainer.innerHTML = `<div class="alert alert-danger"><i class="ri-error-warning-line"></i> Error: ${data.message || 'Failed to save static configuration'}</div>`;
}
}
})
.catch(error => {
console.error('Save static config error:', error);
const statusContainer = document.getElementById('config-status');
if (statusContainer) {
statusContainer.innerHTML = '<div class="alert alert-danger"><i class="ri-error-warning-line"></i> Error: Failed to save static configuration</div>';
}
});
}
// Handle button functionality
function setupButtonHandlers() {
// Save All Changes button
const saveAllBtn = document.getElementById('config-save-all');
if (saveAllBtn) {
saveAllBtn.addEventListener('click', () => {
console.log('Save All Changes clicked');
saveLiveConfiguration();
});
}
// Refresh button
const refreshBtn = document.getElementById('config-refresh');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
console.log('Refresh clicked');
location.reload();
});
}
// Reset button
const resetBtn = document.getElementById('config-reset');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
console.log('Reset clicked');
if (confirm('Are you sure you want to reset all settings to default values? This action cannot be undone.')) {
resetToDefaults();
}
});
}
// Static config save button
const saveStaticBtn = document.getElementById('config-save-static');
if (saveStaticBtn) {
saveStaticBtn.addEventListener('click', saveStaticConfig);
}
}
// Save live configuration changes
function saveLiveConfiguration() {
const liveInputs = document.querySelectorAll('.config-toggle, .config-number, .config-text');
const updates = {};
liveInputs.forEach(input => {
const varName = input.dataset.var;
let value;
if (input.type === 'checkbox') {
value = input.checked;
} else if (input.type === 'number') {
value = parseInt(input.value) || 0;
} else {
value = input.value;
}
updates[varName] = value;
});
console.log('Saving live configuration:', updates);
// Show status message
const statusContainer = document.getElementById('config-status');
if (statusContainer) {
statusContainer.innerHTML = '<div class="alert alert-info"><i class="ri-loader-4-line"></i> Saving configuration...</div>';
}
// Send to backend via fetch API
fetch('/admin/api/config/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ updates: updates })
})
.then(response => response.json())
.then(data => {
if (statusContainer) {
if (data.status === 'success') {
statusContainer.innerHTML = '<div class="alert alert-success"><i class="ri-check-line"></i> Configuration saved successfully! Reloading page...</div>';
// Reload the page after a short delay
setTimeout(() => {
location.reload();
}, 1000);
} else {
statusContainer.innerHTML = `<div class="alert alert-danger"><i class="ri-error-warning-line"></i> Error: ${data.message || 'Failed to save configuration'}</div>`;
}
}
})
.catch(error => {
console.error('Save error:', error);
if (statusContainer) {
statusContainer.innerHTML = '<div class="alert alert-danger"><i class="ri-error-warning-line"></i> Error: Failed to save configuration</div>';
}
});
}
// Reset all settings to defaults
function resetToDefaults() {
console.log('Resetting to defaults');
// Reset all form inputs
document.querySelectorAll('.config-toggle, .config-number, .config-text').forEach(input => {
if (input.type === 'checkbox') {
input.checked = false;
} else {
input.value = '';
}
});
// Update badges
Object.keys(window.CURRENT_CONFIG).forEach(varName => {
updateConfigBadge(varName, null);
});
// Show status message
const statusContainer = document.getElementById('config-status');
if (statusContainer) {
statusContainer.innerHTML = '<div class="alert alert-warning"><i class="ri-restart-line"></i> Settings reset to defaults. Click "Save All Changes" to apply.</div>';
}
}
// Initialize when DOM is ready
document.addEventListener("DOMContentLoaded", () => {
console.log('DOM loaded, initializing configuration interface');
// Initialize form values
initializeConfigValues();
// Setup button handlers
setupButtonHandlers();
// Set up event listeners for form changes
document.addEventListener('change', (e) => {
if (e.target.matches('[data-var]')) {
handleConfigChange(e.target);
}
});
console.log('Configuration interface initialized - ready for API calls');
});
+918 -26
View File
@@ -1,29 +1,921 @@
{% import 'macro/accordion.html' as accordion %} {% import 'macro/accordion.html' as accordion %}
{{ accordion.header('Configuration variables', 'configuration', 'admin', icon='list-settings-line') }} <!-- Helper macro for config badges with truncation -->
<ul class="list-group list-group-flush"> {% macro config_badges(var_name) %}
{% for entry in configuration %} {% set current_value = env_values[var_name] %}
<li class="list-group-item"> {% set default_value = config_defaults[var_name] %}
<strong>{{ entry.name }}</strong>: {% set is_explicitly_set = env_explicit_values[var_name] %}
{% if entry.value == none or entry.value == '' %}
<span class="badge rounded-pill text-bg-secondary">Unset</span> <!-- Value badge -->
{% elif entry.value == true %} {% if not is_explicitly_set %}
<span class="badge rounded-pill text-bg-success">True</span> <!-- Not in .env file, using default -->
{% elif entry.value == false %} <span class="badge rounded-pill text-bg-secondary ms-2" data-badge-var="{{ var_name }}" data-badge-type="value">Unset</span>
<span class="badge rounded-pill text-bg-danger">False</span> <span class="badge rounded-pill text-bg-light text-dark ms-1" data-badge-var="{{ var_name }}" data-badge-type="default">Default Value</span>
{% else %} {% elif current_value is sameas true %}
{% if entry.is_secret() %} <span class="badge rounded-pill text-bg-success ms-2" data-badge-var="{{ var_name }}" data-badge-type="value">True</span>
<span class="badge rounded-pill text-bg-success">Set</span> {% elif current_value is sameas false %}
{% else %} <span class="badge rounded-pill text-bg-danger ms-2" data-badge-var="{{ var_name }}" data-badge-type="value">False</span>
<code>{{ entry.value }}</code> {% elif current_value == default_value %}
{% endif %} <!-- Explicitly set to default value -->
{% endif %} <span class="badge rounded-pill text-bg-light text-dark ms-2" data-badge-var="{{ var_name }}" data-badge-type="value">Default: {{ default_value }}</span>
<span class="badge rounded-pill text-bg-light border">Env: {{ entry.env_name }}</span> {% else %}
{% if entry.extra_name %}<span class="badge rounded-pill text-bg-light border">Env: {{ entry.extra_name }}</span>{% endif %} <!-- For text/number fields that have been changed, show "Default: X" -->
{% if entry.is_changed() %} <span class="badge rounded-pill text-bg-light text-dark ms-2" data-badge-var="{{ var_name }}" data-badge-type="value">Default: {{ default_value }}</span>
<span class="badge rounded-pill text-bg-warning">Changed</span> {% endif %}
{% endif %}
</li> <!-- Changed badge -->
{% endfor %} {% if current_value != default_value %}
</ul> <span class="badge rounded-pill text-bg-warning ms-1" data-badge-var="{{ var_name }}" data-badge-type="changed">Changed</span>
{% endif %}
{% endmacro %}
{{ accordion.header('Configuration Management', 'configuration-management', 'admin', icon='settings-4-line') }}
<div class="p-3">
<!-- Configuration Status -->
<div id="config-status" class="mb-3">
<!-- Status indicators -->
</div>
<!-- Badge Legend -->
<div class="alert alert-info mb-3">
<h6 class="mb-2"><i class="ri-information-line"></i> Badge Legend</h6>
<div class="row g-2">
<div class="col-md-6">
<small>
<span class="badge rounded-pill text-bg-success">True</span> Boolean setting enabled<br>
<span class="badge rounded-pill text-bg-danger">False</span> Boolean setting disabled<br>
<span class="badge rounded-pill text-bg-primary">Set</span> Custom value configured
</small>
</div>
<div class="col-md-6">
<small>
<span class="badge rounded-pill text-bg-secondary">Unset</span> Not in .env file<br>
<span class="badge rounded-pill text-bg-light text-dark">Default Value</span> Using default value<br>
<span class="badge rounded-pill text-bg-warning">Changed</span> Modified from default
</small>
</div>
</div>
</div>
<!-- Sub-Drawers -->
<div class="accordion" id="configuration-accordion">
<!-- Live Settings Sub-Drawer -->
<div class="accordion-item">
<h2 class="accordion-header" id="live-settings-heading">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#live-settings-collapse" aria-expanded="true" aria-controls="live-settings-collapse">
<i class="ri-settings-4-line me-2"></i> Live Settings
<span class="badge text-bg-success ms-2">Changes Applied On Save</span>
</button>
</h2>
<div id="live-settings-collapse" class="accordion-collapse collapse show" aria-labelledby="live-settings-heading" data-bs-parent="#configuration-accordion">
<div class="accordion-body">
<!-- Action buttons -->
<div class="d-flex gap-2 justify-content-end mb-4">
<button id="config-save-all" class="btn btn-success">
<i class="ri-save-line"></i> Save All Changes
</button>
<button id="config-refresh" class="btn btn-outline-secondary">
<i class="ri-refresh-line"></i> Refresh
</button>
<button id="config-reset" class="btn btn-outline-secondary">
<i class="ri-restart-line"></i> Reset to Defaults
</button>
</div>
<!-- Live configuration controls -->
<!-- Menu Visibility -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3">Menu Visibility</h6>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ADD_SET" data-var="BK_HIDE_ADD_SET">
<label class="form-check-label" for="BK_HIDE_ADD_SET">
BK_HIDE_ADD_SET {{ config_badges('BK_HIDE_ADD_SET') }}
<div class="text-muted small">Hide the "Add Set" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ADD_BULK_SET" data-var="BK_HIDE_ADD_BULK_SET">
<label class="form-check-label" for="BK_HIDE_ADD_BULK_SET">
BK_HIDE_ADD_BULK_SET {{ config_badges('BK_HIDE_ADD_BULK_SET') }}
<div class="text-muted small">Hide the "Add Bulk Set" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ADMIN" data-var="BK_HIDE_ADMIN">
<label class="form-check-label" for="BK_HIDE_ADMIN">
BK_HIDE_ADMIN {{ config_badges('BK_HIDE_ADMIN') }}
<div class="text-muted small">Hide the "Admin" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_INSTRUCTIONS" data-var="BK_HIDE_ALL_INSTRUCTIONS">
<label class="form-check-label" for="BK_HIDE_ALL_INSTRUCTIONS">
BK_HIDE_ALL_INSTRUCTIONS {{ config_badges('BK_HIDE_ALL_INSTRUCTIONS') }}
<div class="text-muted small">Hide the "Instructions" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_MINIFIGURES" data-var="BK_HIDE_ALL_MINIFIGURES">
<label class="form-check-label" for="BK_HIDE_ALL_MINIFIGURES">
BK_HIDE_ALL_MINIFIGURES {{ config_badges('BK_HIDE_ALL_MINIFIGURES') }}
<div class="text-muted small">Hide the "Minifigures" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_PARTS" data-var="BK_HIDE_ALL_PARTS">
<label class="form-check-label" for="BK_HIDE_ALL_PARTS">
BK_HIDE_ALL_PARTS {{ config_badges('BK_HIDE_ALL_PARTS') }}
<div class="text-muted small">Hide the "Parts" menu entry</div>
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_PROBLEMS_PARTS" data-var="BK_HIDE_ALL_PROBLEMS_PARTS">
<label class="form-check-label" for="BK_HIDE_ALL_PROBLEMS_PARTS">
BK_HIDE_ALL_PROBLEMS_PARTS {{ config_badges('BK_HIDE_ALL_PROBLEMS_PARTS') }}
<div class="text-muted small">Hide the "Problems" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_SETS" data-var="BK_HIDE_ALL_SETS">
<label class="form-check-label" for="BK_HIDE_ALL_SETS">
BK_HIDE_ALL_SETS {{ config_badges('BK_HIDE_ALL_SETS') }}
<div class="text-muted small">Hide the "Sets" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_STORAGES" data-var="BK_HIDE_ALL_STORAGES">
<label class="form-check-label" for="BK_HIDE_ALL_STORAGES">
BK_HIDE_ALL_STORAGES {{ config_badges('BK_HIDE_ALL_STORAGES') }}
<div class="text-muted small">Hide the "Storages" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_STATISTICS" data-var="BK_HIDE_STATISTICS">
<label class="form-check-label" for="BK_HIDE_STATISTICS">
BK_HIDE_STATISTICS {{ config_badges('BK_HIDE_STATISTICS') }}
<div class="text-muted small">Hide the "Statistics" menu entry</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_WISHES" data-var="BK_HIDE_WISHES">
<label class="form-check-label" for="BK_HIDE_WISHES">
BK_HIDE_WISHES {{ config_badges('BK_HIDE_WISHES') }}
<div class="text-muted small">Hide the "Wishes" menu entry</div>
</label>
</div>
</div>
</div>
<!-- Table Display -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Table Display</h6>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_SET_INSTRUCTIONS" data-var="BK_HIDE_SET_INSTRUCTIONS">
<label class="form-check-label" for="BK_HIDE_SET_INSTRUCTIONS">
BK_HIDE_SET_INSTRUCTIONS {{ config_badges('BK_HIDE_SET_INSTRUCTIONS') }}
<div class="text-muted small">Hide instructions section in set details</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_TABLE_DAMAGED_PARTS" data-var="BK_HIDE_TABLE_DAMAGED_PARTS">
<label class="form-check-label" for="BK_HIDE_TABLE_DAMAGED_PARTS">
BK_HIDE_TABLE_DAMAGED_PARTS {{ config_badges('BK_HIDE_TABLE_DAMAGED_PARTS') }}
<div class="text-muted small">Hide the "Damaged" column in parts tables</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_TABLE_MISSING_PARTS" data-var="BK_HIDE_TABLE_MISSING_PARTS">
<label class="form-check-label" for="BK_HIDE_TABLE_MISSING_PARTS">
BK_HIDE_TABLE_MISSING_PARTS {{ config_badges('BK_HIDE_TABLE_MISSING_PARTS') }}
<div class="text-muted small">Hide the "Missing" column in parts tables</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_TABLE_CHECKED_PARTS" data-var="BK_HIDE_TABLE_CHECKED_PARTS">
<label class="form-check-label" for="BK_HIDE_TABLE_CHECKED_PARTS">
BK_HIDE_TABLE_CHECKED_PARTS {{ config_badges('BK_HIDE_TABLE_CHECKED_PARTS') }}
<div class="text-muted small">Hide the "Checked" column in parts tables</div>
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_SHOW_GRID_FILTERS" data-var="BK_SHOW_GRID_FILTERS">
<label class="form-check-label" for="BK_SHOW_GRID_FILTERS">
BK_SHOW_GRID_FILTERS {{ config_badges('BK_SHOW_GRID_FILTERS') }}
<div class="text-muted small">Show filter controls on grid views by default</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_SHOW_GRID_SORT" data-var="BK_SHOW_GRID_SORT">
<label class="form-check-label" for="BK_SHOW_GRID_SORT">
BK_SHOW_GRID_SORT {{ config_badges('BK_SHOW_GRID_SORT') }}
<div class="text-muted small">Show sort options on grids by default</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_INDEPENDENT_ACCORDIONS" data-var="BK_INDEPENDENT_ACCORDIONS">
<label class="form-check-label" for="BK_INDEPENDENT_ACCORDIONS">
BK_INDEPENDENT_ACCORDIONS {{ config_badges('BK_INDEPENDENT_ACCORDIONS') }}
<div class="text-muted small">Make accordion sections independent (can open multiple)</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_SETS_CONSOLIDATION" data-var="BK_SETS_CONSOLIDATION">
<label class="form-check-label" for="BK_SETS_CONSOLIDATION">
BK_SETS_CONSOLIDATION {{ config_badges('BK_SETS_CONSOLIDATION') }}
<div class="text-muted small">Enable set consolidation/grouping functionality</div>
</label>
</div>
</div>
</div>
<!-- Pagination Settings -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Pagination Settings</h6>
<!-- Sets and Parts (Top Row) -->
<div class="row g-4 mb-4">
<!-- Sets Column -->
<div class="col-md-6">
<h6 class="fw-bold text-secondary mb-3">Sets</h6>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_SETS_SERVER_SIDE_PAGINATION" data-var="BK_SETS_SERVER_SIDE_PAGINATION">
<label class="form-check-label" for="BK_SETS_SERVER_SIDE_PAGINATION">
BK_SETS_SERVER_SIDE_PAGINATION {{ config_badges('BK_SETS_SERVER_SIDE_PAGINATION') }}
<div class="text-muted small">Enable/disable pagination for sets</div>
</label>
</div>
</div>
<div class="col-12">
<label for="BK_SETS_PAGINATION_SIZE_DESKTOP" class="form-label">
BK_SETS_PAGINATION_SIZE_DESKTOP {{ config_badges('BK_SETS_PAGINATION_SIZE_DESKTOP') }}
<div class="text-muted small">Sets per page on desktop</div>
</label>
<input type="number" class="form-control config-number" id="BK_SETS_PAGINATION_SIZE_DESKTOP" data-var="BK_SETS_PAGINATION_SIZE_DESKTOP" min="1" max="100">
</div>
<div class="col-12">
<label for="BK_SETS_PAGINATION_SIZE_MOBILE" class="form-label">
BK_SETS_PAGINATION_SIZE_MOBILE {{ config_badges('BK_SETS_PAGINATION_SIZE_MOBILE') }}
<div class="text-muted small">Sets per page on mobile</div>
</label>
<input type="number" class="form-control config-number" id="BK_SETS_PAGINATION_SIZE_MOBILE" data-var="BK_SETS_PAGINATION_SIZE_MOBILE" min="1" max="50">
</div>
</div>
</div>
<!-- Parts Column -->
<div class="col-md-6">
<h6 class="fw-bold text-secondary mb-3">Parts</h6>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_PARTS_SERVER_SIDE_PAGINATION" data-var="BK_PARTS_SERVER_SIDE_PAGINATION">
<label class="form-check-label" for="BK_PARTS_SERVER_SIDE_PAGINATION">
BK_PARTS_SERVER_SIDE_PAGINATION {{ config_badges('BK_PARTS_SERVER_SIDE_PAGINATION') }}
<div class="text-muted small">Enable/disable pagination for parts</div>
</label>
</div>
</div>
<div class="col-12">
<label for="BK_PARTS_PAGINATION_SIZE_DESKTOP" class="form-label">
BK_PARTS_PAGINATION_SIZE_DESKTOP {{ config_badges('BK_PARTS_PAGINATION_SIZE_DESKTOP') }}
<div class="text-muted small">Parts per page on desktop</div>
</label>
<input type="number" class="form-control config-number" id="BK_PARTS_PAGINATION_SIZE_DESKTOP" data-var="BK_PARTS_PAGINATION_SIZE_DESKTOP" min="1" max="100">
</div>
<div class="col-12">
<label for="BK_PARTS_PAGINATION_SIZE_MOBILE" class="form-label">
BK_PARTS_PAGINATION_SIZE_MOBILE {{ config_badges('BK_PARTS_PAGINATION_SIZE_MOBILE') }}
<div class="text-muted small">Parts per page on mobile</div>
</label>
<input type="number" class="form-control config-number" id="BK_PARTS_PAGINATION_SIZE_MOBILE" data-var="BK_PARTS_PAGINATION_SIZE_MOBILE" min="1" max="50">
</div>
</div>
</div>
</div>
<!-- Minifigures and Problems (Bottom Row) -->
<div class="row g-4 mb-4">
<!-- Minifigures Column -->
<div class="col-md-6">
<h6 class="fw-bold text-secondary mb-3">Minifigures</h6>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_MINIFIGURES_SERVER_SIDE_PAGINATION" data-var="BK_MINIFIGURES_SERVER_SIDE_PAGINATION">
<label class="form-check-label" for="BK_MINIFIGURES_SERVER_SIDE_PAGINATION">
BK_MINIFIGURES_SERVER_SIDE_PAGINATION {{ config_badges('BK_MINIFIGURES_SERVER_SIDE_PAGINATION') }}
<div class="text-muted small">Enable/disable pagination for minifigures</div>
</label>
</div>
</div>
<div class="col-12">
<label for="BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP" class="form-label">
BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP {{ config_badges('BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP') }}
<div class="text-muted small">Minifigures per page on desktop</div>
</label>
<input type="number" class="form-control config-number" id="BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP" data-var="BK_MINIFIGURES_PAGINATION_SIZE_DESKTOP" min="1" max="100">
</div>
<div class="col-12">
<label for="BK_MINIFIGURES_PAGINATION_SIZE_MOBILE" class="form-label">
BK_MINIFIGURES_PAGINATION_SIZE_MOBILE {{ config_badges('BK_MINIFIGURES_PAGINATION_SIZE_MOBILE') }}
<div class="text-muted small">Minifigures per page on mobile</div>
</label>
<input type="number" class="form-control config-number" id="BK_MINIFIGURES_PAGINATION_SIZE_MOBILE" data-var="BK_MINIFIGURES_PAGINATION_SIZE_MOBILE" min="1" max="50">
</div>
</div>
</div>
<!-- Problems Column -->
<div class="col-md-6">
<h6 class="fw-bold text-secondary mb-3">Problems</h6>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_PROBLEMS_SERVER_SIDE_PAGINATION" data-var="BK_PROBLEMS_SERVER_SIDE_PAGINATION">
<label class="form-check-label" for="BK_PROBLEMS_SERVER_SIDE_PAGINATION">
BK_PROBLEMS_SERVER_SIDE_PAGINATION {{ config_badges('BK_PROBLEMS_SERVER_SIDE_PAGINATION') }}
<div class="text-muted small">Enable/disable pagination for problems</div>
</label>
</div>
</div>
<div class="col-12">
<label for="BK_PROBLEMS_PAGINATION_SIZE_DESKTOP" class="form-label">
BK_PROBLEMS_PAGINATION_SIZE_DESKTOP {{ config_badges('BK_PROBLEMS_PAGINATION_SIZE_DESKTOP') }}
<div class="text-muted small">Problem parts per page on desktop</div>
</label>
<input type="number" class="form-control config-number" id="BK_PROBLEMS_PAGINATION_SIZE_DESKTOP" data-var="BK_PROBLEMS_PAGINATION_SIZE_DESKTOP" min="1" max="100">
</div>
<div class="col-12">
<label for="BK_PROBLEMS_PAGINATION_SIZE_MOBILE" class="form-label">
BK_PROBLEMS_PAGINATION_SIZE_MOBILE {{ config_badges('BK_PROBLEMS_PAGINATION_SIZE_MOBILE') }}
<div class="text-muted small">Problem parts per page on mobile</div>
</label>
<input type="number" class="form-control config-number" id="BK_PROBLEMS_PAGINATION_SIZE_MOBILE" data-var="BK_PROBLEMS_PAGINATION_SIZE_MOBILE" min="1" max="50">
</div>
</div>
</div>
</div>
<!-- Client side pagination -->
<h6 class="fw-bold text-secondary mb-3">Client side pagination</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="BK_DEFAULT_TABLE_PER_PAGE" class="form-label">
BK_DEFAULT_TABLE_PER_PAGE {{ config_badges('BK_DEFAULT_TABLE_PER_PAGE') }}
<div class="text-muted small">Default number of items per page in tables</div>
</label>
<input type="number" class="form-control config-number" id="BK_DEFAULT_TABLE_PER_PAGE" data-var="BK_DEFAULT_TABLE_PER_PAGE" min="1" max="500">
</div>
</div>
<!-- Features & External Services -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Features & External Services</h6>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_RANDOM" data-var="BK_RANDOM">
<label class="form-check-label" for="BK_RANDOM">
BK_RANDOM {{ config_badges('BK_RANDOM') }}
<div class="text-muted small">Shuffle the lists on the front page</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_BRICKLINK_LINKS" data-var="BK_BRICKLINK_LINKS">
<label class="form-check-label" for="BK_BRICKLINK_LINKS">
BK_BRICKLINK_LINKS {{ config_badges('BK_BRICKLINK_LINKS') }}
<div class="text-muted small">Display BrickLink links wherever applicable</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_REBRICKABLE_LINKS" data-var="BK_REBRICKABLE_LINKS">
<label class="form-check-label" for="BK_REBRICKABLE_LINKS">
BK_REBRICKABLE_LINKS {{ config_badges('BK_REBRICKABLE_LINKS') }}
<div class="text-muted small">Display Rebrickable links wherever applicable</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_SKIP_SPARE_PARTS" data-var="BK_SKIP_SPARE_PARTS">
<label class="form-check-label" for="BK_SKIP_SPARE_PARTS">
BK_SKIP_SPARE_PARTS {{ config_badges('BK_SKIP_SPARE_PARTS') }}
<div class="text-muted small">Skip spare parts when importing sets</div>
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_USE_REMOTE_IMAGES" data-var="BK_USE_REMOTE_IMAGES">
<label class="form-check-label" for="BK_USE_REMOTE_IMAGES">
BK_USE_REMOTE_IMAGES {{ config_badges('BK_USE_REMOTE_IMAGES') }}
<div class="text-muted small">Use remote images from Rebrickable CDN instead of local storage</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_STATISTICS_SHOW_CHARTS" data-var="BK_STATISTICS_SHOW_CHARTS">
<label class="form-check-label" for="BK_STATISTICS_SHOW_CHARTS">
BK_STATISTICS_SHOW_CHARTS {{ config_badges('BK_STATISTICS_SHOW_CHARTS') }}
<div class="text-muted small">Show collection growth charts on statistics page</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_STATISTICS_DEFAULT_EXPANDED" data-var="BK_STATISTICS_DEFAULT_EXPANDED">
<label class="form-check-label" for="BK_STATISTICS_DEFAULT_EXPANDED">
BK_STATISTICS_DEFAULT_EXPANDED {{ config_badges('BK_STATISTICS_DEFAULT_EXPANDED') }}
<div class="text-muted small">Expand all statistics sections by default</div>
</label>
</div>
</div>
</div>
<!-- Advanced Settings -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Advanced Settings</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="BK_PEERON_DOWNLOAD_DELAY" class="form-label">
BK_PEERON_DOWNLOAD_DELAY {{ config_badges('BK_PEERON_DOWNLOAD_DELAY') }}
<div class="text-muted small">Delay between Peeron downloads in milliseconds</div>
</label>
<input type="number" class="form-control config-number" id="BK_PEERON_DOWNLOAD_DELAY" data-var="BK_PEERON_DOWNLOAD_DELAY" min="0" max="10000">
</div>
<div class="col-md-6">
<label for="BK_PEERON_MIN_IMAGE_SIZE" class="form-label">
BK_PEERON_MIN_IMAGE_SIZE {{ config_badges('BK_PEERON_MIN_IMAGE_SIZE') }}
<div class="text-muted small">Minimum valid image size in bytes</div>
</label>
<input type="number" class="form-control config-number" id="BK_PEERON_MIN_IMAGE_SIZE" data-var="BK_PEERON_MIN_IMAGE_SIZE" min="1000" max="1000000">
</div>
<div class="col-md-6">
<label for="BK_REBRICKABLE_PAGE_SIZE" class="form-label">
BK_REBRICKABLE_PAGE_SIZE {{ config_badges('BK_REBRICKABLE_PAGE_SIZE') }}
<div class="text-muted small">Number of items per page for Rebrickable API requests</div>
</label>
<input type="number" class="form-control config-number" id="BK_REBRICKABLE_PAGE_SIZE" data-var="BK_REBRICKABLE_PAGE_SIZE" min="100" max="5000">
</div>
<div class="col-md-6">
<label for="BK_ADMIN_DEFAULT_EXPANDED_SECTIONS" class="form-label">
BK_ADMIN_DEFAULT_EXPANDED_SECTIONS {{ config_badges('BK_ADMIN_DEFAULT_EXPANDED_SECTIONS') }}
<div class="text-muted small">Admin sections to expand by default (comma-separated)</div>
</label>
<input type="text" class="form-control config-text" id="BK_ADMIN_DEFAULT_EXPANDED_SECTIONS" data-var="BK_ADMIN_DEFAULT_EXPANDED_SECTIONS">
</div>
</div>
<!-- Default Ordering & Formatting -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Default Ordering & Formatting</h6>
<div class="row g-3">
<div class="col-12">
<label for="BK_INSTRUCTIONS_ALLOWED_EXTENSIONS" class="form-label">
BK_INSTRUCTIONS_ALLOWED_EXTENSIONS {{ config_badges('BK_INSTRUCTIONS_ALLOWED_EXTENSIONS') }}
<div class="text-muted small">Allowed file extensions for instructions (comma-separated)</div>
</label>
<input type="text" class="form-control config-text" id="BK_INSTRUCTIONS_ALLOWED_EXTENSIONS" data-var="BK_INSTRUCTIONS_ALLOWED_EXTENSIONS">
</div>
<div class="col-12">
<label for="BK_MINIFIGURES_DEFAULT_ORDER" class="form-label">
BK_MINIFIGURES_DEFAULT_ORDER {{ config_badges('BK_MINIFIGURES_DEFAULT_ORDER') }}
<div class="text-muted small">SQL ORDER BY clause for minifigures listing</div>
</label>
<input type="text" class="form-control config-text" id="BK_MINIFIGURES_DEFAULT_ORDER" data-var="BK_MINIFIGURES_DEFAULT_ORDER">
</div>
<div class="col-12">
<label for="BK_PARTS_DEFAULT_ORDER" class="form-label">
BK_PARTS_DEFAULT_ORDER {{ config_badges('BK_PARTS_DEFAULT_ORDER') }}
<div class="text-muted small">SQL ORDER BY clause for parts listing</div>
</label>
<input type="text" class="form-control config-text" id="BK_PARTS_DEFAULT_ORDER" data-var="BK_PARTS_DEFAULT_ORDER">
</div>
<div class="col-12">
<label for="BK_SETS_DEFAULT_ORDER" class="form-label">
BK_SETS_DEFAULT_ORDER {{ config_badges('BK_SETS_DEFAULT_ORDER') }}
<div class="text-muted small">SQL ORDER BY clause for sets listing</div>
</label>
<input type="text" class="form-control config-text" id="BK_SETS_DEFAULT_ORDER" data-var="BK_SETS_DEFAULT_ORDER">
</div>
<div class="col-12">
<label for="BK_PURCHASE_LOCATION_DEFAULT_ORDER" class="form-label">
BK_PURCHASE_LOCATION_DEFAULT_ORDER {{ config_badges('BK_PURCHASE_LOCATION_DEFAULT_ORDER') }}
<div class="text-muted small">SQL ORDER BY clause for purchase locations</div>
</label>
<input type="text" class="form-control config-text" id="BK_PURCHASE_LOCATION_DEFAULT_ORDER" data-var="BK_PURCHASE_LOCATION_DEFAULT_ORDER">
</div>
<div class="col-12">
<label for="BK_STORAGE_DEFAULT_ORDER" class="form-label">
BK_STORAGE_DEFAULT_ORDER {{ config_badges('BK_STORAGE_DEFAULT_ORDER') }}
<div class="text-muted small">SQL ORDER BY clause for storage locations</div>
</label>
<input type="text" class="form-control config-text" id="BK_STORAGE_DEFAULT_ORDER" data-var="BK_STORAGE_DEFAULT_ORDER">
</div>
<div class="col-12">
<label for="BK_WISHES_DEFAULT_ORDER" class="form-label">
BK_WISHES_DEFAULT_ORDER {{ config_badges('BK_WISHES_DEFAULT_ORDER') }}
<div class="text-muted small">SQL ORDER BY clause for wishes listing</div>
</label>
<input type="text" class="form-control config-text" id="BK_WISHES_DEFAULT_ORDER" data-var="BK_WISHES_DEFAULT_ORDER">
</div>
</div>
<!-- URL Patterns & Links -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">URL Patterns & Links</h6>
<div class="row g-3">
<div class="col-12">
<label for="BK_BRICKLINK_LINK_PART_PATTERN" class="form-label">
BK_BRICKLINK_LINK_PART_PATTERN {{ config_badges('BK_BRICKLINK_LINK_PART_PATTERN') }}
<div class="text-muted small">Pattern for BrickLink part links (supports {part} and {color})</div>
</label>
<input type="text" class="form-control config-text" id="BK_BRICKLINK_LINK_PART_PATTERN" data-var="BK_BRICKLINK_LINK_PART_PATTERN">
</div>
<div class="col-12">
<label for="BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN" class="form-label">
BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN {{ config_badges('BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN') }}
<div class="text-muted small">Pattern for Rebrickable minifigure links (supports {figure})</div>
</label>
<input type="text" class="form-control config-text" id="BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN" data-var="BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN">
</div>
<div class="col-12">
<label for="BK_REBRICKABLE_LINK_PART_PATTERN" class="form-label">
BK_REBRICKABLE_LINK_PART_PATTERN {{ config_badges('BK_REBRICKABLE_LINK_PART_PATTERN') }}
<div class="text-muted small">Pattern for Rebrickable part links (supports {part} and {color})</div>
</label>
<input type="text" class="form-control config-text" id="BK_REBRICKABLE_LINK_PART_PATTERN" data-var="BK_REBRICKABLE_LINK_PART_PATTERN">
</div>
<div class="col-12">
<label for="BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN" class="form-label">
BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN {{ config_badges('BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN') }}
<div class="text-muted small">Pattern for Rebrickable instruction links (supports {path})</div>
</label>
<input type="text" class="form-control config-text" id="BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN" data-var="BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN">
</div>
<div class="col-12">
<label for="BK_PEERON_INSTRUCTION_PATTERN" class="form-label">
BK_PEERON_INSTRUCTION_PATTERN {{ config_badges('BK_PEERON_INSTRUCTION_PATTERN') }}
<div class="text-muted small">Pattern for Peeron instruction URLs (supports {set_number} and {version_number})</div>
</label>
<input type="text" class="form-control config-text" id="BK_PEERON_INSTRUCTION_PATTERN" data-var="BK_PEERON_INSTRUCTION_PATTERN">
</div>
<div class="col-12">
<label for="BK_PEERON_SCAN_PATTERN" class="form-label">
BK_PEERON_SCAN_PATTERN {{ config_badges('BK_PEERON_SCAN_PATTERN') }}
<div class="text-muted small">Pattern for Peeron scan URLs (supports {set_number} and {version_number})</div>
</label>
<input type="text" class="form-control config-text" id="BK_PEERON_SCAN_PATTERN" data-var="BK_PEERON_SCAN_PATTERN">
</div>
<div class="col-12">
<label for="BK_PEERON_THUMBNAIL_PATTERN" class="form-label">
BK_PEERON_THUMBNAIL_PATTERN {{ config_badges('BK_PEERON_THUMBNAIL_PATTERN') }}
<div class="text-muted small">Pattern for Peeron thumbnail URLs (supports {set_number} and {version_number})</div>
</label>
<input type="text" class="form-control config-text" id="BK_PEERON_THUMBNAIL_PATTERN" data-var="BK_PEERON_THUMBNAIL_PATTERN">
</div>
</div>
<!-- Images & Resources -->
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Images & Resources</h6>
<div class="row g-3">
<div class="col-12">
<label for="BK_REBRICKABLE_IMAGE_NIL" class="form-label">
BK_REBRICKABLE_IMAGE_NIL {{ config_badges('BK_REBRICKABLE_IMAGE_NIL') }}
<div class="text-muted small">URL for missing image placeholder</div>
</label>
<input type="text" class="form-control config-text" id="BK_REBRICKABLE_IMAGE_NIL" data-var="BK_REBRICKABLE_IMAGE_NIL">
</div>
<div class="col-12">
<label for="BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE" class="form-label">
BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE {{ config_badges('BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE') }}
<div class="text-muted small">URL for missing minifigure image placeholder</div>
</label>
<input type="text" class="form-control config-text" id="BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE" data-var="BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE">
</div>
<div class="col-12">
<label for="BK_RETIRED_SETS_FILE_URL" class="form-label">
BK_RETIRED_SETS_FILE_URL {{ config_badges('BK_RETIRED_SETS_FILE_URL') }}
<div class="text-muted small">URL to the retired sets CSV file</div>
</label>
<input type="text" class="form-control config-text" id="BK_RETIRED_SETS_FILE_URL" data-var="BK_RETIRED_SETS_FILE_URL">
</div>
<div class="col-12">
<label for="BK_RETIRED_SETS_PATH" class="form-label">
BK_RETIRED_SETS_PATH {{ config_badges('BK_RETIRED_SETS_PATH') }}
<div class="text-muted small">Local path to store retired sets CSV</div>
</label>
<input type="text" class="form-control config-text" id="BK_RETIRED_SETS_PATH" data-var="BK_RETIRED_SETS_PATH">
</div>
<div class="col-12">
<label for="BK_THEMES_FILE_URL" class="form-label">
BK_THEMES_FILE_URL {{ config_badges('BK_THEMES_FILE_URL') }}
<div class="text-muted small">URL to the Rebrickable themes CSV file</div>
</label>
<input type="text" class="form-control config-text" id="BK_THEMES_FILE_URL" data-var="BK_THEMES_FILE_URL">
</div>
<div class="col-12">
<label for="BK_THEMES_PATH" class="form-label">
BK_THEMES_PATH {{ config_badges('BK_THEMES_PATH') }}
<div class="text-muted small">Local path to store themes CSV</div>
</label>
<input type="text" class="form-control config-text" id="BK_THEMES_PATH" data-var="BK_THEMES_PATH">
</div>
</div>
</div>
</div>
</div>
<!-- Static Settings Sub-Drawer -->
<div class="accordion-item">
<h2 class="accordion-header" id="static-settings-heading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#static-settings-collapse" aria-expanded="false" aria-controls="static-settings-collapse">
<i class="ri-database-2-line me-2"></i> Static Settings
<span class="badge text-bg-warning ms-2">Requires Restart</span>
</button>
</h2>
<div id="static-settings-collapse" class="accordion-collapse collapse" aria-labelledby="static-settings-heading" data-bs-parent="#configuration-accordion">
<div class="accordion-body">
<!-- Static configuration with editable fields -->
<div class="alert alert-warning">
<h6><i class="ri-warning-line"></i> Restart Required</h6>
These settings require an application restart to take effect. Values can be edited here and will be saved to the .env file.
</div>
<!-- Authentication -->
<h6 class="fw-bold text-secondary border-bottom pb-1 mb-3">Authentication & Security</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="static-BK_AUTHENTICATION_PASSWORD" class="form-label">
BK_AUTHENTICATION_PASSWORD {{ config_badges('BK_AUTHENTICATION_PASSWORD') }}
<div class="text-muted small">Password for authentication system</div>
</label>
<input type="password" class="form-control config-static" id="static-BK_AUTHENTICATION_PASSWORD" data-var="BK_AUTHENTICATION_PASSWORD">
</div>
<div class="col-md-6">
<label for="static-BK_AUTHENTICATION_KEY" class="form-label">
BK_AUTHENTICATION_KEY {{ config_badges('BK_AUTHENTICATION_KEY') }}
<div class="text-muted small">Secret key for session signing</div>
</label>
<input type="password" class="form-control config-static" id="static-BK_AUTHENTICATION_KEY" data-var="BK_AUTHENTICATION_KEY">
</div>
</div>
<!-- Server Configuration -->
<h6 class="fw-bold text-secondary border-bottom pb-1 mb-3 mt-4">Server Configuration</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="static-BK_HOST" class="form-label">
BK_HOST {{ config_badges('BK_HOST') }}
<div class="text-muted small">Server host address to bind to</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_HOST" data-var="BK_HOST">
</div>
<div class="col-md-6">
<label for="static-BK_PORT" class="form-label">
BK_PORT {{ config_badges('BK_PORT') }}
<div class="text-muted small">Port number for the web server</div>
</label>
<input type="number" class="form-control config-static" id="static-BK_PORT" data-var="BK_PORT" min="1" max="65535">
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-static-toggle" type="checkbox" id="static-BK_DEBUG" data-var="BK_DEBUG">
<label class="form-check-label" for="static-BK_DEBUG">
BK_DEBUG {{ config_badges('BK_DEBUG') }}
<div class="text-muted small">Enable debug mode</div>
</label>
</div>
</div>
<div class="col-md-6">
<label for="static-BK_DOMAIN_NAME" class="form-label">
BK_DOMAIN_NAME {{ config_badges('BK_DOMAIN_NAME') }}
<div class="text-muted small">Domain name for CORS configuration</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_DOMAIN_NAME" data-var="BK_DOMAIN_NAME">
</div>
<div class="col-md-6">
<label for="static-BK_TIMEZONE" class="form-label">
BK_TIMEZONE {{ config_badges('BK_TIMEZONE') }}
<div class="text-muted small">Application timezone</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_TIMEZONE" data-var="BK_TIMEZONE">
</div>
</div>
<!-- Database & Storage -->
<h6 class="fw-bold text-secondary border-bottom pb-1 mb-3 mt-4">Database & Storage</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="static-BK_DATABASE_PATH" class="form-label">
BK_DATABASE_PATH {{ config_badges('BK_DATABASE_PATH') }}
<div class="text-muted small">Path to the SQLite database file</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_DATABASE_PATH" data-var="BK_DATABASE_PATH">
</div>
<div class="col-md-6">
<label for="static-BK_INSTRUCTIONS_FOLDER" class="form-label">
BK_INSTRUCTIONS_FOLDER {{ config_badges('BK_INSTRUCTIONS_FOLDER') }}
<div class="text-muted small">Folder for instruction files</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_INSTRUCTIONS_FOLDER" data-var="BK_INSTRUCTIONS_FOLDER">
</div>
<div class="col-md-6">
<label for="static-BK_PARTS_FOLDER" class="form-label">
BK_PARTS_FOLDER {{ config_badges('BK_PARTS_FOLDER') }}
<div class="text-muted small">Folder for part images</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_PARTS_FOLDER" data-var="BK_PARTS_FOLDER">
</div>
<div class="col-md-6">
<label for="static-BK_SETS_FOLDER" class="form-label">
BK_SETS_FOLDER {{ config_badges('BK_SETS_FOLDER') }}
<div class="text-muted small">Folder for set images</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_SETS_FOLDER" data-var="BK_SETS_FOLDER">
</div>
<div class="col-md-6">
<label for="static-BK_MINIFIGURES_FOLDER" class="form-label">
BK_MINIFIGURES_FOLDER {{ config_badges('BK_MINIFIGURES_FOLDER') }}
<div class="text-muted small">Folder for minifigure images</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_MINIFIGURES_FOLDER" data-var="BK_MINIFIGURES_FOLDER">
</div>
</div>
<!-- API Configuration -->
<h6 class="fw-bold text-secondary border-bottom pb-1 mb-3 mt-4">API Configuration</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="static-BK_REBRICKABLE_API_KEY" class="form-label">
BK_REBRICKABLE_API_KEY {{ config_badges('BK_REBRICKABLE_API_KEY') }}
<div class="text-muted small">API key for Rebrickable integration</div>
</label>
<input type="password" class="form-control config-static" id="static-BK_REBRICKABLE_API_KEY" data-var="BK_REBRICKABLE_API_KEY">
</div>
<div class="col-md-6">
<label for="static-BK_USER_AGENT" class="form-label">
BK_USER_AGENT {{ config_badges('BK_USER_AGENT') }}
<div class="text-muted small">User agent string for HTTP requests</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_USER_AGENT" data-var="BK_USER_AGENT">
</div>
<div class="col-md-6">
<label for="static-BK_REBRICKABLE_USER_AGENT" class="form-label">
BK_REBRICKABLE_USER_AGENT {{ config_badges('BK_REBRICKABLE_USER_AGENT') }}
<div class="text-muted small">User agent for Rebrickable API requests</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_REBRICKABLE_USER_AGENT" data-var="BK_REBRICKABLE_USER_AGENT">
</div>
</div>
<!-- Date & Currency Formats -->
<h6 class="fw-bold text-secondary border-bottom pb-1 mb-3 mt-4">Date & Currency Formats</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="static-BK_DATABASE_TIMESTAMP_FORMAT" class="form-label">
BK_DATABASE_TIMESTAMP_FORMAT {{ config_badges('BK_DATABASE_TIMESTAMP_FORMAT') }}
<div class="text-muted small">Database timestamp format</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_DATABASE_TIMESTAMP_FORMAT" data-var="BK_DATABASE_TIMESTAMP_FORMAT">
</div>
<div class="col-md-6">
<label for="static-BK_FILE_DATETIME_FORMAT" class="form-label">
BK_FILE_DATETIME_FORMAT {{ config_badges('BK_FILE_DATETIME_FORMAT') }}
<div class="text-muted small">File datetime format</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_FILE_DATETIME_FORMAT" data-var="BK_FILE_DATETIME_FORMAT">
</div>
<div class="col-md-6">
<label for="static-BK_PURCHASE_DATE_FORMAT" class="form-label">
BK_PURCHASE_DATE_FORMAT {{ config_badges('BK_PURCHASE_DATE_FORMAT') }}
<div class="text-muted small">Purchase date format</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_PURCHASE_DATE_FORMAT" data-var="BK_PURCHASE_DATE_FORMAT">
</div>
<div class="col-md-6">
<label for="static-BK_PURCHASE_CURRENCY" class="form-label">
BK_PURCHASE_CURRENCY {{ config_badges('BK_PURCHASE_CURRENCY') }}
<div class="text-muted small">Purchase currency</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_PURCHASE_CURRENCY" data-var="BK_PURCHASE_CURRENCY">
</div>
</div>
<!-- Socket Configuration -->
<h6 class="fw-bold text-secondary border-bottom pb-1 mb-3 mt-4">Socket Configuration</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="static-BK_SOCKET_NAMESPACE" class="form-label">
BK_SOCKET_NAMESPACE {{ config_badges('BK_SOCKET_NAMESPACE') }}
<div class="text-muted small">Socket.IO namespace</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_SOCKET_NAMESPACE" data-var="BK_SOCKET_NAMESPACE">
</div>
<div class="col-md-6">
<label for="static-BK_SOCKET_PATH" class="form-label">
BK_SOCKET_PATH {{ config_badges('BK_SOCKET_PATH') }}
<div class="text-muted small">Socket.IO path</div>
</label>
<input type="text" class="form-control config-static" id="static-BK_SOCKET_PATH" data-var="BK_SOCKET_PATH">
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input config-static-toggle" type="checkbox" id="static-BK_NO_THREADED_SOCKET" data-var="BK_NO_THREADED_SOCKET">
<label class="form-check-label" for="static-BK_NO_THREADED_SOCKET">
BK_NO_THREADED_SOCKET {{ config_badges('BK_NO_THREADED_SOCKET') }}
<div class="text-muted small">Disable threaded socket mode</div>
</label>
</div>
</div>
</div>
<!-- Save Button for Static Settings -->
<div class="d-flex gap-2 justify-content-end mt-4">
<button id="config-save-static" class="btn btn-warning">
<i class="ri-save-line"></i> Save Static Settings to .env
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{{ accordion.footer() }} {{ accordion.footer() }}
<!-- Initialize Configuration Data -->
<script type="text/javascript">
window.CURRENT_CONFIG = {
{% for env_name, value in env_values.items() %}
'{{ env_name }}': {{ value|tojson }},
{% endfor %}
};
window.DEFAULT_CONFIG = {
{% for env_name, value in config_defaults.items() %}
'{{ env_name }}': {{ value|tojson }},
{% endfor %}
};
window.EXPLICIT_VALUES = {
{% for env_name, is_explicit in env_explicit_values.items() %}
'{{ env_name }}': {{ is_explicit|tojson }},
{% endfor %}
};
</script>
<!-- Configuration Management JavaScript -->
<script src="{{ url_for('static', filename='scripts/admin_config.js') }}"></script>