Enforce public/private visibility for web UI pages

Dev-only pages (sources, trends, complexity, idea-analysis, false-positives,
similarity, landscape, export) now require @admin_required and are hidden
from nav in production mode. Citations page keeps the graph public but
hides influence/BCP tabs behind --dev flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 20:52:43 +01:00
parent dec8667193
commit ae5e5f8cbf
4 changed files with 85 additions and 8 deletions

View File

@@ -84,6 +84,44 @@ All agents should:
- Use `rich` for console output - Use `rich` for console output
- Save multi-step workflows as scripts in `scripts/` - Save multi-step workflows as scripts in `scripts/`
## Web UI: Public vs Dev-Only Pages
The web dashboard runs in two modes: production (default) and dev (`--dev` flag).
**When adding new pages, always decide which mode they belong to.**
Use `@admin_required` decorator on dev-only routes, and `{% if is_admin %}` in `base.html` nav links.
### Public pages (visible to everyone)
Pages showing **publicly available data** or **high-level results** that are defensible:
- Overview, Draft Explorer, Draft Detail — browsable catalog
- Authors — public data from Datatracker
- Citations — public citation data
- Ratings — score distributions (aggregate, not per-draft methodology)
- Timeline — submission trends (factual)
- Search — search functionality
- About, Impressum, Datenschutz — legal/info pages
### Dev-only pages (`@admin_required`, `--dev` mode)
Pages exposing **internal methodology**, **LLM judgments**, **cost data**, or **debatable analysis**:
- Gap Explorer, Gap Generation — internal gap analysis, draft generation
- Monitor — pipeline health, API costs, token usage
- Analytics — pageview tracking
- Compare — side-by-side draft comparison (uses Claude)
- AI Ask/Synthesize — Claude-powered Q&A (costs tokens)
- Annotations — internal notes
- False Positives — exposes filtering methodology, raw LLM judgment calls
- Complexity — correlations between LLM ratings and structural metrics (methodologically debatable)
- Idea Analysis — LLM-generated novelty scores could be challenged
- Trends — safety ratio uses internal category mappings
- Sources — rating comparisons across standards bodies (could offend orgs)
- Similarity — embedding-based methodology
- Landscape — t-SNE map (methodology-dependent)
- Obsidian Export — internal tool
### Decision criteria for new pages
- **Public if**: data comes from public sources (Datatracker, standards body websites), or shows aggregate statistics without exposing LLM methodology
- **Dev-only if**: page reveals how Claude rates/classifies things, shows internal cost/token data, compares organizations in potentially sensitive ways, or uses methodology that could be questioned without full context
## Current Status (2026-03-03) ## Current Status (2026-03-03)
- v0.2.0, 361 drafts (101 new, unprocessed) - v0.2.0, 361 drafts (101 new, unprocessed)

View File

