Showcase Mode: full read-only admin access for client demos
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) <noreply@anthropic.com>
This commit is contained in:
parent
dfab02f114
commit
9f48a050c8
131
llm_team_ui.py
131
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
|
||||
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"]})
|
||||
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 & SECURITY TAB -->
|
||||
<div id="tab-security" class="tab-content">
|
||||
<div class="card">
|
||||
<h3>Demo Mode
|
||||
<button class="btn" id="admin-demo-btn" style="margin-left:auto" onclick="adminToggleDemo()">Enable Demo</button>
|
||||
<div class="card" style="border-color:rgba(217,70,239,0.3)">
|
||||
<h3 style="color:#d946ef">Showcase Mode
|
||||
<div style="margin-left:auto;display:flex;gap:6px">
|
||||
<button class="btn" id="admin-showcase-btn" onclick="toggleShowcase()">Enable Showcase</button>
|
||||
<button class="btn btn-r" id="admin-demo-off" onclick="demoOff()" style="display:none">Turn Off</button>
|
||||
</div>
|
||||
</h3>
|
||||
<p style="font-size:12px;color:var(--text2);margin-bottom:10px">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.</p>
|
||||
<p style="font-size:12px;color:var(--text2);margin-bottom:10px"><strong>Showcase mode</strong> 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 <strong>cannot</strong> change settings, ban IPs, delete data, or modify configs. Perfect for demos.</p>
|
||||
<div id="demo-status-admin" style="font-size:13px">Status: <strong style="color:var(--text2)">Off</strong></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@ -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: <strong style="color:var(--green)">ON</strong>' + (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: <strong style="color:#d946ef">SHOWCASE MODE</strong> — 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: <strong style="color:var(--green)">DEMO ON</strong>' + (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: <strong style="color:var(--text2)">Off</strong>';
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user