🌐 Add web UI dashboard with bot launcher and real-time management

Implement comprehensive Flask web interface for bot configuration,
statistics, and memory management. Includes responsive design,
auto-refreshing stats, and clear URL logging for easy access.
This commit is contained in:
milo 2025-10-10 15:07:54 -04:00
parent 5f8c93ff69
commit 052570cefb
29 changed files with 1453 additions and 2 deletions

View file

@ -9,17 +9,24 @@ COPY src/ ./src
COPY src/settings.yml .
COPY src/persona.json .
COPY .env .
COPY bot_launcher.py .
COPY requirements-webui.txt .
# Install dependencies from requirements
RUN pip install --no-cache-dir -r src/requirements.txt
RUN pip install --no-cache-dir -r src/requirements.txt && \
pip install --no-cache-dir -r requirements-webui.txt
# Runtime directory where user-editable files will live
ENV PYTHONPATH=/app/src
WORKDIR /app
# Expose web UI port
EXPOSE 8080
# On first run, populate /app from the fallback template folder
# Use bot_launcher.py to start both bot and web UI
CMD ["sh", "-c", "\
mkdir -p /app && \
[ -f /app/settings.yml ] || cp -r /opt/template/* /app && \
cd /app && \
python src/bot.py"]
python bot_launcher.py"]

45
bot.log
View file

