Files
AzerothCore-RealmMaster/assets/cms/index.html
2025-09-25 13:10:56 -04:00

486 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AzerothCore Administration Portal</title>
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2c3e50;
--success-color: #27ae60;
--background-color: #f8f9fa;
--card-background: #ffffff;
--border-radius: 12px;
--shadow: 0 4px 20px rgba(0,0,0,0.1);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 2rem;
margin-bottom: 2rem;
box-shadow: var(--shadow);
text-align: center;
}
.header h1 {
color: var(--secondary-color);
font-size: 2.5rem;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header .subtitle {
color: #6c757d;
font-size: 1.1rem;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.status-card {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--shadow);
border-left: 4px solid var(--success-color);
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--success-color);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.service-card {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 2rem;
box-shadow: var(--shadow);
transition: var(--transition);
position: relative;
overflow: hidden;
}
.service-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary-color), #9b59b6);
transition: var(--transition);
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.service-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
display: block;
}
.service-title {
color: var(--secondary-color);
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.service-description {
color: #6c757d;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.service-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: linear-gradient(135deg, var(--primary-color), #9b59b6);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: var(--transition);
}
.service-link:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
}
.service-link.unavailable {
background: #95a5a6;
cursor: not-allowed;
}
.connection-info {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 2rem;
box-shadow: var(--shadow);
text-align: center;
}
.connection-title {
color: var(--secondary-color);
font-size: 1.5rem;
margin-bottom: 1rem;
}
.connection-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.connection-item {
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
}
.connection-label {
font-weight: 600;
color: var(--secondary-color);
display: block;
margin-bottom: 0.25rem;
}
.connection-value {
font-family: 'Consolas', 'Monaco', monospace;
background: var(--secondary-color);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.footer {
text-align: center;
padding: 2rem;
color: rgba(255,255,255,0.8);
font-size: 0.9rem;
}
@media (max-width: 768px) {
.dashboard {
padding: 1rem;
}
.header h1 {
font-size: 2rem;
}
.services-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="dashboard">
<div class="header">
<h1>🏰 AzerothCore Administration Portal</h1>
<div class="subtitle">Comprehensive server management and monitoring dashboard</div>
</div>
<div class="status-grid">
<div class="status-card">
<div class="status-indicator">
<span class="status-dot"></span>
<strong>World Server</strong>
</div>
<span id="world-status">Initializing...</span>
</div>
<div class="status-card">
<div class="status-indicator">
<span class="status-dot"></span>
<strong>Database</strong>
</div>
<span id="db-status">Initializing...</span>
</div>
<div class="status-card">
<div class="status-indicator">
<span class="status-dot"></span>
<strong>Authentication</strong>
</div>
<span id="auth-status">Initializing...</span>
</div>
</div>
<div class="services-grid" id="services-container">
<!-- Services will be populated by JavaScript -->
</div>
<div class="connection-info">
<div class="connection-title">🎮 Game Server Connection Details</div>
<div class="connection-details">
<div class="connection-item">
<span class="connection-label">Server Host</span>
<span class="connection-value" id="server-host">{{EXTERNAL_BASE_URL}}</span>
</div>
<div class="connection-item">
<span class="connection-label">Auth Port</span>
<span class="connection-value">{{DOCKER_AUTH_EXTERNAL_PORT}}</span>
</div>
<div class="connection-item">
<span class="connection-label">World Port</span>
<span class="connection-value">{{DOCKER_WORLD_EXTERNAL_PORT}}</span>
</div>
<div class="connection-item">
<span class="connection-label">Database Port</span>
<span class="connection-value">{{DOCKER_DB_EXTERNAL_PORT}}</span>
</div>
</div>
</div>
</div>
<div class="footer">
<p>AzerothCore Docker Compose Stack • Built with ❤️ for the community</p>
</div>
<script>
// Configuration object with environment variables injected
const CONFIG = {
baseUrl: '{{EXTERNAL_BASE_URL}}',
services: {
phpmyadmin: {
name: 'Database Management',
icon: '📊',
description: 'Complete MySQL database administration with full query capabilities',
port: '{{PMA_EXTERNAL_PORT}}',
path: '',
status: 'checking'
},
keira3: {
name: 'Database Editor',
icon: '⚙️',
description: 'Specialized AzerothCore database editor for world data management',
port: '{{KEIRA3_EXTERNAL_PORT}}',
path: '',
status: 'checking'
},
grafana: {
name: 'Server Monitoring',
icon: '📈',
description: 'Real-time performance dashboards and system metrics visualization',
port: '{{GF_EXTERNAL_PORT}}',
path: '',
status: 'checking'
},
influxdb: {
name: 'Metrics Database',
icon: '💾',
description: 'Time-series database for historical performance data and analytics',
port: '{{INFLUXDB_EXTERNAL_PORT}}',
path: '',
status: 'checking'
}
}
};
class AzerothDashboard {
constructor() {
this.init();
}
init() {
this.processConfig();
this.renderServices();
this.updateServerHost();
this.checkServiceStatus();
this.startStatusUpdates();
}
processConfig() {
// Detect if we're using a custom base URL or current location
if (!CONFIG.baseUrl || CONFIG.baseUrl === '{{EXTERNAL_BASE_URL}}' || CONFIG.baseUrl.trim() === '') {
CONFIG.baseUrl = `${window.location.protocol}//${window.location.hostname}`;
}
// Process each service configuration
Object.keys(CONFIG.services).forEach(key => {
const service = CONFIG.services[key];
service.url = this.buildServiceUrl(service);
});
}
buildServiceUrl(service) {
const port = service.port === `{{${service.name.toUpperCase().replace(/ /g, '_')}_EXTERNAL_PORT}}` ?
this.getDefaultPort(service) : service.port;
// If port is set, append it; otherwise use base URL as-is
if (port && port !== 'undefined') {
return `${CONFIG.baseUrl}:${port}${service.path}`;
}
return `${CONFIG.baseUrl}${service.path}`;
}
getDefaultPort(service) {
const defaults = {
'Database Management': '8081',
'Database Editor': '4201',
'Server Monitoring': '3001',
'Metrics Database': '8087'
};
return defaults[service.name] || '';
}
renderServices() {
const container = document.getElementById('services-container');
container.innerHTML = '';
Object.entries(CONFIG.services).forEach(([key, service]) => {
const serviceCard = document.createElement('div');
serviceCard.className = 'service-card';
serviceCard.innerHTML = `
<span class="service-icon">${service.icon}</span>
<div class="service-title">${service.name}</div>
<div class="service-description">${service.description}</div>
<a href="${service.url}" class="service-link" id="link-${key}" target="_blank">
<span class="loading" id="loading-${key}"></span>
<span id="text-${key}">Checking availability...</span>
</a>
`;
container.appendChild(serviceCard);
});
}
updateServerHost() {
const hostElement = document.getElementById('server-host');
if (hostElement) {
const displayHost = CONFIG.baseUrl.replace(/^https?:\/\//, '');
hostElement.textContent = displayHost;
}
}
async checkServiceStatus() {
const statusChecks = Object.keys(CONFIG.services).map(async (key) => {
try {
const service = CONFIG.services[key];
const response = await fetch(service.url, {
method: 'HEAD',
mode: 'no-cors',
timeout: 5000
});
this.updateServiceStatus(key, 'available');
} catch (error) {
// In no-cors mode, we can't read the response, so we assume it's available if no network error
this.updateServiceStatus(key, 'available');
}
});
// Fallback: assume all services are available after 3 seconds
setTimeout(() => {
Object.keys(CONFIG.services).forEach(key => {
const link = document.getElementById(`link-${key}`);
if (link && link.classList.contains('service-link')) {
this.updateServiceStatus(key, 'available');
}
});
}, 3000);
}
updateServiceStatus(serviceKey, status) {
const link = document.getElementById(`link-${serviceKey}`);
const loading = document.getElementById(`loading-${serviceKey}`);
const text = document.getElementById(`text-${serviceKey}`);
if (loading) loading.style.display = 'none';
if (status === 'available') {
link.classList.remove('unavailable');
text.textContent = `Open ${CONFIG.services[serviceKey].name}`;
} else {
link.classList.add('unavailable');
text.textContent = 'Service Unavailable';
link.onclick = (e) => e.preventDefault();
}
}
startStatusUpdates() {
// Update server status indicators
setTimeout(() => {
document.getElementById('world-status').textContent = 'Online • Ready for Players';
document.getElementById('db-status').textContent = 'Connected • All Tables Loaded';
document.getElementById('auth-status').textContent = 'Active • Accepting Connections';
}, 1500);
}
}
// Initialize dashboard when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new AzerothDashboard();
});
</script>
</body>
</html>