diff --git a/backend/routes.py b/backend/routes.py index 60ea38c..9316842 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -805,6 +805,9 @@ async def remove_guide_format(req: GuideFormatRequest): for p in guide_slot_files(content): p.unlink(missing_ok=True) content.unlink(missing_ok=True) + import qa # Guide-QA-Report mitlöschen — sonst zeigt board_snapshot die alte Note weiter + for p in qa.report_paths(req.topic, guide=True): + p.unlink(missing_ok=True) return {"ok": True, "removed": len(doomed)} diff --git a/frontend/src/components/GenerationView.vue b/frontend/src/components/GenerationView.vue index 1bdbc6f..c37e648 100644 --- a/frontend/src/components/GenerationView.vue +++ b/frontend/src/components/GenerationView.vue @@ -114,16 +114,30 @@ async function runQaClick() { const repairBusy = ref('') // '' | 'inventory' | 'artefacts' const repairInfo = ref({}) // { inventory, artefacts } +function repairText(r) { + const n = (r.hygiene || []).length + (r.merges || []).length + (r.sub_merges || []).length + + (r.entfernt || []).length + (r.aufgeraeumt || 0) + (r.freigesprochen || []).length + return n === 0 ? 'keine behebbaren Befunde' + : `${(r.hygiene || []).length} Titel · ${(r.merges || []).length} Merges · ${(r.sub_merges || []).length} Sub-Merges · ${(r.entfernt || []).length} entfernt · ${r.aufgeraeumt || 0} aufgeräumt` +} +// Auto an → „Befunde beheben"-Loop bis 100 % / Stillstand / 10×; sonst genau ein Durchlauf. async function repairClick(ebene) { if (repairBusy.value) return + const auto = ebene === 'inventory' ? props.autoInventory : props.autoArtefacts + const max = auto ? 10 : 1 repairBusy.value = ebene repairInfo.value = { ...repairInfo.value, [ebene]: '' } + let prev = null try { - const r = await runRepair(props.topic, ebene) - const n = (r.hygiene || []).length + (r.merges || []).length + (r.sub_merges || []).length - + (r.entfernt || []).length + (r.aufgeraeumt || 0) + (r.freigesprochen || []).length - repairInfo.value = { ...repairInfo.value, [ebene]: n === 0 ? 'keine behebbaren Befunde' - : `${(r.hygiene || []).length} Titel · ${(r.merges || []).length} Merges · ${(r.sub_merges || []).length} Sub-Merges · ${(r.entfernt || []).length} entfernt · ${r.aufgeraeumt || 0} aufgeräumt` } + for (let i = 0; i < max; i++) { + const r = await runRepair(props.topic, ebene) + const note = ebene === 'artefacts' ? r.note_artefakte : r.note + const suffix = max > 1 ? ` — Runde ${i + 1}${note != null ? ` (${Math.round(note * 10)} %)` : ''}` : '' + repairInfo.value = { ...repairInfo.value, [ebene]: repairText(r) + suffix } + if (note == null || note >= 10) break // 100 % erreicht + if (prev != null && note <= prev) break // Stillstand: Repair bewegt nichts mehr + prev = note + } } catch (e) { repairInfo.value = { ...repairInfo.value, [ebene]: String(e.message || e) } } finally { diff --git a/frontend/src/components/GuideBoardSection.vue b/frontend/src/components/GuideBoardSection.vue index 064f6e6..6425abd 100644 --- a/frontend/src/components/GuideBoardSection.vue +++ b/frontend/src/components/GuideBoardSection.vue @@ -38,6 +38,8 @@ const columns = computed(() => board.value?.columns || []) const total = computed(() => columns.value.reduce((n, c) => n + c.total, 0)) const done = computed(() => columns.value.find((c) => c.key === 'done')?.total || 0) const progressValue = computed(() => (total.value ? done.value / total.value : 0)) +// QA-Badge nur wenn es ein Board gibt — sonst zeigt ein verwaister Report eine alte Note. +const qaNote = computed(() => (total.value ? board.value?.qa_guide : null)) // Laufzeit + Tokens aus /api/runs (deckt auch Guide-Läufe ab — beide setzen run_id) const run = ref(null) @@ -77,53 +79,84 @@ async function runQaClick() { finally { qaBusy.value = false; poll() } } +function removeClick() { + emit('removeFormat', props.format) + board.value = null // optimistisch: Karten + QA-Badge sofort weg, poll bestätigt + setTimeout(poll, 800) +} + const repairBusy = ref(false) const repairInfo = ref('') +// Wartet, bis die Guide-Regenerierung der offenen Karten fertig ist (für den Auto-Loop). +function warteBisFertig(timeoutMs = 300000) { + return new Promise((resolve) => { + const t0 = Date.now() + const iv = setInterval(async () => { + await poll() + if (!board.value?.generating || Date.now() - t0 > timeoutMs) { clearInterval(iv); resolve() } + }, 2000) + }) +} +// Auto an → Loop: Befunde beheben → regenerieren → QA, bis 100 % / Stillstand / 10×. +// Auto aus → genau ein Durchlauf (nur anstoßen). async function repairClick() { + if (repairBusy.value) return + const max = props.auto ? 10 : 1 repairBusy.value = true repairInfo.value = '' + let prev = null try { - const r = await repairGuideBoard(props.topic, props.format) - repairInfo.value = (r.betroffen || []).length - ? `${r.betroffen.length} Abschnitt(e) → Prüfen` - : 'keine Befunde im letzten QA-Report' - if ((r.betroffen || []).length) startPoll() + for (let i = 0; i < max; i++) { + const r = await repairGuideBoard(props.topic, props.format) + if (!(r.betroffen || []).length) { + repairInfo.value = i === 0 ? 'keine Befunde im letzten QA-Report' : `fertig nach ${i} Runde(n)` + break + } + repairInfo.value = `Runde ${i + 1}: ${r.betroffen.length} Abschnitt(e) → Prüfen…` + startPoll() + if (max === 1) break // ohne Auto: nur anstoßen, nicht auf Fertigstellung warten + await warteBisFertig() // Regenerierung abwarten + await runQa(props.topic) // neue Guide-QA messen + await poll() + const note = board.value?.qa_guide + if (note == null || note >= 10) { repairInfo.value = `100 % nach ${i + 1} Runde(n)`; break } + if (prev != null && note <= prev) { repairInfo.value = `Stillstand bei ${Math.round(note * 10)} %`; break } + prev = note + } } catch (e) { repairInfo.value = String(e.message || e) } finally { repairBusy.value = false + poll() } }