Add bot management dashboard with health checks and status monitoring
Refactor main.py to implement Flask app, SQLAlchemy models for Bot and BotLog, and health check functionality. Update pyproject.toml with new dependencies and add new HTML templates for the user interface. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 5f598d52-420e-4e2c-88ea-a4c3e41fdcb6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/e72fc1b7-94bd-4d6c-801f-cbac2fae245c/jW8PJKQ Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
02e50ed478
commit
3d4e07b965
11 changed files with 2743 additions and 277 deletions
32
.replit
32
.replit
|
|
@ -13,3 +13,35 @@ ignorePorts = true
|
|||
|
||||
[agent]
|
||||
expertMode = true
|
||||
|
||||
[[ports]]
|
||||
localPort = 5000
|
||||
externalPort = 80
|
||||
|
||||
[[ports]]
|
||||
localPort = 33105
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
[[workflows.workflow]]
|
||||
name = "Project"
|
||||
mode = "parallel"
|
||||
author = "agent"
|
||||
|
||||
[[workflows.workflow.tasks]]
|
||||
task = "workflow.run"
|
||||
args = "Bot Master Dashboard"
|
||||
|
||||
[[workflows.workflow]]
|
||||
name = "Bot Master Dashboard"
|
||||
author = "agent"
|
||||
|
||||
[[workflows.workflow.tasks]]
|
||||
task = "shell.exec"
|
||||
args = "python main.py"
|
||||
waitForPort = 5000
|
||||
|
||||
[workflows.workflow.metadata]
|
||||
outputType = "webview"
|
||||
|
|
|
|||
228
main.py
228
main.py
|
|
@ -1,42 +1,210 @@
|
|||
# This code is based on the following example:
|
||||
# https://discordpy.readthedocs.io/en/stable/quickstart.html#a-minimal-bot
|
||||
|
||||
import os
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
import discord
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(24))
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL")
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
|
||||
"pool_recycle": 300,
|
||||
"pool_pre_ping": True,
|
||||
}
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
client = discord.Client(intents=intents)
|
||||
class Bot(db.Model):
|
||||
__tablename__ = 'bots'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
health_endpoint = db.Column(db.String(500))
|
||||
admin_token = db.Column(db.String(200))
|
||||
bot_type = db.Column(db.String(50), default='discord')
|
||||
status = db.Column(db.String(20), default='unknown')
|
||||
last_checked = db.Column(db.DateTime)
|
||||
guild_count = db.Column(db.Integer, default=0)
|
||||
command_count = db.Column(db.Integer, default=0)
|
||||
uptime_seconds = db.Column(db.Integer, default=0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'has_health_endpoint': bool(self.health_endpoint),
|
||||
'has_admin_token': bool(self.admin_token),
|
||||
'bot_type': self.bot_type,
|
||||
'status': self.status,
|
||||
'last_checked': self.last_checked.isoformat() if self.last_checked else None,
|
||||
'guild_count': self.guild_count,
|
||||
'command_count': self.command_count,
|
||||
'uptime_seconds': self.uptime_seconds,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print('We have logged in as {0.user}'.format(client))
|
||||
class BotLog(db.Model):
|
||||
__tablename__ = 'bot_logs'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
bot_id = db.Column(db.Integer, db.ForeignKey('bots.id'), nullable=False)
|
||||
level = db.Column(db.String(20), default='info')
|
||||
message = db.Column(db.Text)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
bot = db.relationship('Bot', backref=db.backref('logs', lazy='dynamic'))
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
@client.event
|
||||
async def on_message(message):
|
||||
if message.author == client.user:
|
||||
return
|
||||
def check_bot_health(bot):
|
||||
if not bot.health_endpoint:
|
||||
return {'status': 'unknown', 'error': 'No health endpoint configured'}
|
||||
|
||||
try:
|
||||
headers = {}
|
||||
if bot.admin_token:
|
||||
headers['Authorization'] = f'Bearer {bot.admin_token}'
|
||||
|
||||
response = requests.get(bot.health_endpoint, headers=headers, timeout=10)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
bot.status = data.get('status', 'online')
|
||||
bot.guild_count = data.get('guilds', data.get('guildCount', 0))
|
||||
bot.command_count = data.get('commands', data.get('commandCount', 0))
|
||||
bot.uptime_seconds = data.get('uptime', 0)
|
||||
bot.last_checked = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return {'status': 'online', 'data': data}
|
||||
else:
|
||||
bot.status = 'offline'
|
||||
bot.last_checked = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return {'status': 'offline', 'error': f'HTTP {response.status_code}'}
|
||||
except requests.exceptions.Timeout:
|
||||
bot.status = 'timeout'
|
||||
bot.last_checked = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return {'status': 'timeout', 'error': 'Request timed out'}
|
||||
except requests.exceptions.ConnectionError:
|
||||
bot.status = 'offline'
|
||||
bot.last_checked = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return {'status': 'offline', 'error': 'Connection failed'}
|
||||
except Exception as e:
|
||||
bot.status = 'error'
|
||||
bot.last_checked = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return {'status': 'error', 'error': str(e)}
|
||||
|
||||
if message.content.startswith('$hello'):
|
||||
await message.channel.send('Hello!')
|
||||
@app.route('/')
|
||||
def dashboard():
|
||||
bots = Bot.query.order_by(Bot.created_at.desc()).all()
|
||||
stats = {
|
||||
'total_bots': len(bots),
|
||||
'online_bots': sum(1 for b in bots if b.status == 'online'),
|
||||
'total_guilds': sum(b.guild_count or 0 for b in bots),
|
||||
'total_commands': sum(b.command_count or 0 for b in bots),
|
||||
}
|
||||
return render_template('dashboard.html', bots=bots, stats=stats)
|
||||
|
||||
@app.route('/bots')
|
||||
def list_bots():
|
||||
bots = Bot.query.order_by(Bot.created_at.desc()).all()
|
||||
return render_template('bots.html', bots=bots)
|
||||
|
||||
try:
|
||||
token = os.getenv("TOKEN") or ""
|
||||
if token == "":
|
||||
raise Exception("Please add your token to the Secrets pane.")
|
||||
client.run(token)
|
||||
except discord.HTTPException as e:
|
||||
if e.status == 429:
|
||||
print(
|
||||
"The Discord servers denied the connection for making too many requests"
|
||||
@app.route('/bots/add', methods=['GET', 'POST'])
|
||||
def add_bot():
|
||||
if request.method == 'POST':
|
||||
name = request.form.get('name')
|
||||
description = request.form.get('description')
|
||||
health_endpoint = request.form.get('health_endpoint')
|
||||
admin_token = request.form.get('admin_token')
|
||||
bot_type = request.form.get('bot_type', 'discord')
|
||||
|
||||
if not name:
|
||||
flash('Bot name is required', 'error')
|
||||
return redirect(url_for('add_bot'))
|
||||
|
||||
bot = Bot(
|
||||
name=name,
|
||||
description=description,
|
||||
health_endpoint=health_endpoint,
|
||||
admin_token=admin_token,
|
||||
bot_type=bot_type
|
||||
)
|
||||
print(
|
||||
"Get help from https://stackoverflow.com/questions/66724687/in-discord-py-how-to-solve-the-error-for-toomanyrequests"
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
db.session.add(bot)
|
||||
db.session.commit()
|
||||
|
||||
if health_endpoint:
|
||||
check_bot_health(bot)
|
||||
|
||||
flash(f'Bot "{name}" added successfully!', 'success')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return render_template('add_bot.html')
|
||||
|
||||
@app.route('/bots/<int:bot_id>')
|
||||
def view_bot(bot_id):
|
||||
bot = Bot.query.get_or_404(bot_id)
|
||||
return render_template('view_bot.html', bot=bot)
|
||||
|
||||
@app.route('/bots/<int:bot_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_bot(bot_id):
|
||||
bot = Bot.query.get_or_404(bot_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
bot.name = request.form.get('name', bot.name)
|
||||
bot.description = request.form.get('description')
|
||||
bot.health_endpoint = request.form.get('health_endpoint')
|
||||
new_token = request.form.get('admin_token')
|
||||
if new_token:
|
||||
bot.admin_token = new_token
|
||||
bot.bot_type = request.form.get('bot_type', 'discord')
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Bot "{bot.name}" updated successfully!', 'success')
|
||||
return redirect(url_for('view_bot', bot_id=bot.id))
|
||||
|
||||
return render_template('edit_bot.html', bot=bot)
|
||||
|
||||
@app.route('/bots/<int:bot_id>/delete', methods=['POST'])
|
||||
def delete_bot(bot_id):
|
||||
bot = Bot.query.get_or_404(bot_id)
|
||||
name = bot.name
|
||||
BotLog.query.filter_by(bot_id=bot.id).delete()
|
||||
db.session.delete(bot)
|
||||
db.session.commit()
|
||||
flash(f'Bot "{name}" deleted successfully!', 'success')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
@app.route('/bots/<int:bot_id>/check', methods=['POST'])
|
||||
def check_bot(bot_id):
|
||||
bot = Bot.query.get_or_404(bot_id)
|
||||
result = check_bot_health(bot)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/bots')
|
||||
def api_list_bots():
|
||||
bots = Bot.query.all()
|
||||
return jsonify([bot.to_dict() for bot in bots])
|
||||
|
||||
@app.route('/api/bots/<int:bot_id>/health')
|
||||
def api_bot_health(bot_id):
|
||||
bot = Bot.query.get_or_404(bot_id)
|
||||
result = check_bot_health(bot)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/check-all', methods=['POST'])
|
||||
def api_check_all():
|
||||
bots = Bot.query.all()
|
||||
results = {}
|
||||
for bot in bots:
|
||||
results[bot.id] = check_bot_health(bot)
|
||||
return jsonify(results)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
|
|
|
|||
1629
poetry.lock
generated
1629
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,12 @@ authors = ["Your Name <you@example.com>"]
|
|||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10.0,<3.11"
|
||||
discord = "^2.3.2"
|
||||
gunicorn = "^23.0.0"
|
||||
discord-py = "^2.6.4"
|
||||
flask = "^3.1.2"
|
||||
flask-sqlalchemy = "^3.1.1"
|
||||
psycopg2-binary = "^2.9.11"
|
||||
requests = "^2.32.5"
|
||||
|
||||
[tool.pyright]
|
||||
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md
|
||||
|
|
|
|||
97
replit.md
Normal file
97
replit.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Bot Master
|
||||
|
||||
A centralized management dashboard for managing multiple Discord bots.
|
||||
|
||||
## Overview
|
||||
|
||||
Bot Master is a Flask-based web application that provides a central dashboard for monitoring and managing multiple Discord bots. It supports:
|
||||
|
||||
- **Bot Registry**: Store configurations for all your bots
|
||||
- **Status Monitoring**: Real-time health checks for each bot
|
||||
- **Statistics**: Track servers, commands, and uptime across all bots
|
||||
- **CRUD Operations**: Add, edit, view, and delete bot configurations
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Python/Flask
|
||||
- **Database**: PostgreSQL (via Flask-SQLAlchemy)
|
||||
- **Frontend**: Jinja2 templates with custom CSS
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── main.py # Flask application entry point
|
||||
├── templates/ # HTML templates
|
||||
│ ├── base.html # Base layout template
|
||||
│ ├── dashboard.html # Main dashboard view
|
||||
│ ├── bots.html # Bot list table view
|
||||
│ ├── add_bot.html # Add new bot form
|
||||
│ ├── view_bot.html # Bot details view
|
||||
│ └── edit_bot.html # Edit bot form
|
||||
├── static/ # Static assets (currently empty)
|
||||
└── attached_assets/ # Uploaded files and extracted bot examples
|
||||
├── bot1/ # Example AeThex bot (basic)
|
||||
└── bot2/ # Example AeThex bot (extended with feed sync)
|
||||
```
|
||||
|
||||
## Database Models
|
||||
|
||||
### Bot
|
||||
- `id`: Primary key
|
||||
- `name`: Bot name (required)
|
||||
- `description`: Bot description
|
||||
- `health_endpoint`: URL for health check API
|
||||
- `admin_token`: Bearer token for authenticated endpoints
|
||||
- `bot_type`: Type of bot (discord, telegram, slack, other)
|
||||
- `status`: Current status (online, offline, unknown, timeout, error)
|
||||
- `last_checked`: Timestamp of last health check
|
||||
- `guild_count`, `command_count`, `uptime_seconds`: Stats from health endpoint
|
||||
- `created_at`, `updated_at`: Timestamps
|
||||
|
||||
### BotLog
|
||||
- Stores log entries per bot (for future logging features)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /` - Dashboard
|
||||
- `GET /bots` - List all bots
|
||||
- `GET /bots/add` - Add bot form
|
||||
- `POST /bots/add` - Create new bot
|
||||
- `GET /bots/<id>` - View bot details
|
||||
- `GET /bots/<id>/edit` - Edit bot form
|
||||
- `POST /bots/<id>/edit` - Update bot
|
||||
- `POST /bots/<id>/delete` - Delete bot
|
||||
- `POST /bots/<id>/check` - Check bot health
|
||||
|
||||
### API (JSON)
|
||||
- `GET /api/bots` - List all bots as JSON
|
||||
- `GET /api/bots/<id>/health` - Check specific bot health
|
||||
- `POST /api/check-all` - Check all bots health
|
||||
|
||||
## Health Check Integration
|
||||
|
||||
Each bot should expose a health endpoint that returns JSON:
|
||||
```json
|
||||
{
|
||||
"status": "online",
|
||||
"guilds": 5,
|
||||
"commands": 12,
|
||||
"uptime": 3600
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
The application runs on port 5000 using the Flask development server.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `FLASK_SECRET_KEY` - Secret key for session management (auto-generated if not set)
|
||||
|
||||
## Example Bots
|
||||
|
||||
The `attached_assets` folder contains two example AeThex Discord bots:
|
||||
- **Bot 1**: Basic version with verify, profile, set-realm, unlink, verify-role commands
|
||||
- **Bot 2**: Extended version with additional help, stats, leaderboard, post commands and feed sync
|
||||
54
templates/add_bot.html
Normal file
54
templates/add_bot.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Add Bot - Bot Master{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Add New Bot</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name">Bot Name *</label>
|
||||
<input type="text" id="name" name="name" class="form-control" required placeholder="e.g., AeThex Main Bot">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control" placeholder="What does this bot do?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bot_type">Bot Type</label>
|
||||
<select id="bot_type" name="bot_type" class="form-control">
|
||||
<option value="discord">Discord Bot</option>
|
||||
<option value="telegram">Telegram Bot</option>
|
||||
<option value="slack">Slack Bot</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="health_endpoint">Health Check Endpoint</label>
|
||||
<input type="url" id="health_endpoint" name="health_endpoint" class="form-control" placeholder="https://your-bot.host.com:8044/health">
|
||||
<small style="color: var(--text-secondary); display: block; margin-top: 0.5rem;">
|
||||
The URL where your bot exposes its health/status endpoint
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="admin_token">Admin Token (Optional)</label>
|
||||
<input type="password" id="admin_token" name="admin_token" class="form-control" placeholder="Bearer token for authenticated endpoints">
|
||||
<small style="color: var(--text-secondary); display: block; margin-top: 0.5rem;">
|
||||
Used for accessing protected bot management endpoints
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary">Add Bot</button>
|
||||
<a href="/" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
274
templates/base.html
Normal file
274
templates/base.html
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Bot Master{% endblock %}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #0f0f23;
|
||||
--bg-secondary: #1a1a35;
|
||||
--bg-card: #252545;
|
||||
--text-primary: #e0e0ff;
|
||||
--text-secondary: #9090b0;
|
||||
--accent: #7c5cff;
|
||||
--accent-hover: #9070ff;
|
||||
--success: #00cc88;
|
||||
--warning: #ffaa00;
|
||||
--danger: #ff4466;
|
||||
--border: #3a3a5a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: var(--bg-secondary);
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navbar-nav a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.navbar-nav a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.flash-messages {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(0, 204, 136, 0.2);
|
||||
border: 1px solid var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(255, 68, 102, 0.2);
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #ff6688;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: rgba(0, 204, 136, 0.2);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: rgba(255, 68, 102, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
background: rgba(144, 144, 176, 0.2);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-timeout, .status-error {
|
||||
background: rgba(255, 170, 0, 0.2);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.status-online .pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="/" class="navbar-brand">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="8.5" cy="10" r="1.5"/>
|
||||
<circle cx="15.5" cy="10" r="1.5"/>
|
||||
<path d="M8 15h8"/>
|
||||
</svg>
|
||||
Bot Master
|
||||
</a>
|
||||
<ul class="navbar-nav">
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/bots">All Bots</a></li>
|
||||
<li><a href="/bots/add">Add Bot</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
108
templates/bots.html
Normal file
108
templates/bots.html
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}All Bots - Bot Master{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.bots-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.bots-table th,
|
||||
.bots-table td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bots-table th {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.bots-table tr:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.bot-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bot-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>All Bots</h1>
|
||||
<a href="/bots/add" class="btn btn-primary">Add Bot</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if bots %}
|
||||
<table class="bots-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bot</th>
|
||||
<th>Status</th>
|
||||
<th>Servers</th>
|
||||
<th>Commands</th>
|
||||
<th>Last Checked</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bot in bots %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="bot-name-cell">
|
||||
<div class="bot-icon">{{ bot.name[0].upper() }}</div>
|
||||
<div>
|
||||
<div style="font-weight: 500;">{{ bot.name }}</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary);">{{ bot.bot_type|capitalize }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ bot.status }}">
|
||||
<span class="pulse"></span>
|
||||
{{ bot.status|capitalize }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ bot.guild_count or 0 }}</td>
|
||||
<td>{{ bot.command_count or 0 }}</td>
|
||||
<td>
|
||||
{% if bot.last_checked %}
|
||||
{{ bot.last_checked.strftime('%b %d, %H:%M') }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/bots/{{ bot.id }}" class="btn btn-secondary btn-sm">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="text-align: center; color: var(--text-secondary); padding: 2rem;">No bots added yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
283
templates/dashboard.html
Normal file
283
templates/dashboard.html
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Bot Master{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.bots-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.bot-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.bot-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.bot-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bot-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bot-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bot-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bot-stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.bot-stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bot-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.refresh-all-btn {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
padding: 1rem;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(124, 92, 255, 0.3);
|
||||
}
|
||||
|
||||
.refresh-all-btn.loading svg {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
<a href="/bots/add" class="btn btn-primary">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
Add Bot
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_bots }}</div>
|
||||
<div class="stat-label">Total Bots</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.online_bots }}</div>
|
||||
<div class="stat-label">Online</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_guilds }}</div>
|
||||
<div class="stat-label">Servers</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_commands }}</div>
|
||||
<div class="stat-label">Commands</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if bots %}
|
||||
<h2>Your Bots</h2>
|
||||
<div class="bots-grid">
|
||||
{% for bot in bots %}
|
||||
<div class="bot-card" data-bot-id="{{ bot.id }}">
|
||||
<div class="bot-header">
|
||||
<div class="bot-name">{{ bot.name }}</div>
|
||||
<span class="status-badge status-{{ bot.status }}">
|
||||
<span class="pulse"></span>
|
||||
{{ bot.status|capitalize }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if bot.description %}
|
||||
<div class="bot-description">{{ bot.description[:100] }}{% if bot.description|length > 100 %}...{% endif %}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bot-stats">
|
||||
<div class="bot-stat">
|
||||
<div class="bot-stat-value">{{ bot.guild_count or 0 }}</div>
|
||||
<div class="bot-stat-label">Servers</div>
|
||||
</div>
|
||||
<div class="bot-stat">
|
||||
<div class="bot-stat-value">{{ bot.command_count or 0 }}</div>
|
||||
<div class="bot-stat-label">Commands</div>
|
||||
</div>
|
||||
<div class="bot-stat">
|
||||
<div class="bot-stat-value">{{ (bot.uptime_seconds // 3600) if bot.uptime_seconds else 0 }}h</div>
|
||||
<div class="bot-stat-label">Uptime</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bot-actions">
|
||||
<a href="/bots/{{ bot.id }}" class="btn btn-secondary btn-sm">View Details</a>
|
||||
<button class="btn btn-secondary btn-sm check-health-btn" data-bot-id="{{ bot.id }}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6M1 20v-6h6"/>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
|
||||
</svg>
|
||||
Check
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary refresh-all-btn" id="refreshAllBtn" title="Refresh All Bots">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6M1 20v-6h6"/>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h2>No Bots Added Yet</h2>
|
||||
<p>Start by adding your first Discord bot to manage it from this dashboard.</p>
|
||||
<a href="/bots/add" class="btn btn-primary">Add Your First Bot</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.querySelectorAll('.check-health-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async function() {
|
||||
const botId = this.dataset.botId;
|
||||
const card = this.closest('.bot-card');
|
||||
const badge = card.querySelector('.status-badge');
|
||||
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<svg class="spinning" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg> Checking...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/bots/${botId}/check`, { method: 'POST' });
|
||||
const result = await response.json();
|
||||
|
||||
badge.className = `status-badge status-${result.status}`;
|
||||
badge.innerHTML = `<span class="pulse"></span> ${result.status.charAt(0).toUpperCase() + result.status.slice(1)}`;
|
||||
|
||||
if (result.data) {
|
||||
card.querySelector('.bot-stat-value').textContent = result.data.guilds || result.data.guildCount || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking health:', error);
|
||||
}
|
||||
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg> Check';
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('refreshAllBtn')?.addEventListener('click', async function() {
|
||||
this.classList.add('loading');
|
||||
this.disabled = true;
|
||||
|
||||
try {
|
||||
await fetch('/api/check-all', { method: 'POST' });
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
console.error('Error refreshing all:', error);
|
||||
}
|
||||
|
||||
this.classList.remove('loading');
|
||||
this.disabled = false;
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.spinning { animation: spin 1s linear infinite; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
54
templates/edit_bot.html
Normal file
54
templates/edit_bot.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit {{ bot.name }} - Bot Master{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Edit Bot</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name">Bot Name *</label>
|
||||
<input type="text" id="name" name="name" class="form-control" required value="{{ bot.name }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control">{{ bot.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bot_type">Bot Type</label>
|
||||
<select id="bot_type" name="bot_type" class="form-control">
|
||||
<option value="discord" {% if bot.bot_type == 'discord' %}selected{% endif %}>Discord Bot</option>
|
||||
<option value="telegram" {% if bot.bot_type == 'telegram' %}selected{% endif %}>Telegram Bot</option>
|
||||
<option value="slack" {% if bot.bot_type == 'slack' %}selected{% endif %}>Slack Bot</option>
|
||||
<option value="other" {% if bot.bot_type == 'other' %}selected{% endif %}>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="health_endpoint">Health Check Endpoint</label>
|
||||
<input type="url" id="health_endpoint" name="health_endpoint" class="form-control" value="{{ bot.health_endpoint or '' }}" placeholder="https://your-bot.host.com:8044/health">
|
||||
<small style="color: var(--text-secondary); display: block; margin-top: 0.5rem;">
|
||||
The URL where your bot exposes its health/status endpoint
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="admin_token">Admin Token</label>
|
||||
<input type="password" id="admin_token" name="admin_token" class="form-control" placeholder="Leave blank to keep unchanged">
|
||||
<small style="color: var(--text-secondary); display: block; margin-top: 0.5rem;">
|
||||
Used for accessing protected bot management endpoints. Leave blank to keep current settings.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="/bots/{{ bot.id }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
254
templates/view_bot.html
Normal file
254
templates/view_bot.html
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ bot.name }} - Bot Master{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.bot-header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.bot-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bot-icon-large {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bot-info h1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bot-type-badge {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bot-actions-top {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item .value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-item .label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.endpoint-display {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
word-break: break-all;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 68, 102, 0.1);
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.danger-zone h3 {
|
||||
color: var(--danger);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bot-header-section">
|
||||
<div class="bot-title">
|
||||
<div class="bot-icon-large">{{ bot.name[0].upper() }}</div>
|
||||
<div class="bot-info">
|
||||
<h1>{{ bot.name }}</h1>
|
||||
<span class="bot-type-badge">{{ bot.bot_type|capitalize }} Bot</span>
|
||||
</div>
|
||||
<span class="status-badge status-{{ bot.status }}" style="margin-left: 1rem;">
|
||||
<span class="pulse"></span>
|
||||
{{ bot.status|capitalize }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bot-actions-top">
|
||||
<button class="btn btn-secondary" id="checkHealthBtn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6M1 20v-6h6"/>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
|
||||
</svg>
|
||||
Check Status
|
||||
</button>
|
||||
<a href="/bots/{{ bot.id }}/edit" class="btn btn-secondary">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if bot.description %}
|
||||
<div class="card" style="margin-bottom: 1.5rem;">
|
||||
<p>{{ bot.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<h3>Statistics</h3>
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<div class="value">{{ bot.guild_count or 0 }}</div>
|
||||
<div class="label">Servers</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="value">{{ bot.command_count or 0 }}</div>
|
||||
<div class="label">Commands</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="value">{{ (bot.uptime_seconds // 3600) if bot.uptime_seconds else 0 }}h</div>
|
||||
<div class="label">Uptime</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<h3>Configuration</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Health Endpoint</span>
|
||||
</div>
|
||||
{% if bot.health_endpoint %}
|
||||
<div class="endpoint-display">{{ bot.health_endpoint }}</div>
|
||||
{% else %}
|
||||
<div class="endpoint-display" style="color: var(--text-secondary);">Not configured</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item" style="margin-top: 1rem;">
|
||||
<span class="info-label">Admin Token</span>
|
||||
<span class="info-value">{{ "Configured" if bot.admin_token else "Not set" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<h3>Details</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Created</span>
|
||||
<span class="info-value">{{ bot.created_at.strftime('%b %d, %Y %H:%M') if bot.created_at else 'Unknown' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Checked</span>
|
||||
<span class="info-value">{{ bot.last_checked.strftime('%b %d, %Y %H:%M') if bot.last_checked else 'Never' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Updated</span>
|
||||
<span class="info-value">{{ bot.updated_at.strftime('%b %d, %Y %H:%M') if bot.updated_at else 'Unknown' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="danger-zone">
|
||||
<h3>Danger Zone</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem;">Once you delete a bot, there is no going back. Please be certain.</p>
|
||||
<form action="/bots/{{ bot.id }}/delete" method="POST" onsubmit="return confirm('Are you sure you want to delete this bot?');">
|
||||
<button type="submit" class="btn btn-danger">Delete Bot</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('checkHealthBtn').addEventListener('click', async function() {
|
||||
const btn = this;
|
||||
const badge = document.querySelector('.status-badge');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<svg class="spinning" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg> Checking...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/bots/{{ bot.id }}/check', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
|
||||
badge.className = `status-badge status-${result.status}`;
|
||||
badge.innerHTML = `<span class="pulse"></span> ${result.status.charAt(0).toUpperCase() + result.status.slice(1)}`;
|
||||
|
||||
if (result.data) {
|
||||
setTimeout(() => location.reload(), 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg> Check Status';
|
||||
});
|
||||
</script>
|
||||
<style>.spinning { animation: spin 1s linear infinite; }</style>
|
||||
{% endblock %}
|
||||
Loading…
Reference in a new issue