- Component v2/components/feedback_widget.html: Button unten links oberhalb der Queue, Klick oeffnet Modal mit vorausgefuellten Kontext-Feldern (URL, Drucksache, Viewport, User-Agent, letzte 15 Klicks, letzte 10 Console-Errors, letzte 5 Page-Loads). Eingaben: Titel, Beschreibung, optional Screenshot - Audit-Trail-Sammler in localStorage (Ringbuffer 30 Klicks, 10 Errors) - Screenshot via self-hosted html2canvas 1.4.1 (194 KB unter app/static/v2/lib/) - Backend POST /api/feedback (rate-limit 5/h): - validiert + html-strippt Inputs - erstellt Gitea-Issue per API mit Label 'feedback' (Label wird idempotent angelegt) - laedt Screenshot als Issue-Asset hoch (Gitea Issue-Attachment-API) - 4 neue Settings: gitea_token, gitea_api_url, gitea_repo_owner, gitea_repo_name - Server .env um GITEA_TOKEN ergaenzt - 10 neue Unit-Tests (mit gemocktem httpx) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
357 lines
17 KiB
HTML
357 lines
17 KiB
HTML
{#
|
|
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>
|