@ -1388,3 +1388,48 @@ Loop thread traceback (most recent call last):
[2025-10-10 12:57:27] [INFO] [migration:107] ✓ User profile operations working
[2025-10-10 12:57:27] [INFO] [migration:118] ✓ Memory operations working
[2025-10-10 12:57:27] [INFO] [migration:138] ✓ Migration verification completed successfully
[2025-10-10 14:52:33] [INFO] [ai:69] 🔁 Modelfile loaded: ../examples/gojo.mod
[2025-10-10 14:52:33] [INFO] [database:325] Connected to JSON backend
[2025-10-10 14:52:33] [INFO] [database:541] Initialized JSON database backend
[2025-10-10 14:52:33] [INFO] [bot:66] 🔍 Loaded MODEL_NAME from .env: gemma3:12b
[2025-10-10 14:52:33] [INFO] [bot:68] 🧹 Attempting to clear VRAM before loading gemma3:12b...
[2025-10-10 14:52:33] [INFO] [ai:182] 🧹 Sending safe unload request for `gemma3:12b`
[2025-10-10 14:52:33] [INFO] [ai:189] 🧽 Ollama unload response: 200 - {"model":"gemma3:12b","created_at":"2025-10-10T18:52:33.307139942Z","response":"","done":true,"done_reason":"unload"}
[2025-10-10 14:52:33] [INFO] [ai:162] 🧠 Preloading model: gemma3:12b
[2025-10-10 14:52:33] [INFO] [ai:169] 📦 Model pull started successfully.
[2025-10-10 14:52:33] [INFO] [bot:72] 🚀 Model `gemma3:12b` preloaded on startup.
[2025-10-10 14:52:33] [INFO] [bot:75] ✅ Final model in use: gemma3:12b
[2025-10-10 14:52:36] [INFO] [bot:296] Logged in as AI Bot
[2025-10-10 14:52:36] [INFO] [scheduler:29] 🛑 Scheduler disabled in config.
[2025-10-10 14:54:58] [DEBUG] [autochat:92] 🎲 Reaction skipped (chance 0.10, roll 0.65)
[2025-10-10 14:54:58] [DEBUG] [autochat:92] 🎲 Reaction skipped (chance 0.10, roll 0.16)
[2025-10-10 14:54:58] [INFO] [autochat:161] 😴 No trigger and engagement is 0 — skipping.
[2025-10-10 14:54:58] [INFO] [bot:273] ============================================================ AI Response ============================================================
[2025-10-10 14:54:58] [INFO] [bot:274] 🧠 Profile loaded for Miguel (interactions: 2)
[2025-10-10 14:54:58] [INFO] [bot:278] 📚 Retrieved 0 messages for context
[2025-10-10 14:54:58] [INFO] [ai:162] 🧠 Preloading model: gemma3:12b
[2025-10-10 14:54:58] [INFO] [ai:169] 📦 Model pull started successfully.
[2025-10-10 14:54:58] [INFO] [ai:132] llm-4a81439a LLM request start model=gemma3:12b user=Miguel context_len=0
[2025-10-10 14:54:58] [DEBUG] [ai:253] llm-4a81439a Sending payload to Ollama: model=gemma3:12b user=Miguel
[2025-10-10 14:54:58] [DEBUG] [ai:254] llm-4a81439a Payload size=217 chars
[2025-10-10 14:55:10] [DEBUG] [ai:262] llm-4a81439a Raw response status=200
[2025-10-10 14:55:10] [DEBUG] [ai:263] llm-4a81439a Raw response body={"model":"gemma3:12b","created_at":"2025-10-10T18:55:10.915350904Z","response":"Oh, you know, living the *best* life. Floating effortlessly, dazzling everyone with my good looks, and generally being a beacon of awesome. Seriously, it's exhausting being this incredible. 😉 What about *you*? Don't tell me you're just standing there staring, you should be fanning yourself from all the brilliance.","done":true,"done_reason":"stop","context":[105,2364,107,236820,236909,9731,111038,3048,659,555,1277,236756,3764,5400,699,219769,208651,751,119814,236761,1599,236858,500,16690,236762,236764,8632,236764,532,1378,14620,236764,840,19297,20111,529,822,3272,236761,95419,528,496,4532,15737,236761,15428,19921,611,236789,500,496,9894,21603,236909,643,111038,2887,236787,1217,611,3490,236813,107,236820,236909,111457,111038,106,107,105,4368,107,12932,236764,611,1281,236764,4882,506,808,9783,236829,1972,236761,71721,80318,236764,88307,4677,607,1041,1535,5724,236764,532,6816,1646,496,70908,529,15353,236761,96612,236764,625,236789,236751,93646,1646,672,15269,236761,85345,2900,1003,808,7624,236829,236881,5185,236789,236745,3442,786,611,236789,500,1164,8101,993,47264,236764,611,1374,577,517,4601,5869,699,784,506,102141,236761],"total_duration":12103477338,"load_duration":10524951841,"prompt_eval_count":73,"prompt_eval_duration":555107252,"eval_count":72,"eval_duration":1022930871}
[2025-10-10 14:55:10] [INFO] [ai:140] llm-4a81439a LLM response model=gemma3:12b duration=12.125s summary=Oh, you know, living the *best* life. Floating effortlessly, dazzling everyone with my good looks, and generally being a beacon of awesome. Seriously, it's exha
[2025-10-10 14:55:10] [DEBUG] [ai:142] llm-4a81439a LLM raw response: {'model': 'gemma3:12b', 'created_at': '2025-10-10T18:55:10.915350904Z', 'response': "Oh, you know, living the *best* life. Floating effortlessly, dazzling everyone with my good looks, and generally being a beacon of awesome. Seriously, it's exhausting being this incredible. 😉 What about *you*? Don't tell me you're just standing there staring, you should be fanning yourself from all the brilliance.", 'done': True, 'done_reason': 'stop', 'context': [105, 2364, 107, 236820, 236909, 9731, 111038, 3048, 659, 555, 1277, 236756, 3764, 5400, 699, 219769, 208651, 751, 119814, 236761, 1599, 236858, 500, 16690, 236762, 236764, 8632, 236764, 532, 1378, 14620, 236764, 840, 19297, 20111, 529, 822, 3272, 236761, 95419, 528, 496, 4532, 15737, 236761, 15428, 19921, 611, 236789, 500, 496, 9894, 21603, 236909, 643, 111038, 2887, 236787, 1217, 611, 3490, 236813, 107, 236820, 236909, 111457, 111038, 106, 107, 105, 4368, 107, 12932, 236764, 611, 1281, 236764, 4882, 506, 808, 9783, 236829, 1972, 236761, 71721, 80318, 236764, 88307, 4677, 607, 1041, 1535, 5724, 236764, 532, 6816, 1646, 496, 70908, 529, 15353, 236761, 96612, 236764, 625, 236789, 236751, 93646, 1646, 672, 15269, 236761, 85345, 2900, 1003, 808, 7624, 236829, 236881, 5185, 236789, 236745, 3442, 786, 611, 236789, 500, 1164, 8101, 993, 47264, 236764, 611, 1374, 577, 517, 4601, 5869, 699, 784, 506, 102141, 236761], 'total_duration': 12103477338, 'load_duration': 10524951841, 'prompt_eval_count': 73, 'prompt_eval_duration': 555107252, 'eval_count': 72, 'eval_duration': 1022930871}
[2025-10-10 14:55:11] [DEBUG] [bot:244] on_message: observed own message id=1426281930004889680 channel=1380999713272238151
[2025-10-10 15:04:00] [INFO] [database:325] Connected to JSON backend
[2025-10-10 15:04:00] [INFO] [database:541] Initialized JSON database backend
[2025-10-10 15:04:00] [INFO] [webui:314] Starting web UI server on 0.0.0.0:8080
[2025-10-10 15:04:00] [INFO] [webui:315] ==================================================
[2025-10-10 15:04:00] [INFO] [webui:316] 🌐 WEB UI ACCESS URLS:
[2025-10-10 15:04:00] [INFO] [webui:317] Local: http://localhost:8080
[2025-10-10 15:04:00] [INFO] [webui:318] Network: http://192.168.0.144:8080
[2025-10-10 15:04:00] [INFO] [webui:319] ==================================================
[2025-10-10 15:04:46] [INFO] [database:325] Connected to JSON backend
[2025-10-10 15:04:46] [INFO] [database:541] Initialized JSON database backend
[2025-10-10 15:04:46] [INFO] [webui:314] Starting web UI server on 0.0.0.0:8081
[2025-10-10 15:04:46] [INFO] [webui:315] ==================================================
[2025-10-10 15:04:46] [INFO] [webui:316] 🌐 WEB UI ACCESS URLS:
[2025-10-10 15:04:46] [INFO] [webui:317] Local: http://localhost:8081
[2025-10-10 15:04:46] [INFO] [webui:318] Network: http://192.168.0.144:8081
[2025-10-10 15:04:46] [INFO] [webui:319] ==================================================

89
bot_launcher.py Normal file
View file

