gwoe-antragspruefer/app/templates/v2/components/feedback_widget.html

357 lines
17 KiB
HTML
Raw Normal View History

{#
feedback_widget.html — Feedback/Bug-Report-Widget mit Audit-Trail und Gitea-Anbindung.
Position: bottom:4rem, left:1rem — über dem Queue-Widget.
Self-contained: Button + Modal + Audit-Trail-Sammler + Submit-Logic.
Wird via {% include %} in base.html eingebunden.
#}
{# ── Feedback-Button ──────────────────────────────────────────────────────── #}
<button id="v2-feedback-btn"
onclick="v2FeedbackOpen()"
aria-label="Feedback oder Bug melden"
title="Feedback / Bug melden"
style="position:fixed;bottom:4rem;left:1rem;
background:var(--ecg-card-bg);border:1px solid var(--ecg-light);
border-radius:6px;padding:0.4rem 0.8rem;
font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);
box-shadow:0 2px 8px rgba(0,0,0,0.1);z-index:100;cursor:pointer;
display:inline-flex;align-items:center;gap:5px;
transition:all 0.2s;white-space:nowrap;">
<span style="display:inline-flex;align-items:center;width:14px;height:14px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" width="14" height="14"><path d="M240,96a8,8,0,0,0-8-8H213.06A88.18,88.18,0,0,0,192,61.34V48a16,16,0,0,0-16-16H80A16,16,0,0,0,64,48V61.34A88.18,88.18,0,0,0,42.94,88H24a8,8,0,0,0,0,16H40.21A88.35,88.35,0,0,0,40,112v8H24a8,8,0,0,0,0,16H40v8a88.35,88.35,0,0,0,.21,8H24a8,8,0,0,0,0,16H42.94A88,88,0,0,0,120,209.72V232a8,8,0,0,0,16,0V209.72A88,88,0,0,0,213.06,168H232a8,8,0,0,0,0-16H215.79A88.35,88.35,0,0,0,216,144v-8h16a8,8,0,0,0,0-16H216v-8a88.35,88.35,0,0,0-.21-8H232A8,8,0,0,0,240,96ZM80,48H176V55.12A88.25,88.25,0,0,0,128,40a88.25,88.25,0,0,0-48,15.12ZM128,192a72,72,0,1,1,72-72A72.08,72.08,0,0,1,128,192Z"/></svg>
</span>
Feedback
</button>
{# ── Feedback-Modal ───────────────────────────────────────────────────────── #}
<div id="v2-feedback-modal"
role="dialog" aria-modal="true" aria-labelledby="v2-feedback-modal-title"
style="display:none;position:fixed;inset:0;z-index:10000;
background:rgba(0,0,0,0.45);
align-items:center;justify-content:center;">
<div style="background:var(--ecg-card-bg);border:1px solid var(--ecg-light);
border-radius:8px;padding:1.5rem;
width:min(680px,96vw);max-height:90vh;overflow-y:auto;
box-shadow:0 8px 32px rgba(0,0,0,0.25);
font-family:var(--font-sans);font-size:13px;color:var(--ecg-dark);
position:relative;">
<button onclick="v2FeedbackClose()"
aria-label="Schließen"
style="position:absolute;top:0.75rem;right:0.75rem;
background:none;border:none;cursor:pointer;
font-size:16px;color:var(--ecg-text-muted);line-height:1;">✕</button>
<h2 id="v2-feedback-modal-title"
style="margin:0 0 1rem;font-size:14px;font-weight:900;
letter-spacing:0.04em;text-transform:uppercase;
color:var(--ecg-blue);">Feedback / Bug melden</h2>
<form id="v2-feedback-form" onsubmit="v2FeedbackSubmit(event)">
{# ── User-Eingaben ────────────────────────────────────────────── #}
<div style="margin-bottom:0.75rem;">
<label for="v2-fb-titel"
style="display:block;margin-bottom:0.25rem;font-size:11px;
font-family:var(--font-mono);text-transform:uppercase;
letter-spacing:0.06em;color:var(--ecg-text-muted);">
Titel <span style="color:var(--ecg-green);">*</span>
</label>
<input id="v2-fb-titel" type="text" required maxlength="200"
placeholder="Kurze Zusammenfassung des Problems"
style="width:100%;box-sizing:border-box;
border:1px solid var(--ecg-light);border-radius:4px;
padding:0.5rem 0.6rem;font-family:var(--font-sans);
font-size:13px;background:var(--ecg-card-bg);
color:var(--ecg-dark);">
</div>
<div style="margin-bottom:0.75rem;">
<label for="v2-fb-beschreibung"
style="display:block;margin-bottom:0.25rem;font-size:11px;
font-family:var(--font-mono);text-transform:uppercase;
letter-spacing:0.06em;color:var(--ecg-text-muted);">
Beschreibung <span style="color:var(--ecg-green);">*</span>
</label>
<textarea id="v2-fb-beschreibung" required maxlength="5000" rows="5"
placeholder="Was ist passiert? Was hast du erwartet?"
style="width:100%;box-sizing:border-box;
border:1px solid var(--ecg-light);border-radius:4px;
padding:0.5rem 0.6rem;font-family:var(--font-sans);
font-size:13px;background:var(--ecg-card-bg);
color:var(--ecg-dark);resize:vertical;"></textarea>
</div>
{# ── Screenshot-Checkbox ──────────────────────────────────────── #}
<div style="margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">
<input id="v2-fb-screenshot" type="checkbox">
<label for="v2-fb-screenshot" style="font-size:12px;cursor:pointer;">
Screenshot anhängen (aktueller Seitenausschnitt)
</label>
</div>
{# ── Audit-Trail-Vorschau ─────────────────────────────────────── #}
<details style="margin-bottom:1rem;">
<summary style="cursor:pointer;font-size:11px;font-family:var(--font-mono);
text-transform:uppercase;letter-spacing:0.06em;
color:var(--ecg-text-muted);">
Mitgesendeter Kontext (Audit-Trail) ▾
</summary>
<div id="v2-fb-audit-preview"
style="margin-top:0.5rem;padding:0.75rem;
background:var(--ecg-bg, #f8f8f5);
border:1px solid var(--ecg-light);border-radius:4px;
font-family:var(--font-mono);font-size:10px;
color:var(--ecg-text-muted);
white-space:pre-wrap;max-height:200px;overflow-y:auto;
word-break:break-all;"></div>
</details>
{# ── Status-Anzeige ───────────────────────────────────────────── #}
<div id="v2-fb-status" style="display:none;margin-bottom:0.75rem;
padding:0.5rem 0.75rem;border-radius:4px;
font-size:12px;"></div>
{# ── Buttons ──────────────────────────────────────────────────── #}
<div style="display:flex;gap:0.75rem;justify-content:flex-end;">
<button type="button" onclick="v2FeedbackClose()"
style="background:none;border:1px solid var(--ecg-light);
border-radius:4px;padding:0.5rem 1rem;cursor:pointer;
font-family:var(--font-sans);font-size:13px;
color:var(--ecg-dark);">Abbrechen</button>
<button type="submit" id="v2-fb-submit-btn"
style="background:var(--ecg-blue,#1a6fa8);border:none;
border-radius:4px;padding:0.5rem 1.25rem;cursor:pointer;
font-family:var(--font-sans);font-size:13px;
color:#fff;font-weight:600;">Absenden</button>
</div>
</form>
</div>
</div>
{# ── Audit-Trail-Sammler + Modal-Logik ────────────────────────────────────── #}
<script>
(function () {
'use strict';
/* ── Ringbuffer-Helper ─────────────────────────────────────── */
var AUDIT_KEY = 'gwoe.audit';
var ERRORS_KEY = 'gwoe.errors';
var MAX_CLICKS = 30;
var MAX_ERRORS = 10;
function ringPush(key, item, max) {
var arr = [];
try { arr = JSON.parse(localStorage.getItem(key) || '[]'); } catch (_) {}
arr.push(item);
if (arr.length > max) arr = arr.slice(arr.length - max);
try { localStorage.setItem(key, JSON.stringify(arr)); } catch (_) {}
}
function ringRead(key) {
try { return JSON.parse(localStorage.getItem(key) || '[]'); } catch (_) { return []; }
}
/* ── CSS-Pfad (kurz) ────────────────────────────────────────── */
function cssPath(el) {
if (!el || el === document.body) return 'body';
var path = [];
var cur = el;
while (cur && cur !== document.body && path.length < 4) {
var tag = cur.tagName ? cur.tagName.toLowerCase() : '';
var id = cur.id ? '#' + cur.id : '';
var cls = cur.className && typeof cur.className === 'string'
? ('.' + cur.className.trim().split(/\s+/).slice(0,2).join('.'))
: '';
path.unshift(tag + id + cls);
cur = cur.parentElement;
}
return path.join(' > ');
}
/* ── Click-Listener ─────────────────────────────────────────── */
document.addEventListener('click', function (e) {
var target = e.target;
if (!target) return;
// Feedback-Modal-Klicks nicht tracken
if (target.closest && target.closest('#v2-feedback-modal')) return;
var text = (target.textContent || target.value || target.alt || '')
.trim().slice(0, 60).replace(/\s+/g, ' ');
ringPush(AUDIT_KEY, {
t: new Date().toISOString(),
el: cssPath(target),
txt: text || null
}, MAX_CLICKS);
}, true);
/* ── Error-Listener ─────────────────────────────────────────── */
window.addEventListener('error', function (e) {
ringPush(ERRORS_KEY, {
t: new Date().toISOString(),
msg: e.message || String(e),
src: (e.filename || '').replace(window.location.origin, '') + ':' + e.lineno
}, MAX_ERRORS);
});
/* ── Modal öffnen/schließen ─────────────────────────────────── */
window.v2FeedbackOpen = function () {
var modal = document.getElementById('v2-feedback-modal');
if (!modal) return;
// Audit-Vorschau befüllen
var preview = document.getElementById('v2-fb-audit-preview');
if (preview) {
var clicks = ringRead(AUDIT_KEY).slice(-15);
var errors = ringRead(ERRORS_KEY).slice(-10);
var lines = [];
lines.push('URL: ' + window.location.href);
lines.push('User-Agent: ' + navigator.userAgent.slice(0, 120));
lines.push('Viewport: ' + window.innerWidth + 'x' + window.innerHeight);
// Drucksache aus URL extrahieren
var dsMatch = window.location.pathname.match(/\/antrag\/([^/?#]+)/);
if (dsMatch) lines.push('Drucksache: ' + decodeURIComponent(dsMatch[1]));
lines.push('');
if (clicks.length) {
lines.push('Letzte Klicks:');
clicks.forEach(function (c) {
lines.push(' ' + c.t.slice(11,19) + ' ' + c.el + (c.txt ? ' "' + c.txt + '"' : ''));
});
}
if (errors.length) {
lines.push('');
lines.push('Console-Errors:');
errors.forEach(function (e) {
lines.push(' ' + e.t.slice(11,19) + ' ' + e.msg + ' @ ' + e.src);
});
}
preview.textContent = lines.join('\n');
}
// Status zurücksetzen
var status = document.getElementById('v2-fb-status');
if (status) { status.style.display = 'none'; status.textContent = ''; }
var submitBtn = document.getElementById('v2-fb-submit-btn');
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Absenden'; }
modal.style.display = 'flex';
var titel = document.getElementById('v2-fb-titel');
if (titel) setTimeout(function () { titel.focus(); }, 50);
};
window.v2FeedbackClose = function () {
var modal = document.getElementById('v2-feedback-modal');
if (modal) modal.style.display = 'none';
};
// Schließen bei Klick auf Backdrop
document.getElementById('v2-feedback-modal').addEventListener('click', function (e) {
if (e.target === this) window.v2FeedbackClose();
});
// Escape-Taste
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
var modal = document.getElementById('v2-feedback-modal');
if (modal && modal.style.display === 'flex') window.v2FeedbackClose();
}
});
/* ── Submit ─────────────────────────────────────────────────── */
window.v2FeedbackSubmit = async function (e) {
e.preventDefault();
var titel = document.getElementById('v2-fb-titel').value.trim();
var beschreibung = document.getElementById('v2-fb-beschreibung').value.trim();
var screenshot = document.getElementById('v2-fb-screenshot').checked;
var submitBtn = document.getElementById('v2-fb-submit-btn');
var statusEl = document.getElementById('v2-fb-status');
function setStatus(msg, ok) {
statusEl.textContent = msg;
statusEl.style.display = 'block';
statusEl.style.background = ok ? 'rgba(0,128,64,0.1)' : 'rgba(200,0,0,0.08)';
statusEl.style.border = '1px solid ' + (ok ? 'rgba(0,128,64,0.3)' : 'rgba(200,0,0,0.2)');
statusEl.style.color = ok ? 'var(--ecg-green)' : '#c00';
}
submitBtn.disabled = true;
submitBtn.textContent = 'Wird gesendet…';
// Audit-Daten sammeln
var clicks = ringRead(AUDIT_KEY).slice(-15);
var errors = ringRead(ERRORS_KEY).slice(-10);
var dsMatch = window.location.pathname.match(/\/antrag\/([^/?#]+)/);
var fd = new FormData();
fd.append('titel', titel);
fd.append('beschreibung', beschreibung);
fd.append('url', window.location.href);
fd.append('user_agent', navigator.userAgent);
fd.append('viewport', window.innerWidth + 'x' + window.innerHeight);
fd.append('drucksache', dsMatch ? decodeURIComponent(dsMatch[1]) : '');
fd.append('klicks_json', JSON.stringify(clicks));
fd.append('errors_json', JSON.stringify(errors));
// Screenshot (optional, via html2canvas)
if (screenshot && window.html2canvas) {
submitBtn.textContent = 'Screenshot wird erstellt…';
try {
var canvas = await window.html2canvas(document.body, {
scale: Math.min(window.devicePixelRatio || 1, 2),
useCORS: true,
allowTaint: false,
logging: false,
});
// Breite auf max 800 px beschränken
var MAX_W = 800;
var finalCanvas = canvas;
if (canvas.width > MAX_W) {
var ratio = MAX_W / canvas.width;
var sc = document.createElement('canvas');
sc.width = MAX_W;
sc.height = Math.round(canvas.height * ratio);
var ctx = sc.getContext('2d');
ctx.drawImage(canvas, 0, 0, sc.width, sc.height);
finalCanvas = sc;
}
var dataUrl = finalCanvas.toDataURL('image/jpeg', 0.7);
fd.append('screenshot', dataUrl);
} catch (err) {
// Screenshot fehlgeschlagen → trotzdem absenden
fd.append('screenshot_error', String(err));
}
}
submitBtn.textContent = 'Wird gesendet…';
try {
var resp = await fetch('/api/feedback', { method: 'POST', body: fd });
var data = await resp.json();
if (resp.ok && data.issue_url) {
setStatus('Danke! Issue angelegt: ' + data.issue_url, true);
submitBtn.textContent = 'Abgeschlossen';
// Felder leeren
document.getElementById('v2-fb-titel').value = '';
document.getElementById('v2-fb-beschreibung').value = '';
document.getElementById('v2-fb-screenshot').checked = false;
setTimeout(window.v2FeedbackClose, 3000);
} else {
setStatus('Fehler: ' + (data.detail || JSON.stringify(data)), false);
submitBtn.disabled = false;
submitBtn.textContent = 'Absenden';
}
} catch (err) {
setStatus('Netzwerkfehler: ' + err.message, false);
submitBtn.disabled = false;
submitBtn.textContent = 'Absenden';
}
};
})();
</script>
{# html2canvas — self-hosted, kein CDN-Call #}
<script src="/static/v2/lib/html2canvas.min.js"></script>