Relocate baseline config to administration subtabs, refine nav row, and add live CPU and RAM reporting
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m12s

This commit is contained in:
2026-10-11 11:08:41 +02:00
parent 1bf3f0ed6f
commit 52519e258e
7 changed files with 193 additions and 34 deletions
+1
View File
@@ -118,6 +118,7 @@ def enroll_client_if_needed(
"is_server": inventory.get("is_server", False), "is_server": inventory.get("is_server", False),
"server_roles": inventory.get("server_roles", []), "server_roles": inventory.get("server_roles", []),
"client_info": inventory, "client_info": inventory,
"metrics": collect_system_metrics(),
"enrollment_secret": enrollment_secret, "enrollment_secret": enrollment_secret,
"version": LOGAR_VERSION "version": LOGAR_VERSION
} }
+37 -1
View File
@@ -11,6 +11,7 @@ import socket
import shutil import shutil
import ctypes import ctypes
import subprocess import subprocess
import time
import warnings import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
@@ -106,7 +107,42 @@ def collect_system_metrics() -> Dict[str, Any]:
# 3. CPU metrics # 3. CPU metrics
try: try:
if hasattr(os, "getloadavg"): if sys.platform == "win32":
class FILETIME(ctypes.Structure):
_fields_ = [("dwLowDateTime", ctypes.c_uint), ("dwHighDateTime", ctypes.c_uint)]
idle, kernel, user = FILETIME(), FILETIME(), FILETIME()
if ctypes.windll.kernel32.GetSystemTimes(ctypes.byref(idle), ctypes.byref(kernel), ctypes.byref(user)):
i1 = (idle.dwHighDateTime << 32) + idle.dwLowDateTime
k1 = (kernel.dwHighDateTime << 32) + kernel.dwLowDateTime
u1 = (user.dwHighDateTime << 32) + user.dwLowDateTime
time.sleep(0.06)
if ctypes.windll.kernel32.GetSystemTimes(ctypes.byref(idle), ctypes.byref(kernel), ctypes.byref(user)):
i2 = (idle.dwHighDateTime << 32) + idle.dwLowDateTime
k2 = (kernel.dwHighDateTime << 32) + kernel.dwLowDateTime
u2 = (user.dwHighDateTime << 32) + user.dwLowDateTime
idle_delta = i2 - i1
total_delta = (k2 - k1) + (u2 - u1)
if total_delta > 0:
pct = 100.0 * (1.0 - (idle_delta / total_delta))
metrics["cpu_percent"] = round(max(0.0, min(100.0, pct)), 1)
elif os.path.exists("/proc/stat"):
def _read_proc_stat():
with open("/proc/stat", "r") as f:
fields = [float(col) for col in f.readline().strip().split()[1:]]
idle = fields[3] + (fields[4] if len(fields) > 4 else 0.0)
total = sum(fields)
return idle, total
i1, t1 = _read_proc_stat()
time.sleep(0.06)
i2, t2 = _read_proc_stat()
total_delta = t2 - t1
idle_delta = i2 - i1
if total_delta > 0:
pct = 100.0 * (1.0 - (idle_delta / total_delta))
metrics["cpu_percent"] = round(max(0.0, min(100.0, pct)), 1)
elif hasattr(os, "getloadavg"):
load1, _, _ = os.getloadavg() load1, _, _ = os.getloadavg()
cpu_count = os.cpu_count() or 1 cpu_count = os.cpu_count() or 1
metrics["cpu_percent"] = round(min(100.0, (load1 / cpu_count) * 100), 1) metrics["cpu_percent"] = round(min(100.0, (load1 / cpu_count) * 100), 1)
+11 -2
View File
@@ -436,6 +436,7 @@ def get_full_system_inventory() -> Dict[str, Any]:
# Memory # Memory
mem_total_mb = 0 mem_total_mb = 0
mem_percent = 0.0
try: try:
if sys.platform == "win32": if sys.platform == "win32":
import ctypes import ctypes
@@ -455,12 +456,19 @@ def get_full_system_inventory() -> Dict[str, Any]:
stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX) stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)): if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
mem_total_mb = int(stat.ullTotalPhys / (1024 * 1024)) mem_total_mb = int(stat.ullTotalPhys / (1024 * 1024))
mem_percent = float(stat.dwMemoryLoad)
elif os.path.exists("/proc/meminfo"): elif os.path.exists("/proc/meminfo"):
tot = 0
avail = 0
with open("/proc/meminfo", "r", encoding="utf-8") as f: with open("/proc/meminfo", "r", encoding="utf-8") as f:
for line in f: for line in f:
if line.startswith("MemTotal:"): if line.startswith("MemTotal:"):
mem_total_mb = int(line.split(":")[1].strip().split()[0]) // 1024 tot = int(line.split(":")[1].strip().split()[0])
break elif line.startswith(("MemAvailable:", "MemFree:")) and avail == 0:
avail = int(line.split(":")[1].strip().split()[0])
if tot > 0:
mem_total_mb = tot // 1024
mem_percent = round(((tot - avail) / tot) * 100, 1)
except Exception: except Exception:
pass pass
@@ -478,6 +486,7 @@ def get_full_system_inventory() -> Dict[str, Any]:
"architecture": os_info.get("architecture"), "architecture": os_info.get("architecture"),
"cpu_count": os.cpu_count() or 1, "cpu_count": os.cpu_count() or 1,
"memory_total_mb": mem_total_mb, "memory_total_mb": mem_total_mb,
"memory_percent": mem_percent,
"disk_total_gb": disk_total_gb, "disk_total_gb": disk_total_gb,
"disk_free_gb": disk_free_gb, "disk_free_gb": disk_free_gb,
"disk_percent_free": disk_pct_free, "disk_percent_free": disk_pct_free,
+37 -12
View File
@@ -62,7 +62,8 @@ try:
WindowsLogCollector, WindowsLogCollector,
LinuxLogCollector, LinuxLogCollector,
get_log_collector, get_log_collector,
is_routine_service_noise is_routine_service_noise,
collect_system_metrics
) )
except ImportError: except ImportError:
from log_collector import ( from log_collector import (
@@ -70,7 +71,8 @@ except ImportError:
WindowsLogCollector, WindowsLogCollector,
LinuxLogCollector, LinuxLogCollector,
get_log_collector, get_log_collector,
is_routine_service_noise is_routine_service_noise,
collect_system_metrics
) )
# Import specialized modular components # Import specialized modular components
@@ -1042,6 +1044,16 @@ def run_local_log_collection_cycle(db_path: str, window_hours: int = 12, min_run
except Exception: except Exception:
inv = {} inv = {}
try:
metrics = collect_system_metrics() if "collect_system_metrics" in globals() else {}
except Exception:
metrics = {}
cpu_p = float(metrics.get("cpu_percent", 0.0))
mem_p = float(metrics.get("memory_percent", 0.0))
disk_p = float(metrics.get("disk_percent_free", inv.get("disk_percent_free", 0.0)))
metrics_json = json.dumps(metrics) if metrics else "{}"
machine_id = get_machine_identifier() machine_id = get_machine_identifier()
os_type = inv.get("os_type", "windows" if sys.platform == "win32" else "linux") os_type = inv.get("os_type", "windows" if sys.platform == "win32" else "linux")
os_name = inv.get("os_name", "") os_name = inv.get("os_name", "")
@@ -1059,9 +1071,11 @@ def run_local_log_collection_cycle(db_path: str, window_hours: int = 12, min_run
c.execute(""" c.execute("""
UPDATE clients UPDATE clients
SET hostname = ?, os_type = ?, cert_fingerprint = 'LOCAL-HOST-BUILTIN', status = 'active', last_seen = ?, SET hostname = ?, os_type = ?, cert_fingerprint = 'LOCAL-HOST-BUILTIN', status = 'active', last_seen = ?,
os_name = ?, is_server = ?, server_roles = ?, client_info_json = ? os_name = ?, is_server = ?, server_roles = ?, client_info_json = ?,
cpu_percent = ?, memory_percent = ?, disk_percent_free = ?, metrics_json = ?
WHERE client_id = ? WHERE client_id = ?
""", (machine_id, os_type, now_sql, os_name, is_server, server_roles, client_info_json, machine_id)) """, (machine_id, os_type, now_sql, os_name, is_server, server_roles, client_info_json,
cpu_p, mem_p, disk_p, metrics_json, machine_id))
except sqlite3.OperationalError: except sqlite3.OperationalError:
c.execute(""" c.execute("""
UPDATE clients UPDATE clients
@@ -1071,9 +1085,10 @@ def run_local_log_collection_cycle(db_path: str, window_hours: int = 12, min_run
else: else:
try: try:
c.execute(""" c.execute("""
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, os_name, is_server, server_roles, client_info_json) INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, os_name, is_server, server_roles, client_info_json, cpu_percent, memory_percent, disk_percent_free, metrics_json)
VALUES (?, ?, ?, 'LOCAL-HOST-BUILTIN', 'active', ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, 'LOCAL-HOST-BUILTIN', 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (machine_id, machine_id, os_type, now_sql, now_sql, os_name, is_server, server_roles, client_info_json)) """, (machine_id, machine_id, os_type, now_sql, now_sql, os_name, is_server, server_roles, client_info_json,
cpu_p, mem_p, disk_p, metrics_json))
except sqlite3.OperationalError: except sqlite3.OperationalError:
c.execute(""" c.execute("""
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen) INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen)
@@ -1461,15 +1476,25 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
req_is_server = 1 if req.is_server else (1 if req.os == "windows_server" else 0) req_is_server = 1 if req.is_server else (1 if req.os == "windows_server" else 0)
req_server_roles = json.dumps(req.server_roles) if req.server_roles else "[]" req_server_roles = json.dumps(req.server_roles) if req.server_roles else "[]"
req_client_info = json.dumps(req.client_info) if req.client_info else "{}" req_client_info = json.dumps(req.client_info) if req.client_info else "{}"
req_metrics = getattr(req, "metrics", None) or {}
cpu_p = float(req_metrics.get("cpu_percent", 0.0))
mem_p = float(req_metrics.get("memory_percent", 0.0))
disk_p = float(req_metrics.get("disk_percent_free", 0.0))
metrics_json = json.dumps(req_metrics) if req_metrics else "{}"
if existing: if existing:
try: try:
c.execute(""" c.execute("""
UPDATE clients UPDATE clients
SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?, client_version = ?, SET hostname = ?, os_type = ?, cert_fingerprint = ?, status = 'active', last_seen = ?, client_version = ?,
os_name = ?, is_server = ?, server_roles = ?, client_info_json = ? os_name = ?, is_server = ?, server_roles = ?, client_info_json = ?,
cpu_percent = CASE WHEN ? > 0.0 THEN ? ELSE cpu_percent END,
memory_percent = CASE WHEN ? > 0.0 THEN ? ELSE memory_percent END,
disk_percent_free = CASE WHEN ? > 0.0 THEN ? ELSE disk_percent_free END,
metrics_json = CASE WHEN ? != '{}' THEN ? ELSE metrics_json END
WHERE client_id = ? WHERE client_id = ?
""", (req.hostname, req.os, fp, now_sql, client_ver, req_os_name, req_is_server, req_server_roles, req_client_info, req.client_id)) """, (req.hostname, req.os, fp, now_sql, client_ver, req_os_name, req_is_server, req_server_roles, req_client_info,
cpu_p, cpu_p, mem_p, mem_p, disk_p, disk_p, metrics_json, metrics_json, req.client_id))
except sqlite3.OperationalError: except sqlite3.OperationalError:
try: try:
c.execute(""" c.execute("""
@@ -1486,9 +1511,9 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
else: else:
try: try:
c.execute(""" c.execute("""
INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, client_version, os_name, is_server, server_roles, client_info_json) INSERT INTO clients (client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, client_version, os_name, is_server, server_roles, client_info_json, cpu_percent, memory_percent, disk_percent_free, metrics_json)
VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (req.client_id, req.hostname, req.os, fp, now_sql, now_sql, client_ver, req_os_name, req_is_server, req_server_roles, req_client_info)) """, (req.client_id, req.hostname, req.os, fp, now_sql, now_sql, client_ver, req_os_name, req_is_server, req_server_roles, req_client_info, cpu_p, mem_p, disk_p, metrics_json))
except sqlite3.OperationalError: except sqlite3.OperationalError:
try: try:
c.execute(""" c.execute("""
+1
View File
@@ -17,6 +17,7 @@ class ClientEnrollRequest(BaseModel):
is_server: Optional[bool] = None is_server: Optional[bool] = None
server_roles: Optional[List[str]] = None server_roles: Optional[List[str]] = None
client_info: Optional[Dict[str, Any]] = None client_info: Optional[Dict[str, Any]] = None
metrics: Optional[Dict[str, Any]] = None
class ClientUpdateRequest(BaseModel): class ClientUpdateRequest(BaseModel):
+102 -19
View File
@@ -358,13 +358,29 @@ class WebUIRenderer:
bp_rules_html = "".join(rule_rows) bp_rules_html = "".join(rule_rows)
bp_banner_html = f""" bp_banner_html = f"""
<div class="card" style="margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; padding: 14px 20px; border: 1px solid rgba(16, 185, 129, 0.35); background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(15, 23, 42, 0.8));">
<div style="display: flex; align-items: center; gap: 14px;">
<span style="font-size: 26px;">🛡️</span>
<div>
<div style="font-weight: 700; font-size: 14px; color: #fff;">Server Role Baseline &amp; Compliance Status</div>
<div style="font-size: 12px; color: var(--text-muted);">{pass_cnt} passing • {warn_cnt} warnings • {fail_cnt} failures ({total_rules} Rules)</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span class="badge badge-active" style="color:{score_color}; font-size:11px; padding:5px 10px;">{score}% COMPLIANT</span>
<button onclick="switchAdminSubtab('baseline')" class="btn btn-outline" style="font-size: 11px; padding: 6px 12px;">🛡️ Baseline Config</button>
</div>
</div>
"""
baseline_admin_html = f"""
<div class="card" style="border: 1px solid rgba(16, 185, 129, 0.35); background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(15, 23, 42, 0.8)); margin-bottom: 24px;"> <div class="card" style="border: 1px solid rgba(16, 185, 129, 0.35); background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(15, 23, 42, 0.8)); margin-bottom: 24px;">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-bottom: 16px;"> <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: center; gap: 14px;"> <div style="display: flex; align-items: center; gap: 14px;">
<div style="font-size: 32px; line-height: 1;">🛡️</div> <div style="font-size: 36px; line-height: 1;">🛡️</div>
<div> <div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 2px;"> <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 2px;">
<h3 style="margin: 0; font-size: 16px; color: #34d399;">Best Practice &amp; Server Role Baseline Analyzer</h3> <h3 style="margin: 0; font-size: 17px; color: #34d399;">Best Practice &amp; Security Baseline Engine</h3>
<span class="badge badge-active" style="font-size: 10px;">AUTHORIZED • ACTIVE</span> <span class="badge badge-active" style="font-size: 10px;">AUTHORIZED • ACTIVE</span>
</div> </div>
<div style="font-size: 12px; color: var(--text-muted);"> <div style="font-size: 12px; color: var(--text-muted);">
@@ -374,7 +390,7 @@ class WebUIRenderer:
</div> </div>
<div style="display: flex; align-items: center; gap: 16px;"> <div style="display: flex; align-items: center; gap: 16px;">
<div style="text-align: right;"> <div style="text-align: right;">
<div style="font-size: 26px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: {score_color};">{score}%</div> <div style="font-size: 28px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: {score_color};">{score}%</div>
<div style="font-size: 11px; color: var(--text-muted); text-transform: uppercase;">Fleet Compliance Score</div> <div style="font-size: 11px; color: var(--text-muted); text-transform: uppercase;">Fleet Compliance Score</div>
</div> </div>
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
@@ -384,15 +400,31 @@ class WebUIRenderer:
</div> </div>
</div> </div>
</div> </div>
</div>
<details style="cursor: pointer;"> <!-- Baseline Rules Configuration Card -->
<summary style="font-size: 12px; font-weight: 700; color: #60a5fa; outline: none; margin-bottom: 12px;"> <div class="card">
▶ View {total_rules} Active Baseline Audit Rules &amp; Remediation Standards <div class="card-header" style="flex-wrap: wrap; gap: 12px;">
</summary> <div>
<div style="margin-top: 10px;"> <div class="card-title" style="font-size: 15px;">Configured Baseline Audit Rules &amp; Checks ({total_rules} Rules)</div>
{bp_rules_html} <div class="subtitle" style="margin-top: 3px;">
Modular compliance checks evaluated across all enrolled server nodes. Defined in <code>src/hub/best_practice_rules.py</code>.
</div>
</div> </div>
</details> <div style="display: flex; gap: 8px; align-items: center;">
<button onclick="switchTab('server-roles')" class="btn btn-outline" style="font-size: 11px; padding: 6px 12px;">🖥️ View Node Results</button>
</div>
</div>
<div style="margin-bottom: 16px; background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.25); border-radius: 8px; padding: 12px 16px;">
<div style="font-size: 12px; color: #93c5fd; line-height: 1.5;">
💡 <strong>Rule Customization:</strong> Baseline rules are cleanly decoupled from the evaluation engine and maintained in <code>src/hub/best_practice_rules.py</code>. You can define new role checks, alter warning thresholds, or add compliance baselines without modifying core server logic.
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
{bp_rules_html}
</div>
</div> </div>
""" """
bp_kpi_value = f"{score}%" bp_kpi_value = f"{score}%"
@@ -400,13 +432,29 @@ class WebUIRenderer:
bp_kpi_badge = f'<span class="badge badge-active" style="color:{score_color};">{score}% COMPLIANT</span>' bp_kpi_badge = f'<span class="badge badge-active" style="color:{score_color};">{score}% COMPLIANT</span>'
else: else:
bp_banner_html = """ bp_banner_html = """
<div class="card" style="margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; padding: 14px 20px; border: 1px solid rgba(234, 179, 8, 0.3); background: linear-gradient(135deg, rgba(30, 41, 59, 0.7), rgba(15, 23, 42, 0.9));">
<div style="display: flex; align-items: center; gap: 14px;">
<span style="font-size: 26px;">🛡️</span>
<div>
<div style="font-weight: 700; font-size: 14px; color: #facc15;">Best Practice Analyzer &amp; Role Baseline Engine</div>
<div style="font-size: 12px; color: var(--text-muted);">Requires 'best_practices' enterprise license capability</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span class="badge" style="background:rgba(234, 179, 8, 0.15); color:#facc15; border:1px solid rgba(234, 179, 8, 0.3);">LOCKED</span>
<button onclick="switchAdminSubtab('baseline')" class="btn btn-outline" style="border-color: #facc15; color: #facc15; font-size: 11px; padding: 6px 12px;">🛡️ Baseline Config</button>
</div>
</div>
"""
baseline_admin_html = """
<div class="card" style="border: 1px solid rgba(234, 179, 8, 0.4); background: linear-gradient(135deg, rgba(30, 41, 59, 0.7), rgba(15, 23, 42, 0.9)); margin-bottom: 24px;"> <div class="card" style="border: 1px solid rgba(234, 179, 8, 0.4); background: linear-gradient(135deg, rgba(30, 41, 59, 0.7), rgba(15, 23, 42, 0.9)); margin-bottom: 24px;">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;"> <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: flex-start; gap: 16px;"> <div style="display: flex; align-items: flex-start; gap: 16px;">
<div style="font-size: 32px; line-height: 1;">🛡️</div> <div style="font-size: 36px; line-height: 1;">🛡️</div>
<div> <div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;"> <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;">
<h3 style="margin: 0; font-size: 16px; color: #facc15;">Best Practice Analyzer &amp; Role Baseline Engine</h3> <h3 style="margin: 0; font-size: 17px; color: #facc15;">Best Practice Analyzer &amp; Role Baseline Engine</h3>
<span class="badge" style="background: rgba(234, 179, 8, 0.2); color: #facc15; border: 1px solid rgba(234, 179, 8, 0.4); font-size: 10px;">ENTERPRISE CAPABILITY • INACTIVE</span> <span class="badge" style="background: rgba(234, 179, 8, 0.2); color: #facc15; border: 1px solid rgba(234, 179, 8, 0.4); font-size: 10px;">ENTERPRISE CAPABILITY • INACTIVE</span>
</div> </div>
<p style="margin: 0; font-size: 13px; color: var(--text-muted); max-width: 820px; line-height: 1.5;"> <p style="margin: 0; font-size: 13px; color: var(--text-muted); max-width: 820px; line-height: 1.5;">
@@ -418,7 +466,30 @@ class WebUIRenderer:
</div> </div>
</div> </div>
<div> <div>
<button onclick="switchTab('pki')" class="btn btn-outline" style="border-color: #facc15; color: #facc15; font-size: 12px;">📜 View Licensing Portal</button> <button onclick="switchAdminSubtab('pki')" class="btn btn-outline" style="border-color: #facc15; color: #facc15; font-size: 12px;">📜 View Licensing Portal</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title" style="font-size: 15px;">Baseline Rules Definition File</div>
<div class="subtitle" style="margin-top: 3px;">
Pre-configured baseline rules ready for evaluation once activated. Defined in <code>src/hub/best_practice_rules.py</code>.
</div>
</div>
</div>
<div style="margin-bottom: 16px; background: #0f172a; padding: 16px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 13px; color: #e2e8f0; margin-bottom: 6px;"><strong>Rules File:</strong> <code>src/hub/best_practice_rules.py</code></div>
<div style="font-size: 12px; color: var(--text-muted); line-height: 1.5;">
Contains modular rule definitions including:
<ul style="margin: 8px 0 0 20px; padding: 0;">
<li><code>BP-SYS-001</code>: Primary Volume Storage Headroom (&gt;= 20% free buffer)</li>
<li><code>BP-SYS-002</code>: Active Memory Saturation (&lt; 85% RAM load)</li>
<li><code>BP-AD-001</code>: Active Directory Domain Controller Isolation (No IIS, RDS, Print)</li>
<li><code>BP-UPD-001</code>: Client Agent Version Baseline Synchronization</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
@@ -634,6 +705,13 @@ class WebUIRenderer:
<div>{os_badge_full}</div> <div>{os_badge_full}</div>
<div style="font-size:12px; font-weight:600; color:#e2e8f0; margin-top:3px;">{os_display_name}</div> <div style="font-size:12px; font-weight:600; color:#e2e8f0; margin-top:3px;">{os_display_name}</div>
<div style="font-size:11px; color:#94a3b8;">{specs_sub}</div> <div style="font-size:11px; color:#94a3b8;">{specs_sub}</div>
<div style="font-size:11px; margin-top:3px; display:flex; align-items:center; gap:8px;">
<span style="color:{cpu_bar_col}; font-family:'JetBrains Mono',monospace;">⚡ CPU {cpu_p}%</span>
<span style="color:#64748b;">•</span>
<span style="color:{mem_bar_col}; font-family:'JetBrains Mono',monospace;">🧠 RAM {mem_p}%</span>
<span style="color:#64748b;">•</span>
<span style="color:{disk_bar_col}; font-family:'JetBrains Mono',monospace;">💾 Free {disk_p}%</span>
</div>
</td> </td>
<td style="max-width: 340px;"> <td style="max-width: 340px;">
<div style="display:flex; flex-wrap:wrap; gap:4px;"> <div style="display:flex; flex-wrap:wrap; gap:4px;">
@@ -1565,7 +1643,7 @@ class WebUIRenderer:
<!-- Administration Sub-Navigation Tabs Row (Row below, revealed when clicking Administration) --> <!-- Administration Sub-Navigation Tabs Row (Row below, revealed when clicking Administration) -->
<div class="tabs-nav-sub" id="adminSubtabs" style="display: none;"> <div class="tabs-nav-sub" id="adminSubtabs" style="display: none;">
<span class="subtabs-label">⚙️ Administration:</span> <button class="subtab-btn" id="tabBtn-baseline" onclick="switchAdminSubtab('baseline')">🛡️ Baseline Config</button>
<button class="subtab-btn" id="tabBtn-hermes" onclick="switchAdminSubtab('hermes')">🤖 Hermes AI &amp; Tokens</button> <button class="subtab-btn" id="tabBtn-hermes" onclick="switchAdminSubtab('hermes')">🤖 Hermes AI &amp; Tokens</button>
<button class="subtab-btn" id="tabBtn-pki" onclick="switchAdminSubtab('pki')">📜 Sub-CA &amp; Licensing</button> <button class="subtab-btn" id="tabBtn-pki" onclick="switchAdminSubtab('pki')">📜 Sub-CA &amp; Licensing</button>
<button class="subtab-btn" id="tabBtn-db" onclick="switchAdminSubtab('db')">💾 DB Engine</button> <button class="subtab-btn" id="tabBtn-db" onclick="switchAdminSubtab('db')">💾 DB Engine</button>
@@ -1874,6 +1952,11 @@ class WebUIRenderer:
{db_tab_pane_html} {db_tab_pane_html}
<!-- Baseline Configuration (Settings Subtab) -->
<div class="tab-pane" id="tabPane-baseline">
{baseline_admin_html}
</div>
<!-- Hermes AI Integration & Tokens (Settings Subtab) --> <!-- Hermes AI Integration & Tokens (Settings Subtab) -->
<div class="tab-pane" id="tabPane-hermes"> <div class="tab-pane" id="tabPane-hermes">
<div class="card"> <div class="card">
@@ -2169,7 +2252,7 @@ class WebUIRenderer:
window.addEventListener('click', () => hideUserDropdown()); window.addEventListener('click', () => hideUserDropdown());
const PRIMARY_TABS = ['fleet', 'server-roles', 'events', 'admin', 'settings']; const PRIMARY_TABS = ['fleet', 'server-roles', 'events', 'admin', 'settings'];
const ADMIN_SUBTABS = ['hermes', 'pki', 'db', 'updates']; const ADMIN_SUBTABS = ['baseline', 'hermes', 'pki', 'db', 'updates'];
const SETTINGS_SUBTABS = ADMIN_SUBTABS; const SETTINGS_SUBTABS = ADMIN_SUBTABS;
function switchTab(tabId) {{ function switchTab(tabId) {{
@@ -2184,11 +2267,11 @@ class WebUIRenderer:
const adminBtn = document.getElementById('tabBtn-admin') || document.getElementById('tabBtn-settings'); const adminBtn = document.getElementById('tabBtn-admin') || document.getElementById('tabBtn-settings');
if (adminBtn) adminBtn.classList.add('active'); if (adminBtn) adminBtn.classList.add('active');
let activeSub = 'hermes'; let activeSub = 'baseline';
try {{ try {{
activeSub = localStorage.getItem('logar_admin_subtab') || localStorage.getItem('logar_settings_subtab') || 'hermes'; activeSub = localStorage.getItem('logar_admin_subtab') || localStorage.getItem('logar_settings_subtab') || 'baseline';
}} catch(e) {{}} }} catch(e) {{}}
if (!ADMIN_SUBTABS.includes(activeSub)) activeSub = 'hermes'; if (!ADMIN_SUBTABS.includes(activeSub)) activeSub = 'baseline';
switchAdminSubtab(activeSub); switchAdminSubtab(activeSub);
return; return;
}} }}
@@ -193,6 +193,9 @@ class TestServerRolesAndBestPractices(unittest.TestCase):
# Administration sub-navigation tabs (row below) # Administration sub-navigation tabs (row below)
self.assertIn("id=\"adminSubtabs\"", html) self.assertIn("id=\"adminSubtabs\"", html)
self.assertNotIn("⚙️ Administration:</span>", html)
self.assertIn("id=\"tabBtn-baseline\"", html)
self.assertIn(">🛡️ Baseline Config</button>", html)
self.assertIn("id=\"tabBtn-hermes\"", html) self.assertIn("id=\"tabBtn-hermes\"", html)
self.assertIn(">🤖 Hermes AI &amp; Tokens</button>", html) self.assertIn(">🤖 Hermes AI &amp; Tokens</button>", html)
self.assertIn("id=\"tabBtn-pki\"", html) self.assertIn("id=\"tabBtn-pki\"", html)
@@ -206,6 +209,7 @@ class TestServerRolesAndBestPractices(unittest.TestCase):
self.assertIn("id=\"tabPane-fleet\"", html) self.assertIn("id=\"tabPane-fleet\"", html)
self.assertIn("id=\"tabPane-server-roles\"", html) self.assertIn("id=\"tabPane-server-roles\"", html)
self.assertIn("id=\"tabPane-events\"", html) self.assertIn("id=\"tabPane-events\"", html)
self.assertIn("id=\"tabPane-baseline\"", html)
self.assertIn("id=\"tabPane-pki\"", html) self.assertIn("id=\"tabPane-pki\"", html)
self.assertIn("id=\"tabPane-db\"", html) self.assertIn("id=\"tabPane-db\"", html)
self.assertIn("id=\"tabPane-updates\"", html) self.assertIn("id=\"tabPane-updates\"", html)