Combine bot management with security and monitoring features

Migrate from Flask-based "Bot Master" to a unified Node.js/discord.js "AeThex Unified Bot" integrating Sentinel security, federation sync, and ticketing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e72fc1b7-94bd-4d6c-801f-cbac2fae245c
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: a4a30d75-2648-45eb-adcc-4aaeaa0072fb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/e72fc1b7-94bd-4d6c-801f-cbac2fae245c/7DQc4BR
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-07 22:17:21 +00:00
parent ddea985e6f
commit 89a8700ddd
29 changed files with 78 additions and 3867 deletions

210
main.py
View file

@ -1,210 +0,0 @@
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
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,
}
db = SQLAlchemy(app)
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,
}
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()
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)}
@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)
@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
)
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)

271
replit.md
View file

@ -1,241 +1,126 @@
# Bot Master
# AeThex Unified Bot
A centralized management dashboard for managing multiple Discord bots.
A single Discord bot combining community features and enterprise security (Sentinel).
## Overview
Bot Master is a Flask-based web application that provides a central dashboard for monitoring and managing multiple Discord bots. It supports:
AeThex Unified Bot handles both community features AND security in one instance:
- **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
- **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking
- **Federation Sync**: Cross-server role synchronization across 5 realms
- **Ticket System**: Support tickets with automatic channel creation
- **Admin Monitoring**: Real-time status, threat monitoring, server overview
## 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
---
# Aethex Sentinel Bot
Enterprise-grade Discord bot for managing a federation of 5 servers.
## Overview
Aethex Sentinel is a TypeScript-based Discord bot built with the Sapphire framework. It provides:
- **Federation Sync**: Cross-server role synchronization
- **Sentinel Security**: Anti-nuke protection with heat-based threat detection
- **Commerce Module**: Ticket system with database persistence
- **Dashboard Updates**: Network status updates in voice channels
## Tech Stack
- **Runtime**: Node.js 20 + TypeScript
- **Framework**: @sapphire/framework
- **Database**: PostgreSQL (via Prisma ORM)
- **Runtime**: Node.js 20
- **Framework**: discord.js v14
- **Database**: Supabase (optional, for user verification)
- **Health Endpoint**: HTTP server on port 8080
## Project Structure
```
sentinel-bot/
├── src/
│ ├── core/
│ │ ├── client.ts - SapphireClient with Prisma
│ │ ├── config.ts - Environment configuration
│ │ └── health.ts - Health endpoint server
│ ├── modules/
│ │ ├── federation/FederationManager.ts - Cross-server role sync
│ │ ├── security/HeatSystem.ts - Anti-nuke protection
│ │ ├── commerce/TicketManager.ts - Ticket system
│ │ └── dashboard/StatusUpdater.ts - Network status updates
│ ├── listeners/
│ │ ├── ready.ts - Bot ready handler
│ │ ├── guildMemberUpdate.ts - Role sync listener
│ │ ├── auditLogCreate.ts - Security monitor
│ │ └── interactionCreate.ts - Button handler
│ ├── commands/
│ │ ├── federation.ts - /federation command
│ │ ├── sentinel.ts - /sentinel command
│ │ ├── ticket.ts - /ticket command
│ │ └── status.ts - /status command
│ └── index.ts - Entry point
├── prisma/
│ └── schema.prisma - Database models
aethex-bot/
├── bot.js # Main entry point
├── package.json
├── tsconfig.json
└── .env.example
├── .env.example
├── commands/
│ ├── admin.js # /admin status|heat|servers|threats|federation
│ ├── federation.js # /federation link|unlink|list
│ ├── status.js # /status - network overview
│ └── ticket.js # /ticket create|close
├── events/
│ └── guildMemberUpdate.js # Federation role sync listener
├── listeners/
│ └── sentinel/
│ ├── antiNuke.js # Channel delete monitor
│ ├── roleDelete.js # Role delete monitor
│ ├── memberBan.js # Mass ban detection
│ └── memberKick.js # Mass kick detection
└── scripts/
└── register-commands.js # Slash command registration
```
## Database Models (Prisma)
- **User**: Discord user profiles with federation membership
- **HeatEvent**: Security events for threat detection
- **Ticket**: Support tickets with transcripts
- **GuildConfig**: Per-guild configuration
- **RoleMapping**: Cross-server role sync mappings
## Commands
| Command | Description |
|---------|-------------|
| `/federation link` | Link a role across servers |
| `/federation unlink` | Remove a role mapping |
| `/federation list` | Show all role mappings |
| `/sentinel heat` | View heat level of a user |
| `/sentinel lockdown` | Enable/disable lockdown mode |
| `/sentinel config` | Configure security thresholds |
| `/ticket create` | Create a support ticket |
| `/ticket close` | Close a ticket with transcript |
| `/admin status` | View bot status and statistics |
| `/admin heat @user` | Check heat level of a user |
| `/admin servers` | View all connected servers |
| `/admin threats` | View active threat monitor |
| `/admin federation` | View federation role mappings |
| `/federation link @role` | Link a role for cross-server sync |
| `/federation unlink @role` | Remove a role from sync |
| `/federation list` | List all linked roles |
| `/ticket create [reason]` | Create a support ticket |
| `/ticket close` | Close the current ticket |
| `/status` | View network status |
## Health Endpoint
## Sentinel Security System
The bot exposes a health endpoint compatible with Bot Master dashboard:
The anti-nuke system uses RAM-based heat tracking for instant response:
- **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban
- **Monitored Actions**: Channel delete, role delete, member ban, member kick
- **Alerts**: Sends to configured alert channel and DMs server owner
- **Whitelist**: Set `WHITELISTED_USERS` env var for trusted users
## Environment Variables
Required:
- `DISCORD_TOKEN` or `DISCORD_BOT_TOKEN` - Bot token
- `DISCORD_CLIENT_ID` - Application ID (currently: 1447339527885553828)
Optional - Federation:
- `HUB_GUILD_ID` - Main hub server
- `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID`
Optional - Security:
- `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking
- `ALERT_CHANNEL_ID` - Channel for security alerts
Optional - Supabase:
- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE` - For user verification features
## Health Endpoint
**GET /health** (port 8080)
```json
{
"status": "online",
"guilds": 5,
"commands": 12,
"commands": 4,
"uptime": 3600,
"timestamp": "2025-12-07T12:00:00.000Z",
"bot": {
"tag": "Aethex Sentinel#1234",
"id": "123456789"
}
"heatMapSize": 0,
"timestamp": "2025-12-07T22:15:00.000Z"
}
```
**GET /stats** (port 8080)
```json
{
"guilds": [
{ "id": "...", "name": "...", "memberCount": 100 }
],
"guilds": [...],
"totalMembers": 500,
"uptime": 3600
"uptime": 3600,
"activeTickets": 0,
"heatEvents": 0
}
```
## Environment Variables
Required secrets (add in Replit Secrets tab):
- `DISCORD_TOKEN` - Discord bot token
- `DATABASE_URL` - PostgreSQL connection string
Optional configuration:
- `HUB_GUILD_ID` - Main hub server ID
- `FEDERATION_GUILD_IDS` - Comma-separated list of guild IDs
- `HEALTH_PORT` - Health server port (default: 8080)
## Running the Bot
```bash
cd sentinel-bot
cd aethex-bot
npm install
npx prisma generate
npx prisma db push
npm run build
node scripts/register-commands.js # Register slash commands (run once)
npm start
```
For development:
```bash
npm run dev
```
## Current Status
## Integration with Bot Master
Once the Sentinel bot is running, add it to Bot Master dashboard:
1. Go to Bot Master dashboard (port 5000)
2. Click "Add Bot"
3. Enter name: "Aethex Sentinel"
4. Enter health endpoint: `http://localhost:8080/health`
5. Select type: "discord"
The dashboard will automatically poll the health endpoint for status updates.
- Bot is running and connected to 5 servers
- All 4 commands registered (/admin, /federation, /status, /ticket)
- Sentinel listeners active (channel/role delete, ban/kick monitoring)
- Health endpoint available at port 8080