@@ -292,6 +292,7 @@ def ratings():
@app.route("/landscape") @app.route("/landscape")
@admin_required
def landscape(): def landscape():
distributions = get_rating_distributions(db()) distributions = get_rating_distributions(db())
tsne_data = get_landscape_tsne(db()) tsne_data = get_landscape_tsne(db())
@@ -326,6 +327,7 @@ def api_architecture():
@app.route("/similarity") @app.route("/similarity")
@admin_required
def similarity(): def similarity():
network = get_similarity_graph(db()) network = get_similarity_graph(db())
return render_template("similarity.html", network=network) return render_template("similarity.html", network=network)
@@ -349,9 +351,10 @@ def authors():
@app.route("/citations") @app.route("/citations")
def citations(): def citations():
from webui.auth import is_admin as check_admin
graph = get_citation_graph(db()) graph = get_citation_graph(db())
influence = get_citation_influence(db()) influence = get_citation_influence(db()) if check_admin() else None
bcp = get_bcp_analysis(db()) bcp = get_bcp_analysis(db()) if check_admin() else None
return render_template("citations.html", graph=graph, influence=influence, bcp=bcp) return render_template("citations.html", graph=graph, influence=influence, bcp=bcp)
@@ -601,6 +604,7 @@ def api_timeline():
@app.route("/api/landscape") @app.route("/api/landscape")
@admin_required
def api_landscape(): def api_landscape():
data = get_landscape_tsne(db()) data = get_landscape_tsne(db())
if request.args.get("format") == "csv": if request.args.get("format") == "csv":
@@ -609,6 +613,7 @@ def api_landscape():
@app.route("/api/similarity") @app.route("/api/similarity")
@admin_required
def api_similarity(): def api_similarity():
data = get_similarity_graph(db()) data = get_similarity_graph(db())
return jsonify(data) return jsonify(data)
@@ -679,6 +684,7 @@ def api_annotate(name: str):
@app.route("/export/obsidian") @app.route("/export/obsidian")
@admin_required
def export_obsidian(): def export_obsidian():
"""Download the entire research corpus as an Obsidian vault (ZIP).""" """Download the entire research corpus as an Obsidian vault (ZIP)."""
data = build_obsidian_vault(db()) data = build_obsidian_vault(db())
@@ -699,24 +705,28 @@ def create_app(dev: bool = False) -> Flask:
@app.route("/sources") @app.route("/sources")
@admin_required
def sources_page(): def sources_page():
data = get_source_comparison(db()) data = get_source_comparison(db())
return render_template("sources.html", data=data) return render_template("sources.html", data=data)
@app.route("/false-positives") @app.route("/false-positives")
@admin_required
def false_positives_page(): def false_positives_page():
data = get_false_positive_profile(db()) data = get_false_positive_profile(db())
return render_template("false_positives.html", data=data) return render_template("false_positives.html", data=data)
@app.route("/api/sources") @app.route("/api/sources")
@admin_required
def api_sources(): def api_sources():
data = get_source_comparison(db()) data = get_source_comparison(db())
return jsonify(data) return jsonify(data)
@app.route("/api/false-positives") @app.route("/api/false-positives")
@admin_required
def api_false_positives(): def api_false_positives():
data = get_false_positive_profile(db()) data = get_false_positive_profile(db())
return jsonify(data) return jsonify(data)
@@ -726,11 +736,13 @@ def api_false_positives():
@app.route("/api/citations/influence") @app.route("/api/citations/influence")
@admin_required
def api_citation_influence(): def api_citation_influence():
return jsonify(get_citation_influence(db())) return jsonify(get_citation_influence(db()))
@app.route("/api/citations/bcp") @app.route("/api/citations/bcp")
@admin_required
def api_bcp_analysis(): def api_bcp_analysis():
return jsonify(get_bcp_analysis(db())) return jsonify(get_bcp_analysis(db()))
@@ -739,12 +751,14 @@ def api_bcp_analysis():
@app.route("/idea-analysis") @app.route("/idea-analysis")
@admin_required
def idea_analysis(): def idea_analysis():
data = get_idea_analysis(db()) data = get_idea_analysis(db())
return render_template("idea_analysis.html", data=data) return render_template("idea_analysis.html", data=data)
@app.route("/api/idea-analysis") @app.route("/api/idea-analysis")
@admin_required
def api_idea_analysis(): def api_idea_analysis():
data = get_idea_analysis(db()) data = get_idea_analysis(db())
return jsonify(data) return jsonify(data)
@@ -754,23 +768,27 @@ def api_idea_analysis():
@app.route("/trends") @app.route("/trends")
@admin_required
def trends(): def trends():
data = get_trends_data(db()) data = get_trends_data(db())
return render_template("trends_analysis.html", data=data) return render_template("trends_analysis.html", data=data)
@app.route("/complexity") @app.route("/complexity")
@admin_required
def complexity(): def complexity():
data = get_complexity_data(db()) data = get_complexity_data(db())
return render_template("complexity.html", data=data) return render_template("complexity.html", data=data)
@app.route("/api/trends") @app.route("/api/trends")
@admin_required
def api_trends(): def api_trends():
return jsonify(get_trends_data(db())) return jsonify(get_trends_data(db()))
@app.route("/api/complexity") @app.route("/api/complexity")
@admin_required
def api_complexity(): def api_complexity():
return jsonify(get_complexity_data(db())) return jsonify(get_complexity_data(db()))

View File

@@ -117,10 +117,12 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"/><circle cx="19" cy="19" r="3" stroke="currentColor" stroke-width="2" fill="none"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"/><circle cx="19" cy="19" r="3" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Idea Clusters Idea Clusters
</a> </a>
{% if is_admin %}
<a href="/idea-analysis" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'idea_analysis' }}"> <a href="/idea-analysis" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'idea_analysis' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Idea Analysis Idea Analysis
</a> </a>
{% endif %}
<a href="/architecture" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'architecture' }}"> <a href="/architecture" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'architecture' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
Architecture Architecture
@@ -135,6 +137,7 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Timeline Timeline
</a> </a>
{% if is_admin %}
<a href="/trends" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'trends' }}"> <a href="/trends" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'trends' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
Trends Trends
@@ -151,10 +154,12 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/><circle cx="5" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="18" r="1.5" fill="currentColor" stroke="none"/><circle cx="18" cy="6" r="1.5" fill="currentColor" stroke="none"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/><circle cx="5" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="18" r="1.5" fill="currentColor" stroke="none"/><circle cx="18" cy="6" r="1.5" fill="currentColor" stroke="none"/></svg>
Similarity Similarity
</a> </a>
{% endif %}
<a href="/citations" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'citations' }}"> <a href="/citations" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'citations' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Citations Citations
</a> </a>
{% if is_admin %}
<a href="/sources" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'sources' }}"> <a href="/sources" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'sources' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Sources Sources
@@ -163,6 +168,7 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
False Positives False Positives
</a> </a>
{% endif %}
<a href="/authors" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'authors' }}"> <a href="/authors" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'authors' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Authors Authors

