Refine BugHunter archive and feedback
mindflux-ci / build (push) Successful in 21s

This commit is contained in:
AliceDarkest
2026-07-13 20:44:35 +02:00
parent dbd1960424
commit 48dfde17eb
6 changed files with 65 additions and 15 deletions
@@ -16,16 +16,16 @@ public class BugHunterSeeder implements CommandLineRunner {
private final BugHunterHuntRepository hunts; private final ObjectMapper mapper;
public BugHunterSeeder(BugHunterHuntRepository hunts, ObjectMapper mapper) { this.hunts = hunts; this.mapper = mapper; }
@Override @Transactional public void run(String... args) {
if (hunts.count() > 0) return;
try {
JsonNode root = mapper.readTree(new ClassPathResource("bughunter/content.json").getInputStream()); int order = 0, count = 0;
JsonNode root = mapper.readTree(new ClassPathResource("bughunter/content.json").getInputStream()); int order = (int) hunts.count(), count = 0, added = 0;
for (JsonNode node : root.path("hunts")) {
if (hunts.existsBySlug(node.path("slug").asText())) continue;
BugHunterHunt hunt = new BugHunterHunt(node.path("slug").asText(), node.path("title").asText(), node.path("language").asText("Java"), node.path("difficulty").asText("junior"), node.path("examScope").asText("AP1"), node.path("description").asText(""), node.path("briefing").asText(""), node.path("consoleOutput").asText(""), order++);
int fileOrder = 0; for (JsonNode file : node.path("files")) hunt.addFile(new BugHunterFile(file.path("filename").asText(), file.path("content").asText(), fileOrder++));
int bugOrder = 0; for (JsonNode bug : node.path("bugs")) { String payload = mapper.writeValueAsString(bug); if (payload.length() <= 8000) { hunt.addBug(new BugHunterBug(bug.path("points").asInt(100), bugOrder++, payload)); count++; } }
if (!hunt.getFiles().isEmpty() && !hunt.getBugs().isEmpty()) hunts.save(hunt);
if (!hunt.getFiles().isEmpty() && !hunt.getBugs().isEmpty()) { hunts.save(hunt); added++; }
}
log.info("BugHunter: {} Fallakten geladen.", count);
log.info("BugHunter: {} neue Fallakten / {} Bugs geladen.", added, count);
} catch (Exception ex) { log.error("BugHunter-Seed fehlgeschlagen", ex); }
}
}
+36
View File
@@ -38,6 +38,42 @@
{"filename":"Auftragsliste.java","content":"import java.util.*;\npublic class Auftragsliste {\n private final List<String> jobs = new ArrayList<>();\n public void add(String job) { jobs.add(job); }\n public List<String> jobs() { return jobs; }\n}"}
],
"bugs": [{"points":200,"file":"Auftragsliste.java","line":5,"category":"sicherheit","evidence":"Die interne, veränderbare Liste wird direkt an parallele Aufrufer zurückgegeben.","fixOptions":[" public List<String> jobs() { return List.copyOf(jobs); }"," public List<String> jobs() { return new ArrayList<>(); }"," public List<String> jobs() { return null; }"," public List<String> jobs() { jobs.clear(); return jobs; }"],"fixIndex":0,"fixedLine":" public List<String> jobs() { return List.copyOf(jobs); }","explanation":"Eine unveränderliche Momentaufnahme verhindert, dass externe Aufrufer die interne Liste gleichzeitig verändern. Für echte gemeinsame Mutation wäre zusätzlich eine threadsichere Datenstruktur nötig."}]
},
{
"slug": "archiv-unter-druck", "title": "Fall #7: Archiv unter Druck", "language": "Java", "difficulty": "senior", "examScope": "AP2_FIAE",
"description": "Der tägliche Audit-Export bricht bei einem leeren Archiv ab und die CSV-Route stürzt nur in der Nachtverarbeitung ab.", "briefing": "Das Compliance-Team benötigt den Export vor 06:00 Uhr. Zwei Fehler liegen in getrennten Dateien; der Stacktrace nennt nur den Controller.", "consoleOutput": "java.lang.NullPointerException: Cannot invoke \"String.equals(Object)\"\n at ExportController.ausliefern(ExportController.java:4)\n[warn] Audit-Archiv enthält heute keine Einträge",
"files": [
{"filename":"AuditExport.java","content":"import java.util.List;\n\npublic class AuditExport {\n public String export(List<String> eintraege) {\n if (eintraege == null && eintraege.isEmpty()) {\n return \"Keine Einträge\";\n }\n return String.join(\"\\n\", eintraege);\n }\n}"},
{"filename":"ExportController.java","content":"public class ExportController {\n public String ausliefern(String format, AuditExport export, java.util.List<String> eintraege) {\n if (format.equals(\"csv\")) {\n return export.export(eintraege);\n }\n return \"Format nicht unterstützt\";\n }\n}"}
],
"bugs": [
{"points":150,"file":"AuditExport.java","line":5,"category":"laufzeit","evidence":"Bei null wird rechts von && trotzdem ein Methodenaufruf ausgeführt.","fixOptions":[" if (eintraege == null || eintraege.isEmpty()) {"," if (eintraege != null && eintraege.isEmpty()) {"," if (eintraege.isEmpty() || eintraege == null) {"," if (eintraege == null & eintraege.isEmpty()) {"],"fixIndex":0,"fixedLine":" if (eintraege == null || eintraege.isEmpty()) {","explanation":"Die ODER-Verknüpfung beendet die Auswertung sofort, wenn die Liste null ist. Danach kann isEmpty sicher geprüft werden."},
{"points":150,"file":"ExportController.java","line":3,"category":"laufzeit","evidence":"Der Stacktrace zeigt auf den Methodenaufruf equals; format kann aus einem optionalen Jobparameter stammen.","fixOptions":[" if (\"csv\".equals(format)) {"," if (format == \"csv\") {"," if (format.equalsIgnoreCase == \"csv\") {"," if (format != \"csv\") {"],"fixIndex":0,"fixedLine":" if (\"csv\".equals(format)) {","explanation":"Wird die feste Zeichenkette links verwendet, liefert equals bei einem null-Wert einfach false statt eine NullPointerException auszulösen."}
]
},
{
"slug": "buchung-im-doppelpack", "title": "Fall #8: Buchung im Doppelpack", "language": "Java", "difficulty": "senior", "examScope": "AP2_FIAE",
"description": "Nach einem Retry erscheinen Bestellungen doppelt in der Buchhaltung; zusätzlich wird eine Nullzahlung nicht erkannt.", "briefing": "Ein Zahlungsanbieter sendet einen Retry, wenn die Antwort verzögert ist. Prüfe die Idempotenz und die Geldwert-Prüfung, bevor der Monatsabschluss startet.", "consoleOutput": "[payment] retry received for order A-1042\n[ledger] duplicate reference A-1042\n[warn] zero amount accepted",
"files": [
{"filename":"BestellungService.java","content":"public class BestellungService {\n private final BestellungRepository repository;\n public BestellungService(BestellungRepository repository) { this.repository = repository; }\n\n public void speichern(Bestellung bestellung) {\n if (repository.existsByReferenz(bestellung.referenz())) {\n repository.save(bestellung);\n }\n }\n}"},
{"filename":"ZahlungPruefung.java","content":"import java.math.BigDecimal;\n\npublic class ZahlungPruefung {\n static boolean istNullzahlung(BigDecimal betrag) {\n return betrag == BigDecimal.ZERO;\n }\n}"}
],
"bugs": [
{"points":150,"file":"BestellungService.java","line":6,"category":"logik","evidence":"Bei einer bereits bekannten Referenz wird die Bestellung erneut gespeichert.","fixOptions":[" if (!repository.existsByReferenz(bestellung.referenz())) {"," if (repository.existsByReferenz(bestellung.referenz()) == null) {"," if (repository.existsByReferenz(bestellung.referenz()) = false) {"," if (repository.existsByReferenz(bestellung.referenz()) || true) {"],"fixIndex":0,"fixedLine":" if (!repository.existsByReferenz(bestellung.referenz())) {","explanation":"Eine Referenz dient hier als Idempotenzschlüssel. Nur unbekannte Bestellungen dürfen den Speichervorgang auslösen."},
{"points":150,"file":"ZahlungPruefung.java","line":4,"category":"logik","evidence":"BigDecimal-Werte können denselben Betrag darstellen, ohne dass sie dasselbe Objekt sind.","fixOptions":[" return betrag != null && betrag.compareTo(BigDecimal.ZERO) == 0;"," return betrag.equals(BigDecimal.ZERO) || betrag == null;"," return betrag == new BigDecimal(\"0\");"," return betrag.toString() == \"0\";"],"fixIndex":0,"fixedLine":" return betrag != null && betrag.compareTo(BigDecimal.ZERO) == 0;","explanation":"compareTo vergleicht den numerischen Wert unabhängig von der Skala. Zusätzlich verhindert die Null-Prüfung einen Absturz."}
]
},
{
"slug": "protokoll-ohne-schutz", "title": "Fall #9: Protokoll ohne Schutz", "language": "PHP", "difficulty": "principal", "examScope": "AP2_FIAE",
"description": "Ein Security-Audit findet Zugangsdaten im Log und eine unsichere Passwortprüfung im internen API-Gateway.", "briefing": "Das Incident-Team hat einen Logauszug gesichert. Finde beide Schwachstellen, ohne die Fehlersuche selbst unbrauchbar zu machen.", "consoleOutput": "[api] Token: eyJhbGciOiJIUzI1NiJ9...\nSecurity finding: CWE-532 Sensitive Information in Log Files\nSecurity finding: CWE-256 Plaintext Storage of a Password",
"files": [
{"filename":"api.php","content":"<?php\nfunction requestApi(string $token, string $path): void {\n error_log('API request ' . $path . ' with token ' . $token);\n // Anfrage senden\n}\n?>"},
{"filename":"auth.php","content":"<?php\nfunction anmelden(array $user, string $passwort): bool {\n return $user['password'] == $passwort;\n}\n?>"}
],
"bugs": [
{"points":200,"file":"api.php","line":3,"category":"sicherheit","evidence":"Der vollständige Bearer-Token wird in eine dauerhaft gespeicherte Logzeile übernommen.","fixOptions":[" error_log('API request ' . $path . ' with token [redigiert]');"," error_log('API request ' . $path . ' with token ' . substr($token, 0));"," error_log($token);"," error_log(base64_encode($token));"],"fixIndex":0,"fixedLine":" error_log('API request ' . $path . ' with token [redigiert]');","explanation":"Authentifizierungsdaten gehören nie in Logs. Für die Diagnose reichen Pfad, Request-ID und ein klarer Hinweis auf einen vorhandenen Token."},
{"points":200,"file":"auth.php","line":3,"category":"sicherheit","evidence":"Das Passwort wird direkt mit einem gespeicherten Wert verglichen; sichere Passwort-Hashes benötigen eine spezialisierte Prüfung.","fixOptions":[" return password_verify($passwort, $user['password_hash']);"," return md5($passwort) == $user['password'];"," return $user['password'] === $passwort;"," return strlen($passwort) > 0;"],"fixIndex":0,"fixedLine":" return password_verify($passwort, $user['password_hash']);","explanation":"password_verify prüft einen Passwort-Hash inklusive Salt und Algorithmus sicher. Klartextvergleiche und MD5 sind dafür ungeeignet."}
]
}
]
}
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -126,8 +126,8 @@ a { color:var(--secondary); }
.mf-card-codeescape { --accent:var(--esc-accent); --glow-s:var(--esc-glow); --glow-w:color-mix(in srgb,var(--esc-accent) 16%,transparent); background:repeating-linear-gradient(0deg,rgba(255,255,255,.015) 0 1px,transparent 1px 4px),var(--surface); }
.mf-card-codeescape::after { content:"ACCESS"; position:absolute; right:12px; top:9px; color:var(--esc-accent); font:700 .52rem/1 "Courier New",monospace; letter-spacing:.12em; opacity:.6; }
.mf-card-bughunter { --accent:var(--bh-accent); --glow-s:var(--bh-glow); --glow-w:color-mix(in srgb,var(--bh-accent) 16%,transparent); background:linear-gradient(135deg,color-mix(in srgb,var(--bh-accent) 9%,transparent),var(--surface)); }
.mf-card-bughunter::after { content:"CASE FILE"; position:absolute; right:12px; top:9px; color:var(--bh-accent); font:700 .52rem/1 "Courier New",monospace; letter-spacing:.12em; opacity:.7; }
.mf-card-bughunter { --accent:var(--esc-accent); --glow-s:var(--esc-glow); --glow-w:color-mix(in srgb,var(--esc-accent) 16%,transparent); background:linear-gradient(135deg,color-mix(in srgb,var(--esc-accent) 8%,transparent),var(--surface)); }
.mf-card-bughunter::after { content:"FALLAKTE"; position:absolute; right:12px; top:9px; color:var(--esc-accent); font:700 .52rem/1 "Courier New",monospace; letter-spacing:.12em; opacity:.7; }
/* ===== Panels / Widgets ===== */
.mf-panel { background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:1.1rem; box-shadow:var(--g-soft); }
+21 -7
View File
@@ -1,7 +1,7 @@
(function () {
'use strict';
const view = document.getElementById('bhView');
let catalog = [], session, index = 0, started = 0, selectedFile = '', selectedLine = 0, selected = '', options = [], answers = [];
let catalog = [], selectedCase = 0, session, index = 0, started = 0, selectedFile = '', selectedLine = 0, selected = '', options = [], answers = [], handoff = null;
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const api = (url, opt) => fetch('/api/bughunter' + url, {headers:{'Content-Type':'application/json'}, ...opt}).then(async response => { const body = await response.json(); if (!response.ok) throw new Error(body.message || 'Serverfehler'); return body; });
const post = (url, body) => api(url, {method:'POST', body:JSON.stringify(body)});
@@ -10,10 +10,15 @@
const state = () => answers[index];
function loadCatalog() { api('/hunts').then(data => { catalog = data.hunts || []; renderCatalog(); }).catch(error); }
function renderCatalog() {
view.innerHTML = `<section class="bh-hero"><span class="bh-stamp">VERTRAULIC</span><span class="bh-kicker">DETektivbüro · DEBUGGING-FALLAKTEN</span><h1>Welche Spur verfolgst du?</h1><p>Arbeite dich durch echte Fehlerbilder: Ort, Kategorie und Fix müssen zusammenpassen. Präzision baut deine Ermittlungsserie auf.</p></section><section class="bh-grid">${catalog.map(h => `<article class="bh-case"><span class="bh-case-no">${esc(h.examScope)} · ${esc(h.difficulty).toUpperCase()}</span><h2>${esc(h.title)}</h2><p>${esc(h.description)}</p><div class="bh-tags"><span>${esc(h.language)}</span><span>${h.fileCount} Dateien</span><span>${h.bugCount} Bug${h.bugCount === 1 ? '' : 's'}</span></div><button class="bh-btn primary" data-start="${esc(h.slug)}">FALL ÖFFNEN</button></article>`).join('')}</section>`;
view.querySelectorAll('[data-start]').forEach(button => button.addEventListener('click', () => begin(button.dataset.start)));
if (!catalog.length) { error(new Error('Zurzeit sind keine Fallakten verfügbar.')); return; }
selectedCase = Math.max(0, Math.min(selectedCase, catalog.length - 1));
const selectedHunt = catalog[selectedCase];
const deck = [selectedHunt, ...catalog.filter((_, i) => i !== selectedCase)];
view.innerHTML = `<section class="bh-hero"><span class="bh-stamp">VERTRAULICH</span><span class="bh-kicker">DEBUG-FALLAKTEN</span><h1>Welche Spur verfolgst du?</h1><p>Öffne eine Akte, lies die Beweise und sichere deinen Befund Schritt für Schritt. Jede Akte bleibt so lange offen, wie du sie brauchst.</p></section><section class="bh-archive"><div class="bh-archive-head"><div><span class="bh-kicker">FALLARCHIV · ${catalog.length} AKTEN</span><h2>${esc(selectedHunt.title)}</h2></div><p>Klicke eine Akte an, um sie nach vorn zu holen.</p></div><div class="bh-case-deck">${deck.map((h, stackIndex) => { const originalIndex = catalog.indexOf(h), active = originalIndex === selectedCase; return `<article class="bh-case ${active ? 'active' : ''}" style="--stack:${stackIndex};--tilt:${(stackIndex % 2 ? 1 : -1) * Math.min(stackIndex, 4) * .45}deg" data-select="${originalIndex}" tabindex="0" role="button" aria-label="${esc(h.title)} öffnen"><span class="bh-case-no">${esc(h.examScope)} · ${esc(h.difficulty).toUpperCase()}</span><h2>${esc(h.title)}</h2>${active ? `<p>${esc(h.description)}</p><div class="bh-tags"><span>${esc(h.language)}</span><span>${h.fileCount} ${h.fileCount === 1 ? 'Datei' : 'Dateien'}</span><span>${h.bugCount} Bug${h.bugCount === 1 ? '' : 's'}</span></div><button class="bh-btn primary" data-start="${esc(h.slug)}">AKTE ÖFFNEN</button>` : '<span class="bh-case-peek">Zum Öffnen nach vorn holen</span>'}</article>`; }).join('')}</div></section>`;
view.querySelectorAll('[data-select]').forEach(card => { const select = () => { selectedCase = Number(card.dataset.select); renderCatalog(); }; card.addEventListener('click', event => { if (!event.target.closest('[data-start]')) select(); }); card.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); select(); } }); });
view.querySelector('[data-start]')?.addEventListener('click', event => { event.stopPropagation(); begin(event.currentTarget.dataset.start); });
}
function begin(slug) { api('/session/' + encodeURIComponent(slug)).then(data => { if (!data.found) throw new Error('Fallakte nicht verfügbar.'); session = data; index = 0; answers = session.bugs.map(() => ({locate:null, classify:null, fix:null})); started = Date.now(); renderBriefing(); }).catch(error); }
function begin(slug) { api('/session/' + encodeURIComponent(slug)).then(data => { if (!data.found) throw new Error('Fallakte nicht verfügbar.'); session = data; index = 0; handoff = null; answers = session.bugs.map(() => ({locate:null, classify:null, fix:null})); started = Date.now(); renderBriefing(); }).catch(error); }
function renderBriefing() {
view.innerHTML = `<section class="bh-briefing"><span class="bh-stamp">NEUE AKTE</span><span class="bh-kicker">${esc(session.examScope)} · ${esc(session.difficulty).toUpperCase()} · ${esc(session.language)}</span><h1>${esc(session.title)}</h1><p>${esc(session.briefing)}</p><pre>${esc(session.consoleOutput)}</pre><div class="bh-rules"><div><b>1</b> Ort markieren</div><div><b>2</b> Fehler einordnen</div><div><b>3</b> sauberen Fix wählen</div></div><button class="bh-btn primary" id="bhStart">ERMITTLUNG STARTEN</button></section>`;
document.getElementById('bhStart').addEventListener('click', () => { selectedFile = session.files[0]?.filename || ''; selectedLine = 0; selected = ''; options = []; renderHunt(); });
@@ -32,6 +37,7 @@
return `<div class="bh-code" aria-label="Code-Datei ${esc(file.filename)}">${file.lines.map((line, i) => { const lineNo = i + 1, marked = phase === 'locate' && selectedLine === lineNo; return `<button class="bh-line ${marked ? 'selected' : ''}" data-line="${lineNo}" ${phase === 'locate' ? '' : 'disabled'}><span>${lineNo}</span><code>${esc(line) || ' '}</code></button>`; }).join('')}</div>`;
}
function renderAction(phase, bug, s) {
if (handoff) return `<section class="bh-action bh-handoff"><span class="bh-kicker">PHASE ABGESCHLOSSEN</span><h2>${esc(handoff.title)}</h2><p>${esc(handoff.message)}</p><button class="bh-btn primary" id="bhContinue">${esc(handoff.nextLabel)}</button></section>`;
const labels = {locate:'1 · ORTEN', classify:'2 · KLASSIFIZIEREN', fix:'3 · FIXEN'};
let content = '';
if (phase === 'locate') content = `<p>Klicke die fehlerhafte Zeile im Code an.</p><p class="bh-selected">${selectedLine ? `${esc(selectedFile)}:${selectedLine}` : 'Noch keine Zeile markiert'}</p>`;
@@ -41,6 +47,7 @@
return `<section class="bh-action"><span class="bh-kicker">${labels[phase]}</span><h2>${phase === 'locate' ? 'Wo liegt der Bug?' : phase === 'classify' ? 'Wie heißt das Problem?' : 'Wie wird er behoben?'}</h2>${content}<p class="bh-attempts">Versuche: ${attempts}/3</p><div id="bhFeedback" class="bh-feedback" aria-live="polite"></div><div class="bh-actions"><button class="bh-btn primary" id="bhCheck">PRÜFEN</button><button class="bh-btn" id="bhReveal">AUFLÖSEN</button></div></section>`;
}
function bindAction(phase, bug) {
if (handoff) { document.getElementById('bhContinue').addEventListener('click', () => { handoff = null; selected = ''; selectedLine = 0; renderHunt(); }); return; }
if (phase === 'locate') view.querySelectorAll('[data-line]').forEach(line => line.addEventListener('click', () => { selectedLine = Number(line.dataset.line); renderHunt(); }));
view.querySelectorAll('[data-choice]').forEach(button => button.addEventListener('click', () => { selected = button.dataset.choice; view.querySelectorAll('[data-choice]').forEach(item => item.classList.remove('selected')); button.classList.add('selected'); }));
document.getElementById('bhCheck').addEventListener('click', () => submit(phase, bug, false));
@@ -53,9 +60,16 @@
post('/check', {bugId:bug.bugId, phase, reveal, submission:sub}).then(result => {
if (!result.resolved) { state()[phase] = phaseState; feedback('Noch nicht stimmig. Lies die Beweise erneut.', 'bad'); return; }
phaseState.revealed = reveal; phaseState.done = true; state()[phase] = phaseState;
if (phase === 'classify') options = result.fixOptions || []; if (phase === 'fix') { showDebrief(result); return; }
selected = ''; if (phase === 'locate') selectedLine = result.location?.line || selectedLine;
feedback(reveal ? 'Auflösung notiert – weiter zur nächsten Phase.' : 'Spur bestätigt – weiter zur nächsten Phase.', 'ok'); setTimeout(renderHunt, 650);
if (phase === 'classify') options = result.fixOptions || [];
if (phase === 'fix') { showDebrief(result); return; }
if (phase === 'locate') {
const location = result.location || sub;
handoff = { title: reveal ? 'Auflösung: Fundort markiert' : 'Fundort bestätigt', message: `Die relevante Stelle ist ${location.file}:${location.line}. Nimm dir einen Moment für den Code, bevor du die Fehlerklasse bestimmst.`, nextLabel: 'ZUR KATEGORIE' };
} else {
const category = result.category || sub.category;
handoff = { title: reveal ? 'Auflösung: Fehlerklasse' : 'Fehlerklasse bestätigt', message: `Der Fehler gehört zur Kategorie „${category}“. Die möglichen Korrekturen liegen jetzt bereit.`, nextLabel: 'ZUR KORREKTUR' };
}
selected = ''; renderHunt();
}).catch(err => feedback(err.message, 'bad'));
}
function showDebrief(result) {
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>BugHunter · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/bughunter.css?v=1"><script src="/js/theme.js"></script></head><body><main class="bh-app"><header class="bh-top"><a href="/dashboard" class="bh-back">← Dashboard</a><div class="bh-brand"><b>BUGHUNTER</b><span>INVESTIGATION BUREAU</span></div><span class="mf-a11y-host"></span></header><section id="bhView" class="bh-view"><div class="bh-loading">FALLAKTEN WERDEN GEÖFFNET …</div></section></main><script src="/js/bughunter.js?v=1"></script></body></html>
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>BugHunter · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/bughunter.css?v=2"><script src="/js/theme.js"></script></head><body><main class="bh-app"><header class="bh-top"><a href="/dashboard" class="bh-back">← Dashboard</a><div class="bh-brand"><b>BUGHUNTER</b><span>DEBUG-FALLAKTEN</span></div><span class="mf-a11y-host"></span></header><section id="bhView" class="bh-view"><div class="bh-loading">FALLAKTEN WERDEN GEÖFFNET …</div></section></main><script src="/js/bughunter.js?v=2"></script></body></html>