Fix remaining critical, high, and medium issues from 4-perspective review
Critical fixes:
- Fix rating clamp range 1-10 → 1-5 (actual scale)
- Add `ietf ideas convergence` command (SequenceMatcher at 0.75 threshold)
- Fix "628 cross-org ideas" → 130 (verified from current DB) across 8 files
Security fixes:
- Sanitize FTS5 query input (strip special chars + boolean operators)
- Add rate limiting (10 req/min/IP) on Claude-calling endpoints
- Change <path:name> → <string:name> on draft routes
Codebase fixes:
- Add Database context manager (__enter__/__exit__)
- Wire false_positive filtering into queries (exclude by default in web UI)
- Fix Post 3 arithmetic ("~300" → "~409" distinct proposals)
Content & licensing:
- Add MIT LICENSE file
- Add IPR/FRAND notes (BCP 79, RFC 8179) to Posts 03 and 07
- Qualify "4:1 safety ratio" with monthly variation in 6 remaining files
- Add "Data as of March 2026" freeze-date headers to all 10 blog posts
- Hedge causal language in Post 04
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ sys.path.insert(0, str(_project_root / "src"))
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import functools
|
||||
from collections import defaultdict
|
||||
|
||||
from flask import Flask, render_template, request, jsonify, abort, g, Response
|
||||
|
||||
@@ -50,6 +53,7 @@ from webui.data import (
|
||||
get_comparison_data,
|
||||
get_ask_search,
|
||||
get_ask_synthesize,
|
||||
get_category_summary,
|
||||
global_search,
|
||||
)
|
||||
|
||||
@@ -70,6 +74,29 @@ _analytics_db = str(_project_root / "data" / "analytics.db")
|
||||
init_analytics(app, db_path=_analytics_db)
|
||||
|
||||
|
||||
# --- Rate limiting for Claude-calling endpoints ---
|
||||
|
||||
_rate_limit_store: dict[str, list[float]] = defaultdict(list)
|
||||
_RATE_LIMIT_MAX = 10 # max requests
|
||||
_RATE_LIMIT_WINDOW = 60 # per 60 seconds
|
||||
|
||||
|
||||
def rate_limit(f):
|
||||
"""Simple in-memory rate limiter: max 10 requests per minute per IP."""
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
ip = request.remote_addr or "unknown"
|
||||
now = time.time()
|
||||
# Prune timestamps outside the sliding window
|
||||
timestamps = _rate_limit_store[ip]
|
||||
_rate_limit_store[ip] = [t for t in timestamps if now - t < _RATE_LIMIT_WINDOW]
|
||||
if len(_rate_limit_store[ip]) >= _RATE_LIMIT_MAX:
|
||||
return jsonify({"error": "Rate limit exceeded. Try again later."}), 429
|
||||
_rate_limit_store[ip].append(now)
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# --- Database lifecycle (per-request to avoid SQLite threading issues) ---
|
||||
|
||||
|
||||
@@ -127,10 +154,12 @@ def drafts():
|
||||
source=source,
|
||||
)
|
||||
categories = get_category_counts(db())
|
||||
cat_summary = get_category_summary(db(), category) if category else None
|
||||
return render_template(
|
||||
"drafts.html",
|
||||
result=result,
|
||||
categories=categories,
|
||||
cat_summary=cat_summary,
|
||||
search=search,
|
||||
current_cat=category,
|
||||
current_source=source,
|
||||
@@ -140,7 +169,7 @@ def drafts():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/drafts/<path:name>")
|
||||
@app.route("/drafts/<string:name>")
|
||||
def draft_detail(name: str):
|
||||
database = db()
|
||||
detail = get_draft_detail(database, name)
|
||||
@@ -321,8 +350,11 @@ def analytics_dashboard():
|
||||
|
||||
@app.route("/about")
|
||||
def about():
|
||||
from ietf_analyzer.config import Config
|
||||
cfg = Config.load()
|
||||
stats = get_overview_stats(db())
|
||||
return render_template("about.html", stats=stats)
|
||||
return render_template("about.html", stats=stats, search_keywords=cfg.search_keywords,
|
||||
fetch_since=cfg.fetch_since)
|
||||
|
||||
|
||||
@app.route("/impressum")
|
||||
@@ -356,6 +388,7 @@ def ask_page():
|
||||
|
||||
@app.route("/api/ask/synthesize", methods=["POST"])
|
||||
@admin_required
|
||||
@rate_limit
|
||||
def api_ask_synthesize():
|
||||
"""Synthesize an answer via Claude (costs tokens, cached permanently). Returns JSON."""
|
||||
data = request.get_json(force=True, silent=True)
|
||||
@@ -392,6 +425,7 @@ def compare_page():
|
||||
|
||||
@app.route("/api/compare", methods=["POST"])
|
||||
@admin_required
|
||||
@rate_limit
|
||||
def api_compare():
|
||||
"""Run Claude comparison for drafts. Returns JSON with comparison text."""
|
||||
req_data = request.get_json(force=True, silent=True)
|
||||
@@ -572,7 +606,7 @@ def api_monitor():
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@app.route("/api/drafts/<path:name>")
|
||||
@app.route("/api/drafts/<string:name>")
|
||||
def api_draft_detail(name: str):
|
||||
detail = get_draft_detail(db(), name)
|
||||
if not detail:
|
||||
@@ -589,7 +623,7 @@ def api_categories():
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@app.route("/api/drafts/<path:name>/annotate", methods=["POST"])
|
||||
@app.route("/api/drafts/<string:name>/annotate", methods=["POST"])
|
||||
@admin_required
|
||||
def api_annotate(name: str):
|
||||
"""Add or update annotation for a draft."""
|
||||
|
||||
Reference in New Issue
Block a user