View file

@ -1,21 +0,0 @@
# Discord Bot Configuration
DISCORD_TOKEN=your_bot_token_here
# Federation Guild IDs
HUB_ID=515711457946632232
FORGE_ID=1245619208805416970
FOUNDATION_ID=1338564560277344287
LABS_ID=1275962459596783686
CORP_ID=373713073594302464
# Security Configuration
WHITELISTED_USERS=113472107526033408,user_id_2
# Dashboard Configuration
STATUS_CHANNEL_ID=voice_channel_id_for_status
# Health Check
HEALTH_PORT=8044
# Database (auto-provided by Replit)
# DATABASE_URL=postgresql://...

File diff suppressed because it is too large Load diff

View file

@ -1,27 +0,0 @@
{
"name": "aethex-sentinel",
"version": "1.0.0",
"description": "Enterprise-grade Discord bot for managing a federation of servers",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@sapphire/framework": "^5.2.1",
"@sapphire/plugin-scheduled-tasks": "^10.0.1",
"discord.js": "^14.16.3",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/node": "^20.17.6",
"prisma": "^5.22.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}

View file

@ -1,65 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id
heatLevel Int @default(0)
balance Float @default(0.0)
roles String[]
lastHeatReset DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tickets Ticket[]
heatEvents HeatEvent[]
}
model HeatEvent {
id Int @id @default(autoincrement())
userId String
action String
timestamp DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId, timestamp])
}
model Ticket {
id Int @id @default(autoincrement())
threadId String @unique
userId String
type String
status String @default("open")
transcript String?
createdAt DateTime @default(now())
closedAt DateTime?
user User @relation(fields: [userId], references: [id])
@@index([userId])
}
model GuildConfig {
id String @id
name String
type String
statusChannelId String?
feedChannelId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model RoleMapping {
id Int @id @default(autoincrement())
sourceGuild String
sourceRole String
targetGuild String
targetRole String
roleName String
@@unique([sourceGuild, sourceRole, targetGuild])
}

View file

@ -1,102 +0,0 @@
import { Command } from '@sapphire/framework';
import { PermissionFlagsBits } from 'discord.js';
import { federationManager } from '../modules/federation/FederationManager';
import { prisma } from '../core/client';
export class FederationCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'federation',
description: 'Manage federation role mappings',
requiredUserPermissions: [PermissionFlagsBits.Administrator],
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((sub) =>
sub
.setName('add-mapping')
.setDescription('Add a role mapping between guilds')
.addStringOption((opt) =>
opt.setName('source-guild').setDescription('Source guild ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('source-role').setDescription('Source role ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('target-guild').setDescription('Target guild ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('target-role').setDescription('Target role ID').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('role-name').setDescription('Friendly name for this mapping').setRequired(true)
)
)
.addSubcommand((sub) =>
sub.setName('list').setDescription('List all role mappings')
)
.addSubcommand((sub) =>
sub.setName('reload').setDescription('Reload mappings from database')
)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'add-mapping') {
const sourceGuild = interaction.options.getString('source-guild', true);
const sourceRole = interaction.options.getString('source-role', true);
const targetGuild = interaction.options.getString('target-guild', true);
const targetRole = interaction.options.getString('target-role', true);
const roleName = interaction.options.getString('role-name', true);
await federationManager.addMapping({
sourceGuild,
sourceRole,
targetGuild,
targetRole,
roleName,
});
return interaction.reply({
content: `✅ Added mapping: "${roleName}" from ${sourceGuild} to ${targetGuild}`,
ephemeral: true,
});
}
if (subcommand === 'list') {
const mappings = await prisma.roleMapping.findMany();
if (mappings.length === 0) {
return interaction.reply({
content: 'No role mappings configured.',
ephemeral: true,
});
}
const list = mappings
.map((m) => `• **${m.roleName}**: ${m.sourceGuild}:${m.sourceRole}${m.targetGuild}:${m.targetRole}`)
.join('\n');
return interaction.reply({
content: `**Federation Role Mappings:**\n${list}`,
ephemeral: true,
});
}
if (subcommand === 'reload') {
await federationManager.loadMappings();
return interaction.reply({
content: '✅ Reloaded role mappings from database.',
ephemeral: true,
});
}
}
}

