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:
@@ -292,6 +292,7 @@ def ratings():
|
||||
|
||||
|
||||
@app.route("/landscape")
|
||||
@admin_required
|
||||
def landscape():
|
||||
distributions = get_rating_distributions(db())
|
||||
tsne_data = get_landscape_tsne(db())
|
||||
@@ -326,6 +327,7 @@ def api_architecture():
|
||||
|
||||
|
||||
@app.route("/similarity")
|
||||
@admin_required
|
||||
def similarity():
|
||||
network = get_similarity_graph(db())
|
||||
return render_template("similarity.html", network=network)
|
||||
@@ -349,9 +351,10 @@ def authors():
|
||||
|
||||
@app.route("/citations")
|
||||
def citations():
|
||||
from webui.auth import is_admin as check_admin
|
||||
graph = get_citation_graph(db())
|
||||
influence = get_citation_influence(db())
|
||||
bcp = get_bcp_analysis(db())
|
||||
influence = get_citation_influence(db()) if check_admin() else None
|
||||
bcp = get_bcp_analysis(db()) if check_admin() else None
|
||||
return render_template("citations.html", graph=graph, influence=influence, bcp=bcp)
|
||||
|
||||
|
||||
@@ -601,6 +604,7 @@ def api_timeline():
|
||||
|
||||
|
||||
@app.route("/api/landscape")
|
||||
@admin_required
|
||||
def api_landscape():
|
||||
data = get_landscape_tsne(db())
|
||||
if request.args.get("format") == "csv":
|
||||
@@ -609,6 +613,7 @@ def api_landscape():
|
||||
|
||||
|
||||
@app.route("/api/similarity")
|
||||
@admin_required
|
||||
def api_similarity():
|
||||
data = get_similarity_graph(db())
|
||||
return jsonify(data)
|
||||
@@ -679,6 +684,7 @@ def api_annotate(name: str):
|
||||
|
||||
|
||||
@app.route("/export/obsidian")
|
||||
@admin_required
|
||||
def export_obsidian():
|
||||
"""Download the entire research corpus as an Obsidian vault (ZIP)."""
|
||||
data = build_obsidian_vault(db())
|
||||
@@ -699,24 +705,28 @@ def create_app(dev: bool = False) -> Flask:
|
||||
|
||||
|
||||
@app.route("/sources")
|
||||
@admin_required
|
||||
def sources_page():
|
||||
data = get_source_comparison(db())
|
||||
return render_template("sources.html", data=data)
|
||||
|
||||
|
||||
@app.route("/false-positives")
|
||||
@admin_required
|
||||
def false_positives_page():
|
||||
data = get_false_positive_profile(db())
|
||||
return render_template("false_positives.html", data=data)
|
||||
|
||||
|
||||
@app.route("/api/sources")
|
||||
@admin_required
|
||||
def api_sources():
|
||||
data = get_source_comparison(db())
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@app.route("/api/false-positives")
|
||||
@admin_required
|
||||
def api_false_positives():
|
||||
data = get_false_positive_profile(db())
|
||||
return jsonify(data)
|
||||
@@ -726,11 +736,13 @@ def api_false_positives():
|
||||
|
||||
|
||||
@app.route("/api/citations/influence")
|
||||
@admin_required
|
||||
def api_citation_influence():
|
||||
return jsonify(get_citation_influence(db()))
|
||||
|
||||
|
||||
@app.route("/api/citations/bcp")
|
||||
@admin_required
|
||||
def api_bcp_analysis():
|
||||
return jsonify(get_bcp_analysis(db()))
|
||||
|
||||
@@ -739,12 +751,14 @@ def api_bcp_analysis():
|
||||
|
||||
|
||||
@app.route("/idea-analysis")
|
||||
@admin_required
|
||||
def idea_analysis():
|
||||
data = get_idea_analysis(db())
|
||||
return render_template("idea_analysis.html", data=data)
|
||||
|
||||
|
||||
@app.route("/api/idea-analysis")
|
||||
@admin_required
|
||||
def api_idea_analysis():
|
||||
data = get_idea_analysis(db())
|
||||
return jsonify(data)
|
||||
@@ -754,23 +768,27 @@ def api_idea_analysis():
|
||||
|
||||
|
||||
@app.route("/trends")
|
||||
@admin_required
|
||||
def trends():
|
||||
data = get_trends_data(db())
|
||||
return render_template("trends_analysis.html", data=data)
|
||||
|
||||
|
||||
@app.route("/complexity")
|
||||
@admin_required
|
||||
def complexity():
|
||||
data = get_complexity_data(db())
|
||||
return render_template("complexity.html", data=data)
|
||||
|
||||
|
||||
@app.route("/api/trends")
|
||||
@admin_required
|
||||
def api_trends():
|
||||
return jsonify(get_trends_data(db()))
|
||||
|
||||
|
||||
@app.route("/api/complexity")
|
||||
@admin_required
|
||||
def api_complexity():
|
||||
return jsonify(get_complexity_data(db()))
|
||||
|
||||
|
||||
@@ -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>
|
||||
Idea Clusters
|
||||
</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' }}">
|
||||
<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
|
||||
</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' }}">
|
||||
<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
|
||||
@@ -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>
|
||||
Timeline
|
||||
</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' }}">
|
||||
<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
|
||||
@@ -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>
|
||||
Similarity
|
||||
</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' }}">
|
||||
<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
|
||||
</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' }}">
|
||||
<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
|
||||
@@ -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>
|
||||
False Positives
|
||||
</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' }}">
|
||||
<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
|
||||
|
||||
@@ -44,9 +44,10 @@
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<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>
|
||||
|
||||
{% if influence %}
|
||||
<!-- Summary stats -->
|
||||
<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">
|
||||
@@ -75,13 +76,18 @@
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-slate-800 mb-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>
|
||||
{% if influence %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -136,6 +142,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if influence %}
|
||||
<!-- ==================== TAB 2: Influence Analysis ==================== -->
|
||||
<div id="tab-influence" class="tab-panel">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
@@ -273,6 +280,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if bcp %}
|
||||
<!-- ==================== TAB 3: BCP Dependencies ==================== -->
|
||||
<div id="tab-bcp" class="tab-panel">
|
||||
<!-- BCP Stats -->
|
||||
@@ -381,14 +391,15 @@
|
||||
<div id="bcpHeatmap" style="height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
const graph = {{ graph | tojson }};
|
||||
const influence = {{ influence | tojson }};
|
||||
const bcp = {{ bcp | tojson }};
|
||||
{% if influence %}const influence = {{ influence | tojson }};{% endif %}
|
||||
{% if bcp %}const bcp = {{ bcp | tojson }};{% endif %}
|
||||
|
||||
const PALETTE = [
|
||||
'#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() {
|
||||
const cats = influence.citations_by_category;
|
||||
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() {
|
||||
const edges = influence.draft_network;
|
||||
@@ -740,10 +752,12 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
showlegend: 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() {
|
||||
const labels = bcp.heatmap_labels;
|
||||
const matrix = bcp.heatmap_matrix;
|
||||
@@ -783,5 +797,6 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
yaxis: { autorange: 'reversed' },
|
||||
}, { responsive: true, displayModeBar: false });
|
||||
})();
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user