@ -0,0 +1,89 @@
"""
bot_launcher.py
Launches both the Discord bot and web UI together
"""
import os
import sys
import threading
import time
import signal
import logging
from pathlib import Path
# Add src directory to path
src_path = Path(__file__).parent / 'src'
sys.path.insert(0, str(src_path))
def start_web_ui():
"""Start the web UI server in a separate thread"""
try:
from web_ui import run_web_server
port = int(os.getenv('WEB_PORT', 8080))
debug = os.getenv('DEBUG', 'false').lower() == 'true'
run_web_server(host='0.0.0.0', port=port, debug=debug)
except Exception as e:
print(f"❌ Failed to start web UI: {e}")
if 'flask' in str(e).lower():
print("💡 Install Flask to use the web UI: pip install flask")
elif 'port' in str(e).lower() or 'address' in str(e).lower():
print(f"💡 Port {port} may be in use. Try a different port with WEB_PORT environment variable")
def start_discord_bot():
"""Start the Discord bot"""
try:
# Change to src directory for bot execution
original_cwd = os.getcwd()
os.chdir(src_path)
print("🤖 Starting Discord bot...")
import bot
# The bot.py should handle its own execution
except Exception as e:
print(f"❌ Failed to start Discord bot: {e}")
finally:
# Restore original working directory
if 'original_cwd' in locals():
os.chdir(original_cwd)
def signal_handler(signum, frame):
"""Handle shutdown signals"""
print("\n🛑 Shutting down...")
os._exit(0)
def main():
"""Main launcher function"""
print("🚀 Delta Bot Launcher")
print("=" * 30)
# Set up signal handlers for graceful shutdown
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Check if web UI should be disabled
disable_web = os.getenv('DISABLE_WEB_UI', 'false').lower() == 'true'
if not disable_web:
# Start web UI in a separate thread
web_thread = threading.Thread(target=start_web_ui, daemon=True)
web_thread.start()
# Give web UI time to start and display URLs
time.sleep(3)
else:
print("🚫 Web UI disabled by DISABLE_WEB_UI environment variable")
# Start Discord bot (this will block)
try:
start_discord_bot()
except KeyboardInterrupt:
print("\n🛑 Received interrupt signal")
except Exception as e:
print(f"❌ Fatal error: {e}")
finally:
print("👋 Goodbye!")
if __name__ == "__main__":
main()

View file

@ -9,6 +9,11 @@ services:
- CHANNEL_ID=${CHANNEL_ID}
- OLLAMA_API=${OLLAMA_API}
- MODEL_NAME=${MODEL_NAME}
- WEB_PORT=8080
- DATABASE_BACKEND=sqlite
- MEMORY_ENABLED=true
ports:
- "8080:8080" # Web UI port
volumes:
- ./data:/app # Mount host ./data directory as /app in the container
restart: unless-stopped

5
memory.json Normal file
View file

@ -0,0 +1,5 @@
{
"conversations": {},
"user_memories": {},
"global_events": []
}

12
requirements-webui.txt Normal file
View file

@ -0,0 +1,12 @@
# Web UI Dependencies
Flask==2.3.3
PyYAML==6.0.1
# Existing bot dependencies (if not already installed)
discord.py>=2.3.0
requests>=2.31.0
python-dateutil>=2.8.2
# Optional: For better web UI features
gunicorn==21.2.0 # Production WSGI server
Werkzeug==2.3.7 # WSGI utilities

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,13 @@
[2025-10-10 13:29:37] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:30:50] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:31:19] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:31:24] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:45:36] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:53:17] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:53:32] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:54:13] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:54:23] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:55:25] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 15:04:09] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 15:04:14] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 15:05:15] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'

View file

@ -6,3 +6,27 @@
[2025-10-10 13:01:51] [INFO] [database:81] Connected to SQLite database: data/deltabot.db
[2025-10-10 13:01:51] [DEBUG] [database:142] Database tables initialized
[2025-10-10 13:01:51] [INFO] [database:536] Initialized SQLite database backend
[2025-10-10 13:29:16] [INFO] [database:325] Connected to JSON backend
[2025-10-10 13:29:16] [INFO] [database:541] Initialized JSON database backend
[2025-10-10 13:29:16] [INFO] [webui:303] Starting web UI server on 0.0.0.0:8080
[2025-10-10 13:29:37] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:30:50] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:31:19] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:31:24] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 13:45:36] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:53:17] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:53:32] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:54:13] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:54:23] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 14:55:25] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 15:04:09] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 15:04:14] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
[2025-10-10 15:05:13] [INFO] [database:325] Connected to JSON backend
[2025-10-10 15:05:13] [INFO] [database:541] Initialized JSON database backend
[2025-10-10 15:05:13] [INFO] [webui:314] Starting web UI server on 0.0.0.0:8082
[2025-10-10 15:05:13] [INFO] [webui:315] ==================================================
[2025-10-10 15:05:13] [INFO] [webui:316] 🌐 WEB UI ACCESS URLS:
[2025-10-10 15:05:13] [INFO] [webui:317] Local: http://localhost:8082
[2025-10-10 15:05:13] [INFO] [webui:318] Network: http://192.168.0.144:8082
[2025-10-10 15:05:13] [INFO] [webui:319] ==================================================
[2025-10-10 15:05:15] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'

