398 lines
16 KiB
HTML
398 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>{{ title }}</title>
|
|
<link rel="stylesheet" href="/admin/static/admin.css" />
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div>
|
|
<h1>rss-news Admin Dashboard</h1>
|
|
<p>Angemeldet als <strong>{{ user }}</strong></p>
|
|
</div>
|
|
<div class="row">
|
|
<a class="linkbtn" href="/admin/connectivity">Connectivity Check</a>
|
|
<form method="post" action="/admin/logout">
|
|
<button type="submit" class="secondary">Logout</button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="container">
|
|
{% if flash_msg %}
|
|
<section class="card flash {{ 'flash-error' if flash_type == 'error' else 'flash-success' }}">
|
|
{{ flash_msg }}
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="stats">
|
|
<article class="stat">
|
|
<div class="label">Quellen</div>
|
|
<div class="value">{{ sources|length }}</div>
|
|
</article>
|
|
<article class="stat">
|
|
<div class="label">Feeds</div>
|
|
<div class="value">{{ feeds|length }}</div>
|
|
</article>
|
|
<article class="stat">
|
|
<div class="label">Artikel</div>
|
|
<div class="value">{{ articles|length }}</div>
|
|
</article>
|
|
<article class="stat">
|
|
<div class="label">Runs</div>
|
|
<div class="value">{{ runs|length }}</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="grid two">
|
|
<article class="card">
|
|
<h2>Quelle anlegen</h2>
|
|
<form method="post" action="/admin/sources/create" class="stack">
|
|
<input name="name" placeholder="Name" required />
|
|
<input name="base_url" placeholder="Base URL" />
|
|
<input name="terms_url" placeholder="Terms URL" />
|
|
<input name="license_name" placeholder="Lizenzname" />
|
|
<select name="risk_level">
|
|
<option value="green">green</option>
|
|
<option value="yellow" selected>yellow</option>
|
|
<option value="red">red</option>
|
|
</select>
|
|
<input name="last_reviewed_at" placeholder="last_reviewed_at (ISO)" />
|
|
<button type="submit">Quelle speichern</button>
|
|
</form>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<h2>Feed anlegen</h2>
|
|
<form method="post" action="/admin/feeds/create" class="stack">
|
|
<input name="name" placeholder="Feed Name" required />
|
|
<input name="url" placeholder="https://..." required />
|
|
<label>Quelle</label>
|
|
<select name="source_id">
|
|
<option value="">-- keine --</option>
|
|
{% for s in sources %}
|
|
<option value="{{ s.id }}">{{ s.name }} (#{{ s.id }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit">Feed speichern</button>
|
|
</form>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Ingestion starten</h2>
|
|
<form method="post" action="/admin/ingestion/run" class="row">
|
|
<select name="feed_id">
|
|
<option value="">Alle aktivierten Feeds</option>
|
|
{% for f in feeds %}
|
|
<option value="{{ f.id }}">{{ f.name }} (#{{ f.id }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit">Ingestion starten</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Publisher ausführen</h2>
|
|
<form method="post" action="/admin/publisher/run" class="row">
|
|
<input name="max_jobs" value="10" />
|
|
<button type="submit">Publisher Run starten</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Rewrite Run (geplante Artikel)</h2>
|
|
<p class="subtle">Verarbeitet alle Artikel im Status <code>rewrite</code> und setzt sie auf <code>publish</code>.</p>
|
|
<form method="post" action="/admin/rewrite/run" class="row">
|
|
<input name="max_jobs" value="10" />
|
|
<button type="submit">Rewrite Run starten</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Quellen + Policy</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Name</th><th>Risk</th><th>Lizenz</th><th>Terms</th><th>Policy</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for s in sources %}
|
|
<tr>
|
|
<td>{{ s.id }}</td>
|
|
<td>{{ s.name }}</td>
|
|
<td>{{ s.risk_level }}</td>
|
|
<td>{{ s.license_name or "-" }}</td>
|
|
<td>{{ s.terms_url or "-" }}</td>
|
|
<td>
|
|
{% if source_policy[s.id] %}
|
|
<span class="badge bad">BLOCKED ({{ source_policy[s.id]|length }})</span>
|
|
<div class="subtle">{{ source_policy[s.id]|join(", ") }}</div>
|
|
{% else %}
|
|
<span class="badge ok">OK</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Quellen verwalten</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Name</th><th>URLs</th><th>Meta</th><th>Aktionen</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for s in sources %}
|
|
{% set source_form_id = 'source-update-' ~ s.id %}
|
|
<tr>
|
|
<td>#{{ s.id }}</td>
|
|
<td>
|
|
<input form="{{ source_form_id }}" name="name" value="{{ s.name }}" required />
|
|
</td>
|
|
<td>
|
|
<input form="{{ source_form_id }}" name="base_url" value="{{ s.base_url or '' }}" placeholder="Base URL" />
|
|
<input form="{{ source_form_id }}" name="terms_url" value="{{ s.terms_url or '' }}" placeholder="Terms URL" />
|
|
<input form="{{ source_form_id }}" name="license_name" value="{{ s.license_name or '' }}" placeholder="Lizenz" />
|
|
</td>
|
|
<td>
|
|
<select form="{{ source_form_id }}" name="risk_level">
|
|
<option value="green" {% if s.risk_level == 'green' %}selected{% endif %}>green</option>
|
|
<option value="yellow" {% if s.risk_level == 'yellow' %}selected{% endif %}>yellow</option>
|
|
<option value="red" {% if s.risk_level == 'red' %}selected{% endif %}>red</option>
|
|
</select>
|
|
<select form="{{ source_form_id }}" name="is_enabled">
|
|
<option value="1" {% if s.is_enabled %}selected{% endif %}>aktiv</option>
|
|
<option value="0" {% if not s.is_enabled %}selected{% endif %}>inaktiv</option>
|
|
</select>
|
|
<input form="{{ source_form_id }}" name="last_reviewed_at" value="{{ s.last_reviewed_at or '' }}" placeholder="last_reviewed_at" />
|
|
<input form="{{ source_form_id }}" name="notes" value="{{ s.notes or '' }}" placeholder="Notiz" />
|
|
</td>
|
|
<td>
|
|
<div class="inline">
|
|
<form method="post" action="/admin/sources/{{ s.id }}/update" id="{{ source_form_id }}" class="inline">
|
|
<button type="submit" class="secondary">Speichern</button>
|
|
</form>
|
|
<form method="post" action="/admin/sources/{{ s.id }}/delete" class="inline" onsubmit="return confirm('Quelle wirklich löschen?');">
|
|
<button type="submit">Löschen</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Feeds verwalten</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Name</th><th>URL</th><th>Quelle</th><th>Status</th><th>Aktionen</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for f in feeds %}
|
|
{% set feed_form_id = 'feed-update-' ~ f.id %}
|
|
<tr>
|
|
<td>#{{ f.id }}</td>
|
|
<td>
|
|
<input form="{{ feed_form_id }}" name="name" value="{{ f.name }}" required />
|
|
</td>
|
|
<td><input form="{{ feed_form_id }}" name="url" value="{{ f.url }}" required /></td>
|
|
<td>
|
|
<select form="{{ feed_form_id }}" name="source_id">
|
|
<option value="">-- keine --</option>
|
|
{% for s in sources %}
|
|
<option value="{{ s.id }}" {% if f.source_id == s.id %}selected{% endif %}>{{ s.name }} (#{{ s.id }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<select form="{{ feed_form_id }}" name="is_enabled">
|
|
<option value="1" {% if f.is_enabled %}selected{% endif %}>aktiv</option>
|
|
<option value="0" {% if not f.is_enabled %}selected{% endif %}>inaktiv</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<div class="inline">
|
|
<form method="post" action="/admin/feeds/{{ f.id }}/update" id="{{ feed_form_id }}" class="inline">
|
|
<button type="submit" class="secondary">Speichern</button>
|
|
</form>
|
|
<form method="post" action="/admin/feeds/{{ f.id }}/delete" class="inline" onsubmit="return confirm('Feed wirklich löschen?');">
|
|
<button type="submit">Löschen</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Artikel (Review)</h2>
|
|
<form method="get" action="/admin/dashboard" class="row filter-row">
|
|
<label>Status-Filter</label>
|
|
<select name="status_filter">
|
|
<option value="" {% if not status_filter %}selected{% endif %}>alle</option>
|
|
{% for s in status_options %}
|
|
<option value="{{ s }}" {% if status_filter == s %}selected{% endif %}>{{ s }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit" class="secondary">Filtern</button>
|
|
<a href="/admin/dashboard" class="linkbtn">Reset</a>
|
|
<a href="/api/articles/export?format=json{% if status_filter %}&status_filter={{ status_filter }}{% endif %}" class="linkbtn">Export JSON</a>
|
|
<a href="/api/articles/export?format=csv{% if status_filter %}&status_filter={{ status_filter }}{% endif %}" class="linkbtn">Export CSV</a>
|
|
</form>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Artikel</th><th>Status</th><th>Details</th><th>Rewrite</th><th>Transition</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for a in articles %}
|
|
<tr>
|
|
<td>{{ a.id }}</td>
|
|
<td>
|
|
<strong>{{ a.title }}</strong><br />
|
|
<span class="subtle">Autor: {{ a.author or "-" }}</span><br />
|
|
<span class="subtle">Datum: {{ a.published_at or "-" }} | Alter: {{ a.days_old if a.days_old is not none else "-" }} Tage | Relevanz: {{ a.relevance }}</span><br />
|
|
<a href="{{ a.source_url }}" target="_blank" rel="noopener">Original öffnen</a>
|
|
<br /><a href="/admin/articles/{{ a.id }}">Details anzeigen</a>
|
|
{% if a.canonical_url and a.canonical_url != a.source_url %}
|
|
<br /><a href="{{ a.canonical_url }}" target="_blank" rel="noopener">Canonical öffnen</a>
|
|
{% endif %}
|
|
</td>
|
|
<td><span class="badge">{{ a.status_ui }}</span></td>
|
|
<td>
|
|
<div class="subtle">Publish: {{ "bereit" if a.publish_ready else "blockiert" }}</div>
|
|
{% if not a.publish_ready and a.publish_blockers %}
|
|
<div class="subtle">{{ a.publish_blockers|join(", ") }}</div>
|
|
{% endif %}
|
|
{% if a.selected_image_url %}
|
|
<div class="subtle">Hauptbild gesetzt</div>
|
|
<a href="{{ a.selected_image_url }}" target="_blank" rel="noopener"><img src="{{ a.selected_image_proxy_url }}" data-fallback-src="{{ a.selected_image_url }}" alt="Hauptbild" class="thumb" loading="lazy" onerror="if(!this.dataset.fallbackUsed){this.dataset.fallbackUsed='1';this.src=this.dataset.fallbackSrc;}else{this.classList.add('img-failed');}" /></a>
|
|
{% endif %}
|
|
{% if a.summary %}
|
|
<div><strong>Summary:</strong> {{ a.summary }}</div>
|
|
{% endif %}
|
|
{% if a.generated_tags %}
|
|
<div><strong>Tags:</strong> {{ a.generated_tags|join("; ") }}</div>
|
|
{% endif %}
|
|
{% if a.content_raw %}
|
|
<details>
|
|
<summary>Volltext anzeigen</summary>
|
|
<div class="pre">{{ a.content_raw }}</div>
|
|
</details>
|
|
{% endif %}
|
|
<div class="subtle">Bilder: {{ a.extracted_images|length }}</div>
|
|
{% if a.extracted_images %}
|
|
<details>
|
|
<summary>Bild-URLs</summary>
|
|
<ul>
|
|
{% for img in a.extracted_images %}
|
|
<li><a href="{{ img }}" target="_blank" rel="noopener">{{ img }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
{% endif %}
|
|
{% if a.press_contact %}
|
|
<details>
|
|
<summary>Pressekontakt</summary>
|
|
<div class="pre">{{ a.press_contact }}</div>
|
|
</details>
|
|
{% endif %}
|
|
{% if a.extraction_error %}
|
|
<div class="subtle">Extraktionsfehler: {{ a.extraction_error }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if a.status_ui in ["new", "rewrite"] %}
|
|
<form method="post" action="/admin/articles/{{ a.id }}/rewrite-run" class="inline">
|
|
<button type="submit">Rewrite ausführen</button>
|
|
</form>
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<form method="post" action="/admin/articles/{{ a.id }}/transition" class="inline">
|
|
<select name="target_status">
|
|
{% for s in allowed_transitions.get(a.status_ui, []) %}
|
|
<option value="{{ s }}">{{ s }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% if allowed_transitions.get(a.status_ui, []) %}
|
|
<button type="submit" class="secondary">Setzen</button>
|
|
{% else %}
|
|
<span class="subtle">keine Aktion</span>
|
|
{% endif %}
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Runs</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Typ</th><th>Status</th><th>Start</th><th>Ende</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in runs %}
|
|
<tr>
|
|
<td>{{ r.id }}</td>
|
|
<td>{{ r.run_type }}</td>
|
|
<td>{{ r.status }}</td>
|
|
<td>{{ r.started_at }}</td>
|
|
<td>{{ r.finished_at or "-" }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Publish Jobs</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Artikel</th><th>Status</th><th>Attempts</th><th>WP Post</th><th>Fehler</th><th>Hinweis</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for j in publish_jobs %}
|
|
<tr>
|
|
<td>{{ j.id }}</td>
|
|
<td>#{{ j.article_id }} {{ j.article_title or "-" }}</td>
|
|
<td>{{ j.status }}</td>
|
|
<td>{{ j.attempts }}/{{ j.max_attempts }}</td>
|
|
<td>
|
|
{% if j.wp_post_url %}
|
|
<a href="{{ j.wp_post_url }}" target="_blank" rel="noopener">#{{ j.wp_post_id }}</a>
|
|
{% elif j.wp_post_id %}
|
|
#{{ j.wp_post_id }}
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if j.error_message %}
|
|
<span class="badge errcat errcat-{{ j.error_category }}">{{ j.error_category }}</span>
|
|
<div>{{ j.error_message }}</div>
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ j.error_hint or "-" }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|