Blog drafting section (dev-only): - BlogDraftGenerator gathers project data (gaps, proposals, stats) as context and calls Claude to produce Medium-style blog posts - DB schema: blog_drafts table with title, content, tags, cost tracking - Web UI: list, generate (async with live preview), detail (rendered + source toggle), edit, and export routes - 6 writing styles: deep-dive, overview, opinion, listicle, comparison, series-post - Nav link added to sidebar under Proposals Bug fixes found via route testing (scripts/test_all_routes.py): - /authors/<id>: Draft.status → Draft.states (correct attribute name) - /false-positives: add missing `import re` in ratings.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
8.7 KiB
Python
226 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Test ALL web UI routes and report any returning 500 errors.
|
|
|
|
Runs against the Flask test client with dev=True so admin routes are accessible.
|
|
Queries SQLite DB for real IDs needed by dynamic routes.
|
|
"""
|
|
import os
|
|
import sys
|
|
import sqlite3
|
|
import traceback
|
|
|
|
# Must run from src/ directory
|
|
src_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src')
|
|
sys.path.insert(0, src_dir)
|
|
|
|
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'drafts.db')
|
|
|
|
|
|
def get_test_ids():
|
|
"""Query the database for real IDs to use in dynamic routes."""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
|
|
ids = {}
|
|
|
|
# Get a draft name
|
|
row = conn.execute("SELECT name FROM drafts LIMIT 1").fetchone()
|
|
ids["draft_name"] = row["name"] if row else "draft-nonexistent"
|
|
|
|
# Get a person_id from authors
|
|
row = conn.execute("SELECT person_id FROM authors WHERE person_id IS NOT NULL LIMIT 1").fetchone()
|
|
ids["person_id"] = row["person_id"] if row else 1
|
|
|
|
# Get a gap_id
|
|
row = conn.execute("SELECT id FROM gaps LIMIT 1").fetchone()
|
|
ids["gap_id"] = row["id"] if row else 1
|
|
|
|
# Get an idea_id
|
|
row = conn.execute("SELECT id FROM ideas LIMIT 1").fetchone()
|
|
ids["idea_id"] = row["id"] if row else 1
|
|
|
|
# Get a proposal_id (may not exist)
|
|
try:
|
|
row = conn.execute("SELECT id FROM proposals LIMIT 1").fetchone()
|
|
ids["proposal_id"] = row["id"] if row else None
|
|
except Exception:
|
|
ids["proposal_id"] = None
|
|
|
|
# Get a blog_draft_id (may not exist)
|
|
try:
|
|
row = conn.execute("SELECT id FROM blog_drafts LIMIT 1").fetchone()
|
|
ids["blog_draft_id"] = row["id"] if row else None
|
|
except Exception:
|
|
ids["blog_draft_id"] = None
|
|
|
|
# Get two draft names for compare
|
|
rows = conn.execute("SELECT name FROM drafts LIMIT 2").fetchall()
|
|
ids["two_drafts"] = ",".join(r["name"] for r in rows) if len(rows) >= 2 else ""
|
|
|
|
conn.close()
|
|
return ids
|
|
|
|
|
|
def main():
|
|
ids = get_test_ids()
|
|
print(f"Test IDs: draft={ids['draft_name']}, person={ids['person_id']}, "
|
|
f"gap={ids['gap_id']}, idea={ids['idea_id']}, "
|
|
f"proposal={ids['proposal_id']}, blog={ids['blog_draft_id']}")
|
|
print()
|
|
|
|
from webui.app import create_app
|
|
app = create_app(dev=True)
|
|
client = app.test_client()
|
|
|
|
# Define all routes to test: (method, url, description)
|
|
routes = [
|
|
# === Pages (public) ===
|
|
("GET", "/", "overview"),
|
|
("GET", "/drafts", "drafts list"),
|
|
("GET", f"/drafts/{ids['draft_name']}", "draft detail"),
|
|
("GET", "/ideas", "ideas"),
|
|
("GET", "/ratings", "ratings"),
|
|
("GET", "/timeline", "timeline"),
|
|
("GET", "/idea-clusters", "idea clusters"),
|
|
("GET", "/architecture", "architecture"),
|
|
("GET", f"/authors/{ids['person_id']}", "author detail"),
|
|
("GET", "/authors", "authors"),
|
|
("GET", "/citations", "citations"),
|
|
("GET", "/about", "about"),
|
|
("GET", "/impressum", "impressum"),
|
|
("GET", "/datenschutz", "datenschutz"),
|
|
("GET", "/search", "search (empty)"),
|
|
("GET", "/search?q=agent", "search (query)"),
|
|
("GET", "/ask", "ask (empty)"),
|
|
|
|
# === API (public) ===
|
|
("GET", "/api/drafts", "api drafts"),
|
|
("GET", "/api/stats", "api stats"),
|
|
("GET", "/api/authors/network", "api author network"),
|
|
("GET", "/api/citations", "api citations"),
|
|
("GET", "/api/search?q=agent", "api search"),
|
|
("GET", "/api/ideas", "api ideas"),
|
|
("GET", "/api/ratings", "api ratings"),
|
|
("GET", "/api/timeline", "api timeline"),
|
|
("GET", "/api/idea-clusters", "api idea clusters"),
|
|
("GET", "/api/categories", "api categories"),
|
|
("GET", f"/api/drafts/{ids['draft_name']}", "api draft detail"),
|
|
("GET", "/api/architecture", "api architecture"),
|
|
|
|
# === Admin pages ===
|
|
("GET", "/gaps", "gaps"),
|
|
("GET", "/gaps/demo", "gaps demo"),
|
|
("GET", f"/gaps/{ids['gap_id']}", "gap detail"),
|
|
("GET", "/api/gaps", "api gaps"),
|
|
("GET", f"/api/gaps/{ids['gap_id']}", "api gap detail"),
|
|
("GET", "/monitor", "monitor"),
|
|
("GET", "/api/monitor", "api monitor"),
|
|
("GET", "/admin/analytics", "analytics"),
|
|
("GET", "/landscape", "landscape"),
|
|
("GET", "/api/landscape", "api landscape"),
|
|
("GET", "/similarity", "similarity"),
|
|
("GET", "/api/similarity", "api similarity"),
|
|
("GET", "/compare", "compare (empty)"),
|
|
("GET", f"/compare?drafts={ids['two_drafts']}", "compare (with drafts)"),
|
|
("GET", "/sources", "sources"),
|
|
("GET", "/false-positives", "false positives"),
|
|
("GET", "/api/sources", "api sources"),
|
|
("GET", "/api/false-positives", "api false positives"),
|
|
("GET", "/api/citations/influence", "api citation influence"),
|
|
("GET", "/api/citations/bcp", "api bcp analysis"),
|
|
("GET", "/idea-analysis", "idea analysis"),
|
|
("GET", "/api/idea-analysis", "api idea analysis"),
|
|
("GET", f"/ideas/{ids['idea_id']}", "idea detail"),
|
|
("GET", "/trends", "trends"),
|
|
("GET", "/complexity", "complexity"),
|
|
("GET", "/api/trends", "api trends"),
|
|
("GET", "/api/complexity", "api complexity"),
|
|
("GET", "/proposals", "proposals"),
|
|
("GET", "/proposals/new", "proposal new (GET)"),
|
|
("GET", "/api/proposals", "api proposals"),
|
|
("GET", "/proposals/intake", "proposal intake (GET)"),
|
|
("GET", "/blog", "blog drafts"),
|
|
("GET", "/blog/generate", "blog generate (GET)"),
|
|
("GET", "/export/obsidian", "obsidian export"),
|
|
]
|
|
|
|
# Add conditional routes that need existing records
|
|
if ids["proposal_id"]:
|
|
routes.extend([
|
|
("GET", f"/proposals/{ids['proposal_id']}", "proposal detail"),
|
|
("GET", f"/proposals/{ids['proposal_id']}/edit", "proposal edit (GET)"),
|
|
("GET", f"/api/proposals/{ids['proposal_id']}", "api proposal detail"),
|
|
])
|
|
|
|
if ids["blog_draft_id"]:
|
|
routes.extend([
|
|
("GET", f"/blog/{ids['blog_draft_id']}", "blog detail"),
|
|
("GET", f"/blog/{ids['blog_draft_id']}/edit", "blog edit (GET)"),
|
|
("GET", f"/blog/{ids['blog_draft_id']}/export", "blog export"),
|
|
])
|
|
|
|
# Run tests
|
|
results = {"ok": [], "error_500": [], "error_other": [], "exception": []}
|
|
|
|
for method, url, desc in routes:
|
|
try:
|
|
if method == "GET":
|
|
resp = client.get(url)
|
|
elif method == "POST":
|
|
resp = client.post(url, json={})
|
|
else:
|
|
continue
|
|
|
|
status = resp.status_code
|
|
if status == 500:
|
|
# Try to extract error from response
|
|
error_text = resp.data.decode("utf-8", errors="replace")[:500]
|
|
results["error_500"].append((desc, url, status, error_text))
|
|
print(f" FAIL 500 {desc:30s} {url}")
|
|
elif status >= 400:
|
|
results["error_other"].append((desc, url, status))
|
|
print(f" WARN {status} {desc:30s} {url}")
|
|
else:
|
|
results["ok"].append((desc, url, status))
|
|
print(f" OK {status} {desc:30s} {url}")
|
|
except Exception as e:
|
|
tb = traceback.format_exc()
|
|
results["exception"].append((desc, url, str(e), tb))
|
|
print(f" EXCP {desc:30s} {url} -> {e}")
|
|
|
|
# Summary
|
|
print("\n" + "=" * 70)
|
|
print(f"SUMMARY: {len(results['ok'])} OK, {len(results['error_500'])} x 500, "
|
|
f"{len(results['error_other'])} x 4xx, {len(results['exception'])} exceptions")
|
|
print("=" * 70)
|
|
|
|
if results["error_500"]:
|
|
print("\n--- 500 ERRORS (BROKEN ROUTES) ---")
|
|
for desc, url, status, error_text in results["error_500"]:
|
|
print(f"\n Route: {desc}")
|
|
print(f" URL: {url}")
|
|
print(f" Error preview:")
|
|
# Show just enough of the error
|
|
for line in error_text.split("\n")[:10]:
|
|
print(f" {line}")
|
|
|
|
if results["exception"]:
|
|
print("\n--- EXCEPTIONS (APP CRASHED) ---")
|
|
for desc, url, err, tb in results["exception"]:
|
|
print(f"\n Route: {desc}")
|
|
print(f" URL: {url}")
|
|
print(f" Exception: {err}")
|
|
# Show last few lines of traceback
|
|
tb_lines = tb.strip().split("\n")
|
|
for line in tb_lines[-5:]:
|
|
print(f" {line}")
|
|
|
|
if results["error_other"]:
|
|
print("\n--- 4xx RESPONSES (may be expected) ---")
|
|
for desc, url, status in results["error_other"]:
|
|
print(f" {status} {desc:30s} {url}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|