gwoe-antragspruefer/app/templates/v2/screens/neu.html
Dotty Dotter 553e99d14e feat(v2): globaler BL-Selector im Header + Auth-gated Sidebar + Queue-Widget
Bundesland-Auswahl:
- Topbar: einziger BL-Selektor mit localStorage.gwoe.bl-Persistenz
- BL-Felder entfernt aus durchsuchen.html, landtag_suche.html, neu.html, auswertungen.html
- Screens hoeren auf v2-bl-changed CustomEvent + initial via window.v2GetGlobalBl()

Sichtbarkeit (Sidebar):
- Durchsuchen + Tags: immer
- Merkliste / Neuer Antrag / Landtag-Suche / Auswertungen / Export / Feed: nur eingeloggt
- Cluster + Batch-Analyse + Administration: nur Admin

Server-Side Schutz:
- _v2_template_context()-Helper liefert is_authenticated, is_admin, v2_bundeslaender
- HTML-Routen mit Depends(require_auth) bzw. require_admin
- 401/403-Browser-Requests redirecten auf /?login=1 statt JSON-Error

Queue-Widget (#149):
- Neues Component-Partial v2/components/queue_widget.html
- Statusbar unten links + Hover-Tooltip mit den letzten 20 Jobs
- 5s-Polling auf /api/queue/status, blendet sich aus wenn keine Jobs

Smoke-Test angepasst an neue Auth-Erwartungen (302 fuer auth-protected Routen).

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

230 lines
7.2 KiB
HTML
Raw Permalink 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)">
<div id="neu-bl-hint" style="display:none;margin-bottom:14px;padding:8px 12px;background:color-mix(in srgb,var(--ecg-teal) 10%,transparent);border:1px solid var(--ecg-teal);border-radius:4px;font-family:var(--font-mono);font-size:11px;color:var(--ecg-teal);">
Bitte zuerst ein Bundesland im Header wählen.
</div>
<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-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 = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const model = document.getElementById('neu-model').value;
if (!drucksache) return;
if (bundesland === 'ALL') {
errEl.style.display = '';
errEl.textContent = 'Bitte zuerst ein Bundesland im Header wählen.';
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;
}
}
document.addEventListener('DOMContentLoaded', function () {
var hint = document.getElementById('neu-bl-hint');
function updateHint() {
var bl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
if (hint) hint.style.display = (bl === 'ALL') ? '' : 'none';
}
updateHint();
window.addEventListener('v2-bl-changed', updateHint);
});
</script>
{% endblock %}