gwoe-antragspruefer/app/templates/v2/screens/neu.html
Dotty Dotter 38bffb23fa fix: Job-Polling vor Redirect statt sofortigem Antrag-nicht-gefunden
Vorher: Klick 'Analysieren' -> POST /api/analyze-drucksache -> sofort
window.location.href = '/antrag/{ds}' -> aber Job laeuft noch im Hintergrund
-> Detail-Seite zeigt 'Antrag nicht gefunden'.

Jetzt:
- already_checked -> sofortiger Redirect
- skipped (nicht abstimmbar) -> Hinweistext im Form
- queued -> Polling auf /status/{job_id} alle 2s, max 3 Min
- completed -> Redirect zur Detail-Seite
- failed/rejected -> Fehlermeldung mit Grund

Anwendung in v2/screens/landtag_suche.html + v2/screens/neu.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:35:55 +02:00

217 lines
6.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "v2/base.html" %}
{% block title %}Neuer Antrag — GWÖ-Antragsprüfer{% endblock %}
{% set v2_active_nav = "neu" %}
{% block head_extra %}
<style>
.neu-form {
max-width: 560px;
}
.neu-form label {
display: block;
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.7;
margin-bottom: 4px;
}
.neu-form input[type="text"],
.neu-form select {
width: 100%;
font-family: var(--font-mono);
font-size: 13px;
padding: 8px 10px;
border: 1px solid var(--ecg-border);
border-radius: 4px;
background: var(--ecg-card-bg);
color: var(--ecg-dark);
margin-bottom: 14px;
}
.neu-form input[type="text"]:focus,
.neu-form select:focus {
outline: none;
border-color: var(--ecg-teal);
}
.neu-submit {
font-family: var(--font-display);
font-size: 13px;
font-weight: 700;
padding: 10px 24px;
background: var(--ecg-teal);
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
letter-spacing: 0.04em;
transition: opacity 0.1s;
}
.neu-submit:hover { opacity: 0.85; }
.neu-submit:disabled { opacity: 0.45; cursor: not-allowed; }
#neu-status {
margin-top: 1rem;
font-family: var(--font-mono);
font-size: 12px;
display: none;
}
.neu-progress {
display: flex;
align-items: center;
gap: 8px;
color: var(--ecg-teal);
}
.neu-spinner {
width: 14px; height: 14px;
border: 2px solid var(--ecg-teal);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.neu-error { color: #c00; }
</style>
{% endblock %}
{% block main %}
<div style="padding:0 0 1.5rem;">
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">Neuer Antrag prüfen</h1>
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
Drucksachen-Nr. eingeben · Analyse startet sofort
</p>
</div>
<div class="v2-kasten outline-blue" style="max-width:560px;margin-bottom:1.5rem;">
<p style="font-size:12px;">
Der Prüfer holt den Antragstext aus dem Landtags-Portal, bewertet ihn nach der GWÖ-Matrix 2.0
und zeigt Wahlprogramm-Treue sowie Verbesserungsvorschläge. Die Analyse dauert 3090 Sekunden.
</p>
</div>
<form class="neu-form" onsubmit="startAnalyse(event)">
<label for="neu-drucksache">Drucksachen-Nummer</label>
<input type="text" id="neu-drucksache" name="drucksache"
placeholder="z. B. 18/12345 oder NRW-18/12345"
required autocomplete="off">
<label for="neu-bl">Bundesland</label>
<select id="neu-bl" name="bundesland">
{% for bl in bundeslaender %}
<option value="{{ bl.code }}"{% if bl.code == 'NRW' %} selected{% endif %}>{{ bl.name }} ({{ bl.code }})</option>
{% endfor %}
</select>
<label for="neu-model">Modell</label>
<select id="neu-model" name="model">
<option value="">Standard ({{ default_model }})</option>
<option value="qwen-plus">qwen-plus</option>
<option value="qwen-max">qwen-max</option>
</select>
<button type="submit" class="neu-submit" id="neu-btn">Analyse starten</button>
</form>
<div id="neu-status">
<div id="neu-progress" class="neu-progress" style="display:none;">
<div class="neu-spinner"></div>
<span id="neu-progress-text">Analyse läuft …</span>
</div>
<div id="neu-error" class="neu-error" style="display:none;"></div>
</div>
{% endblock %}
{% block body_scripts %}
<script>
async function startAnalyse(e) {
e.preventDefault();
const btn = document.getElementById('neu-btn');
const statusEl = document.getElementById('neu-status');
const progEl = document.getElementById('neu-progress');
const progText = document.getElementById('neu-progress-text');
const errEl = document.getElementById('neu-error');
const drucksache = document.getElementById('neu-drucksache').value.trim();
const bundesland = document.getElementById('neu-bl').value;
const model = document.getElementById('neu-model').value;
if (!drucksache) return;
btn.disabled = true;
statusEl.style.display = '';
progEl.style.display = '';
errEl.style.display = 'none';
progText.textContent = 'Analyse läuft … (3090 s)';
try {
const fd = new FormData();
fd.append('drucksache', drucksache);
fd.append('bundesland', bundesland);
if (model) fd.append('model', model);
const resp = await fetch('/api/analyze-drucksache', { method: 'POST', body: fd });
if (resp.status === 401 || resp.status === 403) {
progEl.style.display = 'none';
errEl.style.display = '';
errEl.textContent = 'Sitzung abgelaufen — bitte erneut anmelden.';
btn.disabled = false;
if (typeof window.v2AuthModalOpen === 'function') window.v2AuthModalOpen();
return;
}
if (!resp.ok) {
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
throw new Error(err.detail || ('HTTP ' + resp.status));
}
const data = await resp.json();
const ds = data.drucksache || drucksache;
if (data.status === 'already_checked') {
progText.textContent = 'Bereits bewertet. Weiterleitung …';
setTimeout(() => { window.location.href = '/antrag/' + encodeURIComponent(ds); }, 400);
return;
}
if (data.status === 'skipped') {
progEl.style.display = 'none';
errEl.style.display = '';
errEl.textContent = 'Antrag-Typ "' + (data.typ || 'unbekannt') + '" ist nicht abstimmbar — keine GWÖ-Bewertung sinnvoll.';
btn.disabled = false;
return;
}
// Job-Polling bis Abschluss, dann redirect
const jobId = data.job_id;
if (!jobId) { window.location.href = '/antrag/' + encodeURIComponent(ds); return; }
let attempts = 0;
const maxAttempts = 90;
while (attempts < maxAttempts) {
await new Promise(r => setTimeout(r, 2000));
attempts++;
const st = await fetch('/status/' + jobId).then(r => r.json()).catch(() => null);
if (!st) continue;
progText.textContent = 'Analyse läuft … (' + (st.status || '?') + ', ~' + (attempts * 2) + 's)';
if (st.status === 'completed') {
progText.textContent = 'Fertig. Weiterleitung …';
setTimeout(() => { window.location.href = '/antrag/' + encodeURIComponent(ds); }, 400);
return;
}
if (st.status === 'failed' || st.status === 'rejected') {
throw new Error('Analyse fehlgeschlagen: ' + (st.error || 'unbekannt'));
}
}
throw new Error('Analyse-Timeout (>3 min)');
} catch (err) {
progEl.style.display = 'none';
errEl.style.display = '';
errEl.textContent = 'Fehler: ' + err.message;
btn.disabled = false;
}
}
</script>
{% endblock %}