View File

@@ -44,9 +44,10 @@
{% block content %} {% block content %}
<div class="mb-6"> <div class="mb-6">
<h1 class="text-2xl font-bold text-white">Citations & Influence</h1> <h1 class="text-2xl font-bold text-white">Citations & Influence</h1>
<p class="text-slate-400 text-sm mt-1">Cross-reference network, citation influence metrics, and BCP dependency analysis across {{ influence.stats.drafts_with_refs }} drafts and {{ influence.stats.total_citations }} total citations.</p> <p class="text-slate-400 text-sm mt-1">Cross-reference network{% if influence %}, citation influence metrics, and BCP dependency analysis across {{ influence.stats.drafts_with_refs }} drafts and {{ influence.stats.total_citations }} total citations{% endif %}.</p>
</div> </div>
{% if influence %}
<!-- Summary stats --> <!-- Summary stats -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6"> <div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden"> <div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
@@ -75,13 +76,18 @@
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div> <div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
</div> </div>
</div> </div>
{% endif %}
<!-- Tabs --> <!-- Tabs -->
<div class="border-b border-slate-800 mb-6"> <div class="border-b border-slate-800 mb-6">
<nav class="flex gap-6"> <nav class="flex gap-6">
<button class="tab-btn active px-1 pb-3 text-sm font-medium text-slate-400" data-tab="graph">Citation Graph</button> <button class="tab-btn active px-1 pb-3 text-sm font-medium text-slate-400" data-tab="graph">Citation Graph</button>
{% if influence %}
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="influence">Influence Analysis</button> <button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="influence">Influence Analysis</button>
{% endif %}
{% if bcp %}
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="bcp">BCP Dependencies</button> <button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="bcp">BCP Dependencies</button>
{% endif %}
</nav> </nav>
</div> </div>
@@ -136,6 +142,7 @@
</div> </div>
</div> </div>
{% if influence %}
<!-- ==================== TAB 2: Influence Analysis ==================== --> <!-- ==================== TAB 2: Influence Analysis ==================== -->
<div id="tab-influence" class="tab-panel"> <div id="tab-influence" class="tab-panel">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
@@ -273,6 +280,9 @@
</div> </div>
</div> </div>
{% endif %}
{% if bcp %}
<!-- ==================== TAB 3: BCP Dependencies ==================== --> <!-- ==================== TAB 3: BCP Dependencies ==================== -->
<div id="tab-bcp" class="tab-panel"> <div id="tab-bcp" class="tab-panel">
<!-- BCP Stats --> <!-- BCP Stats -->
@@ -381,14 +391,15 @@
<div id="bcpHeatmap" style="height: 500px;"></div> <div id="bcpHeatmap" style="height: 500px;"></div>
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
<script> <script>
const graph = {{ graph | tojson }}; const graph = {{ graph | tojson }};
const influence = {{ influence | tojson }}; {% if influence %}const influence = {{ influence | tojson }};{% endif %}
const bcp = {{ bcp | tojson }}; {% if bcp %}const bcp = {{ bcp | tojson }};{% endif %}
const PALETTE = [ const PALETTE = [
'#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b',
@@ -635,8 +646,9 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
})(); })();
// =========================================================== // ===========================================================
// Plotly: Citation density by category (Tab 2) // Plotly: Citation density by category (Tab 2) — admin only
// =========================================================== // ===========================================================
{% if influence %}
(function() { (function() {
const cats = influence.citations_by_category; const cats = influence.citations_by_category;
if (!cats || cats.length === 0) return; if (!cats || cats.length === 0) return;
@@ -668,7 +680,7 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
})(); })();
// =========================================================== // ===========================================================
// Plotly: Draft-to-Draft Network (Tab 2) // Plotly: Draft-to-Draft Network (Tab 2) — admin only
// =========================================================== // ===========================================================
(function() { (function() {
const edges = influence.draft_network; const edges = influence.draft_network;
@@ -740,10 +752,12 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
showlegend: false, showlegend: false,
}, { responsive: true, displayModeBar: false }); }, { responsive: true, displayModeBar: false });
})(); })();
{% endif %}
// =========================================================== // ===========================================================
// Plotly: BCP Co-citation Heatmap (Tab 3) // Plotly: BCP Co-citation Heatmap (Tab 3) — admin only
// =========================================================== // ===========================================================
{% if bcp %}
(function() { (function() {
const labels = bcp.heatmap_labels; const labels = bcp.heatmap_labels;
const matrix = bcp.heatmap_matrix; const matrix = bcp.heatmap_matrix;
@@ -783,5 +797,6 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
yaxis: { autorange: 'reversed' }, yaxis: { autorange: 'reversed' },
}, { responsive: true, displayModeBar: false }); }, { responsive: true, displayModeBar: false });
})(); })();
{% endif %}
</script> </script>
{% endblock %} {% endblock %}