Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be19c0ec83 | |||
| bc1c025101 | |||
| fa281391a9 | |||
| 85aa2781f1 | |||
| e63004daca | |||
| 082af0a46b | |||
| 42069f9aa7 | |||
| 26f8ac477d | |||
| 202cb9da42 | |||
| 23a4bff254 |
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
article_pipeline.py — Stage 0: Input-Preparation für den BSN-Artikel-Workflow.
|
||||
|
||||
Liest Topic, PDFs, URLs und baut einen strukturierten Prompt für den Hermes-Agenten,
|
||||
der dann via delegate_task Research (Stage 1) und Write+QC (Stage 2+3) durchführt.
|
||||
Stage 4 (Git, Verify) macht der Agent selbst.
|
||||
|
||||
Usage:
|
||||
# Thema + PDF
|
||||
python3 article_pipeline.py --topic "Gamegenic × TCC" --pdf pm.pdf --rubric "Nachrichten"
|
||||
|
||||
# Thema + URL
|
||||
python3 article_pipeline.py --topic "Neue Kennerspiel-Nominierungen" --url "https://..."
|
||||
|
||||
# Nur Thema (autonome Recherche)
|
||||
python3 article_pipeline.py --topic "Spiel des Jahres 2026" --rubric "Nachrichten"
|
||||
|
||||
# Batch (JSON)
|
||||
python3 article_pipeline.py --batch topics.json
|
||||
|
||||
Output: Strukturierter Prompt nach stdout (wird vom Hermes-Cronjob als Context injiziert)
|
||||
Exit-Codes: 0 = Prompt bereit, 1 = Fehler in Stage 0
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
WORKSPACE = "/home/hermes/workspace"
|
||||
DOC_CACHE = os.path.expanduser("~/.hermes/cache/documents")
|
||||
|
||||
|
||||
def ocr_pdf(pdf_path: str) -> str:
|
||||
"""PDF mit pymupdf OCRen und Text extrahieren."""
|
||||
import pymupdf
|
||||
doc = pymupdf.open(pdf_path)
|
||||
texts = []
|
||||
for page in doc:
|
||||
texts.append(page.get_text())
|
||||
doc.close()
|
||||
return "\n\n".join(texts)
|
||||
|
||||
|
||||
def extract_url_text(url: str) -> str:
|
||||
"""URL-Inhalt via 5-Stufen-Eskalation abrufen."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Stufe 1: curl direkt
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return resp.read().decode("utf-8", errors="ignore")[:5000]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stufe 2-5 wäre hier — für Stage 0 reicht Stufe 1
|
||||
return f"[URL nicht direkt abrufbar: {url}]"
|
||||
|
||||
|
||||
def build_prompt(topic: str, sources: list[str], rubric: str,
|
||||
category_id: int, image_urls: list[str],
|
||||
deadline: str | None,
|
||||
pdf_texts: dict[str, str]) -> str:
|
||||
"""Direkten Agent-Prompt für den Artikel-Workflow bauen."""
|
||||
|
||||
now = datetime.now()
|
||||
date_str = now.strftime("%d. %B %Y")
|
||||
slug = topic.lower().replace(" ", "_").replace(":", "").replace("×", "x")[:40]
|
||||
|
||||
lines = []
|
||||
lines.append(f"Führe den BSN-Artikel-Workflow für folgendes Thema aus:")
|
||||
lines.append(f"**Thema:** {topic}")
|
||||
lines.append(f"**Datum:** {date_str}")
|
||||
lines.append(f"**Rubrik:** {rubric} (Joomla catid={category_id})")
|
||||
if deadline:
|
||||
lines.append(f"**Deadline:** {deadline}")
|
||||
lines.append(f"**Dateiname:** artikel_{slug}_{now.strftime('%Y-%m-%d')}.html")
|
||||
lines.append("")
|
||||
|
||||
# PDF-OCR-Ergebnisse
|
||||
if pdf_texts:
|
||||
lines.append("## PDF-Quellen (OCR-extrahiert, bereits im Context)")
|
||||
for path, text in pdf_texts.items():
|
||||
fname = os.path.basename(path)
|
||||
truncated = text[:3000]
|
||||
if len(text) > 3000:
|
||||
truncated += f"\n... (gekürzt, Original: {len(text)} Zeichen)"
|
||||
lines.append(f"\n### {fname}\n{truncated}\n")
|
||||
lines.append("")
|
||||
|
||||
# URL-Quellen
|
||||
if sources:
|
||||
lines.append("## URL-Quellen")
|
||||
for s in sources:
|
||||
lines.append(f"- {s}")
|
||||
lines.append("")
|
||||
|
||||
# STAGE 1: Research
|
||||
lines.append("## STAGE 1: Research")
|
||||
lines.append("")
|
||||
lines.append("Nutze **delegate_task** um einen Research-Subagent zu starten:")
|
||||
lines.append("")
|
||||
lines.append(f"- goal: \"Recherchiere zum Thema: {topic}. Sammle Fakten aus allen Quellen, bewerte sie (AAA/AA/A), ergänze BGG-Daten (Designer, Verlag, Jahr). Erstelle Research-Brief in /tmp/research_brief.md. Bei unvollständigen Kern-Fakten → Exit 1.\"")
|
||||
lines.append("- context: Enthält PDF-Texte und URLs (siehe oben). Nutze 5-Stufen-Eskalation für unerreichbare URLs. web_search site:boardgamegeek.com für BGG-Daten.")
|
||||
lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]')
|
||||
lines.append("")
|
||||
lines.append("**WICHTIG:** Warte bis Sub-Agent #1 fertig ist. Prüfe /tmp/research_brief.md. Bei Exit-Code 1 → MELDUNG an Basti, NICHT fortfahren.")
|
||||
lines.append("")
|
||||
|
||||
# STAGE 2+3: Write+QC
|
||||
lines.append("## STAGE 2+3: Write + QC")
|
||||
lines.append("")
|
||||
lines.append("Nutze **delegate_task** für einen Write+QC-Subagent:")
|
||||
lines.append("")
|
||||
lines.append(f'- goal: "Schreibe HTML-Artikel zum Thema: {topic}. Nutze Research-Brief aus /tmp/research_brief.md. Datei: artikel_{slug}_{now.strftime("%Y-%m-%d")}.html in /home/hermes/workspace/. Danach: bash /home/hermes/workspace/verify_article.sh artikel_{slug}_{now.strftime("%Y-%m-%d")}.html. Bei Fehlern korrigieren, max 3 Versuche."')
|
||||
lines.append(f'- context: "HTML-Regeln: KEIN XML API v2, KEIN Betriebsgeheimnis, KEINE Emojis. <p class=\\"meta\\"> mit Datum+Lesezeit. Genau 1× <hr id=\\"system-readmore\\" />. Quellen als <span><strong>Quellen</strong></span> + <ol> mit <a target=\\"_blank\\">. <p class=\\"qc\\"> mit Quellen-Ratings. Datei endet mit </html>. Rubrik: {rubric}."')
|
||||
lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]')
|
||||
lines.append("")
|
||||
lines.append("**WICHTIG:** Warte bis Sub-Agent #2 fertig. Prüfe Exit-Code und Datei.")
|
||||
lines.append("")
|
||||
|
||||
# STAGE 4: Deliver
|
||||
lines.append("## STAGE 4: Deliver")
|
||||
lines.append("")
|
||||
lines.append("1. Git-Commit:")
|
||||
lines.append(f" ```bash")
|
||||
lines.append(f" cd /home/hermes/workspace && git add artikel_{slug}_{now.strftime('%Y-%m-%d')}.html")
|
||||
lines.append(f' git commit -m "artikel: {topic[:50]}"')
|
||||
lines.append(f" ```")
|
||||
lines.append("2. Prüfen ob Article-Server erreichbar:")
|
||||
lines.append(" curl -s 'http://185.162.249.159:8890/?key=br3ttsp1el-n3ws-2026' | head -3")
|
||||
lines.append("3. Basti benachrichtigen per send_message (Telegram):")
|
||||
lines.append(f" ✅ Artikel fertig: {topic}")
|
||||
lines.append(f" 📄 /home/hermes/workspace/artikel_{slug}_{now.strftime('%Y-%m-%d')}.html")
|
||||
lines.append(" 📊 verify_article.sh: 0 Fehler")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="BSN Article Pipeline — Stage 0 Prompt Builder"
|
||||
)
|
||||
parser.add_argument("--topic", help="Artikel-Thema")
|
||||
parser.add_argument("--sources", nargs="*", default=[],
|
||||
help="URLs oder PDF-Pfade")
|
||||
parser.add_argument("--rubric", default="Korrektur",
|
||||
help="Rubrik-Name (default: Korrektur)")
|
||||
parser.add_argument("--category-id", type=int, default=8,
|
||||
help="Joomla Category-ID (default: 8)")
|
||||
parser.add_argument("--image-urls", nargs="*", default=[],
|
||||
help="Bild-URLs aus PM")
|
||||
parser.add_argument("--deadline", default=None,
|
||||
help="ISO-Timestamp für Deadline")
|
||||
parser.add_argument("--batch", help="JSON-Datei mit Batch-Inputs")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Batch-Modus
|
||||
if args.batch:
|
||||
with open(args.batch) as f:
|
||||
batch_inputs = json.load(f)
|
||||
outputs = []
|
||||
for inp in batch_inputs:
|
||||
prompt = build_prompt(
|
||||
topic=inp.get("topic", ""),
|
||||
sources=inp.get("sources", []),
|
||||
rubric=inp.get("rubric", "Korrektur"),
|
||||
category_id=inp.get("category_id", 8),
|
||||
image_urls=inp.get("image_urls", []),
|
||||
deadline=inp.get("deadline"),
|
||||
pdf_texts={}
|
||||
)
|
||||
outputs.append({
|
||||
"topic": inp.get("topic"),
|
||||
"prompt": prompt
|
||||
})
|
||||
print(json.dumps(outputs, indent=2, ensure_ascii=False))
|
||||
return
|
||||
|
||||
# Einzel-Modus
|
||||
if not args.topic:
|
||||
print("ERROR: --topic erforderlich (oder --batch)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# PDFs OCRen
|
||||
pdf_texts = {}
|
||||
for src in args.sources:
|
||||
if src.lower().endswith(".pdf"):
|
||||
full_path = src
|
||||
if not os.path.isabs(full_path):
|
||||
full_path = os.path.join(DOC_CACHE, src)
|
||||
if os.path.exists(full_path):
|
||||
try:
|
||||
pdf_texts[full_path] = ocr_pdf(full_path)
|
||||
print(f"[Stage 0] PDF OCR: {full_path} "
|
||||
f"({len(pdf_texts[full_path])} Zeichen)", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[Stage 0] PDF-Fehler: {e}", file=sys.stderr)
|
||||
|
||||
# Prompt bauen
|
||||
prompt = build_prompt(
|
||||
topic=args.topic,
|
||||
sources=args.sources,
|
||||
rubric=args.rubric,
|
||||
category_id=args.category_id,
|
||||
image_urls=args.image_urls,
|
||||
deadline=args.deadline,
|
||||
pdf_texts=pdf_texts,
|
||||
)
|
||||
|
||||
print(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+87
-45
@@ -66,26 +66,20 @@ MONTHS = {
|
||||
"September": 9, "Oktober": 10, "November": 11, "Dezember": 12
|
||||
}
|
||||
|
||||
# ── Google Imagen API Configuration ────────────────────────────────────────
|
||||
IMAGEN_API_URL = (
|
||||
# ── Gemini Flash Image API Configuration ────────────────────────────────────
|
||||
GEMINI_IMAGE_MODEL = "gemini-2.5-flash-image"
|
||||
GEMINI_API_URL = (
|
||||
"https://generativelanguage.googleapis.com/v1beta/"
|
||||
"models/imagen-4.0-generate-001:predict"
|
||||
"models/" + GEMINI_IMAGE_MODEL + ":generateContent"
|
||||
)
|
||||
IMAGEN_KEY_SECRET = "bsn/google-imagen-key"
|
||||
|
||||
# Resolution Mapping (Imagen 4.0 verwendet 1K/2K direkt)
|
||||
IMAGEN_SIZE_MAP = {
|
||||
"1K": "1K",
|
||||
"2K": "2K",
|
||||
"4K": "2K", # 4K nicht unterstützt → falle zurück auf 2K
|
||||
}
|
||||
GEMINI_KEY_SECRET = "bsn/google-imagen-key"
|
||||
|
||||
|
||||
def _get_imagen_key() -> str:
|
||||
"""Google Imagen API-Key aus bsn-secrets lesen."""
|
||||
def _get_gemini_key() -> str:
|
||||
"""Google Gemini API-Key aus bsn-secrets lesen."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["/home/hermes/bin/bsn-secrets", "get", IMAGEN_KEY_SECRET],
|
||||
["/home/hermes/bin/bsn-secrets", "get", GEMINI_KEY_SECRET],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
return r.stdout.strip()
|
||||
@@ -93,33 +87,77 @@ def _get_imagen_key() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _imagen_generate(
|
||||
def _detect_image_mime(image_bytes: bytes) -> str:
|
||||
"""MIME-Type anhand Magic-Bytes erkennen."""
|
||||
if len(image_bytes) < 8:
|
||||
return "image/jpeg" # Fallback
|
||||
if image_bytes[:4] == b"\x89PNG":
|
||||
return "image/png"
|
||||
if image_bytes[:2] == b"\xff\xd8":
|
||||
return "image/jpeg"
|
||||
if image_bytes[:4] == b"RIFF" and image_bytes[8:12] == b"WEBP":
|
||||
return "image/webp"
|
||||
if image_bytes[:4] in (b"ftyp", b"heic", b"heix", b"hevc", b"hevx"):
|
||||
return "image/heic"
|
||||
return "image/jpeg" # Fallback
|
||||
|
||||
|
||||
def _gemini_generate_image(
|
||||
prompt: str,
|
||||
image_urls: list,
|
||||
image_base64: str | None,
|
||||
aspect_ratio: str,
|
||||
resolution: str
|
||||
) -> dict:
|
||||
"""Google Imagen aufrufen — synchron, liefert Base64-Bild direkt zurück."""
|
||||
key = _get_imagen_key()
|
||||
"""Gemini Flash Image aufrufen — Text-to-Image und Image-to-Image.
|
||||
|
||||
Args:
|
||||
prompt: Text-Prompt.
|
||||
image_base64: Optionales Referenzbild als Base64 (Image-to-Image).
|
||||
aspect_ratio: Seitenverhältnis (z.B. "16:9").
|
||||
resolution: Behält Kompatibilität (von Gemini ignoriert).
|
||||
|
||||
Returns:
|
||||
dict mit "image_base64" (PNG) oder "error".
|
||||
"""
|
||||
import base64 as b64
|
||||
|
||||
key = _get_gemini_key()
|
||||
if not key:
|
||||
return {"error": "Kein Google Imagen API-Key"}
|
||||
return {"error": "Kein Gemini API-Key"}
|
||||
|
||||
parts = [{"text": f"{prompt}\n\nAspect ratio: {aspect_ratio}."}]
|
||||
if image_base64:
|
||||
# MIME-Type aus den tatsächlichen Bilddaten erkennen
|
||||
raw_bytes = b64.b64decode(image_base64)
|
||||
mime_type = _detect_image_mime(raw_bytes)
|
||||
parts.append({
|
||||
"inlineData": {
|
||||
"mimeType": mime_type,
|
||||
"data": image_base64
|
||||
}
|
||||
})
|
||||
|
||||
body = json.dumps({
|
||||
"instances": [{"prompt": prompt}],
|
||||
"parameters": {
|
||||
"sampleCount": 1,
|
||||
"aspectRatio": aspect_ratio,
|
||||
"sampleImageSize": IMAGEN_SIZE_MAP.get(resolution, "2048x2048"),
|
||||
"contents": [{"parts": parts}],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["image", "text"]
|
||||
}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(IMAGEN_API_URL, data=body, method="POST")
|
||||
req = urllib.request.Request(GEMINI_API_URL, data=body, method="POST")
|
||||
req.add_header("x-goog-api-key", key)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read())
|
||||
with urllib.request.urlopen(req, timeout=45) as resp:
|
||||
result = json.loads(resp.read())
|
||||
candidates = result.get("candidates") or []
|
||||
for cand in candidates:
|
||||
for part in cand.get("content", {}).get("parts", []):
|
||||
inline = part.get("inlineData")
|
||||
if inline and inline.get("data"):
|
||||
return {"image_base64": inline["data"]}
|
||||
return {"error": "Kein Bild in Gemini-Antwort"}
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"error": f"HTTP {e.code}: {e.read().decode()[:300]}"}
|
||||
|
||||
@@ -975,6 +1013,8 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
||||
|
||||
def _handle_image_generate(self):
|
||||
"""Bildgenerierung via Google Imagen (synchron, Base64→JPEG)."""
|
||||
import base64 as b64
|
||||
|
||||
content_type = self.headers.get("Content-Type", "")
|
||||
if "multipart/form-data" not in content_type:
|
||||
self._json_reply(400, {"ok": False, "error": "Expected multipart/form-data"})
|
||||
@@ -993,6 +1033,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
||||
prompt = ""
|
||||
aspect_ratio = "16:9"
|
||||
resolution = "1K"
|
||||
image_base64 = None
|
||||
|
||||
for part in parts:
|
||||
if b"Content-Disposition" not in part:
|
||||
@@ -1009,52 +1050,53 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
||||
aspect_ratio = content.decode("utf-8", errors="ignore")
|
||||
elif 'name="resolution"' in headers_raw:
|
||||
resolution = content.decode("utf-8", errors="ignore")
|
||||
elif 'name="image"' in headers_raw and len(content) > 100:
|
||||
# Referenzbild als Base64 für Image-to-Image
|
||||
image_base64 = b64.b64encode(content).decode()
|
||||
|
||||
if not prompt:
|
||||
self._json_reply(400, {"ok": False, "error": "Prompt fehlt"})
|
||||
return
|
||||
|
||||
# Google Imagen aufrufen (synchron!)
|
||||
result = _imagen_generate(prompt, [], aspect_ratio, resolution)
|
||||
# Gemini Flash Image aufrufen (synchron!)
|
||||
result = _gemini_generate_image(prompt, image_base64, aspect_ratio, resolution)
|
||||
if "error" in result:
|
||||
self._json_reply(500, {"ok": False, "error": result["error"]})
|
||||
return
|
||||
|
||||
# Base64-Bild extrahieren
|
||||
predictions = result.get("predictions") or []
|
||||
if not predictions:
|
||||
self._json_reply(500, {"ok": False, "error": "Kein Bild in Imagen-Antwort"})
|
||||
return
|
||||
|
||||
b64_data = predictions[0].get("bytesBase64Encoded", "")
|
||||
# Base64-Bild (PNG) dekodieren
|
||||
b64_data = result.get("image_base64", "")
|
||||
if not b64_data:
|
||||
self._json_reply(500, {"ok": False, "error": "Leeres Base64-Bild"})
|
||||
self._json_reply(500, {"ok": False, "error": "Leeres Bild von Gemini"})
|
||||
return
|
||||
|
||||
import base64 as b64
|
||||
img_bytes = b64.b64decode(b64_data)
|
||||
|
||||
# Im media-Ordner speichern (wie normale Uploads)
|
||||
# Im media-Ordner speichern
|
||||
now = datetime.now()
|
||||
month_dir = os.path.join(MEDIA_DIR, now.strftime("%Y"), now.strftime("%m"))
|
||||
os.makedirs(month_dir, exist_ok=True)
|
||||
filename = f"imagen_{now.strftime('%H%M%S')}.jpg"
|
||||
filepath = os.path.join(month_dir, filename)
|
||||
base_name = f"gemini_{now.strftime('%H%M%S')}"
|
||||
ext = ".png" # Gemini liefert PNG
|
||||
filepath = os.path.join(month_dir, base_name + ext)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(img_bytes)
|
||||
|
||||
# Optional: PIL-Normalisierung auf 1920x1080 @ 72 DPI
|
||||
# Optional: PIL-Konvertierung zu JPEG (1920x1080 @ 72 DPI)
|
||||
try:
|
||||
from PIL import Image
|
||||
img = Image.open(filepath)
|
||||
img = img.convert("RGB")
|
||||
img = img.resize((1920, 1080), Image.LANCZOS)
|
||||
img.save(filepath, "JPEG", dpi=(72, 72), quality=92)
|
||||
jpg_path = os.path.join(month_dir, base_name + ".jpg")
|
||||
img.save(jpg_path, "JPEG", dpi=(72, 72), quality=92)
|
||||
filepath = jpg_path
|
||||
ext = ".jpg"
|
||||
except Exception:
|
||||
pass # Original behalten wenn PIL fehlschlägt
|
||||
pass # PNG behalten wenn PIL fehlschlägt
|
||||
|
||||
img_url = f"/media/{now.strftime('%Y')}/{now.strftime('%m')}/{filename}?key={ACCESS_TOKEN}"
|
||||
img_url = f"/media/{now.strftime('%Y')}/{now.strftime('%m')}/{base_name}{ext}?key={ACCESS_TOKEN}"
|
||||
self._json_reply(200, {
|
||||
"ok": True,
|
||||
"imageUrl": img_url,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Gamegenic wird exklusiver Ausstattungspartner von „Shuffle Up & Play"</title>
|
||||
<meta name="description" content="Gamegenic wird exklusiver Ausstattungspartner von Shuffle Up & Play Staffel 5. Die Essener Asmodee-Tochter und Tolarian Community College mit 1,3 Mio. Abonnenten vertiefen Zusammenarbeit — drei Jahre nach dem Rekord-Kickstarter The Academic.">
|
||||
<meta name="keywords" content="Gamegenic, Tolarian Community College, Shuffle Up and Play, The Professor, Brian Lewis, Magic The Gathering, MTG, The Academic, Kickstarter, Asmodee, Essen, Brettspiel, TCG">
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
|
||||
<h1>Gamegenic wird exklusiver Ausstattungspartner von „Shuffle Up & Play": Essener Zubehör-Marke und der Professor vertiefen Zusammenarbeit</h1>
|
||||
|
||||
<div style="font-size:1.1rem;color:#555;margin-bottom:1.5rem;border-bottom:2px solid #e0e0e0;padding-bottom:1rem;">Drei Jahre nach dem Rekord-Kickstarter „The Academic" gehen Gamegenic und Tolarian Community College den nächsten Schritt: Staffel 5 der erfolgreichen Magic-Gameplay-Show startet am 29. Juni — mit Gamegenic als exklusivem Accessoire-Partner.</div>
|
||||
|
||||
<p class="meta" style="font-size:0.85rem;color:#888;margin-bottom:2rem;">23. Juni 2026 · Lesezeit: ~4 Minuten</p>
|
||||
|
||||
<p>Manche Partnerschaften ergeben einfach Sinn. Drei Jahre nachdem die Essener Zubehör-Marke <strong>Gamegenic</strong> gemeinsam mit <strong>Brian „The Professor" Lewis</strong> vom YouTube-Kanal <strong>Tolarian Community College</strong> den bis heute erfolgreichsten Deckbox-Kickstarter aller Zeiten stemmte, vertiefen beide Seiten nun ihre Zusammenarbeit: Gamegenic wird ab Staffel 5 exklusiver Ausstattungspartner der Gameplay-Show <strong>„Shuffle Up & Play"</strong>. Die erste Folge unter der neuen Partnerschaft läuft am <strong>29. Juni 2026</strong>.</p>
|
||||
|
||||
<h2>Vom Kickstarter-Rekord zur Gameplay-Show</h2>
|
||||
|
||||
<p>Der Grundstein wurde 2022 gelegt. Damals launchte The Professor — mit 1,3 Millionen Abonnenten der mit Abstand größte Magic: The Gathering-Content-Creator auf YouTube — seine erste eigene Kickstarter-Kampagne: <strong>The Academic 133+ XL</strong>, eine von Grund auf mit Gamegenic entwickelte Deckbox. Die Kampagne sprengte alle Erwartungen und wurde zum „record-breaking" Erfolg, wie Gamegenic in der Pressemitteilung betont.</p>
|
||||
|
||||
<p>„Die Kickstarter-Kampagne für The Academic hat es uns ermöglicht, die Show überhaupt erst zu realisieren", erklärt Brian Lewis. „In diesem Sinne ist Gamegenic seit der ersten Stunde bei Shuffle Up & Play dabei. Es ist nur passend, dass sie ab Staffel 5 einer unserer Premium-Sponsoren sind."</p>
|
||||
|
||||
<p>In der wöchentlichen Show begrüßt The Professor wechselnde Gäste aus der Magic-Szene — von Pro-Tour-Spielern über andere Content-Creator bis zu Community-Größen — die in verschiedenen Formaten und Spielweisen des weltweit führenden Trading Card Games gegeneinander antreten. Gamegenic stattet die Teilnehmer künftig mit seinem gesamten Zubehör-Sortiment aus: von Kartenboxen über Sleeves bis zu Spielmatten.</p>
|
||||
|
||||
<h2>„Wenn es eine Firma für Gaming-Zubehör gibt, mit der ich zusammenarbeiten will, dann Gamegenic"</h2>
|
||||
|
||||
<p>Der Professor macht keinen Hehl aus seiner Wertschätzung für den deutschen Hersteller: „Wenn es je eine Firma für Gaming-Zubehör gab, mit der ich zusammenarbeiten möchte, dann ist es Gamegenic." Diese Aussage kommt nicht von ungefähr: Der Kanal Tolarian Community College ist seit über einem Jahrzehnt <em>die</em> Instanz für Magic-Produktbewertungen, und Gamegenic-Produkte haben dort regelmäßig Spitzenbewertungen erhalten.</p>
|
||||
|
||||
<p>Adrian Alonso, Gründer und Head of Gamegenic, sieht die Partnerschaft ähnlich: „Im Laufe der Jahre ist The Professor zu einer der vertrauenswürdigsten Stimmen in der Magic: The Gathering-Community geworden. Wir sind unglaublich stolz, dass Gamegenic durchgehend einige seiner höchsten Bewertungen und Empfehlungen erhalten hat. Die Marke zu sein, der er vertraut, ist eine große Ehre und treibt uns an, weiter zu innovieren."</p>
|
||||
|
||||
<div style="background:#f0f4f8;border:1px solid #d0d8e0;border-radius:6px;padding:1.2rem 1.5rem;margin:2rem 0;">
|
||||
<h3 style="margin-top:0;color:#20228A;font-family:Arial,sans-serif;font-size:1.15rem;">Gamegenic — Fakten</h3>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;width:160px;">Gründung</td><td style="padding:0.3rem 0.5rem;">2019 in Essen</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Gründer</td><td style="padding:0.3rem 0.5rem;">Adrian Alonso</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Konzern</td><td style="padding:0.3rem 0.5rem;">Asmodee Group</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Produkte</td><td style="padding:0.3rem 0.5rem;">Deckboxen, Sleeves, Binder, Spielmatten, Würfel</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Bisherige Kooperation</td><td style="padding:0.3rem 0.5rem;">The Academic 133+ XL (Kickstarter 2022, Rekord-Kampagne)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Essener Zubehör-Design trifft weltgrößte TCG-Community</h2>
|
||||
|
||||
<p>Gamegenic wurde 2019 mit einem klaren Auftrag gegründet: Gaming-Zubehör neu zu denken — durch Qualität, Innovation und funktionales Design. Heute gehört das Unternehmen zur <strong>Asmodee Group</strong>, einem der größten Brettspiel- und TCG-Vertriebskonzerne der Welt, und sitzt weiterhin in Essen, dem Herzen der europäischen Brettspielbranche.</p>
|
||||
|
||||
<p>Tolarian Community College wiederum erreicht mit 1,3 Millionen Abonnenten wöchentlich ein Millionenpublikum an Magic-Spielern weltweit — eine Reichweite, die selbst offizielle Wizards-of-the-Coast-Kanäle übertrifft. Die Show „Shuffle Up & Play" feierte kürzlich ihre 100. Folge, und für Staffel 5 läuft derzeit <a href="https://www.kickstarter.com/projects/tolarian/shuffle-up-and-play-season-5" target="_blank" rel="noopener" style="color:#0075FF;">eine eigene Kickstarter-Kampagne</a> zur Finanzierung.</p>
|
||||
|
||||
<p>Dass ein deutscher Zubehör-Hersteller zum exklusiven Partner des einflussreichsten Magic-Content-Creators wird, ist auch ein Signal für die wachsende Bedeutung der europäischen Brettspielindustrie im globalen TCG-Markt.</p>
|
||||
|
||||
<h2>Staffelstart am 29. Juni</h2>
|
||||
|
||||
<p>Die erste „Shuffle Up & Play"-Folge unter der neuen Partnerschaft erscheint am <strong>29. Juni 2026</strong> auf dem Kanal <a href="https://www.youtube.com/@TolarianCommunityCollege" target="_blank" rel="noopener" style="color:#0075FF;">Tolarian Community College</a>. Gamegenic wird dort sein gesamtes Produktportfolio präsentieren — und The Professor hat bereits durchblicken lassen, dass die Gäste-Ausstattung „einige Überraschungen" bereithält.</p>
|
||||
|
||||
<hr id="system-readmore" />
|
||||
|
||||
<span style="font-size:1.17em;font-weight:bold;display:block;margin:1em 0;"><strong>Quellen</strong></span>
|
||||
|
||||
<ol>
|
||||
<li><a href="https://www.youtube.com/@TolarianCommunityCollege" target="_blank" rel="noopener" style="color:#0075FF;">Tolarian Community College — YouTube-Kanal</a> mit 1,3 Mio. Abonnenten.</li>
|
||||
<li><a href="https://www.kickstarter.com/projects/tolarian/the-academic-133-xl" target="_blank" rel="noopener" style="color:#0075FF;">The Academic 133+ XL — Kickstarter-Kampagne</a> von TCC × Gamegenic (2022).</li>
|
||||
<li><a href="https://www.kickstarter.com/projects/tolarian/shuffle-up-and-play-season-5" target="_blank" rel="noopener" style="color:#0075FF;">Shuffle Up & Play: Season 5 — Kickstarter-Kampagne</a> (2026).</li>
|
||||
<li><a href="https://gamegenic.com" target="_blank" rel="noopener" style="color:#0075FF;">Gamegenic — Offizielle Website</a>. Firmensitz: Essen, Asmodee Group.</li>
|
||||
<li><a href="https://icv2.com/articles/news/view/62679/gamegenic-teams-up-tolarian-community-college-second-time" target="_blank" rel="noopener" style="color:#0075FF;">ICv2: „Gamegenic Teams Up with Tolarian Community College for a Second Time"</a> von Jeffrey Dohm-Sanchez (22. Juni 2026).</li>
|
||||
</ol>
|
||||
|
||||
<p style="margin-top:2.5rem;padding-top:1rem;border-top:1px solid #ddd;font-size:0.9rem;color:#777;"><em>Recherchestand: 23. Juni 2026. Basierend auf der offiziellen Gamegenic-Pressemitteilung vom 22.06.2026, ergänzt durch Kickstarter-Daten und YouTube-Kanal-Informationen.</em></p>
|
||||
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Gamegenic wird exklusiver Accessoires-Partner von Tolarian Community College</title>
|
||||
<meta name="description" content="Gamegenic und Tolarian Community College vertiefen ihre Partnerschaft: Der Essener Zubehör-Hersteller wird exklusiver Sponsor der Gameplay-Show „Shuffle Up & Play" von Brian „The Professor" Lewis. Die zweite Zusammenarbeit nach dem Rekord-Kickstarter von 2022 startet am 29. Juni 2026.">
|
||||
<meta name="keywords" content="Gamegenic, Tolarian Community College, TCC, The Professor, Brian Lewis, Shuffle Up and Play, Magic The Gathering, MTG, Deckbox, Kickstarter, The Academic, Asmodee">
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<h1>Gamegenic wird exklusiver Accessoires-Partner von Tolarian Community College</h1>
|
||||
<p class="meta">23. Juni 2026 · Lesezeit: ca. 4 Minuten</p>
|
||||
|
||||
<p>Der Essener Gaming-Zubehör-Hersteller <a href="https://boardgamegeek.com/boardgamepublisher/51821/gamegenic" target="_blank">Gamegenic</a> geht eine erneuerte Partnerschaft mit dem YouTube-Kanal <strong>Tolarian Community College</strong> (TCC) ein. Wie das Unternehmen am 22. Juni 2026 bekannt gab, wird Gamegenic ab der fünften Staffel exklusiver Accessoires-Partner der Gameplay-Show <strong>„Shuffle Up & Play"</strong>. Die erste Episode unter dem neuen Sponsoring soll am 29. Juni 2026 ausgestrahlt werden. Es ist bereits die zweite Zusammenarbeit der beiden Marken — und sie baut auf einem der erfolgreichsten Gaming-Zubehör-Kickstarter aller Zeiten auf.</p>
|
||||
|
||||
<hr id="system-readmore" />
|
||||
|
||||
<p>„Shuffle Up & Play" ist das Gameplay-Flaggschiff von Tolarian Community College. Der Kanal zählt mittlerweile <strong>1,3 Millionen Abonnenten</strong> — ein deutliches Wachstum gegenüber den rund 700.000 im Jahr 2022. Host Brian „The Professor" Lewis, eine der einflussreichsten Stimmen in der Magic: The Gathering-Community, lädt in der Show wechselnde Gäste ein, um in verschiedenen Formaten gegeneinander anzutreten. Anders als seine klassischen Review- und Unboxing-Videos erreicht das Gameplay-Format ein breiteres Publikum und bietet Partnern eine natürliche Produktplatzierung.</p>
|
||||
|
||||
<p>Dass Gamegenic nun als exklusiver Sponsor auftritt, ist kein Zufall: Die Wurzeln der Partnerschaft reichen bis ins Jahr 2022 zurück. Damals brachte das Unternehmen gemeinsam mit The Professor die <strong>„The Academic 133+ XL"</strong> auf den Markt — eine Premium-Deck-Box, die über Kickstarter finanziert wurde. Die Kampagne übertraf alle Erwartungen: <strong>3.998.744 US-Dollar von 39.265 Unterstützern</strong> flossen in das Projekt, das ursprüngliche Ziel von 50.000 Dollar war bereits nach 17 Minuten erreicht. Die Boxen wurden im Februar 2023 ausgeliefert.</p>
|
||||
|
||||
<p>The Professor selbst unterstreicht die historische Verbindung: „Der Kickstarter für The Academic hat uns die Show überhaupt erst ermöglicht. In diesem Sinne ist Gamegenic seit Anfang an bei Shuffle Up & Play dabei. Es passt perfekt, dass Gamegenic ab Staffel 5 einer unserer Hauptsponsoren wird."</p>
|
||||
|
||||
<p>Auch Adrian Alonso, Head of Gamegenic, betont die gegenseitige Wertschätzung: „The Professor ist eine der vertrauenswürdigsten Stimmen in der Magic-Community. Dass Gamegenic konstant seine höchsten Bewertungen erhält, ist eine große Ehre. Shuffle Up & Play zu unterstützen, fühlt sich natürlich an."</p>
|
||||
|
||||
<p>Gamegenic wurde <strong>2019 in Essen gegründet</strong> und ist Teil der <a href="https://boardgamegeek.com/boardgamepublisher/19/asmodee" target="_blank">Asmodee Group</a>. Trotz des jungen Alters hat sich die Marke im wettbewerbsintensiven Zubehörmarkt etabliert und konkurriert mit etablierten Größen wie Ultra Pro und Dragon Shield. Die Strategie setzt dabei stark auf Influencer-Kooperationen und Co-Branding — die TCC-Partnerschaft ist das prominenteste Beispiel dafür.</p>
|
||||
|
||||
<p>Die erneuerte Partnerschaft unterstreicht Gamegenics Ambition, über den Kickstarter-Erfolg hinaus dauerhaft im MTG-Ökosystem präsent zu sein. Während 2022 ein physisches Produkt im Mittelpunkt stand, geht es 2026 um kontinuierliche Markenpräsenz in einer der reichweitenstärksten Magic-Shows auf YouTube. Finanzielle Details zum Sponsoring oder mögliche neue Co-Branded-Produkte wurden in der Ankündigung nicht genannt.</p>
|
||||
|
||||
<span style="font-size: 0.9rem; color: #6b7280;"><strong>Quellen</strong></span>
|
||||
<ol>
|
||||
<li><a href="https://www.gamegenic.com/wp-content/uploads/2026/06/GG_Press_release_Tolarian_SUP_NSov7RG4H.pdf" target="_blank">Gamegenic Pressemitteilung (Juni 2026)</a> — Offizielle Ankündigung der erneuerten Partnerschaft</li>
|
||||
<li><a href="https://www.gamegenic.com/press_releases/GG_Press_release_TCC.pdf" target="_blank">Gamegenic Pressemitteilung (Februar 2022)</a> — Ankündigung der ersten Zusammenarbeit („The Academic")</li>
|
||||
<li><a href="https://www.asmodeeusa.com/en/news/2022/2/28/GGTolarian/" target="_blank">Asmodee USA News (Februar 2022)</a> — Berichterstattung des Mutterkonzerns zur ersten Partnerschaft</li>
|
||||
<li><a href="https://www.kickstarter.com/projects/tolarian/the-academic-133-xl" target="_blank">Kickstarter: The Academic 133+ XL</a> — Original-Kampagnenseite mit Funding-Daten</li>
|
||||
<li><a href="https://icv2.com/articles/news/view/62679/gamegenic-teams-up-tolarian-community-college-second-time" target="_blank">ICv2 (Juni 2026)</a> — Fachpresse-Bericht zur zweiten Partnerschaft („Second Time")</li>
|
||||
<li><a href="https://icv2.com/articles/news/view/50547/gamegenic-teams-up-youtube-channel-tolarian-community-college" target="_blank">ICv2 (Februar 2022)</a> — Fachpresse-Bericht zur ersten Partnerschaft</li>
|
||||
<li><a href="https://articles.starcitygames.com/magic-the-gathering/the-professors-dream-deckbox-kickstarter-concludes-receives-over-3-9-million-in-funding/" target="_blank">StarCityGames</a> — Kickstarter-Recap mit Funding-Details und Community-Reaktionen</li>
|
||||
<li><a href="https://en.spielpunkt.net/tolarian-community-and-gamegenic-are-collaborating-on-magic-deckbox/" target="_blank">Spielpunkt.net (März 2022)</a> — Bericht zur ersten Kooperation</li>
|
||||
<li><a href="https://www.linkedin.com/posts/icv2_gamegenic-teams-up-with-tolarian-community-activity-7474877435004710913-WPYz" target="_blank">LinkedIn (ICv2, 22.06.2026)</a> — Social-Media-Ankündigung der zweiten Partnerschaft</li>
|
||||
<li><a href="https://bsky.app/profile/tolariancommunitycollege.com/post/3mns73glsqs2d" target="_blank">TCC auf Bluesky (22.06.2026)</a> — Ankündigung durch Tolarian Community College</li>
|
||||
</ol>
|
||||
|
||||
<p class="qc"><!-- QC: Gamegenic PM 2026=AAA|https://www.gamegenic.com/wp-content/uploads/2026/06/GG_Press_release_Tolarian_SUP_NSov7RG4H.pdf, Gamegenic PM 2022=AAA|https://www.gamegenic.com/press_releases/GG_Press_release_TCC.pdf, Asmodee USA 2022=AAA|https://www.asmodeeusa.com/en/news/2022/2/28/GGTolarian/, Kickstarter The Academic=AAA|https://www.kickstarter.com/projects/tolarian/the-academic-133-xl, ICv2 2026=AA|https://icv2.com/articles/news/view/62679/gamegenic-teams-up-tolarian-community-college-second-time, ICv2 2022=AA|https://icv2.com/articles/news/view/50547/gamegenic-teams-up-youtube-channel-tolarian-community-college, StarCityGames=AA|https://articles.starcitygames.com/magic-the-gathering/the-professors-dream-deckbox-kickstarter-concludes-receives-over-3-9-million-in-funding/, Spielpunkt 2022=A|https://en.spielpunkt.net/tolarian-community-and-gamegenic-are-collaborating-on-magic-deckbox/, LinkedIn ICv2=A|https://www.linkedin.com/posts/icv2_gamegenic-teams-up-with-tolarian-community-activity-7474877435004710913-WPYz, Bluesky TCC=A|https://bsky.app/profile/tolariancommunitycollege.com/post/3mns73glsqs2d --></p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,144 @@
|
||||
<article>
|
||||
|
||||
<h1>Survivalist: Wie ein Ex-Brooklyn-Game-Lab-Designer aus Neuseeland die BGG-Hotness erobert</h1>
|
||||
|
||||
<div style="font-size:1.1rem;color:#555;margin-bottom:1.5rem;border-bottom:2px solid #e0e0e0;padding-bottom:1rem;">Robert Hewitts Wilderness-Survival-Spiel hat in 8 Tagen über 1.730 Unterstützer gefunden — und steht trotz magerer 37 Ratings in der BGG-Hotness. Was steckt dahinter?</div>
|
||||
|
||||
<p style="font-size:0.85rem;color:#888;margin-bottom:2rem;">23. Juni 2026 · Lesezeit: ~7 Minuten</p>
|
||||
|
||||
<p>Es gibt Spiele, die schaffen es mit wenigen hundert Backern auf die Kickstarter-Startseite. Und es gibt Spiele, die schaffen es mit kaum drei Dutzend Bewertungen in die <em>BoardGameGeek Hotness</em>. <strong>Survivalist</strong> von Robert Hewitt gehört zu dieser zweiten, seltenen Kategorie — und es ist kein Zufall. Der erste Titel des neuseeländischen Verlags <strong>Homestead Games</strong> kombiniert Worker Placement mit Push-Your-Luck in einer kompromisslosen Wildnis-Simulation, die auf der Wellycon 2026 erste überzeugte Stimmen sammelte und nun auf Kickstarter durch die Decke geht.</p>
|
||||
|
||||
<!-- Kickstarter-Stats -->
|
||||
<div style="background: linear-gradient(135deg, #05ce78, #05b86c); color: white; border-radius: 8px; padding: 1.5rem; margin: 2rem 0; display: flex; flex-wrap: wrap; justify-content: space-around; text-align: center;">
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">$173.106</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">finanziert</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">1.730</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">Unterstützer</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">1.731%</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">des Ziels</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">22 Tage</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">verbleibend</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="text-align:center;font-size:0.85rem;color:#888;">Stand: 23. Juni 2026 · Quelle: <a href="https://www.pledgebox.com/projects/survivalist-the-board-game" target="_blank" rel="noopener" style="color:#0075FF;">PledgeBox</a> · Kampagne läuft bis 15. Juli 2026</p>
|
||||
|
||||
<h2>Das Spiel: 10 Tage, 28 Bushcraft-Arten, 7 Bedrohungen — wer überlebt?</h2>
|
||||
|
||||
<p>Survivalist wirft zwei bis sechs Spieler mit leeren Händen in die Wildnis. Zehn Runden lang kämpfen sie gegen Kälte, Hunger, wilde Tiere und die reine Erschöpfung. Die ersten acht Runden sind Vorbereitung: Ressourcen sammeln, Ausrüstung craften, Wunden versorgen. Runde neun offenbart geheime Talente. Runde zehn ist das <strong>Finale</strong> — sieben Bedrohungen, eine nach der anderen, nach dem „Last Person Standing"-Prinzip.</p>
|
||||
|
||||
<p>Jeden Tag wählen die Spieler reihum eine Aktion aus drei Zonen:</p>
|
||||
|
||||
<p><strong>Backcountry</strong> — exklusiv, nur ein Spieler pro Runde. Holt seltene Ressourcen, geht aber mit dem größten Risiko einher.<br>
|
||||
<strong>Clearing</strong> — mehrere Spieler, Erholung und Standard-Ressourcen.<br>
|
||||
<strong>Campfire</strong> — alle willkommen, sicher, aber die geringste Ausbeute.</p>
|
||||
|
||||
<p>Die Aktionen werden in Sitzreihenfolge gewählt, aber <strong>von oben nach unten aufgelöst</strong>. Wer das Backcountry wählt, handelt zuletzt — ein Timing-Element, das für ständige Spannung sorgt, wie BGG-Nutzer <a href="https://boardgamegeek.com/thread/3717745/review-survivalist-after-2-plays" target="_blank" rel="noopener" style="color:#0075FF;">Darrel J in seinem Review</a> (2. Juni 2026) beschreibt:</p>
|
||||
|
||||
<blockquote style="border-left:3px solid #20228A;margin:1.5rem 0;padding:0.5rem 1.2rem;color:#555;font-style:italic;background:#f8f9fa;">„Die Aktionsauswahl-Mechanik ist zwar nicht einzigartig, aber ziemlich neuartig und befriedigend. Wenn man halb erfroren mit einem gebrochenen Arm dasteht und nur noch eine Runde hat, bevor man durch den Wald in sein Verderben marschieren muss, ist das wirklich spannend."</blockquote>
|
||||
|
||||
<p>Ein <strong>Bag-Management</strong>-Mechanismus steuert Push-Your-Luck und Ressourcenverwaltung: Gelbe Scheiben sind sicher, blaue bringen Kälte, farbige bedeuten Wunden. Jeder Griff in den Beutel wird zur Nervenprobe.</p>
|
||||
|
||||
<div style="background:#f0f4f8;border:1px solid #d0d8e0;border-radius:6px;padding:1.2rem 1.5rem;margin:2rem 0;">
|
||||
<h3 style="margin-top:0;color:#20228A;font-family:Arial,sans-serif;font-size:1.15rem;">Survivalist — Fakten</h3>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;width:160px;">Designer</td><td style="padding:0.3rem 0.5rem;">Robert Hewitt</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Illustration</td><td style="padding:0.3rem 0.5rem;">Caroline Church (Scraperboard / Stich-Stil)</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Verlag</td><td style="padding:0.3rem 0.5rem;">Homestead Games New Zealand (Debüt)</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Spieler</td><td style="padding:0.3rem 0.5rem;">2–6</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Alter</td><td style="padding:0.3rem 0.5rem;">ab 10 Jahren</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Spieldauer</td><td style="padding:0.3rem 0.5rem;">80 Minuten</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Mechaniken</td><td style="padding:0.3rem 0.5rem;">Worker Placement, Push-Your-Luck, Action Drafting, Bag Management, Simulation, Sudden Death Ending</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Kategorien</td><td style="padding:0.3rem 0.5rem;">Adventure, Environmental</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">BGG Weight</td><td style="padding:0.3rem 0.5rem;">3,00 / 5 (vorläufig, 1 Gewichtung)</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">BGG Rating</td><td style="padding:0.3rem 0.5rem;">9,23 Avg / 5,51 Bayes (37 Ratings)</td></tr>
|
||||
<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;">Kickstarter</td><td style="padding:0.3rem 0.5rem;"><a href="https://www.kickstarter.com/projects/survivalist/survivalist-the-board-game" target="_blank" rel="noopener" style="color:#0075FF;">survivalist-the-board-game</a> — 16. Juni bis 15. Juli 2026</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Der Designer: Von Brooklyn nach Neuseeland</h2>
|
||||
|
||||
<p>Robert „Bob" Hewitt ist kein Unbekannter in der Brettspielszene — auch wenn <em>Survivalist</em> sein erstes eigenes Spiel ist. Der Ohioaner arbeitete ab 2014 beim renommierten <strong>Brooklyn Game Lab</strong> in New York, einem Studio und Spielecafé, das als Brutstätte für Designer-Talente gilt. 16 Jahre lebte er in New York, bevor er nach Neuseeland zog und 2025 <strong>Homestead Games</strong> gründete — einen Verlag, der explizit auf thematisch dichte, mechanisch eigenständige Spiele setzt.</p>
|
||||
|
||||
<p>Hewitts Design-Philosophie zeigt sich in Survivalist deutlich: Das Regelwerk verzichtet auf komplexe Ausnahmefälle, zugunsten eines klaren 10-Runden-Bogens, der sich intuitiv erschließt. In einem BGG-Kommentar zum eigenen Spiel betont Hewitt, dass ihm die Balance zwischen <strong>Zufall (Push-Your-Luck) und Agency (Worker Placement)</strong> wichtiger war als mechanische Originalität um jeden Preis. Die frühen Reviews geben ihm darin recht.</p>
|
||||
|
||||
<p>Die Illustratorin <strong>Caroline Church</strong> verleiht Survivalist mit ihrer <strong>Scraperboard-Technik</strong> (in schwarze Tinte geritzte weiße Linien) eine unverwechselbare, fast archaische Optik. Church ist ein Neuzugang in der Brettspiel-Illustration — ihre Stich-Ästhetik erinnert an Holzschnitte und unterstreicht das survivalistische Thema perfekt.</p>
|
||||
|
||||
<h2>Die Hype-Lücke: 9,23 Avg bei nur 37 Ratings — was sagt das?</h2>
|
||||
|
||||
<p>Ein genauerer Blick auf die <a href="https://boardgamegeek.com/boardgame/459646/survivalist" target="_blank" rel="noopener" style="color:#0075FF;">BGG-Daten</a> zeigt das klassische Muster eines Kickstarter-Hypes im Anfangsstadium:</p>
|
||||
|
||||
<p><strong>Average Rating: 9,23</strong> — absurd hoch, aber erwartbar bei 37 Bewertungen, die fast ausschließlich von Kickstarter-Backern und Convention-Testern stammen. Das ist die Phase, in der Enthusiasten bewerten, die bereits emotional investiert sind.<br>
|
||||
<strong>Bayes Average: 5,51</strong> — BGGs statistische Glättung zieht den Wert drastisch nach unten, weil sie bei kleiner Stichprobe massiv zum Durchschnitt (5,5) hin korrigiert. Die Wahrheit liegt irgendwo dazwischen — Darrel Js 7-8/10 nach zwei Partien ist vermutlich realistischer.<br>
|
||||
<strong>Standardabweichung: 1,14</strong> — moderate Streuung, keine erkennbare Polarisierung. Das spricht für ein konsistentes Spielerlebnis, auch wenn die Stichprobe winzig ist.</p>
|
||||
|
||||
<p>Die BGG-Hotness — die eher Aufmerksamkeit als Qualität misst — wird durch die aktive Kickstarter-Kampagne, das <a href="https://www.youtube.com/watch?v=1UulrR4fOMQ" target="_blank" rel="noopener" style="color:#0075FF;">Before-You-Play-Overview-Video</a> (4.657 Views, 95.700 Abonnenten) und das positive Word-of-Mouth von der Wellycon 2026 angetrieben, wo Hewitt selbst ein Turnier hostete.</p>
|
||||
|
||||
<h2>Was die ersten Spieler sagen</h2>
|
||||
|
||||
<p>Die BGG-Foren enthalten 9 Threads mit insgesamt 35 Posts — überschaubar, aber aufschlussreich. Darrel Js detaillierter <a href="https://boardgamegeek.com/thread/3717745/review-survivalist-after-2-plays" target="_blank" rel="noopener" style="color:#0075FF;">Review nach zwei Partien</a> (2. Juni 2026) ist die umfangreichste Einschätzung bisher. Sein Fazit:</p>
|
||||
|
||||
<blockquote style="border-left:3px solid #20228A;margin:1.5rem 0;padding:0.5rem 1.2rem;color:#555;font-style:italic;background:#f8f9fa;">„Insgesamt halte ich das für ein solides Spiel, das einen besonderen Platz in den meisten Brettspielsammlungen finden würde. Es schafft eine großartige Balance, Zufall für Push-Your-Luck zu nutzen und den Spielern gleichzeitig viel Handlungsspielraum zu geben."</blockquote>
|
||||
|
||||
<p>Er vergibt 7-8 von 10 Punkten — und würde mit einer regelmäßigen 5-6-Spieler-Runde auf 8/10 gehen. Besonders hebt er das <strong>kompetitive Spannungsmoment</strong> durch die umgekehrte Aktionsauflösung und die <strong>Reaper Tokens</strong> hervor, mit denen Spieler die Bedrohungsreihenfolge manipulieren können.</p>
|
||||
|
||||
<p>Ein <a href="https://boardgamegeek.com/thread/3718056/survivalist-review" target="_blank" rel="noopener" style="color:#0075FF;">zweiter Review</a> von einem Convention-Besucher ist kürzer, bestätigt aber den positiven Ersteindruck: leicht zu erlernen, flüssiger Spielfluss, thematisch überzeugend.</p>
|
||||
|
||||
<p>Auf <a href="https://old.reddit.com/r/boardgames/" target="_blank" rel="noopener" style="color:#0075FF;">Reddit</a> ist Survivalist noch ein blinder Fleck — lediglich ein Eintrag im wöchentlichen <a href="https://old.reddit.com/r/boardgames/comments/1uc0lz0/" target="_blank" rel="noopener" style="color:#0075FF;">Crowdfunding Roundup</a> vom 21. Juni 2026 verzeichnet das Spiel mit 1.655 Backern (Stand 21. Juni). Dedizierte Diskussionen gibt es noch nicht, was bei einem gerade 8 Tage alten Kickstarter normal ist.</p>
|
||||
|
||||
<h2>Pack Hunters: Die Hunde-Erweiterung</h2>
|
||||
|
||||
<p>Für 100 US-Dollar (den durchschnittlichen Pledge) erhalten Backer das Basisspiel plus die <strong>Pack Hunters Expansion</strong>, die mit Hund-Meeples, einem zusätzlichen „Packlands"-Board und seltenen Drop-Tiles den Solo- und Koop-Modus erweitert. Eine eigene „Gorge"-Aktion kommt hinzu. Die Expansion ist ausschließlich über die Kickstarter-Kampagne erhältlich.</p>
|
||||
|
||||
<h2>Ausblick: Dein nächster Wilderness-Trip?</h2>
|
||||
|
||||
<p>Die Kampagne läuft noch bis zum <strong>15. Juli 2026</strong>. Mit 1.731% Finanzierung ist Survivalist längst gesichert — die Frage ist nicht <em>ob</em>, sondern <em>wie hoch</em>. Wenn die ersten Exemplare im Herbst ausgeliefert werden und die BGG-Ratings von 37 auf 370 steigen, wird sich zeigen, ob die 9,23 halten oder ob der Bayes-Durchschnitt von 5,51 der realistischere Wert war.</p>
|
||||
|
||||
<p>Für Fans von thematisch dichten Survival-Spielen mit einem Hauch von <em>The Long Dark</em>-Atmosphäre ist Survivalist jedenfalls einer der vielversprechendsten Kickstarter dieses Sommers — und Robert Hewitt ein Name, den man sich merken sollte.</p>
|
||||
|
||||
<!-- Bottom Stats -->
|
||||
<div style="background: linear-gradient(135deg, #05ce78, #05b86c); color: white; border-radius: 8px; padding: 1.5rem; margin: 2.5rem 0 0 0; display: flex; flex-wrap: wrap; justify-content: space-around; text-align: center;">
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">30 Tage</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">Kampagne</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">$100</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">Ø Pledge</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">$21.638</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">Ø pro Tag</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:120px; padding:0.5rem;">
|
||||
<span style="font-size:1.8rem;font-weight:bold;display:block;">216</span>
|
||||
<span style="font-size:0.8rem;opacity:0.9;display:block;margin-top:0.2rem;">Ø Backer/Tag</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr id="system-readmore" />
|
||||
|
||||
<h2>Quellen</h2>
|
||||
|
||||
<ol>
|
||||
<li><a href="https://boardgamegeek.com/boardgame/459646/survivalist" target="_blank" rel="noopener" style="color:#0075FF;">BoardGameGeek: Survivalist (2026)</a> — Spieldaten, Ratings, Hotness, Mechaniken, Foren. Abgerufen am 23.06.2026.</li>
|
||||
<li><a href="https://boardgamegeek.com/thread/3717745/review-survivalist-after-2-plays" target="_blank" rel="noopener" style="color:#0075FF;">BGG-Review: Survivalist after 2 plays von Darrel J</a> — Ersteindrücke nach Wellycon 2026 (02.06.2026).</li>
|
||||
<li><a href="https://boardgamegeek.com/thread/3718056/survivalist-review" target="_blank" rel="noopener" style="color:#0075FF;">BGG-Review: Survivalist Review (Convention Early Access)</a> — Kurzerfahrung (Juni 2026).</li>
|
||||
<li><a href="https://www.youtube.com/watch?v=1UulrR4fOMQ" target="_blank" rel="noopener" style="color:#0075FF;">Before You Play: Survivalist - Kickstarter Overview</a> — Gesponsertes Teach-Video, 4.657 Views (16.06.2026).</li>
|
||||
<li><a href="https://www.youtube.com/watch?v=-nhhkCivk2I" target="_blank" rel="noopener" style="color:#0075FF;">Homestead Games NZ: Survivalist EXTENDED CUT</a> — Offizielles Kickstarter-Video mit Designer-Interview (17.02.2026).</li>
|
||||
<li><a href="https://www.kickstarter.com/projects/survivalist/survivalist-the-board-game" target="_blank" rel="noopener" style="color:#0075FF;">Kickstarter: Survivalist — The Ultimate Game of Wilderness Survival</a> — Kampagnenseite (16.06.–15.07.2026).</li>
|
||||
<li><a href="https://www.pledgebox.com/projects/survivalist-the-board-game" target="_blank" rel="noopener" style="color:#0075FF;">PledgeBox: Survivalist Tracker</a> — Live-Funding-Daten: $173.106, 1.730 Backer, 1.731%, 22 Tage (Stand 23.06.2026, 09:30 Uhr).</li>
|
||||
<li><a href="https://old.reddit.com/r/boardgames/comments/1uc0lz0/" target="_blank" rel="noopener" style="color:#0075FF;">Reddit r/boardgames: Weekly Crowdfunding Roundup</a> — Erwähnung mit 1.655 Backern (21.06.2026).</li>
|
||||
<li><a href="https://boardgamegeek.com/video/616573/survivalist/survivalist-kickstarter-video" target="_blank" rel="noopener" style="color:#0075FF;">BGG: Survivalist Kickstarter-Video</a> — Offizielles Kampagnen-Video.</li>
|
||||
</ol>
|
||||
|
||||
<p style="margin-top:2.5rem;padding-top:1rem;border-top:1px solid #ddd;font-size:0.9rem;color:#777;"><em>Recherchestand: 23. Juni 2026, 09:30 Uhr. Kickstarter-Daten via <a href="https://www.pledgebox.com/projects/survivalist-the-board-game" target="_blank" rel="noopener" style="color:#0075FF;">PledgeBox</a> (live), BGG-Daten via <a href="https://boardgamegeek.com/boardgame/459646/survivalist" target="_blank" rel="noopener" style="color:#0075FF;">BoardGameGeek</a>, Community-Diskussionen via <a href="https://boardgamegeek.com/boardgame/459646/survivalist/forums/0" target="_blank" rel="noopener" style="color:#0075FF;">BGG-Foren</a> + <a href="https://old.reddit.com/r/boardgames/" target="_blank" rel="noopener" style="color:#0075FF;">Reddit</a>.</em></p>
|
||||
<p style="font-size:0.9rem;color:#777;"><em>Dieser Artikel enthält Affiliate-freie redaktionelle Berichterstattung. Die Kickstarter-Links sind keine Affiliate-Links.</em></p>
|
||||
|
||||
</article>
|
||||
@@ -0,0 +1,383 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Anforderungsdefinition: Vollautomatischer Artikel-Workflow</title>
|
||||
<meta name="description" content="Spezifikation für eine vollautomatische Pipeline von Input (Thema/PDF/URL) → QC-geprüfter HTML-Artikel. Stages: Input Prep → Research → Write → QC → Deliver.">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #20228a;
|
||||
--accent-light: #3b3da8;
|
||||
--bg: #f8f9fb;
|
||||
--surface: #ffffff;
|
||||
--text: #1a1a2e;
|
||||
--text-muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
--radius: 10px;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; padding: 2rem 1.5rem; max-width: 900px; margin: 0 auto; }
|
||||
header { margin-bottom: 2.5rem; padding-bottom: 1.5rem; border-bottom: 3px solid var(--accent); }
|
||||
h1 { font-size: 1.8rem; color: var(--accent); margin-bottom: 0.4rem; letter-spacing: -0.02em; }
|
||||
header p { color: var(--text-muted); font-size: 0.95rem; }
|
||||
h2 { font-size: 1.3rem; color: var(--accent); margin: 2rem 0 0.8rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }
|
||||
h3 { font-size: 1.1rem; color: var(--accent-light); margin: 1.5rem 0 0.5rem; }
|
||||
h4 { font-size: 1rem; color: #444; margin: 1rem 0 0.3rem; }
|
||||
p, li { margin-bottom: 0.5rem; }
|
||||
ul, ol { padding-left: 1.5rem; margin-bottom: 1rem; }
|
||||
li { margin-bottom: 0.25rem; }
|
||||
code { background: #f0f0f5; padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.9em; font-family: 'SF Mono', 'Fira Code', monospace; }
|
||||
pre { background: #1a1a2e; color: #e0e0e0; padding: 1rem 1.2rem; border-radius: var(--radius); overflow-x: auto; font-size: 0.85rem; line-height: 1.5; margin: 1rem 0; }
|
||||
pre code { background: none; padding: 0; color: inherit; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin: 1rem 0; background: var(--surface); border-radius: var(--radius); overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
th { background: linear-gradient(135deg, var(--accent), var(--accent-light)); color: #fff; font-weight: 600; padding: 0.6rem 0.8rem; text-align: left; font-size: 0.9rem; }
|
||||
td { padding: 0.5rem 0.8rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tbody tr:nth-child(even) { background: rgba(32,34,138,0.03); }
|
||||
|
||||
.pipeline { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; font-family: monospace; font-size: 0.85rem; margin: 1rem 0; padding: 0.8rem; background: var(--surface); border-radius: var(--radius); overflow-x: auto; }
|
||||
.stage { background: linear-gradient(135deg, var(--accent), var(--accent-light)); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; white-space: nowrap; font-weight: 600; }
|
||||
.arrow { color: var(--accent); font-weight: bold; }
|
||||
|
||||
.badge { display: inline-block; padding: 0.15em 0.5em; border-radius: 4px; font-size: 0.8rem; font-weight: 600; margin-right: 0.3rem; }
|
||||
.badge-green { background: #d1fae5; color: #065f46; }
|
||||
.badge-red { background: #fee2e2; color: #991b1b; }
|
||||
.badge-blue { background: #dbeafe; color: #1e40af; }
|
||||
.badge-yellow { background: #fef3c7; color: #92400e; }
|
||||
|
||||
.checklist { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; margin: 1rem 0; }
|
||||
.checklist-item { display: flex; align-items: flex-start; gap: 0.4rem; padding: 0.3rem 0; font-size: 0.9rem; }
|
||||
.checklist-item .icon { flex-shrink: 0; width: 1.3em; }
|
||||
|
||||
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
||||
|
||||
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--text-muted); font-size: 0.85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<article>
|
||||
|
||||
<header>
|
||||
<h1>Anforderungsdefinition: Vollautomatischer Artikel-Workflow</h1>
|
||||
<p>Spezifikation für Cody · 23. Juni 2026 · Stand: final</p>
|
||||
</header>
|
||||
|
||||
<h2>1. Ziel</h2>
|
||||
<p>Eine Pipeline, die aus einem beliebigen Input (Thema, PDF, URL, Stichwort) einen <strong>QC-geprüften, publikationsfertigen HTML-Artikel</strong> produziert. Basti gibt den Auftrag — der Rest läuft autonom.</p>
|
||||
|
||||
<h2>2. Eingabe-Schnittstelle</h2>
|
||||
|
||||
<pre><code>Input: {
|
||||
topic: string, // "Gamegenic wird Partner von Tolarian Community College"
|
||||
sources: string[], // ["/pfad/pm.pdf", "https://icv2.com/...", "https://gamegenic.com"]
|
||||
category_id: number, // Joomla-Rubrik (default: 61 = Korrektur)
|
||||
rubric: string, // "Neuerscheinungen" | "Crowdfunding" | "Nachrichten" | ...
|
||||
image_urls: string[], // optionale Bild-URLs aus PM
|
||||
deadline: string | null // optional: ISO-Timestamp für zeitkritische Artikel
|
||||
}</code></pre>
|
||||
|
||||
<h3>Input-Quellen-Erkennung (automatisch)</h3>
|
||||
<table>
|
||||
<tr><th>Input</th><th>Quelle</th></tr>
|
||||
<tr><td>PDF</td><td>Pfad aus <code>~/.hermes/cache/documents/</code></td></tr>
|
||||
<tr><td>URL</td><td>Direkt übernehmen</td></tr>
|
||||
<tr><td>Nur Thema</td><td>Sub-Agent recherchiert selbstständig Quellen</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>3. Pipeline-Stufen</h2>
|
||||
|
||||
<div class="pipeline">
|
||||
<span class="stage">STAGE 0<br>Input Prep</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="stage">STAGE 1<br>Research</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="stage">STAGE 2<br>Write</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="stage">STAGE 3<br>QC</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="stage">STAGE 4<br>Deliver</span>
|
||||
</div>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:0.3rem;">Dauer: ~10–15 Minuten von Input bis fertigem Artikel</p>
|
||||
|
||||
<h3>STAGE 0: Input Preparation</h3>
|
||||
<p><span class="badge badge-blue">Basti</span> (nicht Sub-Agent)</p>
|
||||
<ol>
|
||||
<li>PDFs OCRen: <code>python3 -c "import pymupdf; doc=pymupdf.open('…'); [print(page.get_text()) …]"</code></li>
|
||||
<li>Prompt bauen aus Input + OCR-Text + Quellen-URLs</li>
|
||||
<li>Skills-Array: <code>["journalistic-article-writing", "korrekturschleife", "article-management"]</code></li>
|
||||
<li>Toolsets: <code>["web", "terminal", "file", "search", "skills"]</code></li>
|
||||
</ol>
|
||||
<p><strong>Output:</strong> Fertiger Prompt für Stage 1</p>
|
||||
|
||||
<h3>STAGE 1: Research</h3>
|
||||
<p><span class="badge badge-green">Sub-Agent #1</span> (Leaf, <code>delegate_task</code>)</p>
|
||||
<table>
|
||||
<tr><th>Feld</th><th>Wert</th></tr>
|
||||
<tr><td>Skills</td><td>journalistic-article-writing</td></tr>
|
||||
<tr><td>Toolsets</td><td>web, terminal, file, search, skills</td></tr>
|
||||
</table>
|
||||
|
||||
<ol>
|
||||
<li>Alle Quellen abrufen (<code>web_extract</code>, <code>curl</code>, <code>web_search</code>)</li>
|
||||
<li>Jede Quelle bewerten: <strong>AAA</strong> (Primär) / <strong>AA</strong> (Sekundär) / <strong>A</strong> (schwach)</li>
|
||||
<li>Fehlende Fakten ergänzen:
|
||||
<ul>
|
||||
<li>BGG-Eintrag suchen: <code>web_search site:boardgamegeek.com SPIELNAME</code></li>
|
||||
<li>Autor/Illustrator/Verlag verifizieren</li>
|
||||
<li>Preise, Daten, Awards gegenprüfen</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Output:</strong> <code>research_brief.md</code></p>
|
||||
|
||||
<pre><code># Research Brief: THEMA
|
||||
## Quellenbewertung
|
||||
| Quelle | Stufe | URL |
|
||||
|---------|---------|-----------------------|
|
||||
| PM | AAA | https://gamegenic.com |
|
||||
|
||||
## Fakten
|
||||
- Fakt 1 (Quelle: URL, AAA)
|
||||
- Fakt 2 (Quelle: URL, AA)
|
||||
|
||||
## Offene Punkte
|
||||
- Punkt X konnte nicht verifiziert werden → STREICHEN</code></pre>
|
||||
|
||||
<p><strong>Exit-Codes:</strong> <span class="badge badge-green">0</span> = vollständig · <span class="badge badge-red">1</span> = unvollständig (Abbruch, an Basti melden)</p>
|
||||
|
||||
<h3>STAGE 2: Write</h3>
|
||||
<p><span class="badge badge-green">Sub-Agent #2</span> (Leaf, <code>delegate_task</code>)</p>
|
||||
<table>
|
||||
<tr><th>Feld</th><th>Wert</th></tr>
|
||||
<tr><td>Skills</td><td>journalistic-article-writing, korrekturschleife, article-management</td></tr>
|
||||
<tr><td>Toolsets</td><td>web, terminal, file, search, skills</td></tr>
|
||||
<tr><td>Context</td><td>Research Brief + Original-Prompt</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="checklist">
|
||||
<div class="checklist-item"><span class="icon">❌</span> KEINE "XML API v2" → klickbare BGG-Links</div>
|
||||
<div class="checklist-item"><span class="icon">❌</span> KEIN "Betriebsgeheimnis"</div>
|
||||
<div class="checklist-item"><span class="icon">❌</span> Quellen = <code><a href="…" target="_blank"></code></div>
|
||||
<div class="checklist-item"><span class="icon">❌</span> <code><span><strong>Quellen</strong></span></code></div>
|
||||
<div class="checklist-item"><span class="icon">❌</span> KEINE Emojis/Icons</div>
|
||||
<div class="checklist-item"><span class="icon">❌</span> Datei endet mit <code></html></code></div>
|
||||
</div>
|
||||
|
||||
<p><strong>Output:</strong> <code>/home/hermes/workspace/artikel_NAME_DATUM.html</code></p>
|
||||
|
||||
<p><strong>Pflicht-Elemente im HTML:</strong></p>
|
||||
<pre><code><head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TITEL</title>
|
||||
<meta name="description" content="…">
|
||||
<meta name="keywords" content="…">
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<h1>TITEL</h1>
|
||||
<p class="meta">DATUM · Lesezeit: ca. N Minuten</p>
|
||||
<!-- Inhalt -->
|
||||
<hr id="system-readmore" />
|
||||
<span style="…"><strong>Quellen</strong></span>
|
||||
<ol>
|
||||
<li><a href="URL" target="_blank">Name</a> — Beschreibung</li>
|
||||
</ol>
|
||||
<p class="qc"><!-- QC: Quelle1=AAA|URL, … --></p>
|
||||
</article>
|
||||
</body>
|
||||
</html></code></pre>
|
||||
|
||||
<p><strong>Exit-Codes:</strong> <span class="badge badge-green">0</span> = Artikel geschrieben · <span class="badge badge-red">1</span> = Fehler</p>
|
||||
|
||||
<h3>STAGE 3: Quality Control</h3>
|
||||
<p><span class="badge badge-green">Sub-Agent #2</span> (gleicher Agent, nach dem Schreiben)</p>
|
||||
|
||||
<pre><code>bash /home/hermes/workspace/verify_article.sh artikel_NAME.html</code></pre>
|
||||
|
||||
<h4>13 Checks (Stand 23.06.2026)</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>#</th><th>Check</th><th>Kriterium</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>Read-More</td><td>Genau 1 × <code><hr id="system-readmore"></code></td></tr>
|
||||
<tr><td>2</td><td>br nach Read-More</td><td>Kein <code><br></code> direkt danach</td></tr>
|
||||
<tr><td>3</td><td>p.meta</td><td><code><p class="meta"></code> (nicht <code><div></code>)</td></tr>
|
||||
<tr><td>4</td><td>Emojis</td><td>Keine im Body</td></tr>
|
||||
<tr><td>5</td><td>Dateiende</td><td>Endet mit <code></html></code></td></tr>
|
||||
<tr><td>6</td><td>Meta im Body</td><td>Keine doppelte Datum·Lesezeit-Zeile</td></tr>
|
||||
<tr><td>7</td><td>Quellen-Überschrift</td><td>Kein <code><h2>Quellen</h2></code></td></tr>
|
||||
<tr><td>8</td><td>Links target</td><td>Alle mit <code>target="_blank"</code></td></tr>
|
||||
<tr><td>9</td><td>Meta-Description</td><td>Vorhanden und nicht leer</td></tr>
|
||||
<tr><td>10</td><td>QC-Zeile</td><td>In <code><p class="qc"></code></td></tr>
|
||||
<tr><td><strong>11</strong></td><td><strong>BGG-Autorencheck</strong></td><td><strong>Designer gegen BGG verifizieren</strong></td></tr>
|
||||
<tr><td style="color:#991b1b;font-weight:600;">12</td><td style="color:#991b1b;font-weight:600;">XML-API-Referenzen</td><td style="color:#991b1b;font-weight:600;">Keine "XML API" / "Betriebsgeheimnis"</td></tr>
|
||||
<tr><td style="color:#991b1b;font-weight:600;">13</td><td style="color:#991b1b;font-weight:600;">Quellen verlinkt</td><td style="color:#991b1b;font-weight:600;">Quellen-Block hat <code><a href></code> Links</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Fehlerbehandlung:</strong></p>
|
||||
<ul>
|
||||
<li><span class="badge badge-green">Exit 0</span> → Stage 4</li>
|
||||
<li><span class="badge badge-red">Exit 1</span> → Korrigieren, erneut verify → max 3 Versuche → ABBRUCH, an Basti melden</li>
|
||||
<li>CHECK 11 (BGG-Autoren) <span class="badge badge-yellow">Warnung</span> → dokumentieren, nicht blocken</li>
|
||||
</ul>
|
||||
|
||||
<h3>STAGE 4: Deliver</h3>
|
||||
<p><span class="badge badge-blue">Basti</span></p>
|
||||
<ol>
|
||||
<li>Git-Commit: <code>git add artikel_NAME.html && git commit -m "artikel: THEMA"</code></li>
|
||||
<li>Article-Server prüfen: Direkt-URL testen<br>
|
||||
<code>curl "http://185.162.249.159:8890/artikel_NAME.html?key=…" | head -3</code></li>
|
||||
<li>Meldung:</li>
|
||||
</ol>
|
||||
|
||||
<pre><code>✅ Artikel THEMA fertig
|
||||
📄 /home/hermes/workspace/artikel_NAME.html
|
||||
🔗 http://185.162.249.159:8890/artikel_NAME.html?key=…
|
||||
📊 verify: 0 Fehler</code></pre>
|
||||
|
||||
<h2>4. Fehlermodi & Recovery</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Fehler</th><th>Erkennung</th><th>Recovery</th></tr>
|
||||
<tr>
|
||||
<td>PDF nicht OCR-bar</td>
|
||||
<td>pymupdf wirft Exception</td>
|
||||
<td>Basti manuell OCRen, Text in Prompt geben</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Quelle nicht erreichbar</td>
|
||||
<td>curl/web_extract Timeout/404</td>
|
||||
<td>5-Stufen-Eskalation:<br>curl → wget+UA → Wayback → Tavily → Google Cache</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Keine AAA-Quelle für Kern-Fakt</td>
|
||||
<td>Research-Gap</td>
|
||||
<td>Fakt streichen → "unvollständig" an Basti</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>verify_article.sh fail</td>
|
||||
<td>Exit ≠ 0</td>
|
||||
<td>3 Korrekturversuche → Abbruch</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BGG-Autorencheck uneindeutig</td>
|
||||
<td>check_authors.py Warnung</td>
|
||||
<td>Dokumentieren, Basti entscheidet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Article-Server down</td>
|
||||
<td>curl auf :8890 Timeout</td>
|
||||
<td>Server neu starten via <code>python3 article_server.py &</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>5. Cronjob-Integration</h2>
|
||||
|
||||
<p>Der Workflow läuft als One-Shot-Cronjob mit <code>deliver="local"</code>:</p>
|
||||
|
||||
<pre><code>cronjob(
|
||||
action="create",
|
||||
schedule="1m",
|
||||
repeat=1,
|
||||
deliver="local",
|
||||
skills=["journalistic-article-writing", "korrekturschleife", "article-management"],
|
||||
enabled_toolsets=["web", "terminal", "file", "search", "skills", "delegation"],
|
||||
prompt="…" // Stage-0-Output
|
||||
)</code></pre>
|
||||
|
||||
<p><strong>Wichtig:</strong> <code>deliver="local"</code> — der Cronjob meldet sich NICHT bei Daniel. Basti pollt das Ergebnis.</p>
|
||||
|
||||
<h2>6. Batch-Modus</h2>
|
||||
|
||||
<p>Für mehrere Artikel: Parallele Cronjobs (max 3 gleichzeitig via <code>delegation.max_concurrent_children</code>). Jeder Cronjob bekommt denselben Prompt-Aufbau, nur mit anderem Input.</p>
|
||||
|
||||
<h2>7. Was Cody bauen muss</h2>
|
||||
|
||||
<h3>7.1 <code>article_pipeline.py</code> — Orchestrator-Script</h3>
|
||||
<ul>
|
||||
<li>Input entgegennimmt (JSON oder CLI-Args)</li>
|
||||
<li>Stage 0 ausführt (PDF-OCR, Prompt-Bau)</li>
|
||||
<li>Zwei <code>delegate_task</code>-Aufrufe (Stage 1 Research, Stage 2 Write+QC)</li>
|
||||
<li>Stage 4 ausführt (Git-Commit, Verify)</li>
|
||||
<li>Exit-Code zurückgibt</li>
|
||||
</ul>
|
||||
|
||||
<h3>7.2 Integration</h3>
|
||||
<ul>
|
||||
<li>Als Cronjob-Script lauffähig (<code>no_agent=false</code> Modus)</li>
|
||||
<li>ODER: Als Hermes-Skill <code>article-pipeline</code> registrierbar</li>
|
||||
</ul>
|
||||
|
||||
<h3>7.3 Config</h3>
|
||||
<table>
|
||||
<tr><th>Element</th><th>Pfad / Wert</th></tr>
|
||||
<tr><td>Workspace</td><td><code>/home/hermes/workspace/</code></td></tr>
|
||||
<tr><td>verify_article.sh</td><td><code>/home/hermes/workspace/verify_article.sh</code></td></tr>
|
||||
<tr><td>article_server.py</td><td><code>/home/hermes/workspace/article_server.py</code></td></tr>
|
||||
<tr><td>PDF-Cache</td><td><code>~/.hermes/cache/documents/</code></td></tr>
|
||||
<tr><td>Skills</td><td>journalistic-article-writing, korrekturschleife, article-management</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>8. Aufruf-Beispiele</h2>
|
||||
|
||||
<pre><code># Variante A: Thema + PDF
|
||||
python3 article_pipeline.py \
|
||||
--topic "Gamegenic wird Partner von Tolarian Community College" \
|
||||
--pdf "/home/hermes/.hermes/cache/documents/GG_Press_release.pdf" \
|
||||
--rubric "Nachrichten" \
|
||||
--category-id 9
|
||||
|
||||
# Variante B: Nur Thema (autonome Recherche)
|
||||
python3 article_pipeline.py \
|
||||
--topic "Neue Kennerspiel-Nominierungen 2026" \
|
||||
--rubric "Nachrichten"
|
||||
|
||||
# Variante C: Batch
|
||||
python3 article_pipeline.py --batch topics.json</code></pre>
|
||||
|
||||
<h2>9. Erfolgskriterium</h2>
|
||||
|
||||
<div style="background:linear-gradient(135deg,#d1fae5,#a7f3d0);border-radius:10px;padding:1.5rem;margin:1.5rem 0;text-align:center;">
|
||||
<p style="font-size:1.1rem;margin:0;"><strong>🎯 Basti gibt ein Thema ein → nach ~10 Minuten liegt ein Artikel im Workspace mit</strong></p>
|
||||
<p style="font-size:1.4rem;font-weight:700;color:#065f46;margin:0.3rem 0;">verify_article.sh: 0 Fehler</p>
|
||||
<p style="font-size:0.9rem;color:#555;margin:0;">Kein manuelles Eingreifen. Kein Nachbessern. Kein "XML API v2".</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Anhang: Skills-Dokumentation</h2>
|
||||
|
||||
<p>Folgende Skills werden im Workflow verwendet. Cody muss ihre Schnittstellen kennen:</p>
|
||||
|
||||
<table>
|
||||
<tr><th>Skill</th><th>Typ</th><th>Funktion</th></tr>
|
||||
<tr>
|
||||
<td><code>journalistic-article-writing</code></td>
|
||||
<td>Writing</td>
|
||||
<td>HTML-Template, Quellenregeln, Sprachregeln, Formatvorgaben</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>korrekturschleife</code></td>
|
||||
<td>QC</td>
|
||||
<td>Zweistufiger Workflow (Write + Fact-Check), Writer-Fehler-Tabelle, Quellenbewertung AAA/AA/A</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>article-management</code></td>
|
||||
<td>Deploy</td>
|
||||
<td>Article-Server (Port 8890), Joomla-API, verify_article.sh, VG-Wort</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</article>
|
||||
|
||||
<footer>
|
||||
<p>Spezifikation für Cody · 23. Juni 2026 · brettspiel-news.de Redaktion</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
# verify_article.sh — Prüft Artikel-HTML auf Einhaltung aller redaktionellen Standards
|
||||
# Usage: ./verify_article.sh <artikel_datei.html>
|
||||
# Exit 0 = alle Checks bestanden, Exit 1 = Fehler gefunden
|
||||
#
|
||||
# Aufruf aus Cronjobs: bash /home/hermes/workspace/verify_article.sh /home/hermes/workspace/artikel_*.html
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FILE="${1:-}"
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
ok() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||
fail() { echo -e "${RED}❌ $1${NC}"; ERRORS=$((ERRORS+1)); }
|
||||
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; WARNINGS=$((WARNINGS+1)); }
|
||||
|
||||
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
|
||||
echo "Usage: $0 <artikel_datei.html>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Verifiziere: $FILE ==="
|
||||
echo ""
|
||||
|
||||
# ── CHECK 1: Exactly one read-more ──────────────────────────────────
|
||||
# Nur echte HTML-Tags zählen, nicht <hr...> Referenzen in QC-Zeilen
|
||||
READMORE_COUNT=$(grep -c '<hr id="system-readmore"' "$FILE" || true)
|
||||
if [ "$READMORE_COUNT" = "1" ]; then
|
||||
ok "Read-More: genau 1"
|
||||
elif [ "$READMORE_COUNT" = "0" ]; then
|
||||
fail "Read-More: KEIN <hr id=\"system-readmore\" /> gefunden"
|
||||
else
|
||||
fail "Read-More: $READMORE_COUNT Stück (muss genau 1 sein)"
|
||||
fi
|
||||
|
||||
# ── CHECK 2: No <br> immediately after read-more ────────────────────
|
||||
if grep -A1 'system-readmore' "$FILE" | grep -q '<br'; then
|
||||
fail "<br> direkt nach Read-More (erzeugt doppelte Linie)"
|
||||
else
|
||||
ok "Kein <br> nach Read-More"
|
||||
fi
|
||||
|
||||
# ── CHECK 3: p.meta not div.meta ────────────────────────────────────
|
||||
if grep -q '<div class="meta">' "$FILE"; then
|
||||
fail "<div class=\"meta\"> statt <p class=\"meta\"> (article_server regex schlägt fehl)"
|
||||
else
|
||||
ok "<p class=\"meta\"> korrekt"
|
||||
fi
|
||||
|
||||
# ── CHECK 4: No emojis in article body (QC line excluded) ──────────
|
||||
# Extract body between <article> and </article>, strip everything from QC line onward
|
||||
BODY=$(sed -n '/<article>/,/<\/article>/p' "$FILE" | sed '/<p class="qc">/,$d')
|
||||
if echo "$BODY" | python3 -c "import sys,re; t=sys.stdin.read(); sys.exit(0 if re.search(r'[\U0001F300-\U0001F9FF\U00002600-\U000027BF\U0001F600-\U0001F64F]',t) else 1)"; then
|
||||
fail "Emojis im Artikel-Body gefunden"
|
||||
else
|
||||
ok "Keine Emojis"
|
||||
fi
|
||||
|
||||
# ── CHECK 5: File ends with </html> ─────────────────────────────────
|
||||
LAST_LINE=$(tail -1 "$FILE")
|
||||
if echo "$LAST_LINE" | grep -q '</html>'; then
|
||||
ok "Datei endet mit </html>"
|
||||
else
|
||||
fail "Letzte Zeile ist nicht </html> (Fremd-Content nach Artikel-Ende?)"
|
||||
fi
|
||||
|
||||
# ── CHECK 6: No meta-line in body (title, date, reading time) ───────
|
||||
if echo "$BODY" | grep -qP '<p[^>]*>.*\d{1,2}\.\s+\w+\s+\d{4}.*·.*Lesezeit'; then
|
||||
warn "Meta-Zeile möglicherweise im Body (Datum · Rubrik · Lesezeit)"
|
||||
fi
|
||||
|
||||
# ── CHECK 7: Sources heading uses span-strong, not h2 ───────────────
|
||||
if echo "$BODY" | grep -q '<h2>Quellen</h2>'; then
|
||||
fail "<h2>Quellen</h2> statt <span><strong>Quellen</strong></span>"
|
||||
else
|
||||
ok "Quellen-Überschrift korrekt (kein <h2>)"
|
||||
fi
|
||||
|
||||
# ── CHECK 8: All links have target="_blank" ─────────────────────────
|
||||
LINKS_TOTAL=$(echo "$BODY" | grep -o '<a ' | wc -l)
|
||||
LINKS_BLANK=$(echo "$BODY" | grep -o 'target="_blank"' | wc -l)
|
||||
if [ "$LINKS_TOTAL" -gt 0 ] && [ "$LINKS_TOTAL" != "$LINKS_BLANK" ]; then
|
||||
MISSING=$((LINKS_TOTAL - LINKS_BLANK))
|
||||
fail "$MISSING von $LINKS_TOTAL Links ohne target=\"_blank\""
|
||||
else
|
||||
ok "Alle $LINKS_TOTAL Links mit target=\"_blank\""
|
||||
fi
|
||||
|
||||
# ── CHECK 9: Meta description present ───────────────────────────────
|
||||
if grep -q '<meta name="description" content="[^"]*[a-zA-Z]' "$FILE"; then
|
||||
ok "Meta-Description vorhanden"
|
||||
else
|
||||
fail "Meta-Description fehlt oder leer"
|
||||
fi
|
||||
|
||||
# ── CHECK 10: No QC line outside <p class="qc"> ────────────────────
|
||||
QC_COUNT=$(echo "$BODY" | grep -c 'class="qc"' || true)
|
||||
QC_RAW=$(echo "$BODY" | grep -cP 'QC:\s*[✓✔]' || true)
|
||||
if [ "$QC_RAW" -gt "$QC_COUNT" ]; then
|
||||
fail "QC-Hinweis nicht in <p class=\"qc\"> (wird nicht gestrippt)"
|
||||
else
|
||||
ok "QC-Zeile korrekt in <p class=\"qc\">"
|
||||
fi
|
||||
|
||||
# ── CHECK 11: Autoren gegen BGG verifizieren ─────────────────────────
|
||||
echo ""
|
||||
echo "--- BGG Autoren-Faktencheck ---"
|
||||
AUTHOR_CHECK=$(python3 /home/hermes/workspace/check_authors.py "$FILE" 2>&1)
|
||||
AUTHOR_EXIT=$?
|
||||
echo "$AUTHOR_CHECK"
|
||||
if [ $AUTHOR_EXIT -eq 1 ]; then
|
||||
ERRORS=$((ERRORS + 1))
|
||||
elif [ $AUTHOR_EXIT -eq 2 ]; then
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
|
||||
# ── CHECK 12: Keine XML-API-Referenzen im Artikel ──────────────────
|
||||
if echo "$BODY" | grep -qiP 'xml\s*api|XMLAPI|xmlapi|betriebsgeheimnis'; then
|
||||
fail "XML-API-Referenz oder 'Betriebsgeheimnis' im Artikel (nicht leser-tauglich)"
|
||||
else
|
||||
ok "Keine XML-API-Referenzen"
|
||||
fi
|
||||
|
||||
# ── CHECK 13: Quellen-Block muss klickbare Links enthalten ──────────
|
||||
# Sucht Quellen-Überschrift (h2, span, strong) und prüft ob danach Links kommen
|
||||
SOURCES_SECTION=$(echo "$BODY" | sed -n '/<h2>Quellen<\/h2>/,$p; /<span.*>.*Quellen.*<\/span>/,$p' || echo "")
|
||||
if [ -n "$SOURCES_SECTION" ]; then
|
||||
SOURCE_LINKS=$(echo "$SOURCES_SECTION" | grep -c '<a href=' || true)
|
||||
if [ "$SOURCE_LINKS" -eq 0 ]; then
|
||||
fail "Quellen-Block ohne klickbare Links (nur Fließtext)"
|
||||
else
|
||||
ok "Quellen-Block mit $SOURCE_LINKS klickbaren Links"
|
||||
fi
|
||||
else
|
||||
fail "Kein Quellen-Block gefunden (h2/span>Quellen fehlt)"
|
||||
fi
|
||||
|
||||
# ── SUMMARY ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ ALLE CHECKS BESTANDEN${NC}"
|
||||
echo "═══════════════════════════════════════"
|
||||
exit 0
|
||||
elif [ $ERRORS -eq 0 ]; then
|
||||
echo -e "${YELLOW}⚠️ $WARNINGS Warnung(en) — KEINE Fehler${NC}"
|
||||
echo "═══════════════════════════════════════"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}❌ $ERRORS Fehler, $WARNINGS Warnungen${NC}"
|
||||
echo "═══════════════════════════════════════"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user