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
283 lines
8.3 KiB
HTML
283 lines
8.3 KiB
HTML
{% 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 %}
|