Skip to content

Commit 052d92a

Browse files
rayketchamclaude
andcommitted
feat: thinktank engine-activity heartbeat panel
User feedback: "think tank is nonexistent." The page rendered a forge-lab + roadmap, but if proposals were empty the engine looked dead — even though the cron is generating ~one idea every 30 minutes. Add a top-of-page heartbeat panel showing: - Accepted (24h) — ideas that survived dedup - Dedup-filtered (24h) — ideas the engine produced and dedup blocked (proves activity even when count isn't growing) - Last super idea — timestamp of the most recent [SUPER] gen - Last 5 generation_runs (collapsed <details>) with category, OK/error This confirms upstream activity at a glance. The data-poverty trap that keeps super-ideas at 5 is unrelated and addressed elsewhere (Phase 6 reasoning + external feeds). CSS bumped to v=6. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0becbda commit 052d92a

4 files changed

Lines changed: 107 additions & 1 deletion

File tree

src/project_forge/web/routes.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ async def scaffold_idea(
396396
@router.get("/thinktank", response_class=HTMLResponse)
397397
async def thinktank_page(request: Request):
398398
"""Think Tank — Forge Lab (AI proposals) + Roadmap (GitHub issues)."""
399+
from datetime import UTC, datetime
400+
399401
from project_forge.scaffold.github import list_self_issues
400402

401403
# Roadmap: GitHub issues
@@ -415,6 +417,50 @@ async def thinktank_page(request: Request):
415417
promoted = [i for i in all_si if i.status == "approved"]
416418
rejected = [i for i in all_si if i.status == "rejected"]
417419

420+
# Engine heartbeat: prove the cron is alive. Show recent generation runs,
421+
# recent filter activity, and the last super-idea event so the page
422+
# doesn't read as "nothing's happening" when the engine is busy upstream.
423+
cursor = await db.db.execute(
424+
"SELECT category, started_at, success, error FROM generation_runs "
425+
"ORDER BY started_at DESC LIMIT 5",
426+
)
427+
recent_runs = []
428+
for row in await cursor.fetchall():
429+
recent_runs.append({
430+
"category": row[0],
431+
"when": row[1],
432+
"ok": bool(row[2]),
433+
"error": row[3],
434+
})
435+
436+
cursor = await db.db.execute(
437+
"SELECT generated_at FROM ideas WHERE name LIKE '[SUPER]%%' "
438+
"ORDER BY generated_at DESC LIMIT 1",
439+
)
440+
row = await cursor.fetchone()
441+
last_super = row[0] if row else None
442+
443+
cursor = await db.db.execute(
444+
"SELECT COUNT(*) FROM filtered_ideas "
445+
"WHERE filtered_at >= datetime('now', '-1 day')",
446+
)
447+
row = await cursor.fetchone()
448+
filtered_24h = row[0] if row else 0
449+
450+
cursor = await db.db.execute(
451+
"SELECT COUNT(*) FROM ideas WHERE generated_at >= datetime('now', '-1 day')",
452+
)
453+
row = await cursor.fetchone()
454+
accepted_24h = row[0] if row else 0
455+
456+
heartbeat = {
457+
"now": datetime.now(UTC).isoformat(),
458+
"recent_runs": recent_runs,
459+
"last_super": last_super,
460+
"filtered_24h": filtered_24h,
461+
"accepted_24h": accepted_24h,
462+
}
463+
418464
return templates.TemplateResponse(
419465
request,
420466
"thinktank.html",
@@ -430,6 +476,7 @@ async def thinktank_page(request: Request):
430476
"rejected": rejected,
431477
"rejected_count": len(rejected),
432478
"error": error,
479+
"heartbeat": heartbeat,
433480
},
434481
)
435482

src/project_forge/web/static/style.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,24 @@ a.stat-card:hover { transform: translateY(-3px); border-color: var(--accent); }
426426
.section-subtitle { font-size: 0.85rem; color: var(--text-dim); font-weight: 400; }
427427
.vertical-card .cat-name { text-transform: capitalize; }
428428
.vertical-card .cat-score { font-size: 0.75rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
429+
430+
.heartbeat-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem 1.5rem; }
431+
.heartbeat-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem; }
432+
.heartbeat-header h3 { margin: 0; }
433+
.heartbeat-now { font-size: 0.75rem; color: var(--text-dim); font-family: var(--font-mono, monospace); }
434+
.heartbeat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; }
435+
.heartbeat-stat { background: var(--bg, #0d1117); border: 1px solid var(--border); border-radius: 8px; padding: 0.75rem 1rem; }
436+
.heartbeat-num { font-size: 1.75rem; font-weight: 700; color: var(--green, #10b981); }
437+
.heartbeat-num-dim { color: var(--text-dim); }
438+
.heartbeat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin-top: 0.25rem; }
439+
.heartbeat-when { font-size: 0.85rem; font-family: var(--font-mono, monospace); margin-top: 0.25rem; }
440+
.heartbeat-runs { margin-top: 1rem; font-size: 0.85rem; }
441+
.heartbeat-runs summary { cursor: pointer; color: var(--text-dim); }
442+
.heartbeat-runs ul { list-style: none; padding-left: 0; margin-top: 0.5rem; }
443+
.heartbeat-runs li { padding: 0.25rem 0; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
444+
.heartbeat-runs code { font-size: 0.75rem; color: var(--text-dim); }
445+
.run-ok { color: var(--green, #10b981); font-size: 0.75rem; }
446+
.run-err { color: var(--red, #ef4444); font-size: 0.75rem; }
429447
.filter-status select {
430448
background: var(--surface);
431449
border: 1px solid var(--border);

src/project_forge/web/templates/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>{% block title %}Project Forge{% endblock %}</title>
77
<meta name="forge-token" content="{{ dashboard_token }}">
8-
<link rel="stylesheet" href="/static/style.css?v=5">
8+
<link rel="stylesheet" href="/static/style.css?v=6">
99
</head>
1010
<body>
1111
<nav class="navbar">

src/project_forge/web/templates/thinktank.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,47 @@ <h1 class="hero-title">Think Tank</h1>
99
</div>
1010
</section>
1111

12+
{% if heartbeat %}
13+
<section class="section">
14+
<div class="container">
15+
<div class="heartbeat-panel">
16+
<div class="heartbeat-header">
17+
<h3>Engine Activity</h3>
18+
<span class="heartbeat-now">as of {{ heartbeat.now[:19] }}Z</span>
19+
</div>
20+
<div class="heartbeat-grid">
21+
<div class="heartbeat-stat">
22+
<div class="heartbeat-num">{{ heartbeat.accepted_24h }}</div>
23+
<div class="heartbeat-label">Accepted (24h)</div>
24+
</div>
25+
<div class="heartbeat-stat">
26+
<div class="heartbeat-num heartbeat-num-dim">{{ heartbeat.filtered_24h }}</div>
27+
<div class="heartbeat-label">Dedup-filtered (24h)</div>
28+
</div>
29+
<div class="heartbeat-stat">
30+
<div class="heartbeat-label">Last super idea</div>
31+
<div class="heartbeat-when">{{ heartbeat.last_super[:19] if heartbeat.last_super else 'never' }}</div>
32+
</div>
33+
</div>
34+
{% if heartbeat.recent_runs %}
35+
<details class="heartbeat-runs">
36+
<summary>Last {{ heartbeat.recent_runs|length }} generation runs</summary>
37+
<ul>
38+
{% for run in heartbeat.recent_runs %}
39+
<li>
40+
<code>{{ run.when[:19] }}</code>
41+
<span class="badge badge-{{ run.category }}">{{ run.category }}</span>
42+
{% if run.ok %}<span class="run-ok">ok</span>{% else %}<span class="run-err">err: {{ (run.error or '')[:60] }}</span>{% endif %}
43+
</li>
44+
{% endfor %}
45+
</ul>
46+
</details>
47+
{% endif %}
48+
</div>
49+
</div>
50+
</section>
51+
{% endif %}
52+
1253
<section class="stats-section">
1354
<div class="container">
1455
<div class="stats-grid">

0 commit comments

Comments
 (0)