View file

@ -1,84 +0,0 @@
import { Command } from '@sapphire/framework';
import { PermissionFlagsBits, EmbedBuilder } from 'discord.js';
import { heatSystem } from '../modules/security/HeatSystem';
import { config } from '../core/config';
export class SentinelCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'sentinel',
description: 'Manage the Sentinel security system',
requiredUserPermissions: [PermissionFlagsBits.Administrator],
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((sub) =>
sub.setName('status').setDescription('View Sentinel security status')
)
.addSubcommand((sub) =>
sub
.setName('clear-heat')
.setDescription('Clear heat level for a user')
.addUserOption((opt) =>
opt.setName('user').setDescription('User to clear heat for').setRequired(true)
)
)
.addSubcommand((sub) =>
sub.setName('unlock').setDescription('Lift lockdown mode on this server')
)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'status') {
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('🛡️ Sentinel Security Status')
.addFields(
{ name: 'Heat Threshold', value: `${config.security.heatThreshold} actions`, inline: true },
{ name: 'Window', value: `${config.security.heatWindowMs}ms`, inline: true },
{ name: 'Whitelisted Users', value: `${config.security.whitelistedUsers.length}`, inline: true },
{
name: 'Monitored Actions',
value: config.security.dangerousActions.join(', '),
inline: false,
}
)
.setTimestamp();
return interaction.reply({ embeds: [embed], ephemeral: true });
}
if (subcommand === 'clear-heat') {
const user = interaction.options.getUser('user', true);
heatSystem.clearHeat(user.id);
return interaction.reply({
content: `✅ Cleared heat for ${user.tag}`,
ephemeral: true,
});
}
if (subcommand === 'unlock') {
if (!interaction.guild) {
return interaction.reply({
content: 'This command must be used in a server.',
ephemeral: true,
});
}
await heatSystem.unlockGuild(interaction.guild);
return interaction.reply({
content: '🔓 Lockdown lifted. @everyone permissions restored.',
ephemeral: true,
});
}
}
}

View file

@ -1,55 +0,0 @@
import { Command } from '@sapphire/framework';
import { EmbedBuilder } from 'discord.js';
import { config } from '../core/config';
export class StatusCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'status',
description: 'View Sentinel bot and network status',
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder.setName(this.name).setDescription(this.description)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const client = this.container.client;
const guildIds = Object.values(config.guilds).filter(id => id);
const guilds = guildIds
.map(id => client.guilds.cache.get(id))
.filter(g => g !== undefined);
const totalMembers = guilds.reduce((sum, g) => sum + g!.memberCount, 0);
const guildList = guilds
.map(g => `• **${g!.name}**: ${g!.memberCount.toLocaleString()} members`)
.join('\n') || 'No federation guilds connected';
const uptimeSeconds = Math.floor(process.uptime());
const hours = Math.floor(uptimeSeconds / 3600);
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
const seconds = uptimeSeconds % 60;
const uptimeStr = `${hours}h ${minutes}m ${seconds}s`;
const embed = new EmbedBuilder()
.setColor(0x5865f2)
.setTitle('🛡️ Aethex Sentinel Status')
.setThumbnail(client.user?.displayAvatarURL() || null)
.addFields(
{ name: '📡 Network', value: `${guilds.length} guilds`, inline: true },
{ name: '👥 Total Members', value: totalMembers.toLocaleString(), inline: true },
{ name: '⏱️ Uptime', value: uptimeStr, inline: true },
{ name: '🏰 Federation Guilds', value: guildList, inline: false }
)
.setFooter({ text: 'Aethex Sentinel • Protecting the Federation' })
.setTimestamp();
return interaction.reply({ embeds: [embed] });
}
}

View file