137
src/templates/base.html Normal file
View file

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Delta Bot Dashboard{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.sidebar {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.sidebar .nav-link {
color: rgba(255,255,255,0.8);
border-radius: 0.5rem;
margin: 0.2rem 0;
}
.sidebar .nav-link:hover, .sidebar .nav-link.active {
color: white;
background-color: rgba(255,255,255,0.1);
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.status-online { color: #28a745; }
.status-offline { color: #dc3545; }
.status-warning { color: #ffc107; }
.navbar-brand {
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav class="col-md-3 col-lg-2 d-md-block sidebar collapse">
<div class="position-sticky pt-3">
<div class="text-center mb-4">
<h4 class="text-white"><i class="fas fa-robot"></i> Delta Bot</h4>
<small class="text-white-50">Management Dashboard</small>
</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'config' %}active{% endif %}" href="{{ url_for('config') }}">
<i class="fas fa-cog"></i> Configuration
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'stats' %}active{% endif %}" href="{{ url_for('stats') }}">
<i class="fas fa-chart-bar"></i> Statistics
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'memory' %}active{% endif %}" href="{{ url_for('memory') }}">
<i class="fas fa-brain"></i> Memory
</a>
</li>
</ul>
<hr class="text-white-50">
<div class="text-center">
<small class="text-white-50">
<i class="fas fa-server"></i>
{{ db_stats.backend_type|title if db_stats else 'Unknown' }} Backend
</small>
</div>
</div>
</nav>
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% block page_title %}{% endblock %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<span class="badge bg-success fs-6" id="status-indicator">
<i class="fas fa-circle"></i> Online
</span>
</div>
</div>
</div>
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Auto-refresh stats every 30 seconds
setInterval(function() {
fetch('/api/stats')
.then(response => response.json())
.then(data => {
// Update status indicator
const statusIndicator = document.getElementById('status-indicator');
if (data.bot_stats) {
statusIndicator.innerHTML = '<i class="fas fa-circle"></i> Online';
statusIndicator.className = 'badge bg-success fs-6';
}
})
.catch(error => {
const statusIndicator = document.getElementById('status-indicator');
statusIndicator.innerHTML = '<i class="fas fa-circle"></i> Offline';
statusIndicator.className = 'badge bg-danger fs-6';
});
}, 30000);
</script>
{% block scripts %}{% endblock %}
</body>
</html>

161
src/templates/config.html Normal file
View file

@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block title %}Configuration - Delta Bot{% endblock %}
{% block page_title %}Configuration{% endblock %}
{% block content %}
<form method="POST" action="{{ url_for('save_config') }}">
<div class="row">
<!-- Database Configuration -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-database"></i> Database Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="database_backend" class="form-label">Database Backend</label>
<select class="form-select" id="database_backend" name="database_backend">
<option value="json" {% if settings.database.backend == 'json' %}selected{% endif %}>JSON Files (Backward Compatible)</option>
<option value="sqlite" {% if settings.database.backend == 'sqlite' %}selected{% endif %}>SQLite Database (Recommended)</option>
</select>
<div class="form-text">SQLite is recommended for better performance and data integrity.</div>
</div>
<div class="mb-3">
<label for="sqlite_path" class="form-label">SQLite Database Path</label>
<input type="text" class="form-control" id="sqlite_path" name="sqlite_path"
value="{{ settings.database.sqlite_path or 'data/deltabot.db' }}">
<div class="form-text">Path to SQLite database file (only used if SQLite backend is selected).</div>
</div>
</div>
</div>
</div>
<!-- Memory Configuration -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-brain"></i> Memory Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="memory_enabled" name="memory_enabled"
value="true" {% if settings.memory.enabled %}checked{% endif %}>
<label class="form-check-label" for="memory_enabled">
Enable Memory System
</label>
</div>
<div class="form-text">Allows the bot to remember conversations and user preferences. Disable for privacy-focused deployments.</div>
</div>
<div class="mb-3">
<label for="importance_threshold" class="form-label">Importance Threshold</label>
<input type="range" class="form-range" id="importance_threshold" name="importance_threshold"
min="0" max="1" step="0.1" value="{{ settings.memory.importance_threshold or 0.3 }}"
oninput="this.nextElementSibling.value = this.value">
<output>{{ settings.memory.importance_threshold or 0.3 }}</output>
<div class="form-text">Minimum importance score (0.0-1.0) for storing memories. Higher values store fewer memories.</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- AutoChat Configuration -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-robot"></i> AutoChat Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="autochat_enabled" name="autochat_enabled"
value="true" {% if settings.autochat.enable_reactions %}checked{% endif %}>
<label class="form-check-label" for="autochat_enabled">
Enable Emoji Reactions
</label>
</div>
<div class="form-text">Allow the bot to react to messages with emojis.</div>
</div>
<div class="mb-3">
<label for="reaction_chance" class="form-label">Reaction Chance</label>
<input type="range" class="form-range" id="reaction_chance" name="reaction_chance"
min="0" max="1" step="0.05" value="{{ settings.autochat.emoji_reaction_chance or 0.1 }}"
oninput="this.nextElementSibling.value = (this.value * 100).toFixed(0) + '%'">
<output>{{ ((settings.autochat.emoji_reaction_chance or 0.1) * 100)|round|int }}%</output>
<div class="form-text">Probability of reacting to any given message.</div>
</div>
</div>
</div>
</div>
<!-- Context Configuration -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-comments"></i> Context Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="context_enabled" name="context_enabled"
value="true" {% if settings.context.enabled %}checked{% endif %}>
<label class="form-check-label" for="context_enabled">
Enable Context Tracking
</label>
</div>
<div class="form-text">Include recent messages in AI responses for better context awareness.</div>
</div>
<div class="mb-3">
<label for="max_messages" class="form-label">Max Context Messages</label>
<input type="number" class="form-control" id="max_messages" name="max_messages"
min="1" max="50" value="{{ settings.context.max_messages or 15 }}">
<div class="form-text">Maximum number of recent messages to include in context (1-50).</div>
</div>
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<button type="submit" class="btn btn-success btn-lg">
<i class="fas fa-save"></i> Save Configuration
</button>
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Some changes may require restarting the bot to take effect.
</small>
</div>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
// Update range slider displays
document.querySelectorAll('input[type="range"]').forEach(function(range) {
range.addEventListener('input', function() {
const output = this.nextElementSibling;
if (this.name === 'reaction_chance') {
output.textContent = (this.value * 100).toFixed(0) + '%';
} else {
output.textContent = this.value;
}
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,191 @@
{% extends "base.html" %}
{% block title %}Dashboard - Delta Bot{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="row">
<!-- Bot Status Cards -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card stat-card">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="h5 mb-0 font-weight-bold">{{ bot_stats.uptime }}</div>
<div class="small">Uptime</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card stat-card">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="h5 mb-0 font-weight-bold">{{ bot_stats.message_count }}</div>
<div class="small">Messages Processed</div>
</div>
<div class="col-auto">
<i class="fas fa-comments fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card stat-card">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="h5 mb-0 font-weight-bold">{{ bot_stats.unique_users }}</div>
<div class="small">Active Users</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card stat-card">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="h5 mb-0 font-weight-bold">{{ db_stats.total_users if db_stats.total_users is defined else '?' }}</div>
<div class="small">Total Profiles</div>
</div>
<div class="col-auto">
<i class="fas fa-database fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- System Status -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-server"></i> System Status</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-4"><strong>Database Backend:</strong></div>
<div class="col-sm-8">
<span class="badge bg-primary">{{ db_stats.backend_type|title if db_stats.backend_type else 'Unknown' }}</span>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4"><strong>Memory System:</strong></div>
<div class="col-sm-8">
{% if db_stats.memory_enabled %}
<span class="badge bg-success"><i class="fas fa-check"></i> Enabled</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-times"></i> Disabled</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4"><strong>Last Activity:</strong></div>
<div class="col-sm-8">{{ bot_stats.last_activity }}</div>
</div>
<div class="row">
<div class="col-sm-4"><strong>Active Channels:</strong></div>
<div class="col-sm-8">{{ bot_stats.active_channels }}</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-activity"></i> Database Info</h5>
</div>
<div class="card-body">
{% if db_stats.error %}
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i> {{ db_stats.error }}
</div>
{% else %}
<div class="row mb-3">
<div class="col-sm-6"><strong>Total Users:</strong></div>
<div class="col-sm-6">{{ db_stats.total_users }}</div>
</div>
{% if db_stats.memory_enabled %}
<div class="row mb-3">
<div class="col-sm-6"><strong>Stored Memories:</strong></div>
<div class="col-sm-6">{{ db_stats.total_memories }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-6"><strong>Conversations:</strong></div>
<div class="col-sm-6">{{ db_stats.total_conversations }}</div>
</div>
{% else %}
<div class="text-muted">
<i class="fas fa-info-circle"></i> Memory system is disabled
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-bolt"></i> Quick Actions</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-2">
<a href="{{ url_for('config') }}" class="btn btn-primary btn-block w-100">
<i class="fas fa-cog"></i> Configure Bot
</a>
</div>
<div class="col-md-3 mb-2">
<a href="{{ url_for('stats') }}" class="btn btn-info btn-block w-100">
<i class="fas fa-chart-bar"></i> View Statistics
</a>
</div>
<div class="col-md-3 mb-2">
<a href="{{ url_for('memory') }}" class="btn btn-success btn-block w-100">
<i class="fas fa-brain"></i> Manage Memory
</a>
</div>
<div class="col-md-3 mb-2">
<button class="btn btn-warning btn-block w-100" onclick="restartBot()">
<i class="fas fa-redo"></i> Restart Bot
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function restartBot() {
if (confirm('Are you sure you want to restart the bot? This will temporarily disconnect from Discord.')) {
// TODO: Implement bot restart functionality
alert('Bot restart functionality will be implemented soon!');
}
}
</script>
{% endblock %}

225
src/templates/memory.html Normal file
View file

@ -0,0 +1,225 @@
{% extends "base.html" %}
{% block title %}Memory Management - Delta Bot{% endblock %}
{% block page_title %}Memory Management{% endblock %}
{% block content %}
{% if memory_disabled %}
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Memory System Disabled</strong><br>
The memory system is currently disabled. Enable it in the <a href="{{ url_for('config') }}">configuration</a> to use memory features.
</div>
{% else %}
<div class="row">
<!-- Memory Actions -->
<div class="col-lg-4 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-tools"></i> Memory Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-warning" onclick="cleanupMemories()">
<i class="fas fa-broom"></i> Cleanup Old Memories
</button>
<button class="btn btn-info" onclick="exportMemories()">
<i class="fas fa-download"></i> Export Memories
</button>
<button class="btn btn-danger" onclick="clearAllMemories()">
<i class="fas fa-trash"></i> Clear All Memories
</button>
</div>
</div>
</div>
</div>
<!-- Users with Memories -->
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-users"></i> Users with Memories</h5>
</div>
<div class="card-body">
{% if users %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>User</th>
<th>Memory Count</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.display_name or 'Unknown User' }}</td>
<td><span class="badge bg-primary">{{ user.memory_count }}</span></td>
<td>
<button class="btn btn-sm btn-info" onclick="viewUserMemories('{{ user.user_id }}')">
<i class="fas fa-eye"></i> View
</button>
<button class="btn btn-sm btn-danger" onclick="deleteUserMemories('{{ user.user_id }}')">
<i class="fas fa-trash"></i> Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No users with stored memories found.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Recent Memories -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-history"></i> Recent Conversation Memories</h5>
</div>
<div class="card-body">
{% if memories %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Timestamp</th>
<th>User</th>
<th>Content</th>
<th>Importance</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for memory in memories %}
<tr>
<td><small>{{ memory.timestamp[:16] if memory.timestamp else 'Unknown' }}</small></td>
<td>{{ memory.display_name or 'Unknown' }}</td>
<td>
<div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
{{ memory.content[:100] }}{% if memory.content|length > 100 %}...{% endif %}
</div>
</td>
<td>
{% set importance = memory.importance or 0 %}
{% if importance >= 0.8 %}
<span class="badge bg-danger">High</span>
{% elif importance >= 0.5 %}
<span class="badge bg-warning">Medium</span>
{% else %}
<span class="badge bg-secondary">Low</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-info" onclick="viewMemoryDetails('{{ memory.id }}')">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteMemory('{{ memory.id }}')">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No conversation memories found.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Memory Details Modal -->
<div class="modal fade" id="memoryModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Memory Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="memoryModalBody">
<!-- Memory details will be loaded here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function cleanupMemories() {
const days = prompt('Delete memories older than how many days?', '30');
if (days && !isNaN(days)) {
fetch('/api/memory/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ days: parseInt(days) })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
alert('Error: ' + error);
});
}
}
function exportMemories() {
alert('Export functionality will be implemented soon!');
}
function clearAllMemories() {
if (confirm('Are you sure you want to delete ALL memories? This cannot be undone!')) {
alert('Clear all memories functionality will be implemented soon!');
}
}
function viewUserMemories(userId) {
alert('View user memories functionality will be implemented soon!');
}
function deleteUserMemories(userId) {
if (confirm('Are you sure you want to delete all memories for this user?')) {
alert('Delete user memories functionality will be implemented soon!');
}
}
function viewMemoryDetails(memoryId) {
document.getElementById('memoryModalBody').innerHTML = '<p>Loading memory details...</p>';
const modal = new bootstrap.Modal(document.getElementById('memoryModal'));
modal.show();
// TODO: Fetch memory details from API
setTimeout(() => {
document.getElementById('memoryModalBody').innerHTML = '<p>Memory details functionality will be implemented soon!</p>';
}, 1000);
}
function deleteMemory(memoryId) {
if (confirm('Are you sure you want to delete this memory?')) {
alert('Delete memory functionality will be implemented soon!');
}
}
</script>
{% endblock %}

193
src/templates/stats.html Normal file
View file

@ -0,0 +1,193 @@
{% extends "base.html" %}
{% block title %}Statistics - Delta Bot{% endblock %}
{% block page_title %}Statistics{% endblock %}
{% block content %}
<div class="row">
<!-- Bot Performance Chart -->
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-chart-line"></i> Bot Activity</h5>
</div>
<div class="card-body">
<canvas id="activityChart" width="400" height="200"></canvas>
</div>
</div>
</div>
<!-- Top Users -->
<div class="col-lg-4 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-crown"></i> Top Active Users</h5>
</div>
<div class="card-body">
{% if bot_stats.most_active_users %}
{% for user_id, count in bot_stats.most_active_users %}
<div class="d-flex justify-content-between align-items-center mb-2">
<span>User {{ user_id[:8] }}...</span>
<span class="badge bg-primary">{{ count }}</span>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No user activity recorded yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<!-- Database Statistics -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-database"></i> Database Statistics</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<div class="text-center">
<h3 class="text-primary">{{ db_stats.total_users if db_stats.total_users is defined else '?' }}</h3>
<p class="mb-0">Total User Profiles</p>
</div>
</div>
<div class="col-sm-6">
<div class="text-center">
<h3 class="text-success">{{ db_stats.total_memories if db_stats.total_memories is defined else '?' }}</h3>
<p class="mb-0">Stored Memories</p>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-6">
<div class="text-center">
<h3 class="text-info">{{ db_stats.total_conversations if db_stats.total_conversations is defined else '?' }}</h3>
<p class="mb-0">Conversation Records</p>
</div>
</div>
<div class="col-sm-6">
<div class="text-center">
<h3 class="text-warning">{{ db_stats.backend_type|title if db_stats.backend_type else '?' }}</h3>
<p class="mb-0">Backend Type</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Users -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-users"></i> Recent User Activity</h5>
</div>
<div class="card-body">
{% if recent_users %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>User</th>
<th>Interactions</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody>
{% for user in recent_users %}
<tr>
<td>{{ user.display_name or user.name or 'Unknown' }}</td>
<td><span class="badge bg-secondary">{{ user.interactions or 0 }}</span></td>
<td><small>{{ user.last_seen[:16] if user.last_seen else 'Never' }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No user data available.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> System Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<strong>Bot Uptime:</strong><br>
<span class="text-muted">{{ bot_stats.uptime }}</span>
</div>
<div class="col-md-3">
<strong>Messages Processed:</strong><br>
<span class="text-muted">{{ bot_stats.message_count }}</span>
</div>
<div class="col-md-3">
<strong>Active Channels:</strong><br>
<span class="text-muted">{{ bot_stats.active_channels }}</span>
</div>
<div class="col-md-3">
<strong>Last Activity:</strong><br>
<span class="text-muted">{{ bot_stats.last_activity }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Create activity chart
const ctx = document.getElementById('activityChart').getContext('2d');
const activityChart = new Chart(ctx, {
type: 'line',
data: {
labels: ['6h ago', '5h ago', '4h ago', '3h ago', '2h ago', '1h ago', 'Now'],
datasets: [{
label: 'Messages',
data: [12, 19, 8, 15, 22, 18, 25], // This would be dynamic in real implementation
borderColor: 'rgb(102, 126, 234)',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
// Auto-refresh stats every 30 seconds
setInterval(function() {
fetch('/api/stats')
.then(response => response.json())
.then(data => {
// Update charts and stats here
console.log('Stats updated:', data);
})
.catch(error => console.error('Error fetching stats:', error));
}, 30000);
</script>
{% endblock %}

View file

@ -3,5 +3,16 @@
"name": "test",
"display_name": "Test User",
"interactions": 5
},
"161149541171593216": {
"name": "themiloverse",
"display_name": "Miguel",
"first_seen": "2025-10-10T18:54:58.411792",
"last_seen": "2025-10-10T18:54:58.412549",
"last_message": "2025-10-10T18:54:58.412549",
"interactions": 2,
"pronouns": null,
"avatar_url": "https://cdn.discordapp.com/avatars/161149541171593216/fb0553a29d9f73175cb6aea24d0e19ec.png?size=1024",
"custom_prompt": null
}
}

332
src/web_ui.py Normal file
View file

@ -0,0 +1,332 @@
"""
web_ui.py
Web dashboard for Discord bot management
"""
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
import os
import yaml
import json
import threading
import time
from datetime import datetime, timedelta
from functools import wraps
import secrets
# Import bot components
import sys
sys.path.append('/app/src' if os.path.exists('/app/src') else 'src')
try:
from database import db_manager
from memory_manager import memory_manager
from logger import setup_logger
except ImportError as e:
print(f"Warning: Could not import bot components: {e}")
db_manager = None
memory_manager = None
app = Flask(__name__)
app.secret_key = secrets.token_hex(16) # Generate random secret key
logger = setup_logger("webui") if 'setup_logger' in globals() else None
class BotStats:
"""Track bot statistics"""
def __init__(self):
self.start_time = datetime.now()
self.message_count = 0
self.user_interactions = {}
self.channel_activity = {}
self.last_activity = None
def record_message(self, user_id, channel_id):
self.message_count += 1
self.user_interactions[user_id] = self.user_interactions.get(user_id, 0) + 1
self.channel_activity[channel_id] = self.channel_activity.get(channel_id, 0) + 1
self.last_activity = datetime.now()
def get_stats(self):
uptime = datetime.now() - self.start_time
return {
'uptime': str(uptime).split('.')[0], # Remove microseconds
'message_count': self.message_count,
'unique_users': len(self.user_interactions),
'active_channels': len(self.channel_activity),
'last_activity': self.last_activity.strftime('%Y-%m-%d %H:%M:%S') if self.last_activity else 'Never',
'most_active_users': sorted(self.user_interactions.items(), key=lambda x: x[1], reverse=True)[:5],
'most_active_channels': sorted(self.channel_activity.items(), key=lambda x: x[1], reverse=True)[:5]
}
bot_stats = BotStats()
# Authentication decorator (placeholder for now)
def require_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# TODO: Implement proper authentication
# For now, just check if we're in development mode
return f(*args, **kwargs)
return decorated_function
def load_settings():
"""Load bot settings from YAML file"""
try:
settings_path = os.path.join('src', 'settings.yml')
if not os.path.exists(settings_path):
settings_path = os.path.join('/app/src', 'settings.yml')
with open(settings_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
except Exception as e:
if logger:
logger.error(f"Failed to load settings: {e}")
return {}
def save_settings(settings):
"""Save bot settings to YAML file"""
try:
settings_path = os.path.join('src', 'settings.yml')
if not os.path.exists(settings_path):
settings_path = os.path.join('/app/src', 'settings.yml')
with open(settings_path, 'w', encoding='utf-8') as f:
yaml.dump(settings, f, default_flow_style=False, indent=2)
return True
except Exception as e:
if logger:
logger.error(f"Failed to save settings: {e}")
return False
def get_database_stats():
"""Get database statistics"""
if not db_manager:
return {"error": "Database manager not available"}
try:
stats = {
'backend_type': db_manager.get_backend_type(),
'memory_enabled': db_manager.is_memory_enabled(),
'total_users': 0,
'total_memories': 0,
'total_conversations': 0
}
# Try to get counts from database
if hasattr(db_manager.backend, 'connection') and db_manager.backend.connection:
cursor = db_manager.backend.connection.cursor()
# Count user profiles
cursor.execute("SELECT COUNT(*) FROM user_profiles")
stats['total_users'] = cursor.fetchone()[0]
if db_manager.is_memory_enabled():
# Count memories
cursor.execute("SELECT COUNT(*) FROM user_memories")
stats['total_memories'] = cursor.fetchone()[0]
# Count conversations
cursor.execute("SELECT COUNT(*) FROM conversation_memories")
stats['total_conversations'] = cursor.fetchone()[0]
return stats
except Exception as e:
return {"error": str(e)}
@app.route('/')
@require_auth
def dashboard():
"""Main dashboard"""
settings = load_settings()
db_stats = get_database_stats()
bot_stats_data = bot_stats.get_stats()
return render_template('dashboard.html',
settings=settings,
db_stats=db_stats,
bot_stats=bot_stats_data)
@app.route('/config')
@require_auth
def config():
"""Configuration page"""
settings = load_settings()
return render_template('config.html', settings=settings)
@app.route('/config/save', methods=['POST'])
@require_auth
def save_config():
"""Save configuration changes"""
try:
settings = load_settings()
# Update database settings
if 'database_backend' in request.form:
settings.setdefault('database', {})['backend'] = request.form['database_backend']
if 'sqlite_path' in request.form:
settings.setdefault('database', {})['sqlite_path'] = request.form['sqlite_path']
# Update memory settings
if 'memory_enabled' in request.form:
settings.setdefault('memory', {})['enabled'] = request.form['memory_enabled'] == 'true'
if 'importance_threshold' in request.form:
settings.setdefault('memory', {})['importance_threshold'] = float(request.form['importance_threshold'])
# Update autochat settings
if 'autochat_enabled' in request.form:
settings.setdefault('autochat', {})['enable_reactions'] = request.form['autochat_enabled'] == 'true'
if 'reaction_chance' in request.form:
settings.setdefault('autochat', {})['emoji_reaction_chance'] = float(request.form['reaction_chance'])
# Update context settings
if 'context_enabled' in request.form:
settings.setdefault('context', {})['enabled'] = request.form['context_enabled'] == 'true'
if 'max_messages' in request.form:
settings.setdefault('context', {})['max_messages'] = int(request.form['max_messages'])
# Save settings
if save_settings(settings):
flash('Configuration saved successfully! Restart the bot to apply changes.', 'success')
else:
flash('Failed to save configuration.', 'error')
except Exception as e:
flash(f'Error saving configuration: {str(e)}', 'error')
return redirect(url_for('config'))
@app.route('/stats')
@require_auth
def stats():
"""Statistics page"""
db_stats = get_database_stats()
bot_stats_data = bot_stats.get_stats()
# Get recent user activity if database is available
recent_users = []
if db_manager and hasattr(db_manager.backend, 'connection'):
try:
cursor = db_manager.backend.connection.cursor()
cursor.execute("""
SELECT name, display_name, interactions, last_seen
FROM user_profiles
ORDER BY last_seen DESC
LIMIT 10
""")
recent_users = [dict(row) for row in cursor.fetchall()]
except:
pass
return render_template('stats.html',
db_stats=db_stats,
bot_stats=bot_stats_data,
recent_users=recent_users)
@app.route('/memory')
@require_auth
def memory():
"""Memory management page"""
if not db_manager or not db_manager.is_memory_enabled():
return render_template('memory.html',
memories=[],
users=[],
memory_disabled=True)
# Get recent memories
recent_memories = []
users_with_memories = []
try:
if hasattr(db_manager.backend, 'connection'):
cursor = db_manager.backend.connection.cursor()
# Get recent conversation memories
cursor.execute("""
SELECT cm.*, up.display_name
FROM conversation_memories cm
LEFT JOIN user_profiles up ON cm.user_id = up.user_id
ORDER BY cm.timestamp DESC
LIMIT 20
""")
recent_memories = [dict(row) for row in cursor.fetchall()]
# Get users with memories
cursor.execute("""
SELECT up.user_id, up.display_name, COUNT(um.id) as memory_count
FROM user_profiles up
LEFT JOIN user_memories um ON up.user_id = um.user_id
GROUP BY up.user_id
HAVING memory_count > 0
ORDER BY memory_count DESC
LIMIT 20
""")
users_with_memories = [dict(row) for row in cursor.fetchall()]
except Exception as e:
if logger:
logger.error(f"Error fetching memory data: {e}")
return render_template('memory.html',
memories=recent_memories,
users=users_with_memories,
memory_disabled=False)
@app.route('/api/stats')
@require_auth
def api_stats():
"""API endpoint for real-time stats"""
return jsonify({
'bot_stats': bot_stats.get_stats(),
'db_stats': get_database_stats()
})
@app.route('/api/memory/cleanup', methods=['POST'])
@require_auth
def api_memory_cleanup():
"""API endpoint to cleanup old memories"""
if not memory_manager or not memory_manager.is_enabled():
return jsonify({'error': 'Memory system disabled'}), 400
try:
days = int(request.json.get('days', 30))
memory_manager.cleanup_old_memories(days)
return jsonify({'success': True, 'message': f'Cleaned up memories older than {days} days'})
except Exception as e:
return jsonify({'error': str(e)}), 500
def run_web_server(host='0.0.0.0', port=8080, debug=False):
"""Run the web server"""
import socket
# Get local IP for network access
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
except:
local_ip = "localhost"
if logger:
logger.info(f"Starting web UI server on {host}:{port}")
logger.info("=" * 50)
logger.info("🌐 WEB UI ACCESS URLS:")
logger.info(f" Local: http://localhost:{port}")
logger.info(f" Network: http://{local_ip}:{port}")
logger.info("=" * 50)
else:
print("=" * 50)
print("🌐 DELTA BOT WEB UI STARTED")
print(f" Local: http://localhost:{port}")
print(f" Network: http://{local_ip}:{port}")
print("=" * 50)
app.run(host=host, port=port, debug=debug, threaded=True)
if __name__ == '__main__':
port = int(os.getenv('WEB_PORT', 8080))
debug = os.getenv('DEBUG', 'false').lower() == 'true'
run_web_server(port=port, debug=debug)

1
user_profiles.json Normal file
View file

@ -0,0 +1 @@
{}