From 9f48a050c8f0b85c8c554259bdda197a08e0458a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 05:19:41 -0500 Subject: [PATCH] Showcase Mode: full read-only admin access for client demos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New mode: Showcase (replaces basic demo mode for client demos) - Visitors see EVERYTHING: Admin, Monitor, Logs, Threat Intel, Lab, History, Meta-Pipelines — all without logging in - Read-only: all GET requests allowed on all routes - Allowed POSTs: team runs, self-analysis, IP enrichment (read-like operations that don't modify system config) - Blocked POSTs: config changes, bans, deletes, bulk archive Admin UI (Security tab): - "Enable Showcase" button (magenta) — one click to activate - "Turn Off" button appears when active - Clear description of what visitors can and can't do - Status shows "SHOWCASE MODE" with magenta styling Banner: - Magenta gradient banner on all pages when showcase is active - Shows: "Showcase Mode — Full Read-Only Access — Admin · Monitor · Logs · Lab · History" - Demo button in nav shows "Showcase" in magenta Co-Authored-By: Claude Opus 4.6 (1M context) --- llm_team_ui.py | 135 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 40 deletions(-) diff --git a/llm_team_ui.py b/llm_team_ui.py index 425e4c2..77dc365 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -70,13 +70,18 @@ LOGIN_RATE_MAX = 5 # IPs that never get rate-limited (your LAN, localhost) ALLOWLIST_IPS = {"127.0.0.1", "::1", "192.168.1.1"} # Demo mode state — toggled by admin at runtime -_demo_mode = {"active": False, "started_by": None} +_demo_mode = {"active": False, "started_by": None, "showcase": False} -# Admin-only write routes — blocked in demo for non-admin users -ADMIN_WRITE_ROUTES = { - "/api/admin/config": ["POST"], - "/api/admin/test-provider": ["POST"], - "/api/auth/login": ["POST"], +# Routes that demo users CAN trigger (read-like POSTs — enrichment, self-analysis, team runs) +DEMO_ALLOWED_POSTS = { + "/api/run", "/api/self-analyze", "/api/admin/security/enrich", +} + +# Routes that demo users CANNOT touch (destructive writes) +DEMO_BLOCKED_POSTS = { + "/api/admin/config", "/api/admin/test-provider", "/api/admin/security/ban", + "/api/admin/security/mass-ban", "/api/demo/toggle", "/api/demo/allowlist", + "/api/runs/bulk-archive", "/api/meta-pipeline", } @@ -123,10 +128,14 @@ def login_required(f): def admin_required(f): @wraps(f) def decorated(*args, **kwargs): - # Demo mode: allow read access (GET), block writes unless admin + # Demo/showcase mode: full read access to everything if is_demo(): if request.method == "GET": return f(*args, **kwargs) + # Allow specific read-like POSTs (enrichment, self-analysis, team runs) + if request.path in DEMO_ALLOWED_POSTS: + return f(*args, **kwargs) + # Block destructive writes if not is_admin(): return jsonify({"error": "demo mode: read-only", "demo": True}), 403 if not session.get("user_id"): @@ -444,16 +453,28 @@ def logout_page(): @app.route("/api/demo/status") def demo_status(): - return jsonify({"active": is_demo(), "started_by": _demo_mode.get("started_by")}) + return jsonify({"active": is_demo(), "showcase": _demo_mode.get("showcase", False), "started_by": _demo_mode.get("started_by")}) @app.route("/api/demo/toggle", methods=["POST"]) def demo_toggle(): if not session.get("user_id") or not is_admin(): return jsonify({"error": "admin only"}), 403 - _demo_mode["active"] = not _demo_mode["active"] - _demo_mode["started_by"] = session.get("username") if _demo_mode["active"] else None - return jsonify({"active": _demo_mode["active"]}) + data = request.json if request.is_json else {} + mode = data.get("mode", "toggle") # toggle, showcase, off + if mode == "off": + _demo_mode["active"] = False + _demo_mode["showcase"] = False + _demo_mode["started_by"] = None + elif mode == "showcase": + _demo_mode["active"] = True + _demo_mode["showcase"] = True + _demo_mode["started_by"] = session.get("username") + else: + _demo_mode["active"] = not _demo_mode["active"] + _demo_mode["showcase"] = _demo_mode["active"] + _demo_mode["started_by"] = session.get("username") if _demo_mode["active"] else None + return jsonify({"active": _demo_mode["active"], "showcase": _demo_mode["showcase"]}) @app.route("/api/demo/allowlist", methods=["GET"]) @@ -2707,27 +2728,42 @@ async function deleteRun(id) { // ─── DEMO MODE ─────────────────────────────── async function checkDemo() { try { - const r = await fetch('/api/demo/status'); - const d = await r.json(); - updateDemoUI(d.active); + var r = await fetch('/api/demo/status'); + var d = await r.json(); + updateDemoUI(d.active, d.showcase); } catch(e) {} } -function updateDemoUI(active) { - const btn = document.getElementById('demo-toggle'); - const banner = document.getElementById('demo-banner'); +function updateDemoUI(active, showcase) { + var btn = document.getElementById('demo-toggle'); + var banner = document.getElementById('demo-banner'); if (btn) { btn.style.display = ''; - btn.textContent = active ? 'Demo ON' : 'Demo'; - btn.style.color = active ? '#22c55e' : 'var(--orange)'; - btn.style.borderColor = active ? 'rgba(34,197,94,0.4)' : 'rgba(245,158,11,0.3)'; + if (showcase) { + btn.textContent = 'Showcase'; + btn.style.color = '#d946ef'; + btn.style.borderColor = 'rgba(217,70,239,0.4)'; + } else if (active) { + btn.textContent = 'Demo ON'; + btn.style.color = '#4ade80'; + btn.style.borderColor = 'rgba(74,222,128,0.4)'; + } else { + btn.textContent = 'Demo'; + btn.style.color = 'var(--orange)'; + btn.style.borderColor = 'rgba(245,158,11,0.3)'; + } } if (active) { if (!banner) { - const b = document.createElement('div'); + var b = document.createElement('div'); b.id = 'demo-banner'; - b.style.cssText = 'position:fixed;top:0;left:0;right:0;background:linear-gradient(90deg,rgba(34,197,94,0.08),rgba(34,197,94,0.15),rgba(34,197,94,0.08));border-bottom:1px solid rgba(34,197,94,0.25);color:#22c55e;text-align:center;font-size:12px;padding:6px;z-index:50;font-weight:600;letter-spacing:1px'; - b.textContent = 'DEMO MODE'; + if (showcase) { + b.style.cssText = 'position:fixed;top:0;left:0;right:0;background:linear-gradient(90deg,rgba(217,70,239,0.05),rgba(217,70,239,0.12),rgba(217,70,239,0.05));border-bottom:2px solid rgba(217,70,239,0.3);color:#d946ef;text-align:center;font-size:11px;padding:8px;z-index:50;font-weight:700;letter-spacing:2px;font-family:JetBrains Mono,monospace;text-transform:uppercase'; + b.textContent = 'Showcase Mode — Full Read-Only Access — Admin · Monitor · Logs · Lab · History'; + } else { + b.style.cssText = 'position:fixed;top:0;left:0;right:0;background:linear-gradient(90deg,rgba(74,222,128,0.05),rgba(74,222,128,0.1),rgba(74,222,128,0.05));border-bottom:1px solid rgba(74,222,128,0.25);color:#4ade80;text-align:center;font-size:11px;padding:6px;z-index:50;font-weight:600;letter-spacing:1px;font-family:JetBrains Mono,monospace;text-transform:uppercase'; + b.textContent = 'Demo Mode'; + } document.body.prepend(b); } } else if (banner) { @@ -2963,11 +2999,14 @@ ADMIN_HTML = r"""
-
-

Demo Mode - +
+

Showcase Mode +
+ + +

-

When active, the public can view and use the Team UI, Lab, and all modes without logging in. Admin settings (API keys, config saves) are read-only for non-admins.

+

Showcase mode gives visitors full read-only access to everything — Admin, Monitor, Logs, Threat Intel, Lab, History. They can run teams, view enrichments, and trigger self-analysis reports. They cannot change settings, ban IPs, delete data, or modify configs. Perfect for demos.

Status: Off
@@ -3210,25 +3249,41 @@ function switchTab(name) { } async function loadDemoStatus() { - const r = await fetch('/api/demo/status'); - const d = await r.json(); - const btn = document.getElementById('admin-demo-btn'); - const st = document.getElementById('demo-status-admin'); - if (d.active) { - btn.textContent = 'Disable Demo'; - btn.className = 'btn btn-r'; - st.innerHTML = 'Status: ON' + (d.started_by ? ' (by ' + d.started_by + ')' : ''); - } else { - btn.textContent = 'Enable Demo'; + var r = await fetch('/api/demo/status'); + var d = await r.json(); + var btn = document.getElementById('admin-showcase-btn'); + var offBtn = document.getElementById('admin-demo-off'); + var st = document.getElementById('demo-status-admin'); + if (d.active && d.showcase) { + btn.textContent = 'Showcase Active'; + btn.className = 'btn'; + btn.style.cssText = 'margin-left:auto;border-color:#d946ef;color:#d946ef'; + offBtn.style.display = ''; + st.innerHTML = 'Status: SHOWCASE MODE — full read-only access for visitors' + (d.started_by ? ' (by ' + d.started_by + ')' : ''); + } else if (d.active) { + btn.textContent = 'Demo Active'; btn.className = 'btn btn-g'; + offBtn.style.display = ''; + st.innerHTML = 'Status: DEMO ON' + (d.started_by ? ' (by ' + d.started_by + ')' : ''); + } else { + btn.textContent = 'Enable Showcase'; + btn.className = 'btn'; + btn.style.cssText = 'border-color:rgba(217,70,239,0.3);color:#d946ef'; + offBtn.style.display = 'none'; st.innerHTML = 'Status: Off'; } } -async function adminToggleDemo() { - await fetch('/api/demo/toggle', {method:'POST'}); +async function toggleShowcase() { + await fetch('/api/demo/toggle', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({mode:'showcase'})}); loadDemoStatus(); - toast('Demo mode toggled'); + toast('Showcase mode enabled', true); +} + +async function demoOff() { + await fetch('/api/demo/toggle', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({mode:'off'})}); + loadDemoStatus(); + toast('Demo mode disabled', true); } async function loadAllowlist() {