@ -1,76 +0,0 @@
import { Command } from '@sapphire/framework';
import { PermissionFlagsBits, ChannelType, TextChannel } from 'discord.js';
import { ticketManager } from '../modules/commerce/TicketManager';
import { prisma } from '../core/client';
export class TicketCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'ticket',
description: 'Manage the ticket system',
requiredUserPermissions: [PermissionFlagsBits.Administrator],
});
}
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((sub) =>
sub
.setName('setup')
.setDescription('Set up the ticket panel in this channel')
)
.addSubcommand((sub) =>
sub.setName('stats').setDescription('View ticket statistics')
)
);
}
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'setup') {
if (!interaction.channel || interaction.channel.type !== ChannelType.GuildText) {
return interaction.reply({
content: 'This command must be used in a text channel.',
ephemeral: true,
});
}
await interaction.deferReply({ ephemeral: true });
await ticketManager.createSupportPanel(interaction.channel as TextChannel);
return interaction.editReply({
content: '✅ Ticket panel created!',
});
}
if (subcommand === 'stats') {
const totalTickets = await prisma.ticket.count();
const openTickets = await prisma.ticket.count({ where: { status: 'open' } });
const closedTickets = await prisma.ticket.count({ where: { status: 'closed' } });
const byType = await prisma.ticket.groupBy({
by: ['type'],
_count: { id: true },
});
const typeStats = byType
.map((t) => `${t.type}: ${t._count.id}`)
.join('\n') || 'No tickets yet';
return interaction.reply({
content:
`**🎫 Ticket Statistics**\n\n` +
`Total: ${totalTickets}\n` +
`Open: ${openTickets}\n` +
`Closed: ${closedTickets}\n\n` +
`**By Type:**\n${typeStats}`,
ephemeral: true,
});
}
}
}

View file

@ -1,37 +0,0 @@
import { SapphireClient } from '@sapphire/framework';
import { GatewayIntentBits, Partials } from 'discord.js';
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
export class SentinelClient extends SapphireClient {
public constructor() {
super({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: [
Partials.Channel,
Partials.Message,
Partials.GuildMember,
],
loadMessageCommandListeners: true,
});
}
public override async login(token?: string): Promise<string> {
await prisma.$connect();
console.log('✅ Database connected');
return super.login(token);
}
public override async destroy(): Promise<void> {
await prisma.$disconnect();
return super.destroy();
}
}

View file

@ -1,41 +0,0 @@
import 'dotenv/config';
export const config = {
token: process.env.DISCORD_TOKEN!,
guilds: {
hub: process.env.HUB_ID!,
gameforge: process.env.FORGE_ID!,
foundation: process.env.FOUNDATION_ID!,
labs: process.env.LABS_ID!,
corp: process.env.CORP_ID!,
},
security: {
heatThreshold: 3,
heatWindowMs: 5000,
dangerousActions: ['CHANNEL_DELETE', 'BAN_ADD', 'KICK', 'ROLE_DELETE', 'WEBHOOK_DELETE'],
whitelistedUsers: process.env.WHITELISTED_USERS?.split(',') || [],
},
dashboard: {
updateIntervalMs: 5 * 60 * 1000,
statusChannelId: process.env.STATUS_CHANNEL_ID,
},
health: {
port: parseInt(process.env.HEALTH_PORT || '8080'),
},
};
export function validateConfig(): void {
const required = ['DISCORD_TOKEN'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('❌ Missing required environment variables:', missing.join(', '));
process.exit(1);
}
console.log('✅ Configuration validated');
}

View file

@ -1,59 +0,0 @@
import http from 'http';
import { SentinelClient } from './client';
import { config } from './config';
export function startHealthServer(client: SentinelClient): void {
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.url === '/health') {
const guilds = client.guilds.cache.size;
const commands = client.stores.get('commands')?.size || 0;
res.writeHead(200);
res.end(JSON.stringify({
status: client.isReady() ? 'online' : 'offline',
guilds,
commands,
uptime: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
bot: {
tag: client.user?.tag || 'Not ready',
id: client.user?.id,
},
}));
return;
}
if (req.url === '/stats') {
const guildStats = client.guilds.cache.map(g => ({
id: g.id,
name: g.name,
memberCount: g.memberCount,
}));
res.writeHead(200);
res.end(JSON.stringify({
guilds: guildStats,
totalMembers: guildStats.reduce((sum, g) => sum + g.memberCount, 0),
uptime: Math.floor(process.uptime()),
}));
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
});
server.listen(config.health.port, () => {
console.log(`🏥 Health server running on port ${config.health.port}`);
});
}

View file

@ -1,27 +0,0 @@
import { config, validateConfig } from './core/config';
import { SentinelClient } from './core/client';
import { startHealthServer } from './core/health';
validateConfig();
const client = new SentinelClient();
client.once('ready', () => {
console.log(`✅ Aethex Sentinel logged in as ${client.user?.tag}`);
console.log(`📡 Watching ${client.guilds.cache.size} guild(s)`);
client.user?.setActivity('🛡️ Protecting the Federation', { type: 3 });
startHealthServer(client);
});
process.on('unhandledRejection', (error: Error) => {
console.error('❌ Unhandled Promise Rejection:', error);
});
process.on('uncaughtException', (error: Error) => {
console.error('❌ Uncaught Exception:', error);
process.exit(1);
});
client.login(config.token);

View file

