Files
AthonetTools/templates/layout.html

329 lines
14 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale-1">
<title>{% block title %}Core Network Tool{% endblock %}</title>
<link rel="icon" href="/static/images/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { min-height: 100vh; }
main { display: flex; flex-wrap: nowrap; height: 100vh; max-height: 100vh; overflow-x: auto; overflow-y: hidden; }
.b-example-vr { flex-shrink: 0; width: 1.5rem; height: 100vh; background-color: rgba(0, 0, 0, .1); border: solid rgba(0, 0, 0, .15); border-width: 1px 0; }
#results-output { background-color: #212529; color: #f8f9fa; border-radius: .25rem; min-height: 300px; font-family: monospace; }
.table-hover tbody tr:hover {
cursor: pointer;
}
.table-compact td,
.table-compact th {
padding-top: 0.2rem;
padding-bottom: 0.2rem;
}
.accordion-button::after {
filter: invert(1) grayscale(100%);
}
.network-card { cursor: pointer; }
.network-card:hover { border-color: #0d6efd !important; }
.nav-pills .nav-link:not(.active) {
color: white;
}
.sidebar-logo {
height: 40px;
width: auto;
transition: transform 0.2s ease-in-out;
}
.sidebar-logo:hover { transform: scale(1.05); }
.menu-header {
padding: .5rem 1rem .25rem;
font-size: .875rem;
color: #6c757d;
font-weight: 500;
}
input#host::placeholder {
color: #6c757d;
opacity: 1;
}
.tree-item {
cursor: pointer;
}
.tree-item:hover { background-color: #495057; }
.tree-item .bi {
transition: transform 0.2s;
}
.tree-item.open > .bi-chevron-right { transform: rotate(90deg); }
/* Scrollable sidebar */
.sidebar-scroll {
max-height: 100vh; /* full viewport height */
overflow-y: auto; /* vertical scroll when needed */
-webkit-overflow-scrolling: touch;
}
@media (min-width: 992px) {
.sidebar-scroll {
position: sticky;
top: 0; /* adjust if navbar is fixed */
}
}
{% block extra_styles %}{% endblock %}
</style>
</head>
<body>
<main>
<div class="d-flex flex-column flex-shrink-0 p-3 text-bg-dark vh-100" style="width: 280px;">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<img src="/static/images/hpe_logo_animated.gif" alt="HPE Logo" class="sidebar-logo">
</a>
<hr>
{% set active_page = active_page|default('vpn_status') %}
<div class="flex-grow-1 overflow-auto" style="min-height:0">
<ul class="nav nav-pills flex-column mb-auto" id="menu">
<li class="menu-header">Dashboard Functions</li>
<li class="nav-item">
<a href="{{ url_for('vpn_status_page') }}" class="nav-link {% if active_page == 'vpn_status' %}active{% endif %}">
<i class="bi bi-shield-lock-fill me-2"></i> m2000 Status
</a>
</li>
<li>
<a href="{{ url_for('network_config_page') }}" class="nav-link {% if active_page == 'network_config' %}active{% endif %}">
<i class="bi bi-hdd-network-fill me-2"></i> Network Config
</a>
</li>
<li>
<a href="{{ url_for('m2000_password_page') }}" class="nav-link {% if active_page == 'm2000_password' %}active{% endif %}">
<i class="bi bi-asterisk me-2"></i> m2000 Password
</a>
</li>
<li>
<a href="{{ url_for('tenants_page') }}" class="nav-link {% if active_page == 'tenants' %}active{% endif %}">
<i class="bi bi-building-fill me-2"></i> Dashboard Tenant
</a>
</li>
<li>
<a href="{{ url_for('users_page') }}" class="nav-link {% if active_page == 'users' %}active{% endif %}">
<i class="bi bi-people-fill me-2"></i> Dashboard Users
</a>
</li>
<li>
<a href="{{ url_for('m2000_reset_page') }}" class="nav-link {% if active_page == 'm2000_reset' %}active{% endif %}">
<i class="bi bi-arrow-counterclockwise me-2"></i> m2000 Config Reset
</a>
</li>
<li><hr class="my-2"></li>
<li class="menu-header">ComcoCore Functions</li>
<li>
<a href="{{ url_for('system_browser_page') }}" class="nav-link {% if active_page == 'system_browser' %}active{% endif %}">
<i class="bi bi-diagram-3-fill me-2"></i> System Browser
</a>
</li>
<li>
<a href="{{ url_for('vpn_switcher_page') }}" class="nav-link {% if active_page == 'vpn_switcher' %}active{% endif %}">
<i class="bi bi-arrow-repeat me-2"></i> VPN Switcher
</a>
</li>
<li>
<a href="{{ url_for('hnk_page') }}" class="nav-link {% if active_page == 'hnk' %}active{% endif %}">
<i class="bi bi-key-fill me-2"></i> Home Network Keys
</a>
</li>
<li>
<a href="{{ url_for('network_clients_page') }}" class="nav-link {% if active_page == 'network_clients' %}active{% endif %}">
<i class="bi bi-person-fill-gear me-2"></i> Network Clients
</a>
</li>
<li>
<a href="{{ url_for('gaf_desk_page') }}" class="nav-link {% if active_page == 'gaf_desk' %}active{% endif %}">
<i class="bi bi-layout-wtf me-2"></i> GAF Desk
</a>
</li>
</ul>
</div>
<hr>
<div id="vpn-controls">
<h6 class="text-white">Dashboard VPNs</h6>
<ul class="list-unstyled">
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-triton" data-vpn-name="Triton">
<label class="form-check-label" for="vpn-triton">Triton</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-star" data-vpn-name="Star">
<label class="form-check-label" for="vpn-star">Star</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-bluebonnet" data-vpn-name="Bluebonnet">
<label class="form-check-label" for="vpn-bluebonnet">Bluebonnet</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-lonestar" data-vpn-name="Lonestar">
<label class="form-check-label" for="vpn-lonestar">Lonestar</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-production" data-vpn-name="Production">
<label class="form-check-label" for="vpn-production">Production</label>
</div>
</li>
</ul>
<hr class="my-2">
<h6 class="text-white">HPE P5G Support VPNs</h6>
<ul class="list-unstyled">
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-us-support" data-vpn-name="US-Support">
<label class="form-check-label" for="vpn-us-support">US Support</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-eu-support" data-vpn-name="EU-Support">
<label class="form-check-label" for="vpn-eu-support">EU Support</label>
</div>
</li>
</ul>
</div>
</div>
<div class="b-example-vr"></div>
<div class="flex-grow-1 p-4 overflow-y-auto">
{% block content %}{% endblock %}
</div>
</main>
<div class="modal fade" id="networkDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="networkDetailModalLabel">Network Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<pre id="modalJsonOutput"></pre>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<script defer>
const resultsOutput = document.getElementById('results-output');
const spinner = document.getElementById('spinner');
const networkDetailModal = document.getElementById('networkDetailModal');
// ADDED: Reusable function to initialize tooltips
function initializeTooltips() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
}
document.addEventListener('DOMContentLoaded', () => {
initializeTooltips();
checkVpnStatus();
});
function formatHostIp(ipString) {
if (!ipString) return '';
if (ipString.includes(':') && !ipString.startsWith('[') && !ipString.endsWith(']')) {
return `[${ipString}]`;
}
return ipString;
}
async function apiCall(endpoint, body, clearResults = true) {
spinner.classList.remove('d-none');
if (clearResults) {
resultsOutput.innerHTML = '';
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) {
if (result.error === 'Network Unreachable') throw new Error(result.message);
throw new Error(result.error || 'Unknown server error');
}
return result;
} catch (error) {
alert(`Error: ${error.message}`);
return null;
} finally {
spinner.classList.add('d-none');
}
}
function getStatusClass(status) {
switch (status) {
case 'DEPLOYED': case 'up': return 'bg-success';
case 'ALL_HW_NOT_ONLINE': return 'bg-danger';
case 'TO_DEPLOY': return 'bg-warning text-dark';
default: return 'bg-secondary';
}
}
networkDetailModal.addEventListener('show.bs.modal', event => {
const card = event.relatedTarget;
const nodeName = card.getAttribute('data-node-name');
const nodeDetails = JSON.parse(card.getAttribute('data-node-details'));
const modalTitle = networkDetailModal.querySelector('.modal-title');
const modalBody = networkDetailModal.querySelector('#modalJsonOutput');
modalTitle.textContent = `Details for: ${nodeName}`;
modalBody.textContent = JSON.stringify(nodeDetails, null, 2);
});
const vpnToggles = document.querySelectorAll('.vpn-toggle');
function updateVpnTogglesUI(activeVpn) {
vpnToggles.forEach(toggle => {
toggle.checked = (toggle.dataset.vpnName === activeVpn);
toggle.disabled = false;
});
}
async function checkVpnStatus() {
try {
const response = await fetch('/api/vpn/status');
const data = await response.json();
if (response.ok) updateVpnTogglesUI(data.active_vpn);
} catch (error) {
console.error("Failed to fetch VPN status:", error);
}
}
vpnToggles.forEach(toggle => {
toggle.addEventListener('change', async (event) => {
const vpnName = event.target.dataset.vpnName;
const turnOn = event.target.checked;
vpnToggles.forEach(t => t.disabled = true);
try {
const response = await fetch('/api/vpn/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vpn_name: vpnName, state: turnOn })
});
const data = await response.json();
if (!response.ok) {
alert(`Error: ${data.error}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
} finally {
checkVpnStatus();
}
});
});
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>