Files
ietf-draft-analyzer/src/webui/templates/overview.html
Christian Nennemann e7527ad68e 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>
2026-03-08 12:47:47 +01:00

218 lines
11 KiB
HTML

{% extends "base.html" %}
{% set active_page = "overview" %}
{% block title %}Overview — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-8 flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-white">Dashboard Overview</h1>
<p class="text-slate-400 text-sm mt-1">IETF AI/Agent Internet-Drafts at a glance. Drafts are fetched from the IETF Datatracker, then analyzed by Claude AI across five dimensions (novelty, maturity, overlap, momentum, relevance) to produce a composite score from 1.0 to 5.0.</p>
</div>
<a href="/export/obsidian"
class="inline-flex items-center gap-2 px-4 py-2 bg-purple-600/80 hover:bg-purple-500 text-white text-sm font-medium rounded-lg transition-colors flex-shrink-0"
title="Download all research data as an Obsidian vault with interlinked notes, Mermaid charts, and YAML frontmatter">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Download for Obsidian
</a>
</div>
<!-- Stat cards -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
<a href="/drafts" class="stat-card rounded-xl border border-slate-800 p-5 relative overflow-hidden hover:border-blue-500/40 transition group">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
<div class="text-3xl font-bold text-blue-400">{{ stats.total_drafts }}</div>
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider group-hover:text-blue-400/70 transition">Total Drafts &rarr;</div>
</a>
<a href="/ratings" class="stat-card rounded-xl border border-slate-800 p-5 relative overflow-hidden hover:border-emerald-500/40 transition group">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
<div class="text-3xl font-bold text-emerald-400">{{ stats.rated_count }}</div>
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider group-hover:text-emerald-400/70 transition">Rated Drafts &rarr;</div>
</a>
<a href="/authors" class="stat-card rounded-xl border border-slate-800 p-5 relative overflow-hidden hover:border-purple-500/40 transition group">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-3xl font-bold text-purple-400">{{ stats.author_count }}</div>
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider group-hover:text-purple-400/70 transition">Authors &rarr;</div>
</a>
<a href="/ideas" class="stat-card rounded-xl border border-slate-800 p-5 relative overflow-hidden hover:border-amber-500/40 transition group">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
<div class="text-3xl font-bold text-amber-400">{{ stats.idea_count }}</div>
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider group-hover:text-amber-400/70 transition">Ideas &rarr;</div>
</a>
<a href="/gaps" class="stat-card rounded-xl border border-slate-800 p-5 relative overflow-hidden hover:border-red-500/40 transition group">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-red-500 to-red-400"></div>
<div class="text-3xl font-bold text-red-400">{{ stats.gap_count }}</div>
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider group-hover:text-red-400/70 transition">Gaps Found &rarr;</div>
</a>
</div>
<!-- Charts row 1: Score distribution + Category donut -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Composite Score Distribution</h2>
<p class="text-xs text-slate-500 mb-3">Weighted average of five AI-rated dimensions (novelty 20%, maturity 20%, uniqueness 20%, momentum 20%, relevance 20%). Higher scores indicate drafts that are novel, mature, unique, gaining traction, and highly relevant to AI agent infrastructure.</p>
<div id="scoreHist" style="height: 300px;"></div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Drafts by Category</h2>
<p class="text-xs text-slate-500 mb-3">Categories are assigned by Claude during analysis. A draft can belong to multiple categories (e.g., both "A2A protocols" and "AI safety/alignment").</p>
<div id="categoryPie" style="height: 300px;"></div>
</div>
</div>
<!-- Timeline (full width) -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-3">Submissions Over Time</h2>
<div id="timeline" style="height: 350px;"></div>
</div>
<!-- Category radar -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-3">Category Rating Profiles</h2>
<div id="radar" style="height: 420px;"></div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
// Shared Plotly config
const PLOTLY_LAYOUT = {
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 30, r: 20, b: 40, l: 40 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const PLOTLY_CONFIG = { responsive: true, displayModeBar: false };
const PALETTE = [
'#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b',
'#06b6d4', '#ec4899', '#84cc16', '#f97316', '#8b5cf6',
'#14b8a6', '#e11d48', '#64748b', '#eab308', '#6366f1',
];
// --- Score histogram ---
const scores = {{ scores | tojson }};
if (scores.length > 0) {
Plotly.newPlot('scoreHist', [{
x: scores,
type: 'histogram',
nbinsx: 20,
marker: {
color: 'rgba(59, 130, 246, 0.7)',
line: { color: '#3b82f6', width: 1 },
},
hovertemplate: 'Score: %{x}<br>Count: %{y}<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: { text: 'Composite Score', font: { size: 11 } } },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: { text: 'Count', font: { size: 11 } } },
}, PLOTLY_CONFIG);
} else {
document.getElementById('scoreHist').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No score data available</p>';
}
// --- Category donut ---
const categories = {{ categories | tojson }};
const catNames = Object.keys(categories);
const catVals = Object.values(categories);
if (catNames.length > 0) {
Plotly.newPlot('categoryPie', [{
labels: catNames,
values: catVals,
type: 'pie',
hole: 0.45,
textinfo: 'label+percent',
textposition: 'outside',
textfont: { size: 10, color: '#94a3b8' },
hovertemplate: '%{label}<br>%{value} drafts (%{percent})<extra></extra>',
marker: { colors: PALETTE },
pull: catVals.map((_, i) => i === 0 ? 0.03 : 0),
}], {
...PLOTLY_LAYOUT,
showlegend: false,
margin: { t: 10, r: 10, b: 10, l: 10 },
}, PLOTLY_CONFIG);
// Click category to filter drafts
document.getElementById('categoryPie').on('plotly_click', function(data) {
const cat = data.points[0].label;
if (cat) window.location.href = '/drafts?cat=' + encodeURIComponent(cat);
});
} else {
document.getElementById('categoryPie').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No category data available</p>';
}
// --- Timeline (stacked area) ---
const timeline = {{ timeline | tojson }};
if (timeline.months && timeline.months.length > 0) {
const timeTraces = timeline.categories.map((cat, i) => ({
x: timeline.months,
y: timeline.series[cat],
name: cat,
type: 'scatter',
mode: 'lines',
stackgroup: 'one',
line: { width: 0.5, color: PALETTE[i % PALETTE.length] },
fillcolor: PALETTE[i % PALETTE.length] + '80',
hovertemplate: '%{x}<br>' + cat + ': %{y}<extra></extra>',
}));
Plotly.newPlot('timeline', timeTraces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: { text: 'Month', font: { size: 11 } } },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: { text: 'Drafts', font: { size: 11 } } },
legend: { font: { size: 10, color: '#94a3b8' }, orientation: 'h', y: -0.2, x: 0.5, xanchor: 'center' },
hovermode: 'x unified',
}, PLOTLY_CONFIG);
} else {
document.getElementById('timeline').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No timeline data available</p>';
}
// --- Category radar ---
const radar = {{ radar | tojson }};
const dims = ['novelty', 'maturity', 'relevance', 'momentum', 'low_overlap'];
const dimLabels = ['Novelty', 'Maturity', 'Relevance', 'Momentum', 'Low Overlap'];
const radarCats = Object.keys(radar);
if (radarCats.length > 0) {
const radarTraces = radarCats.map((cat, i) => {
const vals = radar[cat];
return {
type: 'scatterpolar',
r: dims.map(d => vals[d]).concat([vals[dims[0]]]),
theta: dimLabels.concat([dimLabels[0]]),
fill: 'toself',
fillcolor: PALETTE[i % PALETTE.length] + '20',
line: { color: PALETTE[i % PALETTE.length], width: 2 },
name: cat + ' (' + vals.count + ')',
opacity: 0.85,
hovertemplate: cat + '<br>%{theta}: %{r:.1f}<extra></extra>',
};
});
Plotly.newPlot('radar', radarTraces, {
...PLOTLY_LAYOUT,
polar: {
bgcolor: 'transparent',
radialaxis: {
visible: true,
range: [0, 5],
gridcolor: '#1e293b',
color: '#64748b',
tickfont: { size: 10 },
},
angularaxis: {
gridcolor: '#1e293b',
color: '#94a3b8',
tickfont: { size: 11 },
},
},
legend: { font: { size: 10, color: '#94a3b8' }, x: 1.05, y: 0.5 },
margin: { t: 30, r: 120, b: 30, l: 60 },
}, PLOTLY_CONFIG);
} else {
document.getElementById('radar').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No radar data available</p>';
}
</script>
{% endblock %}