@ -1,40 +0,0 @@
import { Listener } from '@sapphire/framework';
import { AuditLogEvent, GuildAuditLogsEntry, Guild } from 'discord.js';
import { heatSystem } from '../modules/security/HeatSystem';
type DangerousAction = 'CHANNEL_DELETE' | 'BAN_ADD' | 'KICK' | 'ROLE_DELETE' | 'WEBHOOK_DELETE';
const DANGEROUS_ACTIONS: Record<number, DangerousAction> = {
[AuditLogEvent.ChannelDelete]: 'CHANNEL_DELETE',
[AuditLogEvent.MemberBanAdd]: 'BAN_ADD',
[AuditLogEvent.MemberKick]: 'KICK',
[AuditLogEvent.RoleDelete]: 'ROLE_DELETE',
[AuditLogEvent.WebhookDelete]: 'WEBHOOK_DELETE',
};
export class AuditLogCreateListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
event: 'guildAuditLogEntryCreate',
});
}
public async run(entry: GuildAuditLogsEntry, guild: Guild): Promise<void> {
const actionType = DANGEROUS_ACTIONS[entry.action];
if (!actionType) return;
const executorId = entry.executorId;
if (!executorId) return;
if (executorId === this.container.client.user?.id) return;
console.log(`⚠️ Dangerous action detected: ${actionType} by ${executorId} in ${guild.name}`);
const triggered = await heatSystem.recordAction(executorId, actionType, guild);
if (triggered) {
console.log(`🚨 Anti-nuke triggered for ${executorId} in ${guild.name}`);
}
}
}

View file

@ -1,28 +0,0 @@
import { Listener } from '@sapphire/framework';
import { GuildMember, Role } from 'discord.js';
import { federationManager } from '../modules/federation/FederationManager';
export class GuildMemberUpdateListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
event: 'guildMemberUpdate',
});
}
public async run(oldMember: GuildMember, newMember: GuildMember): Promise<void> {
const oldRoles = oldMember.roles.cache;
const newRoles = newMember.roles.cache;
const addedRoles = newRoles.filter(role => !oldRoles.has(role.id));
const removedRoles = oldRoles.filter(role => !newRoles.has(role.id));
for (const role of addedRoles.values()) {
await federationManager.syncRoleGrant(newMember, role);
}
for (const role of removedRoles.values()) {
await federationManager.syncRoleRemove(newMember, role);
}
}
}

View file

@ -1,46 +0,0 @@
import { Listener } from '@sapphire/framework';
import { Interaction } from 'discord.js';
import { ticketManager } from '../modules/commerce/TicketManager';
export class InteractionCreateListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
event: 'interactionCreate',
});
}
public async run(interaction: Interaction): Promise<void> {
if (!interaction.isButton()) return;
const customId = interaction.customId;
try {
if (customId === 'ticket_rental') {
await ticketManager.createTicket(interaction, 'rental');
} else if (customId === 'ticket_support') {
await ticketManager.createTicket(interaction, 'support');
} else if (customId === 'ticket_billing') {
await ticketManager.createTicket(interaction, 'billing');
} else if (customId === 'ticket_invoice') {
await ticketManager.generateInvoice(interaction);
} else if (customId === 'ticket_close') {
await ticketManager.closeTicket(interaction);
} else if (customId === 'invoice_pay') {
await interaction.reply({
content: '✅ Invoice marked as paid! (Mock transaction)',
ephemeral: true,
});
}
} catch (error) {
console.error('❌ Button interaction error:', error);
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: 'An error occurred while processing your request.',
ephemeral: true,
});
}
}
}
}

View file

@ -1,28 +0,0 @@
import { Listener } from '@sapphire/framework';
import { Client } from 'discord.js';
import { federationManager } from '../modules/federation/FederationManager';
import { StatusUpdater } from '../modules/dashboard/StatusUpdater';
export class ReadyListener extends Listener {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
super(context, {
...options,
once: true,
event: 'ready',
});
}
public async run(client: Client<true>): Promise<void> {
console.log(`✅ Aethex Sentinel ready as ${client.user.tag}`);
console.log(`📡 Connected to ${client.guilds.cache.size} guild(s)`);
await federationManager.loadMappings();
const statusUpdater = new StatusUpdater(client);
statusUpdater.start();
client.user.setActivity('🛡️ Protecting the Federation', { type: 3 });
console.log('✅ All modules initialized');
}
}

View file

