This commit is contained in:
ilyastar9999
2025-12-02 14:01:34 +03:00
parent 96e1e5a7e0
commit cffbd77b74
31 changed files with 3335 additions and 0 deletions

10
web/Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 %}