init
This commit is contained in:
10
web/Dockerfile
Normal file
10
web/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]
|
||||
231
web/app.py
Normal file
231
web/app.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Web Dashboard for A/D Infrastructure Control
|
||||
Flask-based dashboard to monitor services, attacks, and alerts
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, jsonify, request, redirect, url_for, session
|
||||
import asyncpg
|
||||
import aiohttp
|
||||
from functools import wraps
|
||||
|
||||
# Configuration
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://adctrl:adctrl@postgres:5432/adctrl")
|
||||
SECRET_TOKEN = os.getenv("SECRET_TOKEN", "change-me-in-production")
|
||||
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "admin123")
|
||||
CONTROLLER_API = os.getenv("CONTROLLER_API", "http://controller:8001")
|
||||
SCOREBOARD_API = os.getenv("SCOREBOARD_API", "http://scoreboard-injector:8002")
|
||||
TELEGRAM_API = os.getenv("TELEGRAM_API", "http://tg-bot:8003")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.getenv("FLASK_SECRET_KEY", "change-me-in-production-flask-secret")
|
||||
|
||||
# Database connection
|
||||
async def get_db_conn():
|
||||
return await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
# Auth decorator
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('logged_in'):
|
||||
return redirect(url_for('login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# API call helper
|
||||
async def api_call(url: str, method: str = "GET", data: dict = {}):
|
||||
"""Make API call to internal services"""
|
||||
headers = {"Authorization": f"Bearer {SECRET_TOKEN}"}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if method == "GET":
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
if resp.status == 200:
|
||||
return await resp.json()
|
||||
return {"error": f"Status {resp.status}"}
|
||||
elif method == "POST":
|
||||
async with session.post(url, headers=headers, json=data) as resp:
|
||||
if resp.status == 200:
|
||||
return await resp.json()
|
||||
return {"error": f"Status {resp.status}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# Routes
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
password = request.form.get('password')
|
||||
if password == WEB_PASSWORD:
|
||||
session['logged_in'] = True
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
return render_template('login.html', error="Invalid password")
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.pop('logged_in', None)
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/services')
|
||||
@login_required
|
||||
def services():
|
||||
return render_template('services.html')
|
||||
|
||||
@app.route('/attacks')
|
||||
@login_required
|
||||
def attacks():
|
||||
return render_template('attacks.html')
|
||||
|
||||
@app.route('/alerts')
|
||||
@login_required
|
||||
def alerts():
|
||||
return render_template('alerts.html')
|
||||
|
||||
# API Endpoints
|
||||
@app.route('/api/dashboard')
|
||||
@login_required
|
||||
def api_dashboard():
|
||||
"""Get dashboard overview data"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
# Fetch data from all services
|
||||
services_data = loop.run_until_complete(api_call(f"{CONTROLLER_API}/services"))
|
||||
scoreboard_stats = loop.run_until_complete(api_call(f"{SCOREBOARD_API}/stats"))
|
||||
telegram_stats = loop.run_until_complete(api_call(f"{TELEGRAM_API}/stats"))
|
||||
|
||||
return jsonify({
|
||||
"services": services_data,
|
||||
"scoreboard": scoreboard_stats,
|
||||
"telegram": telegram_stats,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
})
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/services')
|
||||
@login_required
|
||||
def api_services():
|
||||
"""Get services list"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(api_call(f"{CONTROLLER_API}/services"))
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/services/<int:service_id>/action', methods=['POST'])
|
||||
@login_required
|
||||
def api_service_action(service_id):
|
||||
"""Perform action on service"""
|
||||
action = request.json.get('action')
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(
|
||||
api_call(f"{CONTROLLER_API}/services/{service_id}/action", "POST", {"action": action})
|
||||
)
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/services/<int:service_id>/logs')
|
||||
@login_required
|
||||
def api_service_logs(service_id):
|
||||
"""Get service logs"""
|
||||
lines = request.args.get('lines', 100)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(api_call(f"{CONTROLLER_API}/services/{service_id}/logs?lines={lines}"))
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/attacks')
|
||||
@login_required
|
||||
def api_attacks_list():
|
||||
"""Get attacks list"""
|
||||
limit = request.args.get('limit', 100)
|
||||
our_attacks = request.args.get('our_attacks')
|
||||
attacks_to_us = request.args.get('attacks_to_us')
|
||||
|
||||
url = f"{SCOREBOARD_API}/attacks?limit={limit}"
|
||||
if our_attacks is not None:
|
||||
url += f"&our_attacks={our_attacks}"
|
||||
if attacks_to_us is not None:
|
||||
url += f"&attacks_to_us={attacks_to_us}"
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(api_call(url))
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/attacks/by-service')
|
||||
@login_required
|
||||
def api_attacks_by_service():
|
||||
"""Get attacks grouped by service"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(api_call(f"{SCOREBOARD_API}/attacks/by-service"))
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/alerts')
|
||||
@login_required
|
||||
def api_alerts_list():
|
||||
"""Get alerts list"""
|
||||
limit = request.args.get('limit', 50)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(api_call(f"{SCOREBOARD_API}/alerts?limit={limit}"))
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@app.route('/api/telegram/send', methods=['POST'])
|
||||
@login_required
|
||||
def api_telegram_send():
|
||||
"""Send telegram message"""
|
||||
message = request.json.get('message')
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
data = loop.run_until_complete(
|
||||
api_call(f"{TELEGRAM_API}/send", "POST", {"message": message})
|
||||
)
|
||||
return jsonify(data)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, debug=False)
|
||||
5
web/requirements.txt
Normal file
5
web/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
flask==3.0.0
|
||||
asyncpg==0.29.0
|
||||
aiohttp==3.9.1
|
||||
python-dotenv==1.0.0
|
||||
gunicorn==21.2.0
|
||||
107
web/templates/alerts.html
Normal file
107
web/templates/alerts.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Alerts - A/D Infrastructure Control{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1>Security Alerts <i class="bi bi-arrow-clockwise refresh-btn" id="refreshAlerts"></i></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>Send Test Alert</h5>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="testMessage" placeholder="Enter test message">
|
||||
<button class="btn btn-primary" onclick="sendTestAlert()">
|
||||
<i class="bi bi-send"></i> Send to Telegram
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Alert History</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group" id="alertsList">
|
||||
<div class="text-center py-3">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function loadAlerts() {
|
||||
$.get('/api/alerts?limit=100', function (data) {
|
||||
if (data && data.length > 0) {
|
||||
let html = '';
|
||||
data.forEach(alert => {
|
||||
let badgeClass = alert.severity === 'critical' ? 'danger' : 'warning';
|
||||
let notifiedBadge = alert.notified
|
||||
? '<span class="badge bg-success">Notified</span>'
|
||||
: '<span class="badge bg-secondary">Pending</span>';
|
||||
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-${badgeClass}">${alert.severity.toUpperCase()}</span>
|
||||
${notifiedBadge}
|
||||
<span class="badge bg-info">${alert.alert_type}</span>
|
||||
<small class="text-muted ms-2">${new Date(alert.created_at).toLocaleString()}</small>
|
||||
</div>
|
||||
<p class="mb-0">${alert.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
$('#alertsList').html(html);
|
||||
} else {
|
||||
$('#alertsList').html('<div class="text-center py-3 text-muted">No alerts</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendTestAlert() {
|
||||
const message = $('#testMessage').val();
|
||||
if (!message) {
|
||||
alert('Please enter a message');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/telegram/send',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ message: message }),
|
||||
success: function (data) {
|
||||
alert('Message sent to Telegram!');
|
||||
$('#testMessage').val('');
|
||||
},
|
||||
error: function (xhr) {
|
||||
alert(`Failed to send message: ${xhr.responseJSON?.detail || 'Unknown error'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
loadAlerts();
|
||||
$('#refreshAlerts').click(loadAlerts);
|
||||
setInterval(loadAlerts, 10000); // Auto-refresh every 10 seconds
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
169
web/templates/attacks.html
Normal file
169
web/templates/attacks.html
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Attacks - A/D Infrastructure Control{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1>Attacks Monitor <i class="bi bi-arrow-clockwise refresh-btn" id="refreshAttacks"></i></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary active" id="filterAll">All Attacks</button>
|
||||
<button type="button" class="btn btn-outline-success" id="filterOur">Our Attacks</button>
|
||||
<button type="button" class="btn btn-outline-danger" id="filterAgainstUs">Attacks to Us</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Attacks by Service</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Total</th>
|
||||
<th>Our Attacks</th>
|
||||
<th>Against Us</th>
|
||||
<th>Points Gained</th>
|
||||
<th>Points Lost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="serviceStatsTable">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Recent Attacks</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Attacker</th>
|
||||
<th>Victim</th>
|
||||
<th>Service</th>
|
||||
<th>Points</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attacksTable">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentFilter = 'all';
|
||||
|
||||
function loadServiceStats() {
|
||||
$.get('/api/attacks/by-service', function (data) {
|
||||
if (data && data.length > 0) {
|
||||
let html = '';
|
||||
data.forEach(stat => {
|
||||
html += `
|
||||
<tr>
|
||||
<td><strong>${stat.service_name}</strong></td>
|
||||
<td>${stat.total_attacks}</td>
|
||||
<td><span class="text-success">${stat.our_attacks}</span></td>
|
||||
<td><span class="text-danger">${stat.attacks_to_us}</span></td>
|
||||
<td><span class="text-success">+${stat.points_gained.toFixed(2)}</span></td>
|
||||
<td><span class="text-danger">-${stat.points_lost.toFixed(2)}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#serviceStatsTable').html(html);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadAttacks() {
|
||||
let url = '/api/attacks?limit=100';
|
||||
if (currentFilter === 'our') {
|
||||
url += '&our_attacks=true';
|
||||
} else if (currentFilter === 'against') {
|
||||
url += '&attacks_to_us=true';
|
||||
}
|
||||
|
||||
$.get(url, function (data) {
|
||||
if (data && data.length > 0) {
|
||||
let html = '';
|
||||
data.forEach(attack => {
|
||||
let typeLabel = '';
|
||||
if (attack.is_our_attack) {
|
||||
typeLabel = '<span class="badge bg-success">Our Attack</span>';
|
||||
} else if (attack.is_attack_to_us) {
|
||||
typeLabel = '<span class="badge bg-danger">Against Us</span>';
|
||||
}
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${new Date(attack.timestamp).toLocaleString()}</td>
|
||||
<td>Team ${attack.attacker_team_id}</td>
|
||||
<td>Team ${attack.victim_team_id}</td>
|
||||
<td>${attack.service_name}</td>
|
||||
<td>${attack.points ? attack.points.toFixed(2) : '-'}</td>
|
||||
<td>${typeLabel}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#attacksTable').html(html);
|
||||
} else {
|
||||
$('#attacksTable').html('<tr><td colspan="6" class="text-center text-muted">No attacks</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setFilter(filter) {
|
||||
currentFilter = filter;
|
||||
$('.btn-group button').removeClass('active');
|
||||
$(`#filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`).addClass('active');
|
||||
loadAttacks();
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
loadServiceStats();
|
||||
loadAttacks();
|
||||
$('#refreshAttacks').click(function () {
|
||||
loadServiceStats();
|
||||
loadAttacks();
|
||||
});
|
||||
|
||||
$('#filterAll').click(() => setFilter('all'));
|
||||
$('#filterOur').click(() => setFilter('our'));
|
||||
$('#filterAgainstUs').click(() => setFilter('against'));
|
||||
|
||||
setInterval(loadAttacks, 5000); // Auto-refresh every 5 seconds
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
92
web/templates/base.html
Normal file
92
web/templates/base.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}A/D Infrastructure Control{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
|
||||
.stat-card.warning {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.service-running {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.service-stopped {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="bi bi-shield-check"></i> A/D Control
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/services"><i class="bi bi-hdd-stack"></i> Services</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/attacks"><i class="bi bi-bullseye"></i> Attacks</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/alerts"><i class="bi bi-exclamation-triangle"></i> Alerts</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/logout"><i class="bi bi-box-arrow-right"></i> Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
137
web/templates/index.html
Normal file
137
web/templates/index.html
Normal file
@@ -0,0 +1,137 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - A/D Infrastructure Control{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1>Dashboard <i class="bi bi-arrow-clockwise refresh-btn" id="refreshDashboard"></i></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Services</h6>
|
||||
<h2 id="totalServices">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card success">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Our Attacks</h6>
|
||||
<h2 id="ourAttacks">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card danger">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Attacks to Us</h6>
|
||||
<h2 id="attacksToUs">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card warning">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Critical Alerts</h6>
|
||||
<h2 id="criticalAlerts">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Services Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="servicesStatus" class="list-group">
|
||||
<div class="text-center py-3">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Recent Alerts</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="recentAlerts" class="list-group">
|
||||
<div class="text-center py-3">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function loadDashboard() {
|
||||
$.get('/api/dashboard', function (data) {
|
||||
// Update stats
|
||||
$('#totalServices').text(data.services ? data.services.length : 0);
|
||||
$('#ourAttacks').text(data.scoreboard?.attacks_by_us || 0);
|
||||
$('#attacksToUs').text(data.scoreboard?.attacks_to_us || 0);
|
||||
$('#criticalAlerts').text(data.scoreboard?.critical_alerts_5min || 0);
|
||||
|
||||
// Update services status
|
||||
if (data.services && data.services.length > 0) {
|
||||
let html = '';
|
||||
data.services.forEach(service => {
|
||||
let statusClass = service.status === 'running' ? 'service-running' : 'service-stopped';
|
||||
let icon = service.status === 'running' ? 'check-circle-fill' : 'x-circle-fill';
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>${service.name}</span>
|
||||
<span class="${statusClass}">
|
||||
<i class="bi bi-${icon}"></i> ${service.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
$('#servicesStatus').html(html);
|
||||
}
|
||||
});
|
||||
|
||||
// Load recent alerts
|
||||
$.get('/api/alerts?limit=5', function (data) {
|
||||
if (data && data.length > 0) {
|
||||
let html = '';
|
||||
data.forEach(alert => {
|
||||
let badgeClass = alert.severity === 'critical' ? 'danger' : 'warning';
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="badge bg-${badgeClass}">${alert.severity}</span>
|
||||
<small class="text-muted ms-2">${new Date(alert.created_at).toLocaleString()}</small>
|
||||
<p class="mb-0 mt-1">${alert.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
$('#recentAlerts').html(html);
|
||||
} else {
|
||||
$('#recentAlerts').html('<div class="text-center py-3 text-muted">No alerts</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
loadDashboard();
|
||||
$('#refreshDashboard').click(loadDashboard);
|
||||
setInterval(loadDashboard, 10000); // Auto-refresh every 10 seconds
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
47
web/templates/login.html
Normal file
47
web/templates/login.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - A/D Infrastructure Control</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="text-center mb-4">
|
||||
<i class="bi bi-shield-check"></i> A/D Control
|
||||
</h2>
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required autofocus>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
130
web/templates/services.html
Normal file
130
web/templates/services.html
Normal file
@@ -0,0 +1,130 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Services - A/D Infrastructure Control{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1>Services <i class="bi bi-arrow-clockwise refresh-btn" id="refreshServices"></i></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Path</th>
|
||||
<th>Status</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="servicesTable">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Modal -->
|
||||
<div class="modal fade" id="logsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Service Logs</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="logsContent"
|
||||
style="max-height: 500px; overflow-y: auto; background: #f8f9fa; padding: 15px;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function loadServices() {
|
||||
$.get('/api/services', function (data) {
|
||||
if (data && data.length > 0) {
|
||||
let html = '';
|
||||
data.forEach(service => {
|
||||
let statusBadge = service.status === 'running'
|
||||
? '<span class="badge bg-success">Running</span>'
|
||||
: '<span class="badge bg-secondary">Stopped</span>';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td><strong>${service.name}</strong></td>
|
||||
<td><code>${service.path}</code></td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${new Date(service.last_updated).toLocaleString()}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-success" onclick="serviceAction(${service.id}, 'start')">
|
||||
<i class="bi bi-play-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-warning" onclick="serviceAction(${service.id}, 'restart')">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="serviceAction(${service.id}, 'stop')">
|
||||
<i class="bi bi-stop-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-info" onclick="viewLogs(${service.id})">
|
||||
<i class="bi bi-file-text"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#servicesTable').html(html);
|
||||
} else {
|
||||
$('#servicesTable').html('<tr><td colspan="5" class="text-center text-muted">No services registered</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function serviceAction(serviceId, action) {
|
||||
if (!confirm(`Are you sure you want to ${action} this service?`)) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/api/services/${serviceId}/action`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ action: action }),
|
||||
success: function (data) {
|
||||
alert(`Service ${action} successful!`);
|
||||
loadServices();
|
||||
},
|
||||
error: function (xhr) {
|
||||
alert(`Failed to ${action} service: ${xhr.responseJSON?.detail || 'Unknown error'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function viewLogs(serviceId) {
|
||||
$.get(`/api/services/${serviceId}/logs?lines=200`, function (data) {
|
||||
$('#logsContent').text(data.logs || 'No logs available');
|
||||
new bootstrap.Modal(document.getElementById('logsModal')).show();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
loadServices();
|
||||
$('#refreshServices').click(loadServices);
|
||||
setInterval(loadServices, 15000); // Auto-refresh every 15 seconds
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user