@ -1,202 +0,0 @@
import {
ButtonInteraction,
ChannelType,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
TextChannel,
ThreadChannel,
Message,
} from 'discord.js';
import { prisma } from '../../core/client';
export class TicketManager {
async createTicket(interaction: ButtonInteraction, type: string): Promise<void> {
const channel = interaction.channel as TextChannel;
if (!channel || channel.type !== ChannelType.GuildText) {
await interaction.reply({ content: 'Cannot create ticket here.', ephemeral: true });
return;
}
await prisma.user.upsert({
where: { id: interaction.user.id },
update: {},
create: { id: interaction.user.id },
});
const thread = await channel.threads.create({
name: `${type}-${interaction.user.username}-${Date.now().toString(36)}`,
type: ChannelType.PrivateThread,
invitable: false,
reason: `Ticket created by ${interaction.user.tag}`,
});
await thread.members.add(interaction.user.id);
await prisma.ticket.create({
data: {
threadId: thread.id,
userId: interaction.user.id,
type,
status: 'open',
},
});
const embed = new EmbedBuilder()
.setColor(0x5865f2)
.setTitle(`🎫 ${type.charAt(0).toUpperCase() + type.slice(1)} Ticket`)
.setDescription(
`Welcome ${interaction.user}!\n\n` +
`A staff member will be with you shortly.\n` +
`Please describe your request in detail.`
)
.setFooter({ text: `Ticket ID: ${thread.id}` })
.setTimestamp();
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('ticket_invoice')
.setLabel('Generate Invoice')
.setStyle(ButtonStyle.Primary)
.setEmoji('📄'),
new ButtonBuilder()
.setCustomId('ticket_close')
.setLabel('Close Ticket')
.setStyle(ButtonStyle.Danger)
.setEmoji('🔒')
);
await thread.send({ embeds: [embed], components: [row] });
await interaction.reply({
content: `✅ Your ticket has been created: ${thread}`,
ephemeral: true,
});
console.log(`🎫 Ticket created: ${thread.name} by ${interaction.user.tag}`);
}
async generateInvoice(interaction: ButtonInteraction): Promise<void> {
const invoiceId = `INV-${Date.now().toString(36).toUpperCase()}`;
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('📄 Invoice Generated')
.setDescription('Here is your mock invoice for the requested service.')
.addFields(
{ name: 'Invoice ID', value: invoiceId, inline: true },
{ name: 'Status', value: 'Pending', inline: true },
{ name: 'Amount', value: '$XX.XX', inline: true },
{ name: 'Service', value: 'Server Rental', inline: false },
)
.setFooter({ text: 'This is a mock invoice for demonstration' })
.setTimestamp();
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('invoice_pay')
.setLabel('Mark as Paid')
.setStyle(ButtonStyle.Success)
.setEmoji('💳')
);
await interaction.reply({ embeds: [embed], components: [row] });
}
async closeTicket(interaction: ButtonInteraction): Promise<void> {
const thread = interaction.channel as ThreadChannel;
if (!thread || thread.type !== ChannelType.PrivateThread) {
await interaction.reply({ content: 'This is not a ticket thread.', ephemeral: true });
return;
}
const ticket = await prisma.ticket.findUnique({
where: { threadId: thread.id },
});
if (!ticket) {
await interaction.reply({ content: 'Ticket not found in database.', ephemeral: true });
return;
}
await interaction.reply({ content: '🔒 Closing ticket and saving transcript...' });
const transcript = await this.generateTranscript(thread);
await prisma.ticket.update({
where: { threadId: thread.id },
data: {
status: 'closed',
transcript,
closedAt: new Date(),
},
});
await thread.setLocked(true, 'Ticket closed');
await thread.setArchived(true, 'Ticket closed');
console.log(`🎫 Ticket closed: ${thread.name}`);
}
private async generateTranscript(thread: ThreadChannel): Promise<string> {
const messages: Message[] = [];
let lastId: string | undefined;
while (true) {
const batch = await thread.messages.fetch({
limit: 100,
before: lastId,
});
if (batch.size === 0) break;
messages.push(...batch.values());
lastId = batch.last()?.id;
}
messages.reverse();
const transcript = messages
.map(m => `[${m.createdAt.toISOString()}] ${m.author.tag}: ${m.content}`)
.join('\n');
return transcript;
}
async createSupportPanel(channel: TextChannel): Promise<void> {
const embed = new EmbedBuilder()
.setColor(0x5865f2)
.setTitle('🎫 Support Center')
.setDescription(
'Need help? Click a button below to open a support ticket.\n\n' +
'**Available Services:**\n' +
'• Server Rental - Rent a game server\n' +
'• Technical Support - Get help with issues\n' +
'• Billing - Payment and invoice questions'
)
.setFooter({ text: 'Aethex Sentinel Support System' });
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('ticket_rental')
.setLabel('Rent Server')
.setStyle(ButtonStyle.Primary)
.setEmoji('🖥️'),
new ButtonBuilder()
.setCustomId('ticket_support')
.setLabel('Technical Support')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🔧'),
new ButtonBuilder()
.setCustomId('ticket_billing')
.setLabel('Billing')
.setStyle(ButtonStyle.Secondary)
.setEmoji('💰')
);
await channel.send({ embeds: [embed], components: [row] });
}
}
export const ticketManager = new TicketManager();

View file

