Add Version and System Info tab in admin dropdown menu
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m55s

This commit is contained in:
2026-10-11 09:51:06 +02:00
parent 72d9646cc4
commit 6b281626da
2 changed files with 239 additions and 4 deletions
+199 -4
View File
@@ -8,6 +8,8 @@ import html
import json import json
import socket import socket
import sqlite3 import sqlite3
import sys
import platform
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
@@ -1106,6 +1108,7 @@ class WebUIRenderer:
</div> </div>
<div class="user-dropdown" id="userDropdown"> <div class="user-dropdown" id="userDropdown">
<div class="user-dropdown-header">Logged in as {auth_user}</div> <div class="user-dropdown-header">Logged in as {auth_user}</div>
<button class="user-dropdown-item" onclick="openVersionInfoModal(); hideUserDropdown();">ℹ️ Version &amp; System Info</button>
<button class="user-dropdown-item" onclick="openTwoFactorModal(); hideUserDropdown();">🛡️ Two-Factor Auth (2FA)</button> <button class="user-dropdown-item" onclick="openTwoFactorModal(); hideUserDropdown();">🛡️ Two-Factor Auth (2FA)</button>
<button class="user-dropdown-item" onclick="openPasswordModal(); hideUserDropdown();">🔑 Change Password</button> <button class="user-dropdown-item" onclick="openPasswordModal(); hideUserDropdown();">🔑 Change Password</button>
<a href="/logout" class="user-dropdown-item danger">🚪 Logout</a> <a href="/logout" class="user-dropdown-item danger">🚪 Logout</a>
@@ -1397,6 +1400,69 @@ class WebUIRenderer:
</div> </div>
</div> </div>
<!-- Version & System Info Modal -->
<div id="versionInfoModal" class="modal">
<div class="modal-content" style="max-width: 580px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 14px;">
<div class="modal-title" style="margin-bottom: 0; display: flex; align-items: center; gap: 8px;">
<span>ℹ️</span> LOGAR Version &amp; Telemetry Info
</div>
<div style="cursor: pointer; font-size: 22px; color: var(--text-muted); line-height: 1;" onclick="closeModal('versionInfoModal')">&times;</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;">
<div style="background: #0b0f19; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px;">Server Hub Release</div>
<div style="font-size: 22px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: #60a5fa;">v{LOGAR_VERSION}</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">LOGAR Central Ingestion Engine</div>
</div>
<div style="background: #0b0f19; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px;">Hosted Client Binaries</div>
<div style="font-size: 22px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: #34d399;">v{hosted_client_ver}</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Edge forwarder distribution release</div>
</div>
</div>
<div style="background: #0b0f19; border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 18px; font-size: 12px; line-height: 1.8;">
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Repository Update Mode:</span>
<strong style="color: #fff;">{update_mode.upper()}</strong>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Distribution Source:</span>
<span style="color: #38bdf8; font-family: 'JetBrains Mono', monospace; font-size: 11px; word-break: break-all;">{html.escape(str(upstream_url))}</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Python Environment:</span>
<span style="color: #fff; font-family: 'JetBrains Mono', monospace; font-size: 11px;">Python {sys.version.split()[0]} ({platform.architecture()[0]})</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Host Platform &amp; OS:</span>
<span style="color: #fff;">{html.escape(platform.platform())}</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Cryptographic Transport:</span>
<span style="color: #34d399; font-weight: 600;">TLS 1.3 mTLS Encrypted</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Storage Backend Engine:</span>
<span style="color: #fff;">{config.get('storage_engine', 'sqlite').upper()} ({config.get('db_path', 'logar_state.db')})</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-muted);">License Status &amp; Seats:</span>
<span style="color: #c084fc; font-weight: 600;">{summary['status']} ({summary['active_seats']}/{summary['max_seats']} Seats)</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
<button onclick="closeModal('versionInfoModal'); switchTab('updates');" class="btn btn-outline" style="font-size: 12px;">
📦 Updates &amp; Distribution Tab
</button>
<button onclick="closeModal('versionInfoModal')" class="btn btn-primary">Close</button>
</div>
</div>
</div>
<!-- Password Modal --> <!-- Password Modal -->
<div id="passwordModal" class="modal"> <div id="passwordModal" class="modal">
<div class="modal-content" style="max-width: 440px;"> <div class="modal-content" style="max-width: 440px;">
@@ -1546,6 +1612,7 @@ class WebUIRenderer:
function openExportProfileModal() {{ openModal('exportProfileModal'); }} function openExportProfileModal() {{ openModal('exportProfileModal'); }}
function openImportLicModal() {{ openModal('importLicModal'); }} function openImportLicModal() {{ openModal('importLicModal'); }}
function openPasswordModal() {{ openModal('passwordModal'); }} function openPasswordModal() {{ openModal('passwordModal'); }}
function openVersionInfoModal() {{ openModal('versionInfoModal'); }}
function downloadLicRequest() {{ function downloadLicRequest() {{
window.location.href = '/api/license/export-lic'; window.location.href = '/api/license/export-lic';
@@ -2241,8 +2308,13 @@ class WebUIRenderer:
db_path = config.get("db_path", "logar_state.db") db_path = config.get("db_path", "logar_state.db")
auth_display = html.escape(str(auth_user or "admin")) auth_display = html.escape(str(auth_user or "admin"))
# Check Best Practice licensing feature # Check Best Practice licensing feature & summary
is_bp_licensed = LicenseManager.is_feature_licensed("best_practices", db_path=db_path, server_state=state) is_bp_licensed = LicenseManager.is_feature_licensed("best_practices", db_path=db_path, server_state=state)
summary = LicenseManager.get_license_summary(state, db_path=db_path)
update_status = state.get("update_status", {})
update_mode = update_status.get("update_mode") or config.get("update_mode", "official")
upstream_url = update_status.get("upstream_url") or config.get("upstream_url", "https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest")
hosted_client_ver = state.get("hosted_client_version") or LOGAR_VERSION
# Query clients and host telemetry # Query clients and host telemetry
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
@@ -2661,6 +2733,34 @@ class WebUIRenderer:
.logo-title {{ font-size: 20px; font-weight: 800; letter-spacing: -0.5px; color: #fff; }} .logo-title {{ font-size: 20px; font-weight: 800; letter-spacing: -0.5px; color: #fff; }}
.logo-sub {{ font-size: 12px; color: var(--text-muted); margin-top: 2px; }} .logo-sub {{ font-size: 12px; color: var(--text-muted); margin-top: 2px; }}
/* User Dropdown */
.user-menu-container {{ position: relative; display: inline-block; }}
.user-tag {{
display: inline-flex; align-items: center; gap: 8px;
padding: 8px 14px; background: #0f172a;
border: 1px solid var(--border); border-radius: 8px;
font-size: 13px; font-weight: 600; cursor: pointer;
user-select: none;
}}
.user-tag:hover {{ border-color: var(--primary); background: #151c2e; }}
.user-dropdown {{
display: none; position: absolute; right: 0; top: calc(100% + 6px);
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
min-width: 220px; z-index: 200; overflow: hidden;
}}
.user-menu-container:hover .user-dropdown, .user-menu-container:focus-within .user-dropdown, .user-dropdown.show {{ display: block; }}
.user-dropdown-header {{ padding: 10px 14px; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); background: #0f172a; border-bottom: 1px solid var(--border); }}
.user-dropdown-item {{
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
font-size: 13px; color: var(--text); text-decoration: none; cursor: pointer;
border: none; width: 100%; background: transparent; text-align: left;
transition: background 0.2s;
}}
.user-dropdown-item:hover {{ background: rgba(59, 130, 246, 0.1); color: #fff; }}
.user-dropdown-item.danger {{ color: #f87171; }}
.user-dropdown-item.danger:hover {{ background: rgba(239, 68, 68, 0.15); }}
/* Buttons & Badges */ /* Buttons & Badges */
.btn {{ .btn {{
display: inline-flex; align-items: center; gap: 6px; display: inline-flex; align-items: center; gap: 6px;
@@ -2777,9 +2877,17 @@ class WebUIRenderer:
<a href="/webui#pki" class="btn btn-outline" style="font-size: 13px;"> <a href="/webui#pki" class="btn btn-outline" style="font-size: 13px;">
📜 Sub-CA &amp; Licensing 📜 Sub-CA &amp; Licensing
</a> </a>
<span style="font-size: 12px; color: var(--text-muted); background: #0f172a; padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border);"> <div class="user-menu-container" tabindex="0">
👤 {auth_display} <div class="user-tag" onclick="toggleUserDropdown(event)">
</span> <span>👤 <strong>{auth_display}</strong></span>
<span style="font-size: 10px; color: var(--text-muted);">▼</span>
</div>
<div class="user-dropdown" id="userDropdown">
<div class="user-dropdown-header">Logged in as {auth_display}</div>
<button class="user-dropdown-item" onclick="openVersionInfoModal(); hideUserDropdown();">ℹ️ Version &amp; System Info</button>
<a href="/logout" class="user-dropdown-item danger">🚪 Logout</a>
</div>
</div>
</div> </div>
</header> </header>
@@ -2900,7 +3008,94 @@ class WebUIRenderer:
</div> </div>
</div> </div>
<!-- Version & System Telemetry Info Modal -->
<div id="versionInfoModal" class="modal-overlay" onclick="closeVersionInfoModal(event)">
<div class="modal" onclick="event.stopPropagation()" style="max-width: 560px;">
<div class="modal-header">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 18px;">ℹ️</span>
<h4 style="font-size: 15px; font-weight: 700; color: #fff;">LOGAR Version &amp; System Telemetry</h4>
</div>
<button onclick="closeVersionInfoModal()" class="btn btn-outline" style="padding: 4px 8px; font-size: 12px;">✕</button>
</div>
<div class="modal-body">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;">
<div style="background: #0b0f19; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px;">Server Hub Release</div>
<div style="font-size: 22px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: #60a5fa;">v{LOGAR_VERSION}</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">LOGAR Central Ingestion Engine</div>
</div>
<div style="background: #0b0f19; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px;">Hosted Client Binaries</div>
<div style="font-size: 22px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: #34d399;">v{hosted_client_ver}</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Edge forwarder distribution release</div>
</div>
</div>
<div style="background: #0b0f19; border: 1px solid var(--border); border-radius: 8px; padding: 14px; font-size: 12px; line-height: 1.8;">
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Repository Update Mode:</span>
<strong style="color: #fff;">{update_mode.upper()}</strong>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Distribution Source:</span>
<span style="color: #38bdf8; font-family: 'JetBrains Mono', monospace; font-size: 11px; word-break: break-all;">{html.escape(str(upstream_url))}</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Python Environment:</span>
<span style="color: #fff; font-family: 'JetBrains Mono', monospace; font-size: 11px;">Python {sys.version.split()[0]} ({platform.architecture()[0]})</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Host Platform &amp; OS:</span>
<span style="color: #fff;">{html.escape(platform.platform())}</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Cryptographic Transport:</span>
<span style="color: #34d399; font-weight: 600;">TLS 1.3 mTLS Encrypted</span>
</div>
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 6px;">
<span style="color: var(--text-muted);">Storage Backend Engine:</span>
<span style="color: #fff;">{config.get('storage_engine', 'sqlite').upper()} ({config.get('db_path', 'logar_state.db')})</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-muted);">License Status &amp; Seats:</span>
<span style="color: #c084fc; font-weight: 600;">{summary['status']} ({summary['active_seats']}/{summary['max_seats']} Seats)</span>
</div>
</div>
</div>
<div class="modal-footer" style="display: flex; justify-content: space-between; align-items: center;">
<a href="/webui#updates" class="btn btn-outline" style="font-size: 12px; text-decoration: none;">
📦 Updates &amp; Distribution Tab
</a>
<button onclick="closeVersionInfoModal()" class="btn btn-primary">Close</button>
</div>
</div>
</div>
<script> <script>
function toggleUserDropdown(e) {{
if (e) e.stopPropagation();
const d = document.getElementById('userDropdown');
if (d) d.classList.toggle('show');
}}
function hideUserDropdown() {{
const d = document.getElementById('userDropdown');
if (d) d.classList.remove('show');
}}
window.addEventListener('click', () => hideUserDropdown());
function openVersionInfoModal() {{
const m = document.getElementById('versionInfoModal');
if (m) m.style.display = 'flex';
}}
function closeVersionInfoModal(e) {{
const m = document.getElementById('versionInfoModal');
if (m) m.style.display = 'none';
}}
let currentFilter = 'all'; let currentFilter = 'all';
function setFilter(category, btn) {{ function setFilter(category, btn) {{
@@ -196,6 +196,46 @@ class TestServerRolesAndBestPractices(unittest.TestCase):
self.assertIn("warn_count", eval_data) self.assertIn("warn_count", eval_data)
self.assertIn("fail_count", eval_data) self.assertIn("fail_count", eval_data)
def test_admin_version_info_modal(self):
"""Verifies admin dropdown menu contains Version & System Info option and renders telemetry modal in both /webui and /client-info."""
from src.version import LOGAR_VERSION
# Check /webui
res_webui = self.client.get("/webui", cookies={SessionManager.SESSION_COOKIE_NAME: self.session_token})
self.assertEqual(res_webui.status_code, 200)
self.assertIn("id=\"userDropdown\"", res_webui.text)
self.assertIn("Version &amp; System Info", res_webui.text)
self.assertIn("openVersionInfoModal()", res_webui.text)
self.assertIn("id=\"versionInfoModal\"", res_webui.text)
self.assertIn(f"v{LOGAR_VERSION}", res_webui.text)
self.assertIn("Server Hub Release", res_webui.text)
self.assertIn("Hosted Client Binaries", res_webui.text)
self.assertIn("Repository Update Mode:", res_webui.text)
self.assertIn("Distribution Source:", res_webui.text)
self.assertIn("Python Environment:", res_webui.text)
self.assertIn("Host Platform &amp; OS:", res_webui.text)
self.assertIn("TLS 1.3 mTLS Encrypted", res_webui.text)
self.assertIn("Storage Backend Engine:", res_webui.text)
self.assertIn("License Status &amp; Seats:", res_webui.text)
# Check /client-info
res_client_info = self.client.get("/client-info", cookies={SessionManager.SESSION_COOKIE_NAME: self.session_token})
self.assertEqual(res_client_info.status_code, 200)
self.assertIn("id=\"userDropdown\"", res_client_info.text)
self.assertIn("Version &amp; System Info", res_client_info.text)
self.assertIn("openVersionInfoModal()", res_client_info.text)
self.assertIn("id=\"versionInfoModal\"", res_client_info.text)
self.assertIn(f"v{LOGAR_VERSION}", res_client_info.text)
self.assertIn("Server Hub Release", res_client_info.text)
self.assertIn("Hosted Client Binaries", res_client_info.text)
self.assertIn("Repository Update Mode:", res_client_info.text)
self.assertIn("Distribution Source:", res_client_info.text)
self.assertIn("Python Environment:", res_client_info.text)
self.assertIn("Host Platform &amp; OS:", res_client_info.text)
self.assertIn("TLS 1.3 mTLS Encrypted", res_client_info.text)
self.assertIn("Storage Backend Engine:", res_client_info.text)
self.assertIn("License Status &amp; Seats:", res_client_info.text)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()