feat(ui): reorder DB Engine tab (cache mode on top, encryption in middle, DB engine switcher below)
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m18s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m18s
This commit is contained in:
+108
-85
@@ -808,6 +808,26 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
|||||||
config["session_secret"] = secrets.token_hex(32)
|
config["session_secret"] = secrets.token_hex(32)
|
||||||
if "db_path" not in config:
|
if "db_path" not in config:
|
||||||
config["db_path"] = DEFAULT_DB_FILE
|
config["db_path"] = DEFAULT_DB_FILE
|
||||||
|
if "server_name" not in config:
|
||||||
|
config["server_name"] = "LOGAR Enterprise Hub"
|
||||||
|
if "tcp_host" not in config:
|
||||||
|
config["tcp_host"] = "0.0.0.0"
|
||||||
|
if "tcp_port" not in config:
|
||||||
|
config["tcp_port"] = 9443
|
||||||
|
if "hermes_host" not in config:
|
||||||
|
config["hermes_host"] = "0.0.0.0"
|
||||||
|
if "hermes_port" not in config:
|
||||||
|
config["hermes_port"] = 8443
|
||||||
|
if "evaluation_window_hours" not in config:
|
||||||
|
config["evaluation_window_hours"] = 12
|
||||||
|
if "min_persistence_runs" not in config:
|
||||||
|
config["min_persistence_runs"] = 2
|
||||||
|
if "storage_engine" not in config:
|
||||||
|
config["storage_engine"] = config.get("db_type", "sqlite")
|
||||||
|
if "cache_mode" not in config:
|
||||||
|
config["cache_mode"] = "direct"
|
||||||
|
if "encrypt_at_rest" not in config:
|
||||||
|
config["encrypt_at_rest"] = False
|
||||||
|
|
||||||
# Resolve relative db_path and cert_dir to writable paths
|
# Resolve relative db_path and cert_dir to writable paths
|
||||||
config["db_path"] = resolve_writable_path(config["db_path"], "LOGAR-Server", DEFAULT_DB_FILE)
|
config["db_path"] = resolve_writable_path(config["db_path"], "LOGAR-Server", DEFAULT_DB_FILE)
|
||||||
@@ -3388,91 +3408,10 @@ def serve_webui(request: Request = None):
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">Database Storage Engine Configuration</span>
|
<span class="card-title">Database Storage Engine Configuration</span>
|
||||||
<span class="badge badge-os">STORAGE SELECTION</span>
|
<span class="badge badge-os">CONFIG & TUNING</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 20px;">
|
<!-- Telemetry Ingestion Cache Mode & Buffer Tuning (Top) -->
|
||||||
<label class="form-label" style="font-size: 14px; font-weight: 700; margin-bottom: 8px;">Select Storage Engine Backend:</label>
|
|
||||||
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
|
||||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px 18px; background: #0f172a; border: 1px solid var(--border); border-radius: 8px;">
|
|
||||||
<input type="radio" name="dbEngineRadio" id="engineRadioSqlite" value="sqlite" {'checked' if storage_info['engine'] == 'sqlite' else ''} onchange="toggleStorageEngineForm()">
|
|
||||||
<span>💾 <strong>Local SQLite 3 (WAL Mode)</strong> <span style="color: var(--text-muted); font-size: 12px;">(Embedded, Zero Configuration)</span></span>
|
|
||||||
</label>
|
|
||||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px 18px; background: #0f172a; border: 1px solid var(--border); border-radius: 8px;">
|
|
||||||
<input type="radio" name="dbEngineRadio" id="engineRadioPostgres" value="postgres" {'checked' if storage_info['engine'] == 'postgres' else ''} onchange="toggleStorageEngineForm()">
|
|
||||||
<span>🐘 <strong>External PostgreSQL Database Server</strong> <span style="color: #34d399; font-size: 12px; font-weight: 600;">(Enterprise Clustering)</span></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- External PostgreSQL Parameters -->
|
|
||||||
<div id="pgConfigSection" style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px; display: {'block' if storage_info['engine'] == 'postgres' else 'none'};">
|
|
||||||
<div style="font-size: 14px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; justify-content: space-between;">
|
|
||||||
<span style="display: flex; align-items: center; gap: 8px;">🐘 External PostgreSQL Connection Parameters</span>
|
|
||||||
<span class="badge badge-active">ENTERPRISE POOL</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-bottom: 16px;">
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Host / IP Address</label>
|
|
||||||
<input type="text" id="pgHost" class="form-control" value="{pg_config.get('host', 'localhost')}" placeholder="localhost">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Port</label>
|
|
||||||
<input type="number" id="pgPort" class="form-control" value="{pg_config.get('port', 5432)}" placeholder="5432">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Database Name</label>
|
|
||||||
<input type="text" id="pgDatabase" class="form-control" value="{pg_config.get('database', 'logar')}" placeholder="logar">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Username</label>
|
|
||||||
<input type="text" id="pgUser" class="form-control" value="{pg_config.get('user', 'postgres')}" placeholder="postgres">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Password</label>
|
|
||||||
<input type="password" id="pgPassword" class="form-control" value="{pg_config.get('password', '')}" placeholder="••••••••">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">SSL Mode</label>
|
|
||||||
<select id="pgSslMode" class="form-control">
|
|
||||||
<option value="prefer" {'selected' if pg_config.get('sslmode') == 'prefer' else ''}>prefer</option>
|
|
||||||
<option value="require" {'selected' if pg_config.get('sslmode') == 'require' else ''}>require</option>
|
|
||||||
<option value="verify-ca" {'selected' if pg_config.get('sslmode') == 'verify-ca' else ''}>verify-ca</option>
|
|
||||||
<option value="verify-full" {'selected' if pg_config.get('sslmode') == 'verify-full' else ''}>verify-full</option>
|
|
||||||
<option value="disable" {'selected' if pg_config.get('sslmode') == 'disable' else ''}>disable</option>
|
|
||||||
<option value="allow" {'selected' if pg_config.get('sslmode') == 'allow' else ''}>allow</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Connect Timeout (sec)</label>
|
|
||||||
<input type="number" id="pgConnectTimeout" class="form-control" value="{pg_config.get('connect_timeout', 5)}" placeholder="5">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label">Connection Pool Size</label>
|
|
||||||
<input type="number" id="pgPoolSize" class="form-control" value="{pg_config.get('pool_size', 10)}" placeholder="10">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
||||||
<button type="button" onclick="testPgConnection()" class="btn btn-secondary">🔍 Test Connection</button>
|
|
||||||
<button type="button" onclick="migrateToPostgres()" class="btn btn-accent">🚀 Migrate SQLite Data to PostgreSQL</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Zero-Knowledge OpenPGP Data Encryption at Rest -->
|
|
||||||
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
|
||||||
<div style="font-size: 14px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
|
||||||
<span>🔐</span> Zero-Knowledge Data Encryption at Rest
|
|
||||||
</div>
|
|
||||||
<label style="display: inline-flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
|
||||||
<input type="checkbox" id="encryptAtRestCheck" {'checked' if config.get('encrypt_at_rest', False) else ''} style="width: 18px; height: 18px; accent-color: var(--accent);">
|
|
||||||
<span style="font-weight: 600; font-size: 14px;">Encrypt Sensitive Telemetry & Log Messages with Server OpenPGP Key</span>
|
|
||||||
</label>
|
|
||||||
<p style="font-size: 12px; color: var(--text-muted); line-height: 1.5; margin-left: 28px;">
|
|
||||||
When enabled, anomaly message payloads stored in the external database are encrypted with the server's OpenPGP public key before writing. Only this hub instance can decrypt records for authorized WebUI and Hermes AI usage, ensuring zero-knowledge privacy on remote database servers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Telemetry Ingestion Cache Mode & Buffer Tuning -->
|
|
||||||
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
||||||
<div style="font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
|
<div style="font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="display: flex; align-items: center; gap: 8px;">⚡ Ingestion Cache & Buffer Mode</span>
|
<span style="display: flex; align-items: center; gap: 8px;">⚡ Ingestion Cache & Buffer Mode</span>
|
||||||
@@ -3508,6 +3447,90 @@ def serve_webui(request: Request = None):
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Zero-Knowledge OpenPGP Data Encryption at Rest (Middle) -->
|
||||||
|
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
||||||
|
<div style="font-size: 14px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>🔐</span> Zero-Knowledge Data Encryption at Rest
|
||||||
|
</div>
|
||||||
|
<label style="display: inline-flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
||||||
|
<input type="checkbox" id="encryptAtRestCheck" {'checked' if config.get('encrypt_at_rest', False) else ''} style="width: 18px; height: 18px; accent-color: var(--accent);">
|
||||||
|
<span style="font-weight: 600; font-size: 14px;">Encrypt Sensitive Telemetry & Log Messages with Server OpenPGP Key</span>
|
||||||
|
</label>
|
||||||
|
<p style="font-size: 12px; color: var(--text-muted); line-height: 1.5; margin-left: 28px;">
|
||||||
|
When enabled, anomaly message payloads stored in the external database are encrypted with the server's OpenPGP public key before writing. Only this hub instance can decrypt records for authorized WebUI and Hermes AI usage, ensuring zero-knowledge privacy on remote database servers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Select Storage Engine Backend (Bottom) -->
|
||||||
|
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
||||||
|
<div style="font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>🗄️</span> Select Storage Engine Backend
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px;">
|
||||||
|
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px 18px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||||
|
<input type="radio" name="dbEngineRadio" id="engineRadioSqlite" value="sqlite" {'checked' if storage_info['engine'] == 'sqlite' else ''} onchange="toggleStorageEngineForm()">
|
||||||
|
<span>💾 <strong>Local SQLite 3 (WAL Mode)</strong> <span style="color: var(--text-muted); font-size: 12px;">(Embedded, Zero Configuration)</span></span>
|
||||||
|
</label>
|
||||||
|
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px 18px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||||
|
<input type="radio" name="dbEngineRadio" id="engineRadioPostgres" value="postgres" {'checked' if storage_info['engine'] == 'postgres' else ''} onchange="toggleStorageEngineForm()">
|
||||||
|
<span>🐘 <strong>External PostgreSQL Database Server</strong> <span style="color: #34d399; font-size: 12px; font-weight: 600;">(Enterprise Clustering)</span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- External PostgreSQL Parameters -->
|
||||||
|
<div id="pgConfigSection" style="background: #151c2e; padding: 20px; border-radius: 10px; border: 1px solid var(--border); display: {'block' if storage_info['engine'] == 'postgres' else 'none'};">
|
||||||
|
<div style="font-size: 14px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<span style="display: flex; align-items: center; gap: 8px;">🐘 External PostgreSQL Connection Parameters</span>
|
||||||
|
<span class="badge badge-active">ENTERPRISE POOL</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-bottom: 16px;">
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Host / IP Address</label>
|
||||||
|
<input type="text" id="pgHost" class="form-control" value="{pg_config.get('host', 'localhost')}" placeholder="localhost">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Port</label>
|
||||||
|
<input type="number" id="pgPort" class="form-control" value="{pg_config.get('port', 5432)}" placeholder="5432">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Database Name</label>
|
||||||
|
<input type="text" id="pgDatabase" class="form-control" value="{pg_config.get('database', 'logar')}" placeholder="logar">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" id="pgUser" class="form-control" value="{pg_config.get('user', 'postgres')}" placeholder="postgres">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" id="pgPassword" class="form-control" value="{pg_config.get('password', '')}" placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">SSL Mode</label>
|
||||||
|
<select id="pgSslMode" class="form-control">
|
||||||
|
<option value="prefer" {'selected' if pg_config.get('sslmode') == 'prefer' else ''}>prefer</option>
|
||||||
|
<option value="require" {'selected' if pg_config.get('sslmode') == 'require' else ''}>require</option>
|
||||||
|
<option value="verify-ca" {'selected' if pg_config.get('sslmode') == 'verify-ca' else ''}>verify-ca</option>
|
||||||
|
<option value="verify-full" {'selected' if pg_config.get('sslmode') == 'verify-full' else ''}>verify-full</option>
|
||||||
|
<option value="disable" {'selected' if pg_config.get('sslmode') == 'disable' else ''}>disable</option>
|
||||||
|
<option value="allow" {'selected' if pg_config.get('sslmode') == 'allow' else ''}>allow</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Connect Timeout (sec)</label>
|
||||||
|
<input type="number" id="pgConnectTimeout" class="form-control" value="{pg_config.get('connect_timeout', 5)}" placeholder="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">Connection Pool Size</label>
|
||||||
|
<input type="number" id="pgPoolSize" class="form-control" value="{pg_config.get('pool_size', 10)}" placeholder="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<button type="button" onclick="testPgConnection()" class="btn btn-secondary">🔍 Test Connection</button>
|
||||||
|
<button type="button" onclick="migrateToPostgres()" class="btn btn-accent">🚀 Migrate SQLite Data to PostgreSQL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Global Save Action Bar -->
|
<!-- Global Save Action Bar -->
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;">
|
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;">
|
||||||
<button type="button" onclick="saveDbConfig()" class="btn btn-primary" style="padding: 10px 24px; font-size: 14px;">
|
<button type="button" onclick="saveDbConfig()" class="btn btn-primary" style="padding: 10px 24px; font-size: 14px;">
|
||||||
@@ -4855,11 +4878,11 @@ def main():
|
|||||||
display_host = "localhost" if hermes_host in ("0.0.0.0", "127.0.0.1") else hermes_host
|
display_host = "localhost" if hermes_host in ("0.0.0.0", "127.0.0.1") else hermes_host
|
||||||
|
|
||||||
print("=" * 64)
|
print("=" * 64)
|
||||||
print(f" LOGAR Server Hub: {config['server_name']}")
|
print(f" LOGAR Server Hub: {config.get('server_name', 'LOGAR Enterprise Hub')}")
|
||||||
print(f" Transport Security: {'mTLS (TLS 1.3)' if ssl_ctx else 'Plain TCP'}")
|
print(f" Transport Security: {'mTLS (TLS 1.3)' if ssl_ctx else 'Plain TCP'}")
|
||||||
print(f" License Quota: {config.get('max_seats', 10)} Active Seats ({ca_info.get('license_id') or 'Local Sub-CA'})")
|
print(f" License Quota: {config.get('max_seats', 10)} Active Seats ({ca_info.get('license_id') or 'Local Sub-CA'})")
|
||||||
print(f" Upstream Authority: {config.get('license_server_url', 'https://logar.eibl.tech')}")
|
print(f" Upstream Authority: {config.get('license_server_url', 'https://logar.eibl.tech')}")
|
||||||
print(f" Server Encryption Fingerprint: {config['server_fingerprint']}")
|
print(f" Server Encryption Fingerprint: {config.get('server_fingerprint', 'N/A')}")
|
||||||
print(f" WebUI & Fleet Dashboard: http://{display_host}:{hermes_port}/webui")
|
print(f" WebUI & Fleet Dashboard: http://{display_host}:{hermes_port}/webui")
|
||||||
print(f" Hermes Reporting API: http://{display_host}:{hermes_port}/api/hermes/report")
|
print(f" Hermes Reporting API: http://{display_host}:{hermes_port}/api/hermes/report")
|
||||||
print("=" * 64)
|
print("=" * 64)
|
||||||
|
|||||||
Reference in New Issue
Block a user