feat: move article generation to background worker queue

- Add generate_articles task handler to worker.py
- template_generate and template_regenerate now enqueue tasks instead
  of running inline (was blocking HTTP request for seconds with 1k articles)
- rebuild_all enqueues per-template + inline rebuilds manual articles
- Update tests to check task enqueue instead of immediate article creation

Subtask 4 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-24 01:17:59 +01:00
parent 81b361859d
commit f0f6e7542f
3 changed files with 80 additions and 34 deletions

View File

@@ -1314,7 +1314,7 @@ async def template_preview(slug: str, row_key: str):
@csrf_protect
async def template_generate(slug: str):
"""Generate articles from template + DuckDB data."""
from ..content import fetch_template_data, generate_articles, load_template
from ..content import fetch_template_data, load_template
try:
config = load_template(slug)
@@ -1330,15 +1330,20 @@ async def template_generate(slug: str):
start_date_str = form.get("start_date", "")
articles_per_day = int(form.get("articles_per_day", 3) or 3)
if not start_date_str:
start_date = date.today()
else:
start_date = date.fromisoformat(start_date_str)
start_date = date.fromisoformat(start_date_str) if start_date_str else date.today()
generated = await generate_articles(
slug, start_date, articles_per_day, limit=500,
from ..worker import enqueue
await enqueue("generate_articles", {
"template_slug": slug,
"start_date": start_date.isoformat(),
"articles_per_day": articles_per_day,
"limit": 500,
})
await flash(
f"Article generation queued for '{config['name']}'. "
"The worker will process it in the background.",
"success",
)
await flash(f"Generated {generated} articles with staggered publish dates.", "success")
return redirect(url_for("admin.articles"))
return await render_template(
@@ -1354,7 +1359,7 @@ async def template_generate(slug: str):
@csrf_protect
async def template_regenerate(slug: str):
"""Re-generate all articles for a template with fresh DuckDB data."""
from ..content import generate_articles, load_template
from ..content import load_template
try:
load_template(slug)
@@ -1362,9 +1367,14 @@ async def template_regenerate(slug: str):
await flash("Template not found.", "error")
return redirect(url_for("admin.templates"))
# Use today as start date, keep existing publish dates via upsert
generated = await generate_articles(slug, date.today(), articles_per_day=500)
await flash(f"Regenerated {generated} articles from fresh data.", "success")
from ..worker import enqueue
await enqueue("generate_articles", {
"template_slug": slug,
"start_date": date.today().isoformat(),
"articles_per_day": 500,
"limit": 500,
})
await flash("Regeneration queued. The worker will process it in the background.", "success")
return redirect(url_for("admin.template_detail", slug=slug))
@@ -1877,13 +1887,29 @@ async def article_rebuild(article_id: int):
@role_required("admin")
@csrf_protect
async def rebuild_all():
"""Re-render all articles."""
articles = await fetch_all("SELECT id FROM articles")
count = 0
for a in articles:
"""Re-render all articles via background worker."""
from ..content import discover_templates
from ..worker import enqueue
templates = discover_templates()
for t in templates:
await enqueue("generate_articles", {
"template_slug": t["slug"],
"start_date": date.today().isoformat(),
"articles_per_day": 500,
"limit": 500,
})
# Manual articles still need inline rebuild
manual = await fetch_all("SELECT id FROM articles WHERE template_slug IS NULL")
for a in manual:
await _rebuild_article(a["id"])
count += 1
await flash(f"Rebuilt {count} articles.", "success")
await flash(
f"Queued rebuild for {len(templates)} templates"
f" + rebuilt {len(manual)} manual articles.",
"success",
)
return redirect(url_for("admin.articles"))

View File

@@ -709,6 +709,22 @@ async def handle_cleanup_seo_metrics(payload: dict) -> None:
print(f"[WORKER] Cleaned up {deleted} old SEO metric rows")
@task("generate_articles")
async def handle_generate_articles(payload: dict) -> None:
"""Generate articles from a template in the background."""
from datetime import date as date_cls
from .content import generate_articles
slug = payload["template_slug"]
start_date = date_cls.fromisoformat(payload["start_date"])
articles_per_day = payload.get("articles_per_day", 3)
limit = payload.get("limit", 500)
count = await generate_articles(slug, start_date, articles_per_day, limit=limit)
print(f"[WORKER] Generated {count} articles for template '{slug}'")
# =============================================================================
# Worker Loop
# =============================================================================