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
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m12s
This commit is contained in:
@@ -118,6 +118,7 @@ def enroll_client_if_needed(
|
||||
"is_server": inventory.get("is_server", False),
|
||||
"server_roles": inventory.get("server_roles", []),
|
||||
"client_info": inventory,
|
||||
"metrics": collect_system_metrics(),
|
||||
"enrollment_secret": enrollment_secret,
|
||||
"version": LOGAR_VERSION
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import socket
|
||||
import shutil
|
||||
import ctypes
|
||||
import subprocess
|
||||
import time
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone, timedelta
|
||||
@@ -106,7 +107,42 @@ def collect_system_metrics() -> Dict[str, Any]:
|
||||
|
||||
# 3. CPU metrics
|
||||
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()
|
||||
cpu_count = os.cpu_count() or 1
|
||||
metrics["cpu_percent"] = round(min(100.0, (load1 / cpu_count) * 100), 1)
|
||||
|
||||
@@ -436,6 +436,7 @@ def get_full_system_inventory() -> Dict[str, Any]:
|
||||
|
||||
# Memory
|
||||
mem_total_mb = 0
|
||||
mem_percent = 0.0
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
@@ -455,12 +456,19 @@ def get_full_system_inventory() -> Dict[str, Any]:
|
||||
stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
|
||||
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
|
||||
mem_total_mb = int(stat.ullTotalPhys / (1024 * 1024))
|
||||
mem_percent = float(stat.dwMemoryLoad)
|
||||
elif os.path.exists("/proc/meminfo"):
|
||||
tot = 0
|
||||
avail = 0
|
||||
with open("/proc/meminfo", "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemTotal:"):
|
||||
mem_total_mb = int(line.split(":")[1].strip().split()[0]) // 1024
|
||||
break
|
||||
tot = int(line.split(":")[1].strip().split()[0])
|
||||
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:
|
||||
pass
|
||||
|
||||
@@ -478,6 +486,7 @@ def get_full_system_inventory() -> Dict[str, Any]:
|
||||
"architecture": os_info.get("architecture"),
|
||||
"cpu_count": os.cpu_count() or 1,
|
||||
"memory_total_mb": mem_total_mb,
|
||||
"memory_percent": mem_percent,
|
||||
"disk_total_gb": disk_total_gb,
|
||||
"disk_free_gb": disk_free_gb,
|
||||
"disk_percent_free": disk_pct_free,
|
||||
|
||||
+37
-12
@@ -62,7 +62,8 @@ try:
|
||||
WindowsLogCollector,
|
||||
LinuxLogCollector,
|
||||
get_log_collector,
|
||||
is_routine_service_noise
|
||||
is_routine_service_noise,
|
||||
collect_system_metrics
|
||||
)
|
||||
except ImportError:
|
||||
from log_collector import (
|
||||
@@ -70,7 +71,8 @@ except ImportError:
|
||||
WindowsLogCollector,
|
||||
LinuxLogCollector,
|
||||
get_log_collector,
|
||||
is_routine_service_noise
|
||||
is_routine_service_noise,
|
||||
collect_system_metrics
|
||||
)
|
||||
|
||||
# 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:
|
||||
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()
|
||||
os_type = inv.get("os_type", "windows" if sys.platform == "win32" else "linux")
|
||||
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("""
|
||||
UPDATE clients
|
||||
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 = ?
|
||||
""", (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:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
@@ -1071,9 +1085,10 @@ def run_local_log_collection_cycle(db_path: str, window_hours: int = 12, min_run
|
||||
else:
|
||||
try:
|
||||
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)
|
||||
VALUES (?, ?, ?, 'LOCAL-HOST-BUILTIN', 'active', ?, ?, ?, ?, ?, ?)
|
||||
""", (machine_id, machine_id, os_type, now_sql, now_sql, 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', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (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:
|
||||
c.execute("""
|
||||
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_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_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:
|
||||
try:
|
||||
c.execute("""
|
||||
UPDATE clients
|
||||
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 = ?
|
||||
""", (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:
|
||||
try:
|
||||
c.execute("""
|
||||
@@ -1486,9 +1511,9 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
||||
else:
|
||||
try:
|
||||
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)
|
||||
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))
|
||||
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', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (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:
|
||||
try:
|
||||
c.execute("""
|
||||
|
||||
@@ -17,6 +17,7 @@ class ClientEnrollRequest(BaseModel):
|
||||
is_server: Optional[bool] = None
|
||||
server_roles: Optional[List[str]] = None
|
||||
client_info: Optional[Dict[str, Any]] = None
|
||||
metrics: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class ClientUpdateRequest(BaseModel):
|
||||
|
||||
+102
-19
@@ -358,13 +358,29 @@ class WebUIRenderer:
|
||||
bp_rules_html = "".join(rule_rows)
|
||||
|
||||
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 & 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 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="font-size: 32px; line-height: 1;">🛡️</div>
|
||||
<div style="font-size: 36px; line-height: 1;">🛡️</div>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 2px;">
|
||||
<h3 style="margin: 0; font-size: 16px; color: #34d399;">Best Practice & Server Role Baseline Analyzer</h3>
|
||||
<h3 style="margin: 0; font-size: 17px; color: #34d399;">Best Practice & Security Baseline Engine</h3>
|
||||
<span class="badge badge-active" style="font-size: 10px;">AUTHORIZED • ACTIVE</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted);">
|
||||
@@ -374,7 +390,7 @@ class WebUIRenderer:
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<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>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
@@ -384,15 +400,31 @@ class WebUIRenderer:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details style="cursor: pointer;">
|
||||
<summary style="font-size: 12px; font-weight: 700; color: #60a5fa; outline: none; margin-bottom: 12px;">
|
||||
▶ View {total_rules} Active Baseline Audit Rules & Remediation Standards
|
||||
</summary>
|
||||
<div style="margin-top: 10px;">
|
||||
{bp_rules_html}
|
||||
<!-- Baseline Rules Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="flex-wrap: wrap; gap: 12px;">
|
||||
<div>
|
||||
<div class="card-title" style="font-size: 15px;">Configured Baseline Audit Rules & Checks ({total_rules} Rules)</div>
|
||||
<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>
|
||||
</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>
|
||||
"""
|
||||
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>'
|
||||
else:
|
||||
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 & 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 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="font-size: 32px; line-height: 1;">🛡️</div>
|
||||
<div style="font-size: 36px; line-height: 1;">🛡️</div>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;">
|
||||
<h3 style="margin: 0; font-size: 16px; color: #facc15;">Best Practice Analyzer & Role Baseline Engine</h3>
|
||||
<h3 style="margin: 0; font-size: 17px; color: #facc15;">Best Practice Analyzer & 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>
|
||||
</div>
|
||||
<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>
|
||||
<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 (>= 20% free buffer)</li>
|
||||
<li><code>BP-SYS-002</code>: Active Memory Saturation (< 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>
|
||||
@@ -634,6 +705,13 @@ class WebUIRenderer:
|
||||
<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: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 style="max-width: 340px;">
|
||||
<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) -->
|
||||
<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 & Tokens</button>
|
||||
<button class="subtab-btn" id="tabBtn-pki" onclick="switchAdminSubtab('pki')">📜 Sub-CA & Licensing</button>
|
||||
<button class="subtab-btn" id="tabBtn-db" onclick="switchAdminSubtab('db')">💾 DB Engine</button>
|
||||
@@ -1874,6 +1952,11 @@ class WebUIRenderer:
|
||||
|
||||
{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) -->
|
||||
<div class="tab-pane" id="tabPane-hermes">
|
||||
<div class="card">
|
||||
@@ -2169,7 +2252,7 @@ class WebUIRenderer:
|
||||
window.addEventListener('click', () => hideUserDropdown());
|
||||
|
||||
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;
|
||||
|
||||
function switchTab(tabId) {{
|
||||
@@ -2184,11 +2267,11 @@ class WebUIRenderer:
|
||||
const adminBtn = document.getElementById('tabBtn-admin') || document.getElementById('tabBtn-settings');
|
||||
if (adminBtn) adminBtn.classList.add('active');
|
||||
|
||||
let activeSub = 'hermes';
|
||||
let activeSub = 'baseline';
|
||||
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) {{}}
|
||||
if (!ADMIN_SUBTABS.includes(activeSub)) activeSub = 'hermes';
|
||||
if (!ADMIN_SUBTABS.includes(activeSub)) activeSub = 'baseline';
|
||||
switchAdminSubtab(activeSub);
|
||||
return;
|
||||
}}
|
||||
|
||||
@@ -193,6 +193,9 @@ class TestServerRolesAndBestPractices(unittest.TestCase):
|
||||
|
||||
# Administration sub-navigation tabs (row below)
|
||||
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(">🤖 Hermes AI & Tokens</button>", 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-server-roles\"", html)
|
||||
self.assertIn("id=\"tabPane-events\"", html)
|
||||
self.assertIn("id=\"tabPane-baseline\"", html)
|
||||
self.assertIn("id=\"tabPane-pki\"", html)
|
||||
self.assertIn("id=\"tabPane-db\"", html)
|
||||
self.assertIn("id=\"tabPane-updates\"", html)
|
||||
|
||||
Reference in New Issue
Block a user