feat(ui,api): clean DB tab header, gray out enterprise features when unlicensed, enforce strict license gating on API
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m22s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m22s
This commit is contained in:
+79
-62
@@ -3073,7 +3073,9 @@ def api_db_flush_buffer():
|
||||
@app.post("/api/db/test-connection", dependencies=[Depends(require_admin_auth)])
|
||||
def api_db_test_connection(req: DbConnectionTestRequest):
|
||||
"""Tests connection to external PostgreSQL database. Requires enterprise_db license feature."""
|
||||
if not is_feature_licensed("enterprise_db"):
|
||||
config = SERVER_STATE.get("config", {})
|
||||
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||
if not is_feature_licensed("enterprise_db", db_path):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature."
|
||||
@@ -3087,12 +3089,13 @@ def api_db_test_connection(req: DbConnectionTestRequest):
|
||||
@app.post("/api/db/switch-backend", dependencies=[Depends(require_admin_auth)])
|
||||
def api_db_switch_backend(req: DbSwitchBackendRequest):
|
||||
"""Switches active database backend between SQLite and PostgreSQL. Requires enterprise_db license feature."""
|
||||
if not is_feature_licensed("enterprise_db"):
|
||||
config = SERVER_STATE.get("config", {})
|
||||
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||
if not is_feature_licensed("enterprise_db", db_path):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature."
|
||||
)
|
||||
config = SERVER_STATE.get("config", {})
|
||||
if req.db_type == "postgres":
|
||||
pg_cfg = {
|
||||
"host": req.host or "localhost",
|
||||
@@ -3130,13 +3133,30 @@ def api_db_switch_backend(req: DbSwitchBackendRequest):
|
||||
@app.post("/api/db/config", dependencies=[Depends(require_admin_auth)])
|
||||
def api_db_update_config(req: DbFullConfigRequest):
|
||||
"""Updates complete database storage engine, cache mode, PostgreSQL parameters, and encryption at rest."""
|
||||
if req.storage_engine == "postgres" and not is_feature_licensed("enterprise_db"):
|
||||
config = SERVER_STATE.get("config", {})
|
||||
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||
is_unlocked = is_feature_licensed("enterprise_db", db_path)
|
||||
|
||||
if not is_unlocked:
|
||||
if req.storage_engine == "postgres":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="External PostgreSQL storage is locked. This capability requires the 'enterprise_db' license feature."
|
||||
)
|
||||
if req.cache_mode == "buffered":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="In-memory ingestion buffering is locked. This capability requires the 'enterprise_db' license feature."
|
||||
)
|
||||
if req.encrypt_at_rest:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Data-at-rest OpenPGP encryption is locked. This capability requires the 'enterprise_db' license feature."
|
||||
)
|
||||
req.storage_engine = "sqlite"
|
||||
req.cache_mode = "direct"
|
||||
req.encrypt_at_rest = False
|
||||
|
||||
config = SERVER_STATE.get("config", {})
|
||||
config["storage_engine"] = req.storage_engine
|
||||
config["db_type"] = req.storage_engine
|
||||
config["cache_mode"] = req.cache_mode
|
||||
@@ -3149,7 +3169,7 @@ def api_db_update_config(req: DbFullConfigRequest):
|
||||
config["batch_size"] = INGEST_QUEUE.batch_size
|
||||
config["max_queue"] = INGEST_QUEUE.max_queue
|
||||
|
||||
if req.pg_config:
|
||||
if req.pg_config and is_unlocked:
|
||||
pg_dict = req.pg_config.model_dump()
|
||||
config["postgres"] = pg_dict
|
||||
if req.storage_engine == "postgres":
|
||||
@@ -3175,12 +3195,13 @@ def api_db_update_config(req: DbFullConfigRequest):
|
||||
@app.post("/api/db/migrate-to-postgres", dependencies=[Depends(require_admin_auth)])
|
||||
def api_db_migrate_to_postgres(req: DbConnectionTestRequest):
|
||||
"""Migrates all telemetry, runs, clients, users, and license configs from SQLite to PostgreSQL."""
|
||||
if not is_feature_licensed("enterprise_db"):
|
||||
config = SERVER_STATE.get("config", {})
|
||||
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||
if not is_feature_licensed("enterprise_db", db_path):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature."
|
||||
)
|
||||
config = SERVER_STATE.get("config", {})
|
||||
sqlite_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||
|
||||
INGEST_QUEUE.flush(db_path=sqlite_path)
|
||||
@@ -3217,8 +3238,6 @@ def serve_webui(request: Request = None):
|
||||
queue_stats = INGEST_QUEUE.get_stats()
|
||||
storage_info = get_storage_engine_info()
|
||||
pg_config = config.get("postgres", {})
|
||||
db_tab_btn_badge = '<span class="badge badge-active" style="margin-left:4px; font-size:10px;">🟢 UNLOCKED</span>' if is_db_unlocked else '<span class="badge" style="background:rgba(239,68,68,0.15); color:#f87171; border:1px solid rgba(239,68,68,0.3); margin-left:4px; font-size:10px;">🔒 ENTERPRISE</span>'
|
||||
|
||||
|
||||
# Fetch clients
|
||||
conn = sqlite3.connect(db_path)
|
||||
@@ -3363,11 +3382,28 @@ def serve_webui(request: Request = None):
|
||||
if detected_host in ("0.0.0.0", "localhost", ""):
|
||||
detected_host = socket.gethostname()
|
||||
|
||||
is_encrypt_at_rest = config.get("encrypt_at_rest", False)
|
||||
is_encrypt_at_rest = config.get("encrypt_at_rest", False) and is_db_unlocked
|
||||
encrypt_status_label = "🔒 OpenPGP Encrypted" if is_encrypt_at_rest else "🔓 Plaintext Storage"
|
||||
encrypt_status_color = "#34d399" if is_encrypt_at_rest else "#94a3b8"
|
||||
|
||||
if is_db_unlocked:
|
||||
# Grayed-out styling & attributes for unlicensed enterprise features
|
||||
ent_badge = '<span class="badge" style="background:rgba(239,68,68,0.15); color:#f87171; border:1px solid rgba(239,68,68,0.3); font-size:10px; margin-left:6px;">ENTERPRISE LICENSE REQUIRED</span>'
|
||||
buf_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||
buf_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||
enc_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||
enc_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||
pg_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||
pg_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||
|
||||
ent_community_notice = "" if is_db_unlocked else f"""
|
||||
<div style="margin-bottom: 20px; background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.25); border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #94a3b8; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
|
||||
<div>
|
||||
<strong style="color: #60a5fa;">💡 Standard Local Storage:</strong> Embedded SQLite 3 storage with synchronous logging is active. High-throughput in-memory buffering, zero-knowledge OpenPGP at-rest encryption, and external PostgreSQL clustering require an <code>enterprise_db</code> license capability.
|
||||
</div>
|
||||
<button onclick="switchTab('pki')" class="btn btn-primary" style="padding: 4px 10px; font-size: 11px;">📜 Manage License</button>
|
||||
</div>
|
||||
"""
|
||||
|
||||
db_tab_pane_html = f"""
|
||||
<!-- Tab 5: Database Engine -->
|
||||
<div class="tab-pane" id="tabPane-db">
|
||||
@@ -3399,7 +3435,7 @@ def serve_webui(request: Request = None):
|
||||
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<div class="kpi-sub">Data-at-Rest Security</div>
|
||||
<div style="font-weight: 700; color: {encrypt_status_color}; margin-top: 4px; font-size: 15px;">{encrypt_status_label}</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Zero-Knowledge Storage on External DB</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">{'Zero-Knowledge Storage on External DB' if is_encrypt_at_rest else 'Local Encrypted/Plaintext Storage'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3411,38 +3447,40 @@ def serve_webui(request: Request = None):
|
||||
<span class="badge badge-os">CONFIG & TUNING</span>
|
||||
</div>
|
||||
|
||||
{ent_community_notice}
|
||||
|
||||
<!-- Telemetry Ingestion Cache Mode & Buffer Tuning (Top) -->
|
||||
<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;">
|
||||
<span style="display: flex; align-items: center; gap: 8px;">⚡ Ingestion Cache & Buffer Mode</span>
|
||||
<button type="button" onclick="flushIngestBuffer()" class="btn btn-accent" style="padding: 4px 10px; font-size: 11px;">🌊 Flush Buffer Now</button>
|
||||
<button type="button" onclick="flushIngestBuffer()" class="btn btn-accent" style="padding: 4px 10px; font-size: 11px; {'' if is_db_unlocked else 'opacity: 0.5; pointer-events: none;'}" {'' if is_db_unlocked else 'disabled'}>🌊 Flush Buffer Now</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 14px;">
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 8px 14px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||
<input type="radio" name="cacheModeRadio" id="cacheModeDirect" value="direct" {'checked' if config.get('cache_mode', 'direct') == 'direct' else ''} onchange="toggleCacheModeParams()">
|
||||
<input type="radio" name="cacheModeRadio" id="cacheModeDirect" value="direct" {'checked' if (config.get('cache_mode', 'direct') == 'direct' or not is_db_unlocked) else ''} onchange="toggleCacheModeParams()">
|
||||
<span>⚡ <strong>Immediate Direct Write</strong> <span style="color: var(--text-muted); font-size: 12px;">(Low Latency / Synchronous Verification)</span></span>
|
||||
</label>
|
||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 8px 14px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||
<input type="radio" name="cacheModeRadio" id="cacheModeBuffered" value="buffered" {'checked' if config.get('cache_mode', 'direct') == 'buffered' else ''} onchange="toggleCacheModeParams()">
|
||||
<span>🚀 <strong>In-Memory Queue Buffer</strong> <span style="color: var(--text-muted); font-size: 12px;">(High-Throughput Batch Flushing)</span></span>
|
||||
<label style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px; {buf_label_style}">
|
||||
<input type="radio" name="cacheModeRadio" id="cacheModeBuffered" value="buffered" {'checked' if (config.get('cache_mode', 'direct') == 'buffered' and is_db_unlocked) else ''} onchange="toggleCacheModeParams()" {buf_disabled_attr}>
|
||||
<span>🚀 <strong>In-Memory Queue Buffer</strong> <span style="color: var(--text-muted); font-size: 12px;">(High-Throughput Batch Flushing)</span> {'' if is_db_unlocked else ent_badge}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bufferParamsSection" style="display: {'grid' if config.get('cache_mode', 'direct') == 'buffered' else 'none'}; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; padding-top: 10px; border-top: 1px solid var(--border);">
|
||||
<div id="bufferParamsSection" style="display: {'grid' if (config.get('cache_mode', 'direct') == 'buffered' and is_db_unlocked) else 'none'}; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; padding-top: 10px; border-top: 1px solid var(--border);">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label">Flush Interval (seconds)</label>
|
||||
<input type="number" id="bufferFlushInterval" step="0.5" class="form-control" value="{config.get('flush_interval', queue_stats.get('flush_interval_seconds', 2.0))}" placeholder="2.0">
|
||||
<input type="number" id="bufferFlushInterval" step="0.5" class="form-control" value="{config.get('flush_interval', queue_stats.get('flush_interval_seconds', 2.0))}" placeholder="2.0" {buf_disabled_attr}>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label">Batch Size Threshold</label>
|
||||
<input type="number" id="bufferBatchSize" class="form-control" value="{config.get('batch_size', queue_stats.get('batch_size_threshold', 100))}" placeholder="100">
|
||||
<input type="number" id="bufferBatchSize" class="form-control" value="{config.get('batch_size', queue_stats.get('batch_size_threshold', 100))}" placeholder="100" {buf_disabled_attr}>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label">Max Buffer Capacity</label>
|
||||
<input type="number" id="bufferMaxQueue" class="form-control" value="{config.get('max_queue', 50000)}" placeholder="50000">
|
||||
<input type="number" id="bufferMaxQueue" class="form-control" value="{config.get('max_queue', 50000)}" placeholder="50000" {buf_disabled_attr}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3452,9 +3490,10 @@ def serve_webui(request: Request = None):
|
||||
<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);">
|
||||
<label style="display: inline-flex; align-items: center; gap: 10px; margin-bottom: 8px; {enc_label_style}">
|
||||
<input type="checkbox" id="encryptAtRestCheck" {'checked' if (config.get('encrypt_at_rest', False) and is_db_unlocked) else ''} style="width: 18px; height: 18px; accent-color: var(--accent);" {enc_disabled_attr}>
|
||||
<span style="font-weight: 600; font-size: 14px;">Encrypt Sensitive Telemetry & Log Messages with Server OpenPGP Key</span>
|
||||
{'' if is_db_unlocked else ent_badge}
|
||||
</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.
|
||||
@@ -3468,17 +3507,17 @@ def serve_webui(request: Request = None):
|
||||
</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()">
|
||||
<input type="radio" name="dbEngineRadio" id="engineRadioSqlite" value="sqlite" {'checked' if (storage_info['engine'] == 'sqlite' or not is_db_unlocked) 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 style="display: inline-flex; align-items: center; gap: 8px; padding: 12px 18px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px; {pg_label_style}">
|
||||
<input type="radio" name="dbEngineRadio" id="engineRadioPostgres" value="postgres" {'checked' if (storage_info['engine'] == 'postgres' and is_db_unlocked) else ''} onchange="toggleStorageEngineForm()" {pg_disabled_attr}>
|
||||
<span>🐘 <strong>External PostgreSQL Database Server</strong> <span style="color: #34d399; font-size: 12px; font-weight: 600;">(Enterprise Clustering)</span> {'' if is_db_unlocked else ent_badge}</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 id="pgConfigSection" style="background: #151c2e; padding: 20px; border-radius: 10px; border: 1px solid var(--border); display: {'block' if (storage_info['engine'] == 'postgres' and is_db_unlocked) 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>
|
||||
@@ -3486,27 +3525,27 @@ def serve_webui(request: Request = None):
|
||||
<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">
|
||||
<input type="text" id="pgHost" class="form-control" value="{pg_config.get('host', 'localhost')}" placeholder="localhost" {pg_disabled_attr}>
|
||||
</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">
|
||||
<input type="number" id="pgPort" class="form-control" value="{pg_config.get('port', 5432)}" placeholder="5432" {pg_disabled_attr}>
|
||||
</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">
|
||||
<input type="text" id="pgDatabase" class="form-control" value="{pg_config.get('database', 'logar')}" placeholder="logar" {pg_disabled_attr}>
|
||||
</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">
|
||||
<input type="text" id="pgUser" class="form-control" value="{pg_config.get('user', 'postgres')}" placeholder="postgres" {pg_disabled_attr}>
|
||||
</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="••••••••">
|
||||
<input type="password" id="pgPassword" class="form-control" value="{pg_config.get('password', '')}" placeholder="••••••••" {pg_disabled_attr}>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label">SSL Mode</label>
|
||||
<select id="pgSslMode" class="form-control">
|
||||
<select id="pgSslMode" class="form-control" {pg_disabled_attr}>
|
||||
<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>
|
||||
@@ -3517,16 +3556,16 @@ def serve_webui(request: Request = None):
|
||||
</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">
|
||||
<input type="number" id="pgConnectTimeout" class="form-control" value="{pg_config.get('connect_timeout', 5)}" placeholder="5" {pg_disabled_attr}>
|
||||
</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">
|
||||
<input type="number" id="pgPoolSize" class="form-control" value="{pg_config.get('pool_size', 10)}" placeholder="10" {pg_disabled_attr}>
|
||||
</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>
|
||||
<button type="button" onclick="testPgConnection()" class="btn btn-secondary" {pg_disabled_attr}>🔍 Test Connection</button>
|
||||
<button type="button" onclick="migrateToPostgres()" class="btn btn-accent" {pg_disabled_attr}>🚀 Migrate SQLite Data to PostgreSQL</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3540,28 +3579,6 @@ def serve_webui(request: Request = None):
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
db_tab_pane_html = f"""
|
||||
<!-- Tab 5: Database Engine (Locked State) -->
|
||||
<div class="tab-pane" id="tabPane-db">
|
||||
<div class="card" style="text-align: center; padding: 48px 24px; border-color: rgba(239, 68, 68, 0.3);">
|
||||
<div style="font-size: 52px; margin-bottom: 16px;">🔒</div>
|
||||
<h2 style="font-size: 22px; font-weight: 800; margin-bottom: 8px;">Enterprise Database & High-Throughput Engine is Locked</h2>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<span class="badge" style="background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.4); padding: 6px 14px; font-size: 12px;">
|
||||
REQUIRES 'enterprise_db' LICENSE FEATURE
|
||||
</span>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: var(--text-muted); max-width: 640px; margin: 0 auto 24px;">
|
||||
Direct PostgreSQL storage backend clustering, automatic schema synchronization, and customizable high-throughput batch-flush controls are gated behind the <strong>enterprise_db</strong> capability.
|
||||
</p>
|
||||
<div style="display: flex; justify-content: center; gap: 12px; flex-wrap: wrap;">
|
||||
<button onclick="switchTab('pki')" class="btn btn-primary">📜 Go to Sub-CA & Licensing</button>
|
||||
<button onclick="openImportLicModal()" class="btn btn-accent">📥 Import Enterprise .lic Package</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html_content = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -3793,7 +3810,7 @@ def serve_webui(request: Request = None):
|
||||
<button class="tab-btn" id="tabBtn-hermes" onclick="switchTab('hermes')">🤖 Hermes AI & Tokens ({len(hermes_tokens)})</button>
|
||||
<button class="tab-btn" id="tabBtn-anomalies" onclick="switchTab('anomalies')">📊 Verified Telemetry ({len(anomalies)})</button>
|
||||
<button class="tab-btn" id="tabBtn-pki" onclick="switchTab('pki')">📜 Sub-CA & Licensing</button>
|
||||
<button class="tab-btn" id="tabBtn-db" onclick="switchTab('db')">💾 DB Engine {db_tab_btn_badge}</button>
|
||||
<button class="tab-btn" id="tabBtn-db" onclick="switchTab('db')">💾 DB Engine</button>
|
||||
</div>
|
||||
|
||||
<!-- KPI Summary Cards -->
|
||||
|
||||
+48
-13
@@ -710,7 +710,53 @@ class TestServerComponent(unittest.TestCase):
|
||||
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
|
||||
self.assertEqual(decrypted_msg, raw_msg)
|
||||
|
||||
# 2. Test updating config via api_db_update_config (direct -> buffered, encrypt_at_rest = True)
|
||||
# 2. Test that without enterprise_db license, buffering, encryption at rest, and postgres are blocked
|
||||
cfg_unlicensed_buf = Server.DbFullConfigRequest(
|
||||
storage_engine="sqlite",
|
||||
cache_mode="buffered",
|
||||
encrypt_at_rest=False
|
||||
)
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_update_config(cfg_unlicensed_buf)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
cfg_unlicensed_enc = Server.DbFullConfigRequest(
|
||||
storage_engine="sqlite",
|
||||
cache_mode="direct",
|
||||
encrypt_at_rest=True
|
||||
)
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_update_config(cfg_unlicensed_enc)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
pg_req = Server.DbFullConfigRequest(
|
||||
storage_engine="postgres",
|
||||
cache_mode="direct",
|
||||
pg_config=Server.DbConnectionTestRequest(host="db.corp.internal", port=5432, database="logar", user="postgres", password="")
|
||||
)
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_update_config(pg_req)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
# 3. Standard local SQLite configuration succeeds when unlicensed
|
||||
cfg_standard = Server.DbFullConfigRequest(
|
||||
storage_engine="sqlite",
|
||||
cache_mode="direct",
|
||||
encrypt_at_rest=False
|
||||
)
|
||||
res_std = Server.api_db_update_config(cfg_standard)
|
||||
self.assertEqual(res_std["status"], "success")
|
||||
|
||||
# 4. Unlock enterprise_db feature in license_config
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# 5. Test updating config with enterprise_db unlocked (direct -> buffered, encrypt_at_rest = True)
|
||||
cfg_req = Server.DbFullConfigRequest(
|
||||
storage_engine="sqlite",
|
||||
cache_mode="buffered",
|
||||
@@ -726,18 +772,7 @@ class TestServerComponent(unittest.TestCase):
|
||||
self.assertEqual(Server.INGEST_QUEUE.flush_interval, 4.0)
|
||||
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
|
||||
|
||||
# 3. Test that switching to external PostgreSQL requires enterprise_db license
|
||||
pg_req = Server.DbFullConfigRequest(
|
||||
storage_engine="postgres",
|
||||
cache_mode="direct",
|
||||
pg_config=Server.DbConnectionTestRequest(host="db.corp.internal", port=5432, database="logar", user="postgres", password="")
|
||||
)
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_update_config(pg_req)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
# 4. Verify storage engine info output
|
||||
# 6. Verify storage engine info output
|
||||
info = Server.get_storage_engine_info(db_path=self.test_db)
|
||||
self.assertEqual(info["cache_mode"], "buffered")
|
||||
self.assertTrue(info["encrypt_at_rest"])
|
||||
|
||||
Reference in New Issue
Block a user