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:
+215
-198
@@ -3073,7 +3073,9 @@ def api_db_flush_buffer():
|
|||||||
@app.post("/api/db/test-connection", dependencies=[Depends(require_admin_auth)])
|
@app.post("/api/db/test-connection", dependencies=[Depends(require_admin_auth)])
|
||||||
def api_db_test_connection(req: DbConnectionTestRequest):
|
def api_db_test_connection(req: DbConnectionTestRequest):
|
||||||
"""Tests connection to external PostgreSQL database. Requires enterprise_db license feature."""
|
"""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(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature."
|
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)])
|
@app.post("/api/db/switch-backend", dependencies=[Depends(require_admin_auth)])
|
||||||
def api_db_switch_backend(req: DbSwitchBackendRequest):
|
def api_db_switch_backend(req: DbSwitchBackendRequest):
|
||||||
"""Switches active database backend between SQLite and PostgreSQL. Requires enterprise_db license feature."""
|
"""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(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature."
|
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":
|
if req.db_type == "postgres":
|
||||||
pg_cfg = {
|
pg_cfg = {
|
||||||
"host": req.host or "localhost",
|
"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)])
|
@app.post("/api/db/config", dependencies=[Depends(require_admin_auth)])
|
||||||
def api_db_update_config(req: DbFullConfigRequest):
|
def api_db_update_config(req: DbFullConfigRequest):
|
||||||
"""Updates complete database storage engine, cache mode, PostgreSQL parameters, and encryption at rest."""
|
"""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"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail="External PostgreSQL storage is locked. This capability requires the 'enterprise_db' license feature."
|
|
||||||
)
|
|
||||||
|
|
||||||
config = SERVER_STATE.get("config", {})
|
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["storage_engine"] = req.storage_engine
|
config["storage_engine"] = req.storage_engine
|
||||||
config["db_type"] = req.storage_engine
|
config["db_type"] = req.storage_engine
|
||||||
config["cache_mode"] = req.cache_mode
|
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["batch_size"] = INGEST_QUEUE.batch_size
|
||||||
config["max_queue"] = INGEST_QUEUE.max_queue
|
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()
|
pg_dict = req.pg_config.model_dump()
|
||||||
config["postgres"] = pg_dict
|
config["postgres"] = pg_dict
|
||||||
if req.storage_engine == "postgres":
|
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)])
|
@app.post("/api/db/migrate-to-postgres", dependencies=[Depends(require_admin_auth)])
|
||||||
def api_db_migrate_to_postgres(req: DbConnectionTestRequest):
|
def api_db_migrate_to_postgres(req: DbConnectionTestRequest):
|
||||||
"""Migrates all telemetry, runs, clients, users, and license configs from SQLite to PostgreSQL."""
|
"""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(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature."
|
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)
|
sqlite_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||||
|
|
||||||
INGEST_QUEUE.flush(db_path=sqlite_path)
|
INGEST_QUEUE.flush(db_path=sqlite_path)
|
||||||
@@ -3217,8 +3238,6 @@ def serve_webui(request: Request = None):
|
|||||||
queue_stats = INGEST_QUEUE.get_stats()
|
queue_stats = INGEST_QUEUE.get_stats()
|
||||||
storage_info = get_storage_engine_info()
|
storage_info = get_storage_engine_info()
|
||||||
pg_config = config.get("postgres", {})
|
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
|
# Fetch clients
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
@@ -3363,205 +3382,203 @@ def serve_webui(request: Request = None):
|
|||||||
if detected_host in ("0.0.0.0", "localhost", ""):
|
if detected_host in ("0.0.0.0", "localhost", ""):
|
||||||
detected_host = socket.gethostname()
|
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_label = "🔒 OpenPGP Encrypted" if is_encrypt_at_rest else "🔓 Plaintext Storage"
|
||||||
encrypt_status_color = "#34d399" if is_encrypt_at_rest else "#94a3b8"
|
encrypt_status_color = "#34d399" if is_encrypt_at_rest else "#94a3b8"
|
||||||
|
|
||||||
if is_db_unlocked:
|
# Grayed-out styling & attributes for unlicensed enterprise features
|
||||||
db_tab_pane_html = f"""
|
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>'
|
||||||
<!-- Tab 5: Database Engine -->
|
buf_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||||
<div class="tab-pane" id="tabPane-db">
|
buf_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||||
<!-- Ingestion Buffer Live Stats -->
|
enc_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||||
<div class="card" style="border-color: rgba(0, 229, 255, 0.3);">
|
enc_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||||
<div class="card-header">
|
pg_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||||
<div>
|
pg_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||||
<span class="card-title" style="color: #00E5FF;">💾 Storage Engine & Live Telemetry Metrics</span>
|
|
||||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">Centralized high-availability SQL pooling, buffer telemetry, and encryption at rest</div>
|
ent_community_notice = "" if is_db_unlocked else f"""
|
||||||
</div>
|
<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;">
|
||||||
<span class="badge badge-active">{storage_info['engine'].upper()} BACKEND</span>
|
<div>
|
||||||
</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 style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-top: 14px;">
|
</div>
|
||||||
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
<button onclick="switchTab('pki')" class="btn btn-primary" style="padding: 4px 10px; font-size: 11px;">📜 Manage License</button>
|
||||||
<div class="kpi-sub">Active Storage Backend</div>
|
</div>
|
||||||
<div style="font-weight: 700; color: #38bdf8; margin-top: 4px; font-size: 15px;">{storage_info['backend_display']}</div>
|
"""
|
||||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">{storage_info['status']}</div>
|
|
||||||
</div>
|
db_tab_pane_html = f"""
|
||||||
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
<!-- Tab 5: Database Engine -->
|
||||||
<div class="kpi-sub">Telemetry Records</div>
|
<div class="tab-pane" id="tabPane-db">
|
||||||
<div class="kpi-value" style="color: #34d399; margin-top: 4px; font-size: 18px;">{storage_info['table_stats']['active_issues']} <span style="font-size: 12px; color: var(--text-muted);">Issues</span> / {storage_info['table_stats']['ingest_runs']} <span style="font-size: 12px; color: var(--text-muted);">Runs</span></div>
|
<!-- Ingestion Buffer Live Stats -->
|
||||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Clients: {storage_info['table_stats']['clients']} | Users: {storage_info['table_stats']['users']}</div>
|
<div class="card" style="border-color: rgba(0, 229, 255, 0.3);">
|
||||||
</div>
|
<div class="card-header">
|
||||||
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
<div>
|
||||||
<div class="kpi-sub">Buffered Events in RAM</div>
|
<span class="card-title" style="color: #00E5FF;">💾 Storage Engine & Live Telemetry Metrics</span>
|
||||||
<div class="kpi-value" id="liveQueueSize" style="color: #c084fc; margin-top: 4px; font-size: 18px;">{queue_stats['queue_size']} <span style="font-size: 12px; color: var(--text-muted);">events</span></div>
|
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">Centralized high-availability SQL pooling, buffer telemetry, and encryption at rest</div>
|
||||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Ingested: {queue_stats['total_ingested']} | Flushed: {queue_stats['total_flushed']}</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="badge badge-active">{storage_info['engine'].upper()} BACKEND</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-top: 14px;">
|
||||||
<!-- Storage Engine & SQL Configurator Form -->
|
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
<div class="card">
|
<div class="kpi-sub">Active Storage Backend</div>
|
||||||
<div class="card-header">
|
<div style="font-weight: 700; color: #38bdf8; margin-top: 4px; font-size: 15px;">{storage_info['backend_display']}</div>
|
||||||
<span class="card-title">Database Storage Engine Configuration</span>
|
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">{storage_info['status']}</div>
|
||||||
<span class="badge badge-os">CONFIG & TUNING</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
<!-- Telemetry Ingestion Cache Mode & Buffer Tuning (Top) -->
|
<div class="kpi-sub">Telemetry Records</div>
|
||||||
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
<div class="kpi-value" style="color: #34d399; margin-top: 4px; font-size: 18px;">{storage_info['table_stats']['active_issues']} <span style="font-size: 12px; color: var(--text-muted);">Issues</span> / {storage_info['table_stats']['ingest_runs']} <span style="font-size: 12px; color: var(--text-muted);">Runs</span></div>
|
||||||
<div style="font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
|
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Clients: {storage_info['table_stats']['clients']} | Users: {storage_info['table_stats']['users']}</div>
|
||||||
<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>
|
|
||||||
</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()">
|
|
||||||
<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>
|
|
||||||
</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 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">
|
|
||||||
</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">
|
|
||||||
</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">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
<!-- Zero-Knowledge OpenPGP Data Encryption at Rest (Middle) -->
|
<div class="kpi-sub">Buffered Events in RAM</div>
|
||||||
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
<div class="kpi-value" id="liveQueueSize" style="color: #c084fc; margin-top: 4px; font-size: 18px;">{queue_stats['queue_size']} <span style="font-size: 12px; color: var(--text-muted);">events</span></div>
|
||||||
<div style="font-size: 14px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">Ingested: {queue_stats['total_ingested']} | Flushed: {queue_stats['total_flushed']}</div>
|
||||||
<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>
|
</div>
|
||||||
|
<div style="background: #0f172a; padding: 14px; border-radius: 8px; border: 1px solid var(--border);">
|
||||||
<!-- Select Storage Engine Backend (Bottom) -->
|
<div class="kpi-sub">Data-at-Rest Security</div>
|
||||||
<div style="background: #0f172a; padding: 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 20px;">
|
<div style="font-weight: 700; color: {encrypt_status_color}; margin-top: 4px; font-size: 15px;">{encrypt_status_label}</div>
|
||||||
<div style="font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
|
<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>
|
||||||
<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 -->
|
|
||||||
<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;">
|
|
||||||
💾 Save Database & Cache Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
|
||||||
else:
|
<!-- Storage Engine & SQL Configurator Form -->
|
||||||
db_tab_pane_html = f"""
|
<div class="card">
|
||||||
<!-- Tab 5: Database Engine (Locked State) -->
|
<div class="card-header">
|
||||||
<div class="tab-pane" id="tabPane-db">
|
<span class="card-title">Database Storage Engine Configuration</span>
|
||||||
<div class="card" style="text-align: center; padding: 48px 24px; border-color: rgba(239, 68, 68, 0.3);">
|
<span class="badge badge-os">CONFIG & TUNING</span>
|
||||||
<div style="font-size: 52px; margin-bottom: 16px;">🔒</div>
|
</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;">
|
{ent_community_notice}
|
||||||
<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
|
<!-- Telemetry Ingestion Cache Mode & Buffer Tuning (Top) -->
|
||||||
</span>
|
<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; {'' if is_db_unlocked else 'opacity: 0.5; pointer-events: none;'}" {'' if is_db_unlocked else 'disabled'}>🌊 Flush Buffer Now</button>
|
||||||
</div>
|
</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.
|
<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' 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; 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' 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" {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" {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" {buf_disabled_attr}>
|
||||||
|
</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; 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.
|
||||||
</p>
|
</p>
|
||||||
<div style="display: flex; justify-content: center; gap: 12px; flex-wrap: wrap;">
|
</div>
|
||||||
<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>
|
<!-- 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' 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; 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' 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>
|
||||||
|
</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" {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" {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" {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" {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="••••••••" {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" {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>
|
||||||
|
<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" {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" {pg_disabled_attr}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Global Save Action Bar -->
|
||||||
|
<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;">
|
||||||
|
💾 Save Database & Cache Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
html_content = f"""<!DOCTYPE html>
|
html_content = f"""<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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-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-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-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>
|
</div>
|
||||||
|
|
||||||
<!-- KPI Summary Cards -->
|
<!-- KPI Summary Cards -->
|
||||||
|
|||||||
+48
-13
@@ -710,7 +710,53 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
|
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
|
||||||
self.assertEqual(decrypted_msg, raw_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(
|
cfg_req = Server.DbFullConfigRequest(
|
||||||
storage_engine="sqlite",
|
storage_engine="sqlite",
|
||||||
cache_mode="buffered",
|
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.flush_interval, 4.0)
|
||||||
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
|
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
|
||||||
|
|
||||||
# 3. Test that switching to external PostgreSQL requires enterprise_db license
|
# 6. Verify storage engine info output
|
||||||
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
|
|
||||||
info = Server.get_storage_engine_info(db_path=self.test_db)
|
info = Server.get_storage_engine_info(db_path=self.test_db)
|
||||||
self.assertEqual(info["cache_mode"], "buffered")
|
self.assertEqual(info["cache_mode"], "buffered")
|
||||||
self.assertTrue(info["encrypt_at_rest"])
|
self.assertTrue(info["encrypt_at_rest"])
|
||||||
|
|||||||
Reference in New Issue
Block a user