From ae5e5f8cbfd09f3e387ee32036b61f400bf44d1b Mon Sep 17 00:00:00 2001 From: Christian Nennemann Date: Sun, 8 Mar 2026 20:52:43 +0100 Subject: [PATCH] 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 --- CLAUDE.md | 38 ++++++++++++++++++++++++++++++ src/webui/app.py | 22 +++++++++++++++-- src/webui/templates/base.html | 6 +++++ src/webui/templates/citations.html | 27 ++++++++++++++++----- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index aeecbc1..7c0c959 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,44 @@ All agents should: - Use `rich` for console output - 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) - v0.2.0, 361 drafts (101 new, unprocessed) diff --git a/src/webui/app.py b/src/webui/app.py index d71107c..4a383a5 100644 --- a/src/webui/app.py +++ b/src/webui/app.py @@ -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())) diff --git a/src/webui/templates/base.html b/src/webui/templates/base.html index 8448252..e29d11b 100644 --- a/src/webui/templates/base.html +++ b/src/webui/templates/base.html @@ -117,10 +117,12 @@ Idea Clusters + {% if is_admin %} Idea Analysis + {% endif %} Architecture @@ -135,6 +137,7 @@ Timeline + {% if is_admin %} Trends @@ -151,10 +154,12 @@ Similarity + {% endif %} Citations + {% if is_admin %} Sources @@ -163,6 +168,7 @@ False Positives + {% endif %} Authors diff --git a/src/webui/templates/citations.html b/src/webui/templates/citations.html index d42dea9..9b0a053 100644 --- a/src/webui/templates/citations.html +++ b/src/webui/templates/citations.html @@ -44,9 +44,10 @@ {% block content %}

Citations & Influence

-

Cross-reference network, citation influence metrics, and BCP dependency analysis across {{ influence.stats.drafts_with_refs }} drafts and {{ influence.stats.total_citations }} total citations.

+

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 %}.

+{% if influence %}
@@ -75,13 +76,18 @@
{{ bcp.coverage.coverage_pct }}%
+{% endif %}
@@ -136,6 +142,7 @@ +{% if influence %}
@@ -273,6 +280,9 @@
+{% endif %} + +{% if bcp %}
@@ -381,14 +391,15 @@
+{% endif %} {% endblock %} {% block extra_scripts %} {% endblock %}