@ -1,126 +0,0 @@
import { Client, VoiceChannel, ChannelType } from 'discord.js';
import { prisma } from '../../core/client';
import { config } from '../../core/config';
export class StatusUpdater {
private client: Client;
private intervalId: NodeJS.Timeout | null = null;
constructor(client: Client) {
this.client = client;
}
start(): void {
if (this.intervalId) return;
this.update();
this.intervalId = setInterval(() => {
this.update();
}, config.dashboard.updateIntervalMs);
console.log(`📊 Status updater started (interval: ${config.dashboard.updateIntervalMs}ms)`);
}
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
console.log('📊 Status updater stopped');
}
}
async update(): Promise<void> {
try {
const guildIds = Object.values(config.guilds).filter(id => id);
let totalMembers = 0;
const guildStats: { id: string; name: string; members: number }[] = [];
for (const guildId of guildIds) {
const guild = this.client.guilds.cache.get(guildId);
if (guild) {
totalMembers += guild.memberCount;
guildStats.push({
id: guild.id,
name: guild.name,
members: guild.memberCount,
});
await prisma.guildConfig.upsert({
where: { id: guild.id },
update: {
name: guild.name,
updatedAt: new Date(),
},
create: {
id: guild.id,
name: guild.name,
type: this.getGuildType(guild.id),
},
});
}
}
if (config.dashboard.statusChannelId && config.guilds.hub) {
await this.updateStatusChannel(totalMembers);
}
console.log(`📊 Network status: ${totalMembers} total members across ${guildStats.length} guilds`);
} catch (error) {
console.error('❌ Failed to update status:', error);
}
}
private async updateStatusChannel(totalMembers: number): Promise<void> {
try {
const hubGuild = this.client.guilds.cache.get(config.guilds.hub);
if (!hubGuild) return;
const channel = hubGuild.channels.cache.get(config.dashboard.statusChannelId!);
if (!channel || channel.type !== ChannelType.GuildVoice) return;
const voiceChannel = channel as VoiceChannel;
const newName = `🟢 Network: ${totalMembers.toLocaleString()} Users`;
if (voiceChannel.name !== newName) {
await voiceChannel.setName(newName);
console.log(`📊 Updated status channel: ${newName}`);
}
} catch (error) {
console.error('❌ Failed to update status channel:', error);
}
}
private getGuildType(guildId: string): string {
const entries = Object.entries(config.guilds);
const found = entries.find(([, id]) => id === guildId);
return found ? found[0] : 'unknown';
}
async getNetworkStats(): Promise<{
totalMembers: number;
totalGuilds: number;
guilds: { id: string; name: string; members: number; type: string }[];
}> {
const guildIds = Object.values(config.guilds).filter(id => id);
const guilds: { id: string; name: string; members: number; type: string }[] = [];
for (const guildId of guildIds) {
const guild = this.client.guilds.cache.get(guildId);
if (guild) {
guilds.push({
id: guild.id,
name: guild.name,
members: guild.memberCount,
type: this.getGuildType(guild.id),
});
}
}
return {
totalMembers: guilds.reduce((sum, g) => sum + g.members, 0),
totalGuilds: guilds.length,
guilds,
};
}
}

View file

@ -1,124 +0,0 @@
import { Guild, GuildMember, Role } from 'discord.js';
import { prisma } from '../../core/client';
import { config } from '../../core/config';
interface RoleMapping {
sourceGuild: string;
sourceRole: string;
targetGuild: string;
targetRole: string;
roleName: string;
}
export class FederationManager {
private roleMappings: RoleMapping[] = [];
async loadMappings(): Promise<void> {
this.roleMappings = await prisma.roleMapping.findMany();
console.log(`📋 Loaded ${this.roleMappings.length} role mappings`);
}
async addMapping(mapping: Omit<RoleMapping, 'id'>): Promise<void> {
await prisma.roleMapping.create({
data: mapping,
});
await this.loadMappings();
}
async syncRoleGrant(member: GuildMember, role: Role): Promise<void> {
const sourceGuildId = member.guild.id;
const mappings = this.roleMappings.filter(
m => m.sourceGuild === sourceGuildId && m.sourceRole === role.id
);
if (mappings.length === 0) return;
console.log(`🔄 Syncing role "${role.name}" for ${member.user.tag} to ${mappings.length} guild(s)`);
for (const mapping of mappings) {
try {
const targetGuild = member.client.guilds.cache.get(mapping.targetGuild);
if (!targetGuild) {
console.warn(`⚠️ Target guild ${mapping.targetGuild} not found`);
continue;
}
const targetMember = await targetGuild.members.fetch(member.id).catch(() => null);
if (!targetMember) {
console.warn(`⚠️ User ${member.user.tag} not in target guild ${targetGuild.name}`);
continue;
}
const targetRole = targetGuild.roles.cache.get(mapping.targetRole);
if (!targetRole) {
console.warn(`⚠️ Target role ${mapping.targetRole} not found in ${targetGuild.name}`);
continue;
}
if (!targetMember.roles.cache.has(targetRole.id)) {
await targetMember.roles.add(targetRole, `Federation sync from ${member.guild.name}`);
console.log(`✅ Granted "${targetRole.name}" to ${member.user.tag} in ${targetGuild.name}`);
}
} catch (error) {
console.error(`❌ Failed to sync role to guild ${mapping.targetGuild}:`, error);
}
}
await this.updateUserRoles(member.id);
}
async syncRoleRemove(member: GuildMember, role: Role): Promise<void> {
const sourceGuildId = member.guild.id;
const mappings = this.roleMappings.filter(
m => m.sourceGuild === sourceGuildId && m.sourceRole === role.id
);
if (mappings.length === 0) return;
console.log(`🔄 Removing synced role "${role.name}" for ${member.user.tag} from ${mappings.length} guild(s)`);
for (const mapping of mappings) {
try {
const targetGuild = member.client.guilds.cache.get(mapping.targetGuild);
if (!targetGuild) continue;
const targetMember = await targetGuild.members.fetch(member.id).catch(() => null);
if (!targetMember) continue;
const targetRole = targetGuild.roles.cache.get(mapping.targetRole);
if (!targetRole) continue;
if (targetMember.roles.cache.has(targetRole.id)) {
await targetMember.roles.remove(targetRole, `Federation sync removal from ${member.guild.name}`);
console.log(`✅ Removed "${targetRole.name}" from ${member.user.tag} in ${targetGuild.name}`);
}
} catch (error) {
console.error(`❌ Failed to remove synced role:`, error);
}
}
await this.updateUserRoles(member.id);
}
private async updateUserRoles(userId: string): Promise<void> {
const syncedRoles = this.roleMappings
.filter(m => m.sourceGuild === config.guilds.hub)
.map(m => m.roleName);
await prisma.user.upsert({
where: { id: userId },
update: { roles: syncedRoles },
create: { id: userId, roles: syncedRoles },
});
}
getGuildType(guildId: string): string | null {
const entries = Object.entries(config.guilds);
const found = entries.find(([, id]) => id === guildId);
return found ? found[0] : null;
}
}
export const federationManager = new FederationManager();

