From fc97964e9574d029e7db2fb8c2db054d483c6b42 Mon Sep 17 00:00:00 2001 From: max Date: Sun, 11 Oct 2026 10:24:44 +0200 Subject: [PATCH] feat(webui): consolidate client-info into main dashboard with primary and settings subtabs layout --- src/hub/Server.py | 13 +- src/hub/web_ui.py | 850 ++++++++++++++++-- tests/test_hub_comprehensive.py | 4 +- tests/test_server.py | 15 +- tests/test_server_roles_and_best_practices.py | 87 +- 5 files changed, 862 insertions(+), 107 deletions(-) diff --git a/src/hub/Server.py b/src/hub/Server.py index db3accc..a14e2a4 100644 --- a/src/hub/Server.py +++ b/src/hub/Server.py @@ -2569,21 +2569,12 @@ def serve_webui(request: Request = None): @app.get("/client-info", response_class=HTMLResponse) @app.get("/webui/client-info", response_class=HTMLResponse) def serve_client_info(request: Request = None): - """Renders the dedicated LOGAR Client & Server Role Directory subpage.""" - auth_user = None + """Redirects to the integrated Server Roles directory tab within the main Web UI.""" if request is not None: auth_user = get_current_user_optional(request) if not auth_user: return RedirectResponse(url="/login", status_code=303) - else: - auth_user = DEFAULT_ADMIN_USER - - html_content = WebUIRenderer.render_client_info_html( - auth_user=auth_user, - request=request, - server_state=SERVER_STATE - ) - return HTMLResponse(content=html_content) + return RedirectResponse(url="/webui#server-roles", status_code=303) @app.get("/api/client-info") diff --git a/src/hub/web_ui.py b/src/hub/web_ui.py index 74a05d8..5414eb0 100644 --- a/src/hub/web_ui.py +++ b/src/hub/web_ui.py @@ -211,26 +211,38 @@ class WebUIRenderer: if not is_mode_official and not is_mode_custom and not is_mode_manual: is_mode_official = True - # Fetch clients safely + # Fetch clients safely with roles, hardware telemetry and specs 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, '[]') - FROM clients ORDER BY last_seen DESC + 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 """) - clients = c.fetchall() + 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") - clients = [r + ("", 0, "[]") for r in c.fetchall()] + 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, '[]') + FROM clients ORDER BY last_seen DESC + """) + raw_clients = [r + ("{}", 0.0, 0.0, 0.0) for r in c.fetchall()] except sqlite3.OperationalError: - c.execute("SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen FROM clients ORDER BY last_seen DESC") - clients = [r + ("", "up_to_date", "", 0, "[]") for r in c.fetchall()] + 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 sqlite3.OperationalError: + c.execute("SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen FROM clients ORDER BY last_seen DESC") + raw_clients = [r + ("", "up_to_date", "", 0, "[]", "{}", 0.0, 0.0, 0.0) for r in c.fetchall()] - # Fetch verified anomalies + # Fetch verified anomalies / events c.execute("SELECT fingerprint, server, severity, message, run_count, last_seen FROM active_issues WHERE status = 'VERIFIED' ORDER BY last_seen DESC LIMIT 50") anomalies_raw = c.fetchall() anomalies = [] @@ -244,17 +256,199 @@ class WebUIRenderer: hermes_tokens = c.fetchall() conn.close() - # Build client rows with exact + relative activity time, version badges, OS differentiation, and seat management actions - client_rows = [] - for cl in clients: - cid, host, ost, fp, st, first, last, cver, up_st = cl[:9] - os_name = cl[9] if len(cl) > 9 else "" - is_server = bool(cl[10]) if len(cl) > 10 else (ost == "windows_server") - roles_raw = cl[11] if len(cl) > 11 else "[]" + 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) + + # Check Best Practice licensing feature & evaluate fleet + is_bp_licensed = LicenseManager.is_feature_licensed("best_practices", db_path=db_path, server_state=state) + 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 banner + 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) + + 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 in src/hub/best_practice_rules.py and activate it in the Licensing Portal at any time. +
+
+
+
+ +
+
+
+ """ + bp_kpi_value = "INACTIVE" + bp_kpi_sub = "Requires 'best_practices' license capability" + bp_kpi_badge = 'LOCKED' + + # Build rows for Tab 1 (Forwarders), Tab 2 (Server Roles), and Tab 3 (Telemetry) + client_rows = [] + server_roles_rows = [] + telemetry_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"] + first = node["first_seen"] + last = node["last_seen"] + cver = node["client_version"] + up_st = node["update_status"] + 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)) @@ -273,23 +467,108 @@ class WebUIRenderer: else: ver_display = f'v{html.escape(cver)}' - # Operating System & Server Classification Badge - if ost == "windows_server" or is_server: - role_cnt_badge = f'{len(roles)} Roles' if roles else '' - os_badge_html = f'🏢 WIN SERVER{role_cnt_badge}' - elif "win" in ost.lower(): + # OS classification badge & display name + 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" + role_cnt_badge = f'{len(roles)} Roles' if roles else '' + os_badge_html = f'🏢 WIN SERVER{role_cnt_badge}' + os_badge_full = '🏢 WINDOWS SERVER' + os_display_name = html.escape(os_name or "Windows Server") + else: + filter_cat = "linux" + os_badge_html = f'🐧 LINUX' + os_badge_full = '🐧 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_html = f'🖥️ WINDOWS' - elif "linux" in ost.lower(): + os_badge_full = '🖥️ WINDOWS CLIENT' + os_display_name = html.escape(os_name or "Windows Workstation") + elif "linux" in ost_lower: + filter_cat = "linux" os_badge_html = f'🐧 LINUX' + os_badge_full = '🐧 LINUX' + os_display_name = html.escape(os_name or "Linux Node") else: + filter_cat = "other" os_badge_html = f'{esc_ost.upper()}' + os_badge_full = 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 "" + + # 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") + disk_bar_col = "#34d399" if disk_p >= 20 else ("#fbbf24" if disk_p >= 10 else "#f87171") + + # 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' + + # Builtin indicator if fp == "LOCAL-HOST-BUILTIN": esc_st = "LOCAL HUB HOST" badge_cls = "badge-active" fp_display = '⚡ Built-in Log Collector' btn_action = 'Integrated (0 Seats)' btn_del = '' + host_tag = '⚡ LOCAL HUB' else: fp_display = f"{esc_fp[:16]}..." btn_action = ( @@ -298,11 +577,15 @@ class WebUIRenderer: f'' ) btn_del = f'' + host_tag = '' + safe_info_json = html.escape(json.dumps(node, indent=2)) + + # 1. Forwarders Row (Tab 1) client_rows.append(f""" {esc_st.upper()} - {esc_host} + {esc_host}{host_tag} {esc_cid} {os_badge_html} {ver_display} @@ -316,9 +599,81 @@ class WebUIRenderer: """) - clients_html = "".join(client_rows) if client_rows else 'No edge clients registered yet.' - # Build anomaly rows + # 2. Server Roles Row (Tab 2) + server_roles_rows.append(f""" + + {esc_st.upper()} + +
{esc_host}{host_tag}
+
{esc_cid}
+ + +
{os_badge_full}
+
{os_display_name}
+
{specs_sub}
+ + +
+ {roles_cell_html} +
+ + {bp_node_badge} + +
v{html.escape(str(cver or LOGAR_VERSION))}
+
{html.escape(rel_time)}
+ + + + + + """) + + # 3. Telemetry Row (Tab 3) + telemetry_rows.append(f""" + + {esc_st.upper()} + +
{esc_host}{host_tag}
+
{esc_cid}
+ + {os_badge_html} + +
+ {cpu_p}% + {cpu_cnt_str.strip(' •')} +
+ + +
+ {mem_p}% + {mem_tot_str} +
+ + +
+ {disk_p}% free +
+ + + {html.escape(ip_str)} + + {last_display} + + + + + """) + + clients_html = "".join(client_rows) if client_rows else 'No edge clients registered yet.' + server_roles_html = "".join(server_roles_rows) if server_roles_rows else 'No monitored nodes registered yet.' + telemetry_html = "".join(telemetry_rows) if telemetry_rows else 'No telemetry reported yet.' + + # Build anomaly rows / events anomaly_rows = [] for an in anomalies: afp, srv, sev, msg, rc, ls = an @@ -393,9 +748,11 @@ class WebUIRenderer: server_update_banner_html = "" update_tab_badge = "" + settings_badge = "" if update_status.get("server_update_available"): up_ver = html.escape(str(update_status.get("latest_upstream_version", ""))) update_tab_badge = ' NEW' + settings_badge = ' NEW' server_update_banner_html = f"""
@@ -985,7 +1342,7 @@ class WebUIRenderer: }} /* Tabs Navigation */ - .tabs-nav {{ display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }} + .tabs-nav {{ display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 8px; flex-wrap: wrap; }} .tab-btn {{ display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; @@ -997,6 +1354,60 @@ class WebUIRenderer: .tab-pane {{ display: none; }} .tab-pane.active {{ display: block; }} + /* Settings Sub-Tabs Navigation (Row below) */ + .tabs-nav-sub {{ + display: flex; align-items: center; gap: 8px; + margin-top: -12px; margin-bottom: 24px; + padding: 10px 16px; background: #0f172a; + border: 1px solid var(--border); border-radius: 10px; + flex-wrap: wrap; animation: fadeInSubtabs 0.2s ease-in-out; + }} + @keyframes fadeInSubtabs {{ + from {{ opacity: 0; transform: translateY(-4px); }} + to {{ opacity: 1; transform: translateY(0); }} + }} + .subtabs-label {{ + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.5px; color: var(--text-muted); margin-right: 6px; + }} + .subtab-btn {{ + display: inline-flex; align-items: center; gap: 6px; + padding: 7px 14px; background: transparent; + border: 1px solid transparent; border-radius: 6px; + color: var(--text-muted); font-size: 12px; font-weight: 600; + cursor: pointer; transition: all 0.2s; + }} + .subtab-btn:hover {{ color: #fff; background: #1e293b; border-color: var(--border); }} + .subtab-btn.active {{ + background: var(--primary); color: #fff; + border-color: var(--primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4); + }} + + /* Filter Toolbar & Pills */ + .filter-toolbar {{ + display: flex; justify-content: space-between; align-items: center; + gap: 12px; margin-bottom: 16px; flex-wrap: wrap; + }} + .filter-pills {{ display: flex; gap: 8px; flex-wrap: wrap; }} + .filter-pill {{ + padding: 7px 14px; border-radius: 8px; font-size: 12px; font-weight: 600; + background: #0f172a; border: 1px solid var(--border); color: var(--text-muted); + cursor: pointer; transition: all 0.2s; + }} + .filter-pill:hover {{ color: #fff; border-color: var(--primary); }} + .filter-pill.active {{ background: var(--primary); color: #fff; border-color: var(--primary); }} + .search-box {{ + padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border); + background: #0f172a; color: #fff; font-size: 13px; min-width: 260px; + outline: none; transition: border-color 0.2s; + }} + .search-box:focus {{ border-color: var(--primary); }} + pre.code-viewer {{ + background: #0b0f19; border: 1px solid var(--border); border-radius: 8px; + padding: 14px; font-family: 'JetBrains Mono', monospace; font-size: 12px; + color: #38bdf8; overflow-x: auto; max-height: 480px; + }} + /* Badges */ .badge {{ display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; @@ -1122,15 +1533,22 @@ class WebUIRenderer: {unlicensed_banner_html} {server_update_banner_html} - -
- - 🖥️ Server Roles & Client Directory - - - - - + +
+ + + + + +
+ + + @@ -1168,7 +1586,7 @@ class WebUIRenderer:
Fleet Forwarders - {len(clients)} Total + {len(clients_data)} Total
{summary['active_seats']} Active Nodes
{summary['revoked_seats']} revoked nodes blocked at TLS socket
@@ -1179,9 +1597,9 @@ class WebUIRenderer:
- Enrolled Forwarders & Seat Management + Enrolled Forwarders & Seat Management
- 📋 Server Roles & Directory (/client-info) +
@@ -1208,53 +1626,146 @@ class WebUIRenderer:
- -
+ +
+ + {bp_banner_html} + + +
+
+
+ Total Nodes + {total_nodes} Enrolled +
+
{total_nodes}
+
Total active edge nodes & hub
+
+
+
+ Windows Server Fleet + {win_servers} Servers +
+
{win_servers}
+
{total_roles_detected} enterprise roles detected
+
+
+
+ Windows Clients + {win_workstations} Clients +
+
{win_workstations}
+
Workstation Windows 10/11 edges
+
+
+
+ Linux Nodes + {linux_nodes} Nodes +
+
{linux_nodes}
+
Linux hosts & containers
+
+
+
+ Best Practice Baseline + {bp_kpi_badge} +
+
{bp_kpi_value}
+
{bp_kpi_sub}
+
+
+ +
-
+
-
Hermes AI Bearer Access Tokens
-
Manage dedicated API tokens for Hermes AI to fetch real-time verified anomalies via GET /api/hermes/report
+
Node Telemetry & Server Roles Inventory
+
+ OS introspection, Active Directory role segregation, and installed enterprise server roles. +
-
-
+ Generate Dedicated Hermes Token
-
- - + + +
+
+ + + + +
+
+
+
- +
- - - - - + + + + + + - {tokens_html} + {server_roles_html}
Token NameBearer Token StatusCreatedLast UsedActionHost NodeOperating SystemInstalled Server RolesComplianceAgent VersionSpecs
- -
-
🚀 Hermes AI Integration Quickstart
-
Configure Hermes to query the verified log telemetry stream with Bearer authorization:
-
curl -H "Authorization: Bearer <HERMES_TOKEN>" http://{detected_host}:{config.get('hermes_port', 8443)}/api/hermes/report
-
- -
+ +
+
+
+
+
Fleet Resource & Hardware Telemetry
+
+ Real-time CPU, RAM, and Disk free space metrics reported by forwarder nodes. +
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + {telemetry_html} + +
StatusHost NodeOperating SystemCPU LoadMemory LoadStorage BufferIP AddressesLast ActivitySpecs
+
+
+
+ + +
- Verified Anomaly Stream (Latest 50 Events • Rolling 12h Window) +
+
Fleet Events & Verified Anomaly Stream
+
Latest 50 verified issues & log events across your fleet (Rolling 12h Window)
+
Direct Hermes Intelligence
@@ -1340,6 +1851,48 @@ class WebUIRenderer:
{db_tab_pane_html} + + +
+
+
+
+
Hermes AI Bearer Access Tokens
+
Manage dedicated API tokens for Hermes AI to fetch real-time verified anomalies via GET /api/hermes/report
+
+
+
+
+ Generate Dedicated Hermes Token
+
+ + +
+
+
+ + + + + + + + + + + + + {tokens_html} + +
Token NameBearer TokenStatusCreatedLast UsedAction
+
+ +
+
🚀 Hermes AI Integration Quickstart
+
Configure Hermes to query the verified log telemetry stream with Bearer authorization:
+
curl -H "Authorization: Bearer <HERMES_TOKEN>" http://{detected_host}:{config.get('hermes_port', 8443)}/api/hermes/report
+
+
+
@@ -1559,6 +2112,26 @@ class WebUIRenderer:
+ + +