feat(pipeline): article age filter, image URL validation, schedule UI, retry button

1. Article age filter (ingestion.py + config.py):
   - New setting pipeline_max_article_age_days=7 (0 = no limit)
   - Skip RSS entries older than N days before expensive extract_article()
   - Prevents old articles from Google Alerts re-entering pipeline

2. Image URL pre-validation (ingestion.py):
   - HEAD request probe for each primary image candidate during ingestion
   - Falls back to next-best candidate if primary returns 4xx
   - Network errors treated as OK to avoid false negatives on flaky servers

3. Stale WP draft cleanup (pipeline.py):
   - Quality gate rejections now delete any pre-existing WP draft (wp_post_id)
   - Prevents orphaned drafts when re-running articles that previously had drafts

4. Schedule overview UI (scheduler.py + admin_ui.py + admin_schedule.html):
   - New /admin/schedule page showing calendar grid of all booked slots
   - Distinguishes Pipeline-DB slots from WordPress-only slots
   - Link added to dashboard navigation

5. Retry for failed articles (admin_ui.py + admin_dashboard.html):
   - New POST /admin/articles/{id}/retry endpoint: resets to 'new', releases slot
   - '🔄 Wiederholen' button shown in dashboard for all 'close' (error) articles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OliverGiertz 2026-04-10 08:44:28 +00:00
parent cf2d826c8a
commit 8676ace102
7 changed files with 344 additions and 5 deletions

View file

@ -374,6 +374,15 @@ def _process_article(article: dict[str, Any], stats: PipelineStats, settings: An
# Release the reserved slot so it's available for the next article
from .scheduler import release_publish_slot
release_publish_slot(article_id)
# Clean up any stale WP draft from a previous pipeline run
stale = get_article_by_id(article_id)
if stale and stale.get("wp_post_id"):
try:
from .wordpress import delete_wp_post
delete_wp_post(int(stale["wp_post_id"]))
logger.info("Artikel #%d: veralteten WP-Draft #%s gelöscht", article_id, stale["wp_post_id"])
except Exception as del_exc:
logger.warning("Artikel #%d: WP-Draft konnte nicht gelöscht werden: %s", article_id, del_exc)
stats.quality_gate_rejected += 1
logger.info("Artikel #%d wegen Qualitätsprüfung abgelehnt: %s", article_id, exc)
# Individual Telegram notification for quality gate rejection