Files
lollms_hub/app/templates/admin/settings.html
Saifeddine ALOUI 3e925a3b94 Refactor and update administrative settings, data store management, and memory handling
This commit updates several core components related to administrative settings, data store management, and memory management within the application.

Specifically, it includes:
- Updates to API routes for managing data stores and proxy response wrapping.
- Refinements in the memory management system.
- Adjustments to database migration logic related to settings.
- Updates to relevant schema and template files.
2026-04-17 22:15:06 +02:00

611 lines
46 KiB
HTML

{% extends "admin/base.html" %}
{% block title %}Settings{% endblock %}
{% block header_title %}Application Settings{% endblock %}
{% block content %}
<div class="space-y-8">
<form action="{{ url_for('admin_settings_post') }}" method="post" enctype="multipart/form-data" class="card-style space-y-8">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- Branding Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Branding</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label for="branding_title" class="block text-sm font-medium text-current">Branding Title</label>
<input type="text" name="branding_title" id="branding_title" value="{{ settings.branding_title }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
{% if settings.branding_logo_url and 'uploads' in settings.branding_logo_url %}
<!-- VIEW 1: An image has been uploaded. Show preview and Remove button. -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-current">Current Logo</label>
<div class="mt-2 flex items-center gap-4 p-4 border border-dashed border-gray-500 rounded-md">
<img src="{{ settings.branding_logo_url }}" alt="Current Logo" class="h-16 w-auto max-w-xs rounded-md bg-white/10 p-1">
<div class="flex-grow">
<p class="text-sm font-semibold">An uploaded logo is active.</p>
<p class="text-xs text-gray-400">To replace it, first remove the current one and save.</p>
</div>
<button type="submit" name="remove_logo" value="true" class="flex items-center justify-center px-3 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md">
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
Remove
</button>
</div>
</div>
{% else %}
<!-- VIEW 2: No uploaded image. Show URL and Upload fields. -->
<div class="md:col-span-2">
<label for="branding_logo_url" class="block text-sm font-medium text-current">Logo from URL</label>
<input type="url" name="branding_logo_url" id="branding_logo_url" value="{{ settings.branding_logo_url or '' }}" placeholder="https://example.com/logo.png" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div class="md:col-span-2">
<label for="logo_file" class="block text-sm font-medium text-current">Or Upload Logo</label>
<input type="file" name="logo_file" id="logo_file" class="mt-1 block w-full text-sm rounded-md border border-gray-600 file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:bg-[var(--color-primary-600)] file:text-white hover:file:bg-[var(--color-primary-700)]">
<p class="mt-1 text-xs text-gray-400">Max 2MB. Allowed types: PNG, JPG, GIF, SVG, WebP.</p>
</div>
{% endif %}
</div>
</div>
<!-- Appearance Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Appearance</h2>
<!-- THEME PREVIEW ENGINE -->
<script>
const THEME_MAP = {{ themes | tojson | safe }};
function applyLiveTheme() {
const selectedStyle = document.querySelector('input[name="ui_style"]:checked').value;
const selectedColor = document.querySelector('input[name="selected_theme"]:checked').value;
// 1. Update Body Class (UI Style)
const body = document.body;
// Remove all existing ui- classes
body.className = body.className.replace(/\bui-\S+/g, '');
body.classList.add('ui-' + selectedStyle);
// 2. Update CSS Variables (Primary Colors)
const colors = THEME_MAP[selectedColor];
if (colors) {
const root = document.documentElement;
root.style.setProperty('--color-primary-500', colors['500']);
root.style.setProperty('--color-primary-600', colors['600']);
root.style.setProperty('--color-primary-700', colors['700']);
root.style.setProperty('--color-primary-800', colors['800']);
}
}
document.addEventListener('DOMContentLoaded', () => {
const inputs = document.querySelectorAll('input[name="ui_style"], input[name="selected_theme"]');
inputs.forEach(input => {
input.addEventListener('change', applyLiveTheme);
});
});
</script>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<label class="block text-sm font-medium text-current mb-2">UI Style</label>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="dark-glass" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'dark-glass' %}checked{% endif %}>
<span>Dark Glass</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="dark-flat" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'dark-flat' %}checked{% endif %}>
<span>Dark Flat</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="black" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'black' %}checked{% endif %}>
<span>Black</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="light-glass" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'light-glass' %}checked{% endif %}>
<span>Light Glass</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="light-flat" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'light-flat' %}checked{% endif %}>
<span>Light Flat</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="white" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'white' %}checked{% endif %}>
<span>White</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="aurora" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'aurora' %}checked{% endif %}>
<span>Aurora</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="dark-neumorphic" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'dark-neumorphic' %}checked{% endif %}>
<span>Dark Neumorphic</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="light-neumorphic" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'light-neumorphic' %}checked{% endif %}>
<span>Light Neumorphic</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="brutalism" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'brutalism' %}checked{% endif %}>
<span>Brutalism</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="retro-terminal" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'retro-terminal' %}checked{% endif %}>
<span>Retro Terminal</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="cyberpunk" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'cyberpunk' %}checked{% endif %}>
<span>Cyberpunk</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="material-flat" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'material-flat' %}checked{% endif %}>
<span>Material Flat</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="ui_style" value="ink" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.ui_style == 'ink' %}checked{% endif %}>
<span>Ink</span>
</label>
</div>
</div>
<div>
<label class="block text-sm font-medium text-current mb-2">Theme Color</label>
<div class="flex flex-wrap items-center gap-4">
{% for theme_name, theme_colors in themes.items() %}
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="selected_theme" value="{{ theme_name }}" class="h-4 w-4 text-[var(--color-primary-600)]" {% if settings.selected_theme == theme_name %}checked{% endif %}>
<div class="flex items-center space-x-1">
<span class="w-5 h-5 rounded-full border border-gray-500" style="background-color: {{ theme_colors['600'] }};"></span>
<span class="capitalize text-sm">{{ theme_name }}</span>
</div>
</label>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Redis Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Redis & Rate Limiting</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="redis_host" class="block text-sm font-medium text-current">Redis Host</label>
<input type="text" name="redis_host" id="redis_host" value="{{ settings.redis_host }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div>
<label for="redis_port" class="block text-sm font-medium text-current">Redis Port</label>
<input type="number" name="redis_port" id="redis_port" value="{{ settings.redis_port }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div>
<label for="redis_username" class="block text-sm font-medium text-current">Redis Username</label>
<input type="text" name="redis_username" id="redis_username" value="{{ settings.redis_username or '' }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div>
<label for="redis_password" class="block text-sm font-medium text-current">Redis Password</label>
<input type="password" name="redis_password" id="redis_password" placeholder="Leave blank to keep current" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div>
<label for="model_update_interval_minutes" class="block text-sm font-medium text-current">Model Refresh Interval (minutes)</label>
<input type="number" name="model_update_interval_minutes" id="model_update_interval_minutes" value="{{ settings.model_update_interval_minutes }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div class="md:col-span-2">
<label for="default_models_path" class="block text-sm font-medium text-current">Default Models Storage Path</label>
<input type="text" name="default_models_path" id="default_models_path" value="{{ settings.default_models_path }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm border border-white/10 bg-black/20 focus:border-sky-500">
<p class="mt-1 text-[10px] text-gray-500 italic">Relative to app root or absolute path. HF models will be downloaded here.</p>
</div>
</div>
</div>
<!-- Instance Management Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Instance Management</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="instance_scan_start_port" class="block text-sm font-medium text-current">Scan Start Port</label>
<input type="number" name="instance_scan_start_port" id="instance_scan_start_port" value="{{ settings.instance_scan_start_port }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
<div>
<label for="instance_scan_end_port" class="block text-sm font-medium text-current">Scan End Port</label>
<input type="number" name="instance_scan_end_port" id="instance_scan_end_port" value="{{ settings.instance_scan_end_port }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]">
</div>
</div>
<p class="mt-2 text-xs text-gray-400">Controls the range of local ports the Fortress scans to discover unmanaged Ollama instances.</p>
</div>
<!-- IP Filtering -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">IP Filtering</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="allowed_ips" class="block text-sm font-medium text-current">Allowed IPs</label>
<textarea name="allowed_ips" id="allowed_ips" rows="3" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]" placeholder="e.g., 192.168.1.10, 10.0.0.0/8, *">{{ settings.allowed_ips }}</textarea>
<p class="mt-1 text-xs text-gray-400">Comma-separated. Use * to allow all.</p>
</div>
<div>
<label for="denied_ips" class="block text-sm font-medium text-current">Denied IPs</label>
<textarea name="denied_ips" id="denied_ips" rows="3" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]" placeholder="e.g., 1.2.3.4">{{ settings.denied_ips }}</textarea>
<p class="mt-1 text-xs text-gray-400">Comma-separated. This list is checked after the allow list.</p>
</div>
</div>
</div>
<!-- Log Management -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Diagnostic Logs & Retention</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="log_max_size_mb" class="block text-sm font-medium text-current">Max Log Size (MB)</label>
<input type="number" name="log_max_size_mb" id="log_max_size_mb" value="{{ settings.log_max_size_mb }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm">
</div>
<div>
<label for="log_backup_count" class="block text-sm font-medium text-current">Rotary History (Files)</label>
<input type="number" name="log_backup_count" id="log_backup_count" value="{{ settings.log_backup_count }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm">
</div>
</div>
<p class="mt-2 text-xs text-gray-400">Controls the automatic cleanup of application logs. Current file: <code class="text-indigo-400">lollms_hub.log</code></p>
</div>
<!-- Neural Memory Recovery -->
<div class="card-style border-l-4 border-fuchsia-500 bg-fuchsia-500/5" id="memory-settings-zone">
<h2 class="card-header text-xl font-bold mb-4 pb-2 flex items-center gap-2">
<svg class="w-6 h-6 text-fuchsia-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
Neural Memory Recovery
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<label class="block text-[10px] font-black text-gray-500 uppercase">Recovery Logic</label>
<select name="memory_recovery_mode" class="w-full bg-black/20 border border-white/10 rounded p-2 text-sm">
<option value="handles" {% if settings.memory_recovery_mode == 'handles' %}selected{% endif %}>Handles (AI Manual Search - Saves Context)</option>
<option value="vector" {% if settings.memory_recovery_mode == 'vector' %}selected{% endif %}>Vector (Automatic RAG - High Fidelity)</option>
</select>
<p class="text-[10px] text-gray-500 italic">Vector mode uses the Shared Routing Vectorizer defined in SB-MRA settings.</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-[10px] font-black text-gray-500 uppercase">Memory Top K</label>
<input type="number" name="memory_vector_top_k" value="{{ settings.memory_vector_top_k }}" class="w-full bg-black/20 rounded p-2 text-xs">
</div>
<div>
<label class="block text-[10px] font-black text-gray-500 uppercase">Similarity Min</label>
<input type="number" step="0.05" name="memory_vector_threshold" value="{{ settings.memory_vector_threshold }}" class="w-full bg-black/20 rounded p-2 text-xs">
</div>
</div>
</div>
</div>
<!-- External Integration Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">External Search & AI Tools</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="google_search_api_key" class="block text-sm font-medium text-current">Google Search API Key (SerpApi)</label>
<input type="password" name="google_search_api_key" id="google_search_api_key" value="{{ settings.google_search_api_key or '' }}" placeholder="Enter your SerpApi key" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm border border-white/10 bg-black/20">
<p class="mt-1 text-[10px] text-gray-500">Required for the 'Google Search' source in Skill Architect.</p>
</div>
</div>
</div>
<!-- Auto-Routing Intelligence (SB-MRA) -->
<div class="card-style border-l-4 border-indigo-500 bg-indigo-500/5">
<h2 class="card-header text-xl font-bold mb-4 pb-2 flex items-center gap-2">
<svg class="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
Cluster Intelligence (SB-MRA)
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<label class="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" name="enable_sb_mra" value="true" class="h-5 w-5 rounded text-indigo-600" {% if settings.enable_sb_mra %}checked{% endif %}>
<div>
<span class="block font-bold group-hover:text-indigo-400">Activate Semantic-Bayesian Routing</span>
<span class="text-[11px] text-gray-500 italic">Enables context-aware optimization and ecological penalties.</span>
</div>
</label>
<div class="grid grid-cols-2 gap-4 pt-2">
<div>
<label class="block text-[10px] font-black text-gray-500 uppercase">Weight: Priority</label>
<input type="number" step="0.1" name="routing_weight_priority" value="{{ settings.routing_weight_priority }}" class="w-full bg-black/20 rounded p-2 text-xs">
</div>
<div>
<label class="block text-[10px] font-black text-gray-500 uppercase">Weight: Reliability</label>
<input type="number" step="0.1" name="routing_weight_reliability" value="{{ settings.routing_weight_reliability }}" class="w-full bg-black/20 rounded p-2 text-xs">
</div>
<div>
<label class="block text-[10px] font-black text-emerald-500 uppercase">Weight: Ecology (EPP)</label>
<input type="number" step="0.1" name="routing_weight_ecology" value="{{ settings.routing_weight_ecology }}" class="w-full bg-black/20 rounded p-2 text-xs">
</div>
<div>
<label class="block text-[10px] font-black text-purple-500 uppercase">Weight: Semantic (SAF)</label>
<input type="number" step="0.1" name="routing_weight_semantic" value="{{ settings.routing_weight_semantic }}" class="w-full bg-black/20 rounded p-2 text-xs">
</div>
</div>
</div>
<div class="space-y-4 border-l border-white/5 pl-6">
<h4 class="text-xs font-bold text-gray-400 uppercase tracking-widest">Routing Vectorizer</h4>
<select name="routing_vectorizer_name" class="w-full bg-black/20 border border-white/10 rounded p-2 text-sm">
<option value="tf_idf" {% if settings.routing_vectorizer_name == 'tf_idf' %}selected{% endif %}>TF-IDF (Fast/CPU)</option>
<option value="sentense_transformer" {% if settings.routing_vectorizer_name == 'sentense_transformer' %}selected{% endif %}>SentenceTransformer (High Quality)</option>
<option value="ollama" {% if settings.routing_vectorizer_name == 'ollama' %}selected{% endif %}>Ollama Loopback</option>
</select>
<input type="text" name="routing_vectorizer_model" value="{{ settings.routing_vectorizer_model or '' }}" placeholder="Model name (e.g. all-MiniLM-L6-v2)" class="w-full bg-black/20 border border-white/10 rounded p-2 text-xs">
</div>
</div>
</div>
<!-- Hub Orchestration -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Hub Orchestration</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2 bg-indigo-500/5 border border-indigo-500/20 p-4 rounded-xl">
<div class="flex items-center justify-between">
<div>
<span class="block font-bold text-indigo-400">Master Guided Tours Toggle</span>
<span class="text-xs text-gray-500">Global control for all interactive walkthroughs.</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="enable_tours" id="master-tours-toggle" value="true" class="sr-only peer" {% if settings.enable_tours %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div id="tour-sub-settings" class="mt-4 pt-4 border-t border-white/10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 {% if not settings.enable_tours %}hidden{% endif %}">
{% set tour_items = [
('Dashboard', 'tour_dashboard', settings.tour_dashboard),
('Models Manager', 'tour_models', settings.tour_models),
('Workflows', 'tour_workflows', settings.tour_workflows),
('Data Stores', 'tour_datastores', settings.tour_datastores),
('Node Studio', 'tour_nodes', settings.tour_nodes)
] %}
{% for label, name, active in tour_items %}
<div class="flex items-center justify-between p-2 bg-black/20 rounded-lg border border-white/5">
<span class="text-[10px] font-black uppercase text-gray-400">{{ label }}</span>
<label class="relative inline-flex items-center scale-75 cursor-pointer">
<input type="checkbox" name="{{ name }}" value="true" class="sr-only peer" {% if active %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-700 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-500"></div>
</label>
</div>
{% endfor %}
</div>
</div>
<script>
document.getElementById('master-tours-toggle').addEventListener('change', (e) => {
document.getElementById('tour-sub-settings').classList.toggle('hidden', !e.target.checked);
});
</script>
<div class="md:col-span-2 bg-indigo-500/5 border border-indigo-500/20 p-4 rounded-xl flex items-center justify-between">
<div>
<span class="block font-bold text-indigo-400">Workflow Debug Mode</span>
<span class="text-xs text-gray-500">When enabled, every node execution in a cognitive graph will emit a live trace to the Live Flow dashboard.</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="enable_debug_mode" value="true" class="sr-only peer" {% if settings.enable_debug_mode %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div>
<label for="admin_agent_name" class="block text-sm font-medium text-current">Management Agent</label>
<select name="admin_agent_name" id="admin_agent_name" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm">
<option value="">None (Disable internal enhancements)</option>
{% for agent_name in agent_names %}
<option value="{{ agent_name }}" {% if settings.admin_agent_name == agent_name %}selected{% endif %}>{{ agent_name }}</option>
{% endfor %}
</select>
<p class="mt-1 text-xs text-gray-400">This agent is used for administrative tasks, like rewriting system prompts using the "Enhance" feature.</p>
</div>
</div>
</div>
<!-- Gateway Protocol Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">Gateway Protocols</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 bg-white/5 p-4 rounded-lg">
<div class="space-y-4">
<label class="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" name="enable_ollama_api" value="true" class="h-5 w-5 rounded text-indigo-600" {% if settings.enable_ollama_api %}checked{% endif %}>
<div>
<span class="block font-bold group-hover:text-indigo-400">Ollama API Protocol</span>
<span class="text-[11px] text-gray-500">Exposes /api/chat, /api/generate on Primary Port ({{ settings.PROXY_PORT }}).</span>
</div>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" name="enable_openai_api" value="true" class="h-5 w-5 rounded text-indigo-600" {% if settings.enable_openai_api %}checked{% endif %}>
<div>
<span class="block font-bold group-hover:text-indigo-400">OpenAI API Protocol</span>
<span class="text-[11px] text-gray-500">Exposes /v1/chat/completions on Port {{ settings.openai_port }}.</span>
</div>
</label>
<div class="pt-2 border-t border-white/5">
<div class="pt-2 border-t border-white/5">
<label class="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" name="enable_bot_mode" value="true" class="h-5 w-5 rounded text-indigo-600" {% if settings.enable_bot_mode %}checked{% endif %}>
<div>
<span class="block font-bold group-hover:text-indigo-400">Integrated Bot Orchestration</span>
<span class="text-[11px] text-gray-500">Enables the <b>Bot Connectors</b> UI and background services for Telegram, Discord, and Slack.</span>
</div>
</label>
<div class="mt-3 p-3 bg-indigo-500/5 border border-indigo-500/20 rounded-lg">
<h4 class="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">Configuration Note</h4>
<p class="text-[11px] text-gray-400 leading-relaxed">
Enabling this starts a background manager that maintains persistent connections to messaging APIs.
It allows your <b>Virtual Agents</b> and <b>Workflows</b> to be reachable via DM or group chat.
</p>
</div>
{% if not settings.enable_bot_mode %}
<div class="mt-2 text-[10px] text-amber-500 flex items-center gap-2 font-bold uppercase tracking-tighter">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Currently Hidden from Sidebar
</div>
{% endif %}
</div>
</div>
<div>
<label for="openai_port" class="block text-sm font-medium">OpenAI Dedicated Port</label>
<input type="number" name="openai_port" id="openai_port" value="{{ settings.openai_port }}" class="mt-1 block w-32 px-3 py-2 rounded-md">
<p class="mt-2 text-[10px] text-gray-500 italic">If set to the same as the Proxy Port ({{ settings.PROXY_PORT }}), both APIs will share the same listener.</p>
</div>
</div>
</div>
<!-- Endpoint Security -->
<div>
<div>
<label for="blocked_ollama_endpoints" class="block text-sm font-medium text-current">Blocked Ollama Endpoints</label>
<textarea name="blocked_ollama_endpoints" id="blocked_ollama_endpoints" rows="3" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]" placeholder="e.g., pull, delete, create">{{ settings.blocked_ollama_endpoints }}</textarea>
<p class="mt-1 text-xs text-gray-400">Comma-separated list of Ollama API paths to block for API key holders (e.g., pull, delete). This protects your backend servers from resource-intensive tasks initiated by users. This does not affect the admin UI's model management features.</p>
</div>
</div>
<!-- HTTPS/SSL Settings -->
<div>
<h2 class="card-header text-xl font-bold mb-4 pb-2">HTTPS/SSL Settings</h2>
<div class="space-y-6">
<!-- SSL Key -->
<div>
<h3 class="text-lg font-semibold text-current mb-2">SSL Private Key</h3>
{% if settings.ssl_keyfile_content %}
<div class="flex items-center gap-4 p-4 border border-dashed border-green-500 rounded-md bg-green-900/20">
<svg class="w-8 h-8 text-green-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
<div class="flex-grow">
<p class="text-sm font-semibold">An uploaded key file is currently active.</p>
<p class="text-xs text-gray-400">Path: <code class="text-xs">{{ settings.ssl_keyfile }}</code></p>
</div>
<button type="submit" name="remove_ssl_key" value="true" class="flex items-center justify-center px-3 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md">
Remove
</button>
</div>
{% else %}
<div>
<label for="ssl_keyfile" class="block text-sm font-medium text-current">Specify Key File Path</label>
<input type="text" name="ssl_keyfile" id="ssl_keyfile" value="{{ settings.ssl_keyfile or '' }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]" placeholder="/path/to/your/key.pem">
</div>
<div class="flex items-center my-3">
<hr class="flex-grow border-gray-600">
<span class="px-2 text-sm text-gray-400">OR</span>
<hr class="flex-grow border-gray-600">
</div>
<div>
<label for="ssl_key_file" class="block text-sm font-medium text-current">Upload Key File</label>
<input type="file" name="ssl_key_file" id="ssl_key_file" class="mt-1 block w-full text-sm rounded-md border border-gray-600 file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:bg-[var(--color-primary-600)] file:text-white hover:file:bg-[var(--color-primary-700)]">
</div>
{% endif %}
</div>
<!-- SSL Certificate -->
<div>
<h3 class="text-lg font-semibold text-current mb-2">SSL Certificate</h3>
{% if settings.ssl_certfile_content %}
<div class="flex items-center gap-4 p-4 border border-dashed border-green-500 rounded-md bg-green-900/20">
<svg class="w-8 h-8 text-green-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
<div class="flex-grow">
<p class="text-sm font-semibold">An uploaded certificate file is currently active.</p>
<p class="text-xs text-gray-400">Path: <code class="text-xs">{{ settings.ssl_certfile }}</code></p>
</div>
<button type="submit" name="remove_ssl_cert" value="true" class="flex items-center justify-center px-3 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md">
Remove
</button>
</div>
{% else %}
<div>
<label for="ssl_certfile" class="block text-sm font-medium text-current">Specify Certificate File Path</label>
<input type="text" name="ssl_certfile" id="ssl_certfile" value="{{ settings.ssl_certfile or '' }}" class="mt-1 block w-full px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)]" placeholder="/path/to/your/cert.pem">
</div>
<div class="flex items-center my-3">
<hr class="flex-grow border-gray-600">
<span class="px-2 text-sm text-gray-400">OR</span>
<hr class="flex-grow border-gray-600">
</div>
<div>
<label for="ssl_cert_file" class="block text-sm font-medium text-current">Upload Certificate File</label>
<input type="file" name="ssl_cert_file" id="ssl_cert_file" class="mt-1 block w-full text-sm rounded-md border border-gray-600 file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:bg-[var(--color-primary-600)] file:text-white hover:file:bg-[var(--color-primary-700)]">
</div>
{% endif %}
</div>
<div class="mt-4 p-3 rounded-md text-sm warning-box">
<strong>Important:</strong> Changes to SSL settings require a full server restart to take effect.
</div>
</div>
</div>
<!-- Maintenance & Guidance Section -->
<div class="pt-8 mt-8 border-t border-white/10 space-y-6">
<h2 class="text-xl font-bold text-current mb-4">System Maintenance & Guidance</h2>
<div class="p-4 bg-white/5 border border-white/10 rounded-lg flex items-center justify-between">
<div>
<h3 class="font-bold text-sm">Reset Guided Tours</h3>
<p class="text-xs text-gray-400">Clear your browser's history for all page-specific and onboarding walkthroughs.</p>
</div>
<button type="button" onclick="resetAllTours()" class="px-4 py-2 bg-amber-600/20 hover:bg-amber-600/40 border border-amber-500/30 rounded text-xs font-black uppercase text-amber-400">Reset All Knowledge</button>
</div>
<script>
function resetAllTours() {
if(!confirm("This will re-enable all help popups across the entire app. Proceed?")) return;
// Clear keys matching the tour pattern
Object.keys(localStorage).forEach(key => {
if (key.startsWith('tour_') || key === 'hub_wizard_complete') {
localStorage.removeItem(key);
}
});
window.location.href = '/admin/dashboard?start_tour=true';
}
</script>
<div class="p-4 bg-red-900/10 border border-red-500/20 rounded-lg flex items-center justify-between">
<div>
<h3 class="font-bold text-sm text-red-400">Update Static Dependencies</h3>
<p class="text-xs text-gray-500">Re-downloads all local JS/CSS libraries (LiteGraph, Chart.js, etc.) from secure sources.</p>
</div>
<button type="submit" form="refresh-assets-form" class="px-4 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded text-xs font-black uppercase">Update Libraries</button>
</div>
</div>
<!-- Final Page Tour Definitions -->
<script>
const SETTINGS_TOUR = [
{
target: null,
text: "Welcome to the <b>Fortress Engine Room</b>. Let's calibrate your cluster settings."
},
{
target: '#memory-settings-zone',
text: "<b>Neural Memory</b>: Choose between 'Handles' (where the AI manually fetches old memories) or 'Vector' (which uses SafeStore RAG to automatically inject relevant facts into the prompt)."
},
{
target: '[name="enable_sb_mra"]',
text: "<b>SB-MRA Logic</b>: This is the brain of the Gateway. It balances Bayesian reliability with ecological impact. Adjust the weights to prioritize speed over carbon savings, or vice versa."
},
{
target: '[name="routing_vectorizer_name"]',
text: "<b>Shared Vectorizer</b>: Used for both SB-MRA semantic routing and Vector Memory recovery. Using <b>SentenceTransformer</b> is recommended for high-fidelity local execution."
},
{
target: '[name="admin_agent_name"]',
text: "<b>Management Agent</b>: Select an agent to act as the Hub's internal 'Architect'. This agent will help build new tools, nodes, and fix code bugs."
}
];
window.addEventListener('load', () => {
window.startCustomTour(SETTINGS_TOUR, 'tour_settings_v1', true);
});
</script>
<!-- Save Button (Properly inside the main form) -->
<div class="flex justify-end pt-8 border-t border-white/10 mt-8">
<button type="submit" class="w-full md:w-auto justify-center py-3 px-10 border border-transparent rounded-xl shadow-2xl text-sm font-black uppercase tracking-widest text-white bg-[var(--color-primary-600)] hover:bg-[var(--color-primary-500)] transition-all transform hover:scale-105 active:scale-95">
Save All Settings
</button>
</div>
</form>
<!-- Hidden maintenance form remains separate as it points to a different route -->
<form id="refresh-assets-form" action="{{ url_for('admin_refresh_vendor_assets') }}" method="post" class="hidden">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
</form>
</div>
{% endblock %}