From b44191ed890d03b7d4ed8a6399926e560100c867 Mon Sep 17 00:00:00 2001 From: max Date: Sun, 11 Oct 2026 10:33:14 +0200 Subject: [PATCH] feat(webui): unify dashboard into 4 primary tabs and administration subtabs row, deprecate separate client-info --- src/hub/web_ui.py | 987 ++---------------- tests/test_server.py | 14 +- tests/test_server_roles_and_best_practices.py | 26 +- 3 files changed, 99 insertions(+), 928 deletions(-) diff --git a/src/hub/web_ui.py b/src/hub/web_ui.py index 5414eb0..6e44c65 100644 --- a/src/hub/web_ui.py +++ b/src/hub/web_ui.py @@ -1535,20 +1535,19 @@ class WebUIRenderer:
- - - - - + + + +
- - - - -
-
+ +
Fleet Resource & Hardware Telemetry
@@ -1758,7 +1755,10 @@ class WebUIRenderer:
- + + + +
@@ -2146,32 +2146,37 @@ class WebUIRenderer: window.addEventListener('click', () => hideUserDropdown()); - const PRIMARY_TABS = ['fleet', 'server-roles', 'telemetry', 'events']; - const SETTINGS_SUBTABS = ['pki', 'db', 'updates', 'hermes']; + const PRIMARY_TABS = ['fleet', 'server-roles', 'events', 'admin', 'settings']; + const ADMIN_SUBTABS = ['hermes', 'pki', 'db', 'updates']; + const SETTINGS_SUBTABS = ADMIN_SUBTABS; function switchTab(tabId) {{ - if (tabId === 'settings') {{ - const subtabsRow = document.getElementById('settingsSubtabs'); + if (tabId === 'telemetry') {{ + tabId = 'server-roles'; + }} + + if (tabId === 'admin' || tabId === 'settings') {{ + const subtabsRow = document.getElementById('adminSubtabs') || document.getElementById('settingsSubtabs'); if (subtabsRow) subtabsRow.style.display = 'flex'; document.querySelectorAll('.tabs-nav .tab-btn').forEach(btn => btn.classList.remove('active')); - const settingsBtn = document.getElementById('tabBtn-settings'); - if (settingsBtn) settingsBtn.classList.add('active'); + const adminBtn = document.getElementById('tabBtn-admin') || document.getElementById('tabBtn-settings'); + if (adminBtn) adminBtn.classList.add('active'); - let activeSub = 'pki'; + let activeSub = 'hermes'; try {{ - activeSub = localStorage.getItem('logar_settings_subtab') || 'pki'; + activeSub = localStorage.getItem('logar_admin_subtab') || localStorage.getItem('logar_settings_subtab') || 'hermes'; }} catch(e) {{}} - if (!SETTINGS_SUBTABS.includes(activeSub)) activeSub = 'pki'; - switchSettingsSubtab(activeSub); + if (!ADMIN_SUBTABS.includes(activeSub)) activeSub = 'hermes'; + switchAdminSubtab(activeSub); return; }} - if (SETTINGS_SUBTABS.includes(tabId)) {{ - switchSettingsSubtab(tabId); + if (ADMIN_SUBTABS.includes(tabId)) {{ + switchAdminSubtab(tabId); return; }} - const subtabsRow = document.getElementById('settingsSubtabs'); + const subtabsRow = document.getElementById('adminSubtabs') || document.getElementById('settingsSubtabs'); if (subtabsRow) subtabsRow.style.display = 'none'; document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); @@ -2180,53 +2185,54 @@ class WebUIRenderer: const btn = document.getElementById('tabBtn-' + tabId); const pane = document.getElementById('tabPane-' + tabId); - if (btn && pane) {{ - btn.classList.add('active'); - pane.classList.add('active'); - try {{ localStorage.setItem('logar_active_tab', tabId); }} catch(e) {{}} - try {{ history.replaceState(null, null, '#' + tabId); }} catch(e) {{}} - }} + if (btn) btn.classList.add('active'); + if (pane) pane.classList.add('active'); + try {{ localStorage.setItem('logar_active_tab', tabId); }} catch(e) {{}} + try {{ history.replaceState(null, null, '#' + tabId); }} catch(e) {{}} }} - function switchSettingsSubtab(subtabId) {{ - const subtabsRow = document.getElementById('settingsSubtabs'); + function switchAdminSubtab(subtabId) {{ + const subtabsRow = document.getElementById('adminSubtabs') || document.getElementById('settingsSubtabs'); if (subtabsRow) subtabsRow.style.display = 'flex'; document.querySelectorAll('.tabs-nav .tab-btn').forEach(btn => btn.classList.remove('active')); - const settingsBtn = document.getElementById('tabBtn-settings'); - if (settingsBtn) settingsBtn.classList.add('active'); + const adminBtn = document.getElementById('tabBtn-admin') || document.getElementById('tabBtn-settings'); + if (adminBtn) adminBtn.classList.add('active'); document.querySelectorAll('.subtab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active')); const subBtn = document.getElementById('tabBtn-' + subtabId); const subPane = document.getElementById('tabPane-' + subtabId); - if (subBtn && subPane) {{ - subBtn.classList.add('active'); - subPane.classList.add('active'); - try {{ - localStorage.setItem('logar_settings_subtab', subtabId); - localStorage.setItem('logar_active_tab', subtabId); - }} catch(e) {{}} - try {{ history.replaceState(null, null, '#' + subtabId); }} catch(e) {{}} - }} + if (subBtn) subBtn.classList.add('active'); + if (subPane) subPane.classList.add('active'); + try {{ + localStorage.setItem('logar_admin_subtab', subtabId); + localStorage.setItem('logar_settings_subtab', subtabId); + localStorage.setItem('logar_active_tab', subtabId); + }} catch(e) {{}} + try {{ history.replaceState(null, null, '#' + subtabId); }} catch(e) {{}} }} + const switchSettingsSubtab = switchAdminSubtab; + window.addEventListener('DOMContentLoaded', () => {{ try {{ const hash = (window.location.hash || '').replace('#', '').trim(); - if (hash === 'settings') {{ - switchTab('settings'); - }} else if (SETTINGS_SUBTABS.includes(hash)) {{ - switchSettingsSubtab(hash); + if (hash === 'admin' || hash === 'settings') {{ + switchTab('admin'); + }} else if (ADMIN_SUBTABS.includes(hash)) {{ + switchAdminSubtab(hash); + }} else if (hash === 'telemetry') {{ + switchTab('server-roles'); }} else if (PRIMARY_TABS.includes(hash)) {{ switchTab(hash); }} else {{ const savedTab = localStorage.getItem('logar_active_tab'); - if (savedTab === 'settings') {{ - switchTab('settings'); - }} else if (savedTab && SETTINGS_SUBTABS.includes(savedTab)) {{ - switchSettingsSubtab(savedTab); + if (savedTab === 'admin' || savedTab === 'settings') {{ + switchTab('admin'); + }} else if (savedTab && ADMIN_SUBTABS.includes(savedTab)) {{ + switchAdminSubtab(savedTab); }} else if (savedTab && PRIMARY_TABS.includes(savedTab)) {{ switchTab(savedTab); }} else {{ @@ -2241,10 +2247,12 @@ class WebUIRenderer: window.addEventListener('hashchange', () => {{ try {{ const hash = (window.location.hash || '').replace('#', '').trim(); - if (hash === 'settings') {{ - switchTab('settings'); - }} else if (SETTINGS_SUBTABS.includes(hash)) {{ - switchSettingsSubtab(hash); + if (hash === 'admin' || hash === 'settings') {{ + switchTab('admin'); + }} else if (ADMIN_SUBTABS.includes(hash)) {{ + switchAdminSubtab(hash); + }} else if (hash === 'telemetry') {{ + switchTab('server-roles'); }} else if (PRIMARY_TABS.includes(hash)) {{ switchTab(hash); }} @@ -3015,854 +3023,11 @@ class WebUIRenderer: server_state: Optional[Dict[str, Any]] = None ) -> str: """ - Renders the dedicated LOGAR Client & Server Role Directory subpage (/client-info). - Explicitly differentiates Windows Client vs Windows Server vs Linux, - displays installed server roles, hardware specs, and Best Practice baseline compliance. + Deprecated wrapper: The Server Roles and Client Directory view is now + natively integrated into the primary WebUI (/webui#server-roles). """ - state = server_state or {} - config = state.get("config", {}) - db_path = config.get("db_path", "logar_state.db") - auth_display = html.escape(str(auth_user or "admin")) - - # Check Best Practice licensing feature & summary - 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 - conn = sqlite3.connect(db_path) - c = conn.cursor() - try: - c.execute(""" - SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, - COALESCE(client_version, ''), COALESCE(update_status, 'up_to_date'), - COALESCE(os_name, ''), COALESCE(is_server, 0), - COALESCE(server_roles, '[]'), COALESCE(client_info_json, '{}'), - COALESCE(cpu_percent, 0.0), COALESCE(memory_percent, 0.0), COALESCE(disk_percent_free, 0.0) - FROM clients - ORDER BY is_server DESC, last_seen DESC - """) - raw_clients = c.fetchall() - except sqlite3.OperationalError: - try: - c.execute(""" - SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, - COALESCE(client_version, ''), COALESCE(update_status, 'up_to_date') - FROM clients ORDER BY last_seen DESC - """) - raw_clients = [r + ("", 0, "[]", "{}", 0.0, 0.0, 0.0) for r in c.fetchall()] - except Exception: - raw_clients = [] - finally: - conn.close() - - clients_data = [] - win_servers = 0 - win_workstations = 0 - linux_nodes = 0 - total_roles_detected = 0 - - for r in raw_clients: - cid, host, ost, fp, st, first, last, cver, up_st, os_name, is_srv, roles_raw, info_raw, cpu_p, mem_p, disk_p = r - try: - roles = json.loads(roles_raw) if roles_raw else [] - if not isinstance(roles, list): - roles = [str(roles)] - except Exception: - roles = [] - try: - info = json.loads(info_raw) if info_raw else {} - if not isinstance(info, dict): - info = {} - except Exception: - info = {} - - is_server_bool = bool(is_srv) or ost == "windows_server" - ost_lower = ost.lower() - - if "server" in ost_lower or is_server_bool: - if "windows" in ost_lower or "win" in ost_lower: - win_servers += 1 - else: - linux_nodes += 1 - elif "windows" in ost_lower or "win" in ost_lower: - win_workstations += 1 - elif "linux" in ost_lower: - linux_nodes += 1 - - total_roles_detected += len(roles) - - clients_data.append({ - "client_id": cid, - "hostname": host, - "os_type": ost, - "os_name": os_name, - "is_server": is_server_bool, - "server_roles": roles, - "client_info": info, - "cert_fingerprint": fp, - "status": st, - "first_seen": first, - "last_seen": last, - "client_version": cver, - "update_status": up_st, - "cpu_percent": float(cpu_p or 0.0), - "memory_percent": float(mem_p or 0.0), - "disk_percent_free": float(disk_p or 0.0) - }) - - total_nodes = len(clients_data) - - # Evaluate fleet Best Practices if licensed - bp_evaluation = None - if is_bp_licensed and BEST_PRACTICES_ENGINE: - try: - bp_evaluation = BEST_PRACTICES_ENGINE.evaluate_fleet(clients_data) - except Exception as ex: - bp_evaluation = {"score": 0, "pass_count": 0, "warn_count": 0, "fail_count": 0, "rules_count": 0, "error": str(ex), "fleet_results": {}} - - # Build Best Practice summary card HTML - if is_bp_licensed and bp_evaluation: - score = bp_evaluation.get("score", 100) - score_color = "#34d399" if score >= 90 else ("#fbbf24" if score >= 70 else "#f87171") - pass_cnt = bp_evaluation.get("pass_count", 0) - warn_cnt = bp_evaluation.get("warn_count", 0) - fail_cnt = bp_evaluation.get("fail_count", 0) - total_rules = bp_evaluation.get("rules_count", 0) - - # Build detailed checks overview - rules_list = BEST_PRACTICES_ENGINE.get_rules() if BEST_PRACTICES_ENGINE else [] - rule_rows = [] - for r in rules_list: - rid = html.escape(str(r.get("rule_id", ""))) - rtitle = html.escape(str(r.get("title", ""))) - rcat = html.escape(str(r.get("category", ""))) - rsev = html.escape(str(r.get("severity", "WARNING"))) - rrec = html.escape(str(r.get("recommendation", ""))) - rdesc = html.escape(str(r.get("description", ""))) - sev_badge = "badge-error" if rsev == "CRITICAL" else ("badge-warning" if rsev == "WARNING" else "badge-os") - - rule_rows.append(f""" -
-
-
- {rid} - {rtitle} - {rsev} -
- {rcat} -
-
{rdesc}
-
Recommendation: {rrec}
-
- """) - - bp_rules_html = "".join(rule_rows) - - bp_banner_html = f""" -
-
-
-
đŸ›Ąī¸
-
-
-

Best Practice & Server Role Baseline Analyzer

- AUTHORIZED â€ĸ ACTIVE -
-
- Comprehensive security baseline, Active Directory role segregation, and storage buffer auditing active across {total_nodes} nodes. -
-
-
-
-
-
{score}%
-
Fleet Compliance Score
-
-
- ✅ {pass_cnt} PASS - âš ī¸ {warn_cnt} WARN - ❌ {fail_cnt} FAIL -
-
-
- -
- - â–ļ View {total_rules} Active Baseline Audit Rules & Remediation Standards - -
- {bp_rules_html} -
-
-
- """ - bp_kpi_value = f"{score}%" - bp_kpi_sub = f"{pass_cnt} passing â€ĸ {warn_cnt} warnings â€ĸ {fail_cnt} failures" - bp_kpi_badge = f'{score}% COMPLIANT' - else: - bp_banner_html = """ -
-
-
-
đŸ›Ąī¸
-
-
-

Best Practice Analyzer & Role Baseline Engine

- ENTERPRISE CAPABILITY â€ĸ INACTIVE -
-

- Automated configuration baseline auditing, Active Directory role isolation checks (e.g. verifying Domain Controllers do not co-locate IIS or Remote Desktop), and fleet hardening standards require an active enterprise license with the best_practices capability enabled. -

-
- 💡 This capability is inactive by default. You can define rules later on and activate it in the LOGAR Licensing Portal at any time. -
-
-
- -
-
- """ - bp_kpi_value = "INACTIVE" - bp_kpi_sub = "Requires 'best_practices' license capability" - bp_kpi_badge = 'LOCKED' - - # Build Host Rows - node_rows = [] - for node in clients_data: - cid = node["client_id"] - host = node["hostname"] - ost = node["os_type"] - os_name = node["os_name"] - is_srv = node["is_server"] - roles = node["server_roles"] - info = node["client_info"] - fp = node["cert_fingerprint"] - st = node["status"] - last = node["last_seen"] - cver = node["client_version"] - cpu_p = node["cpu_percent"] - mem_p = node["memory_percent"] - disk_p = node["disk_percent_free"] - - esc_cid = html.escape(str(cid)) - esc_host = html.escape(str(host)) - badge_st_cls = "badge-active" if st == "active" else "badge-revoked" - rel_time = format_relative_time(last) - - # Node Classification Badge & Filter Category - ost_lower = ost.lower() - if "server" in ost_lower or is_srv: - if "windows" in ost_lower or "win" in ost_lower: - filter_cat = "windows_server" - os_badge = 'đŸĸ WINDOWS SERVER' - os_display_name = html.escape(os_name or "Windows Server") - else: - filter_cat = "linux" - os_badge = '🐧 LINUX SERVER' - os_display_name = html.escape(os_name or "Linux Node") - elif "windows" in ost_lower or "win" in ost_lower: - filter_cat = "windows_client" - os_badge = 'đŸ–Ĩī¸ WINDOWS CLIENT' - os_display_name = html.escape(os_name or "Windows Workstation") - elif "linux" in ost_lower: - filter_cat = "linux" - os_badge = '🐧 LINUX' - os_display_name = html.escape(os_name or "Linux Node") - else: - filter_cat = "other" - os_badge = f'{html.escape(ost.upper())}' - os_display_name = html.escape(os_name or ost) - - edition_str = html.escape(str(info.get("edition") or "")) - build_str = html.escape(str(info.get("build") or "")) - arch_str = html.escape(str(info.get("architecture") or "")) - specs_sub = f"{edition_str} â€ĸ Build {build_str} â€ĸ {arch_str}".strip(" â€ĸ") if (edition_str or build_str or arch_str) else "" - - # Server Roles Badges - role_badges = [] - if roles: - for r in roles: - esc_r = html.escape(str(r)) - r_lower = str(r).lower() - if "active directory" in r_lower or "domain" in r_lower: - role_badges.append(f'🌐 {esc_r}') - elif "dns" in r_lower: - role_badges.append(f'📡 {esc_r}') - elif "dhcp" in r_lower: - role_badges.append(f'đŸ“ļ {esc_r}') - elif "web server" in r_lower or "iis" in r_lower or "nginx" in r_lower or "apache" in r_lower: - role_badges.append(f'🌍 {esc_r}') - elif "hyper-v" in r_lower or "kvm" in r_lower or "qemu" in r_lower: - role_badges.append(f'⚡ {esc_r}') - elif "remote desktop" in r_lower or "terminal" in r_lower or "ssh" in r_lower: - role_badges.append(f'đŸ–Ĩī¸ {esc_r}') - elif "file" in r_lower or "storage" in r_lower or "samba" in r_lower or "nfs" in r_lower: - role_badges.append(f'📁 {esc_r}') - elif "print" in r_lower or "cups" in r_lower: - role_badges.append(f'đŸ–¨ī¸ {esc_r}') - else: - role_badges.append(f'âš™ī¸ {esc_r}') - roles_cell_html = "".join(role_badges) - else: - roles_cell_html = 'Workstation / Member Node (0 Roles)' - - # Hardware specs & telemetry - ip_list = info.get("ip_addresses") or [] - ip_str = ", ".join(ip_list[:2]) if ip_list else "Local" - mem_tot = info.get("memory_total_mb") - mem_tot_str = f"{round(mem_tot / 1024, 1)} GB" if mem_tot else "" - cpu_cnt = info.get("cpu_count") - cpu_cnt_str = f"{cpu_cnt} Cores â€ĸ " if cpu_cnt else "" - - cpu_bar_col = "#34d399" if cpu_p < 75 else ("#fbbf24" if cpu_p < 90 else "#f87171") - mem_bar_col = "#34d399" if mem_p < 75 else ("#fbbf24" if mem_p < 90 else "#f87171") - - telemetry_html = f""" -
-
CPU: {cpu_cnt_str}{cpu_p}%
-
RAM: {mem_tot_str} ({mem_p}% in use)
-
Disk Free: {disk_p}%
-
{html.escape(ip_str)}
-
- """ - - # Builtin indicator - if fp == "LOCAL-HOST-BUILTIN": - host_tag = '⚡ LOCAL HUB' - else: - host_tag = '' - - # Best Practice node compliance result - if is_bp_licensed and bp_evaluation and "fleet_results" in bp_evaluation: - node_eval = bp_evaluation["fleet_results"].get(cid) or bp_evaluation["fleet_results"].get(host) - if node_eval: - n_checks = node_eval.get("checks", []) - n_fails = sum(1 for c in n_checks if c.get("status") == "FAIL") - n_warns = sum(1 for c in n_checks if c.get("status") == "WARN") - n_passes = sum(1 for c in n_checks if c.get("status") == "PASS") - if n_fails > 0: - bp_node_badge = f'❌ {n_fails} FAIL' - elif n_warns > 0: - bp_node_badge = f'âš ī¸ {n_warns} WARN' - else: - bp_node_badge = f'✅ {n_passes} PASS' - else: - bp_node_badge = '✅ HEALTHY' - else: - bp_node_badge = '🔒 Locked' - - # Client info JSON payload for inspect modal - safe_info_json = html.escape(json.dumps(node, indent=2)) - - node_rows.append(f""" - - -
- {st.upper()} -
- - -
{esc_host}{host_tag}
-
{esc_cid}
- - -
{os_badge}
-
{os_display_name}
-
{specs_sub}
- - -
- {roles_cell_html} -
- - {telemetry_html} - -
v{html.escape(str(cver or LOGAR_VERSION))}
-
{html.escape(rel_time)}
-
{html.escape(str(last[:19]))}
- - {bp_node_badge} - - - - - """) - - nodes_table_html = "".join(node_rows) if node_rows else 'No monitored nodes registered yet.' - - # Render complete subpage HTML - return f""" - - - - - LOGAR | Server & Client Role Directory - - - - - - -
- -
-
-
đŸĸ
-
-
LOGAR Server & Client Role Directory
-
OS Classification â€ĸ Windows & Linux Server Roles â€ĸ Hardware Telemetry â€ĸ Best Practice Baseline
-
-
-
- - ← Back to Fleet Hub - - - 📜 Sub-CA & Licensing - -
-
- 👤 {auth_display} - â–ŧ -
-
-
Logged in as {auth_display}
- - đŸšĒ Logout -
-
-
-
- - -
-
-
- Total Monitored Nodes - {total_nodes} Enrolled -
-
{total_nodes}
-
Total active edge nodes and local hub host
-
- -
-
- Windows Server Fleet - {win_servers} Servers -
-
{win_servers}
-
{total_roles_detected} enterprise server roles inventoried
-
- -
-
- Windows Workstations - {win_workstations} Clients -
-
{win_workstations}
-
Desktop Windows 10/11 edge forwarders
-
- -
-
- Linux Server Nodes - {linux_nodes} Nodes -
-
{linux_nodes}
-
Linux distributions and cloud containers
-
- -
-
- Best Practice Baseline - {bp_kpi_badge} -
-
{bp_kpi_value}
-
{bp_kpi_sub}
-
-
- - - {bp_banner_html} - - -
-
-
-

Node Telemetry & Server Roles Inventory

-
- Real-time hardware metrics, OS edition introspection, and installed enterprise server roles across your fleet. -
-
-
- - -
-
- - - - -
-
- -
-
- -
- - - - - - - - - - - - - - - {nodes_table_html} - -
StatusHost NodeOperating SystemInstalled Server RolesHardware TelemetryAgent VersionComplianceSpecs
-
-
-
- - - - - - - - - -""" - + return WebUIRenderer.render_dashboard_html( + auth_user=auth_user, + request=request, + server_state=server_state + ) diff --git a/tests/test_server.py b/tests/test_server.py index 3f3c7ef..22bf188 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -824,16 +824,16 @@ class TestServerComponent(unittest.TestCase): html_content = resp.body.decode("utf-8") # Verify clean tab buttons without number suffixes - self.assertIn('>đŸ’ģ Forwarders', html_content) - self.assertIn('>đŸĸ Server Roles', html_content) - self.assertIn('>📊 Telemetry', html_content) - self.assertIn('>📜 Events', html_content) - self.assertIn('>âš™ī¸ Settings', html_content) + self.assertIn('>đŸ’ģ Fleet Forwarders', html_content) + self.assertIn('>đŸ–Ĩī¸ Server Roles & Client Directory', html_content) + self.assertIn('>📊 Events', html_content) + self.assertIn('>âš™ī¸ Administration', html_content) + self.assertIn('>🤖 Hermes AI & Tokens', html_content) self.assertIn('>📜 Sub-CA & Licensing', html_content) self.assertIn('>💾 DB Engine', html_content) - self.assertIn('>🤖 Hermes AI & Tokens', html_content) + self.assertIn('>đŸ“Ļ Updates & Distribution', html_content) self.assertNotIn('đŸŸĸ UNLOCKED', html_content) - self.assertNotIn('Forwarders (', html_content) + self.assertNotIn('Fleet Forwarders (', html_content) # Verify Licensed Capabilities card is present self.assertIn('Licensed Capabilities & Feature Modules', html_content) diff --git a/tests/test_server_roles_and_best_practices.py b/tests/test_server_roles_and_best_practices.py index 502cd35..e321510 100644 --- a/tests/test_server_roles_and_best_practices.py +++ b/tests/test_server_roles_and_best_practices.py @@ -173,30 +173,36 @@ class TestServerRolesAndBestPractices(unittest.TestCase): self.assertIn("Node Telemetry & Server Roles Inventory", res_follow.text) def test_dashboard_tabs_structure(self): - """Verifies primary tabs (Forwarders, Server Roles, Telemetry, Events, Settings) and secondary settings subtabs (Sub-CA, DB, Updates, Hermes).""" + """Verifies primary tabs (Fleet Forwarders, Server Roles & Client Directory, Events, Administration) and secondary admin subtabs (Hermes, Sub-CA, DB, Updates).""" res = self.client.get("/webui", cookies={SessionManager.SESSION_COOKIE_NAME: self.session_token}) self.assertEqual(res.status_code, 200) html = res.text - # Primary navigation tabs + # Primary navigation tabs (only 4 tabs on top row) self.assertIn("id=\"mainTabsNav\"", html) self.assertIn("id=\"tabBtn-fleet\"", html) + self.assertIn(">đŸ’ģ Fleet Forwarders", html) self.assertIn("id=\"tabBtn-server-roles\"", html) - self.assertIn("id=\"tabBtn-telemetry\"", html) + self.assertIn(">đŸ–Ĩī¸ Server Roles & Client Directory", html) self.assertIn("id=\"tabBtn-events\"", html) - self.assertIn("id=\"tabBtn-settings\"", html) + self.assertIn(">📊 Events", html) + self.assertIn("id=\"tabBtn-admin\"", html) + self.assertIn(">âš™ī¸ Administration", html) - # Settings sub-navigation tabs (row below) - self.assertIn("id=\"settingsSubtabs\"", html) - self.assertIn("id=\"tabBtn-pki\"", html) - self.assertIn("id=\"tabBtn-db\"", html) - self.assertIn("id=\"tabBtn-updates\"", html) + # Administration sub-navigation tabs (row below) + self.assertIn("id=\"adminSubtabs\"", html) self.assertIn("id=\"tabBtn-hermes\"", html) + self.assertIn(">🤖 Hermes AI & Tokens", html) + self.assertIn("id=\"tabBtn-pki\"", html) + self.assertIn(">📜 Sub-CA & Licensing", html) + self.assertIn("id=\"tabBtn-db\"", html) + self.assertIn(">💾 DB Engine", html) + self.assertIn("id=\"tabBtn-updates\"", html) + self.assertIn(">đŸ“Ļ Updates & Distribution", html) # Tab panes self.assertIn("id=\"tabPane-fleet\"", html) self.assertIn("id=\"tabPane-server-roles\"", html) - self.assertIn("id=\"tabPane-telemetry\"", html) self.assertIn("id=\"tabPane-events\"", html) self.assertIn("id=\"tabPane-pki\"", html) self.assertIn("id=\"tabPane-db\"", html)