View file

@ -1,131 +0,0 @@
import { Guild, GuildMember, PermissionFlagsBits } from 'discord.js';
import { prisma } from '../../core/client';
import { config } from '../../core/config';
type DangerousAction = 'CHANNEL_DELETE' | 'BAN_ADD' | 'KICK' | 'ROLE_DELETE' | 'WEBHOOK_DELETE';
interface HeatEvent {
userId: string;
action: DangerousAction;
timestamp: Date;
}
export class HeatSystem {
private heatCache: Map<string, HeatEvent[]> = new Map();
isWhitelisted(userId: string): boolean {
return config.security.whitelistedUsers.includes(userId);
}
async recordAction(userId: string, action: DangerousAction, guild: Guild): Promise<boolean> {
if (this.isWhitelisted(userId)) {
return false;
}
const now = new Date();
const event: HeatEvent = { userId, action, timestamp: now };
await prisma.heatEvent.create({
data: { userId, action, timestamp: now },
});
const events = this.heatCache.get(userId) || [];
events.push(event);
const windowStart = new Date(now.getTime() - config.security.heatWindowMs);
const recentEvents = events.filter(e => e.timestamp >= windowStart);
this.heatCache.set(userId, recentEvents);
console.log(`🔥 Heat event: ${action} by ${userId} (${recentEvents.length}/${config.security.heatThreshold})`);
if (recentEvents.length >= config.security.heatThreshold) {
console.log(`🚨 THRESHOLD EXCEEDED for ${userId}!`);
await this.triggerLockdown(userId, guild, recentEvents);
return true;
}
return false;
}
private async triggerLockdown(userId: string, guild: Guild, events: HeatEvent[]): Promise<void> {
console.log(`🔒 LOCKDOWN INITIATED in ${guild.name}`);
try {
const member = await guild.members.fetch(userId).catch(() => null);
if (member && member.bannable) {
await member.ban({
reason: `[SENTINEL] Anti-nuke triggered: ${events.length} dangerous actions in ${config.security.heatWindowMs}ms`,
deleteMessageSeconds: 0,
});
console.log(`🔨 Banned ${member.user.tag} for nuke attempt`);
}
} catch (error) {
console.error('❌ Failed to ban attacker:', error);
}
try {
const everyoneRole = guild.roles.everyone;
await everyoneRole.setPermissions(
everyoneRole.permissions.remove([
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.AddReactions,
PermissionFlagsBits.CreatePublicThreads,
PermissionFlagsBits.CreatePrivateThreads,
]),
'[SENTINEL] Lockdown mode activated'
);
console.log(`🔒 Locked down @everyone permissions`);
} catch (error) {
console.error('❌ Failed to modify @everyone permissions:', error);
}
await this.sendLockdownAlert(guild, userId, events);
await prisma.user.upsert({
where: { id: userId },
update: { heatLevel: events.length },
create: { id: userId, heatLevel: events.length },
});
}
private async sendLockdownAlert(guild: Guild, attackerId: string, events: HeatEvent[]): Promise<void> {
const owner = await guild.fetchOwner().catch(() => null);
if (!owner) return;
try {
await owner.send({
content: `🚨 **LOCKDOWN ALERT** 🚨\n\n` +
`Server: **${guild.name}**\n` +
`Attacker ID: \`${attackerId}\`\n` +
`Actions detected: ${events.length}\n` +
`Actions: ${events.map(e => e.action).join(', ')}\n\n` +
`The attacker has been banned and @everyone permissions have been restricted.\n` +
`Please review audit logs and restore permissions when safe.`,
});
} catch (error) {
console.error('❌ Failed to DM server owner:', error);
}
}
async unlockGuild(guild: Guild): Promise<void> {
try {
const everyoneRole = guild.roles.everyone;
await everyoneRole.setPermissions(
everyoneRole.permissions.add([
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.AddReactions,
]),
'[SENTINEL] Lockdown lifted'
);
console.log(`🔓 Lifted lockdown in ${guild.name}`);
} catch (error) {
console.error('❌ Failed to unlock guild:', error);
}
}
clearHeat(userId: string): void {
this.heatCache.delete(userId);
}
}
export const heatSystem = new HeatSystem();

View file

@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,54 +0,0 @@
{% 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 %}

View file

@ -1,274 +0,0 @@
<!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>

View file

@ -1,108 +0,0 @@
{% 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 %}

View file

@ -1,283 +0,0 @@
{% 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 %}

View file

@ -1,54 +0,0 @@
{% 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 %}

View file

@ -1,254 +0,0 @@
{% 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 %}