From c4785184256e553b38a5dded8dd43b177559f1a5 Mon Sep 17 00:00:00 2001 From: Nitsu45 Date: Wed, 21 Oct 2026 23:05:13 +0200 Subject: [PATCH] Add readable case details and completed results browser --- .../test-results/web-case-details-20261021.md | 29 +++ docs/web-console.md | 35 ++- src/viano_bug_analysis/api/app.py | 34 ++- src/viano_bug_analysis/api/cases.py | 228 ++++++++++++++++++ src/viano_bug_analysis/api/dashboard.py | 1 + src/viano_bug_analysis/web/case-detail.js | 114 +++++++++ src/viano_bug_analysis/web/dashboard.css | 2 + src/viano_bug_analysis/web/dashboard.js | 14 +- src/viano_bug_analysis/web/index.html | 11 +- tests/dashboard-smoke.mjs | 11 + tests/test_dashboard.py | 54 +++++ tests/ui_dashboard_server.py | 21 ++ 12 files changed, 542 insertions(+), 12 deletions(-) create mode 100644 docs/test-results/web-case-details-20261021.md create mode 100644 src/viano_bug_analysis/api/cases.py create mode 100644 src/viano_bug_analysis/web/case-detail.js diff --git a/docs/test-results/web-case-details-20261021.md b/docs/test-results/web-case-details-20261021.md new file mode 100644 index 0000000..265f0fd --- /dev/null +++ b/docs/test-results/web-case-details-20261021.md @@ -0,0 +1,29 @@ +# Falldetailansicht und Ergebnisliste – 2026-10-21 + +Ziel: Erhaltene Fallakten direkt im Browser lesen, ohne ZIP-Verzeichnisse manuell +zu durchsuchen. Keine Änderung an Analyseprompts, Budgets oder Architekturvorgaben. + +Implementierung: projektgebundene Leseendpunkte, Ergebnisliste mit Suche und +Projektfilter, Revisionsauswahl, Beobachtungen, Interpretation, Hypothesen mit +Confidence und Befunden, Diagnose, Coding-/Human-Dev-Aufträge, API-Patchfeedback, +Sperrhinweise und vollständiger aktueller ZIP-Download. + +Prüfungen: + +- `pytest tests/test_dashboard.py -q`: 18 bestanden. Authentifizierung, Projektscope, + vollständiger ZIP-Download, ungültige Revisionen und wiedereröffnete Fallakte: + alte Interpretation bleibt in Revision 1, erscheint nicht in Revision 2. +- Zusammen mit `tests/test_worker.py`: 36 Tests bestanden. +- `node tests/dashboard-smoke.mjs`: PASS im echten unsichtbaren Edge-Browser, + Desktop und 390-Pixel-Mobilansicht, keine horizontalen Überläufe oder JS-Fehler. + Ergebnisnavigation, Coding-/Human-Aufträge und Datenentfernung bei Abmeldung. +- Browserdaten sind rein synthetisch; ihre 35.300 Tokens sind Anzeigeprüfwerte, + keine realen Modellmessungen. Keine kostenpflichtigen LLM-/DB-Anfragen. +- Vor Testbeginn: Test-Workspaces 16.939.129.428 Bytes, zusätzlich kleines + Fehlerkorpus und temporäre pytest-Daten deutlich unter 20 GB insgesamt. + Browser-/Server-Testverzeichnisse werden nach geprüften Pfaden entfernt. + +Grenzen: Sperrhinweise sind Information, keine administrative Freigabeoberfläche. +Das Belegverzeichnis ersetzt nicht die vollständigen Recherchepayloads im ZIP. +Große Darstellungen werden gekennzeichnet gekürzt; Paketinhalt bleibt unverändert. +Die Revisionshistorie benötigt keine aufbewahrten älteren ZIP-Generationen. diff --git a/docs/web-console.md b/docs/web-console.md index 18340fb..11aebb4 100644 --- a/docs/web-console.md +++ b/docs/web-console.md @@ -70,8 +70,35 @@ Die Oberfläche bietet ein einfaches Formular für neue wowdiag-/wowsrc-Aufträg für abgeschlossene ZIP-Akten mit Testerfeedback. Es verwendet dieselbe Multipart- API und denselben Idempotenzmechanismus wie externe Programme. Bei unveränderten Eingaben verwendet eine Wiederholung denselben Schlüssel. Anhänge und Fallakten -werden nicht clientseitig bearbeitet. Vollständige Falldetailansichten und eine -Bedienoberfläche für administrative Worker-Eingriffe bleiben weitere Ausbauschritte. +werden nicht clientseitig bearbeitet. Administrative Worker-Eingriffe bleiben ein +weiterer Ausbauschritt. + +## Fallakte und Ergebnisse + +Fallnamen verlinken auf eine lesbare Detailansicht mit Testerbeobachtungen, +Interpretation, Hypothesen, Confidence, stützenden und widersprechenden Befunden, +Diagnose sowie Coding- und Human-Dev-Auftrag. Befunde bleiben ausdrücklich +KI-Bewertungen mit Belegreferenzen. Das Belegverzeichnis zeigt Herkunft und +Abdeckungsinformationen; die vollständigen Recherchepayloads stehen im ZIP. +Ein expliziter Auftrag „Kein Auftrag“ wird von einem noch nicht erstellten Dokument +unterschieden. Die separate Ergebnisliste enthält abgeschlossene und zur +menschlichen Prüfung ausgeleitete Fälle, mit Projektfilter und Suche. + +Die Revisionsauswahl liest akzeptierte Artefakte aus der Checkpointhistorie der +aktuellen vollständigen Akte. Ergebnisse neuerer Revisionen erscheinen nicht in +älteren Ansichten. Über die Übergabe-API erfasste Patch-Rückmeldungen werden bis +zur gewählten Revision dargestellt. Bei gesperrten Fällen erscheinen Sperrgrund +und passende Betreiberhinweise; die Ansicht setzt keine Budgets zurück und hebt +keine Sperren auf. + +`GET /v1/cases/{key}?revision=N` liefert diese projektgebundene Ansicht. +`GET /v1/cases/{key}/package` lädt die vollständige aktuelle Akte einschließlich +aller erhaltenen Revisionen herunter, auch als Zwischenstand eines laufenden Falls. +Beide Endpunkte benötigen denselben Bearer-Token und verwenden `no-store`. +Der Schlüssel stammt aus der Übersicht, nicht aus einem lokalen Dateipfad. +Große Anzeigeinhalte werden begrenzt und als gekürzt gekennzeichnet. Der Download +bleibt vollständig; die Anzeige verändert keine Paketgeneration und löst keine +LLM-Aufrufe aus. ## Datenschutz und Betrieb @@ -97,7 +124,9 @@ node tests/dashboard-smoke.mjs Der Browser-Smoke-Test verwendet einen lokalen temporären API-Server, einen unsichtbaren Edge-Browser und ausdrücklich künstliche Fall-/Verbrauchsdaten. Er prüft Desktop, Mobilansicht, Projektfilter, Formularumschaltung, Abmeldung, -Tokenanzeige, Fortschritt und Browserfehler. Unter Windows wird der installierte +Tokenanzeige, Fortschritt, Ergebnisliste, Falldetails, Aufträge und Browserfehler. +Die API-Tests prüfen zusätzlich Projektzugriff, ZIP-Download und Revisionsisolation. +Unter Windows wird der installierte Edge erwartet; über `BROWSER_PATH` lässt sich ein anderes Chromium angeben. Die Testserver/-browser werden beendet, ihre erzeugten temporären Verzeichnisse nach Pfadprüfung entfernt. Screenshots als Prüfergebnis liegen unter diff --git a/src/viano_bug_analysis/api/app.py b/src/viano_bug_analysis/api/app.py index 2f36dcb..38d6504 100644 --- a/src/viano_bug_analysis/api/app.py +++ b/src/viano_bug_analysis/api/app.py @@ -18,12 +18,16 @@ from .config import APIError from .contracts import schema from .service import IntakeService from .dashboard import Dashboard +from .cases import Cases +from viano_bug_analysis.lifecycle.errors import LifecycleError +from viano_bug_analysis.package import PackageReadError from importlib.resources import files def create_app(config): service = IntakeService(config) dashboard = Dashboard(service) + cases = Cases(service) tokens = config.tokens() service.recover() active = 0 @@ -55,14 +59,39 @@ def create_app(config): response.headers['Cache-Control'] = 'no-store' return response + async def case_detail(request): + client = authenticate(request) + revision = request.query_params.get('revision') + if revision is not None: + if not revision.isascii() or not revision.isdigit() or len(revision) > 7: + raise APIError('API_REVISION_INVALID', 422) + revision = int(revision) + try: + detail = await run_in_threadpool(cases.detail, request.path_params['handle'], client, revision) + except (LifecycleError, PackageReadError, ValueError, TypeError, KeyError): + raise APIError('API_CASE_UNAVAILABLE', 409) from None + response = JSONResponse(detail) + response.headers['Cache-Control'] = 'no-store' + return response + + async def case_package(request): + client = authenticate(request) + try: + path = await run_in_threadpool(cases.head, request.path_params['handle'], client) + except (LifecycleError, PackageReadError, ValueError, TypeError, KeyError): + raise APIError('API_CASE_UNAVAILABLE', 409) from None + response = FileResponse(path, media_type='application/zip', filename='case.zip') + response.headers['Cache-Control'] = 'no-store' + return response + async def web(request): from starlette.responses import Response name = request.path_params.get('asset', 'index.html') - if name not in {'index.html', 'dashboard.js', 'dashboard.css'}: + if name not in {'index.html', 'dashboard.js', 'dashboard.css', 'case-detail.js'}: raise APIError('API_NOT_FOUND', 404) data = files('viano_bug_analysis.web').joinpath(name).read_bytes() response = Response(data, media_type={'index.html': 'text/html', 'dashboard.js': 'text/javascript', - 'dashboard.css': 'text/css'}[name]) + 'dashboard.css': 'text/css', 'case-detail.js':'text/javascript'}[name]) response.headers.update({'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'", 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'Cache-Control': 'no-store'}) return response @@ -136,6 +165,7 @@ def create_app(config): app = Starlette(routes=[Route('/', web), Route('/assets/{asset}', web), Route('/v1/overview', overview), Route('/healthz', health), Route('/v1/profiles', profiles), + Route('/v1/cases/{handle}', case_detail), Route('/v1/cases/{handle}/package', case_package), Route('/v1/schema', contract), Route('/v1/submissions', submit, methods=['POST']), Route('/v1/submissions/{submission}', status), Route('/v1/submissions/{submission}/package', package)], exception_handlers={APIError: error, HTTPException: http_error, OSError: storage_error}) diff --git a/src/viano_bug_analysis/api/cases.py b/src/viano_bug_analysis/api/cases.py new file mode 100644 index 0000000..3230304 --- /dev/null +++ b/src/viano_bug_analysis/api/cases.py @@ -0,0 +1,228 @@ +"""Read-only, project-scoped semantic views of complete portable case history.""" +import re +import json + +from viano_bug_analysis.lifecycle.checkpoints import checkpoint_chain +from viano_bug_analysis.lifecycle.storage import key +from viano_bug_analysis.package import PackageReader +from .config import APIError +from .dashboard import read_json, FINISHED + + +def case_key(case_id, package_id): + return key(case_id) + '.' + key(package_id) + + +def select(document, fields): + return {name: document[name] for name in fields if name in document} + + +def matches(doc, manifest, revision): + identity = doc.get('case', doc) + return (isinstance(identity, dict) and identity.get('analysis_revision') == revision + and identity.get('case_id', manifest['case_id']) == manifest['case_id'] + and identity.get('package_id', manifest['package_id']) == manifest['package_id']) + + +def references(reader, chain, revision): + mapping = { + 'case-built': {'case':'case_ref'}, + 'case-interpreted': {'case_interpretation':'interpretation_ref'}, + 'hypotheses-built': {'hypotheses':'hypotheses_ref'}, + 'investigation-state': {'investigation_state':'state_ref'}, + 'evidence-stored': {'evidence_index':'index_ref'}, + 'diagnosis-complete': {'diagnosis':'diagnosis_ref', 'failure_report':'failure_ref'}, + 'change-request-complete': {'change_request':'change_request_ref', 'decision':'decision_ref', 'human':'human_ref'}, + } + result = {} + for _, cp in reversed(chain): + if cp['analysis_revision'] != revision: + continue + details = cp['details'] + for pointer, name in mapping.get(details.get('operation'), {}).items(): + if details.get(name): + result[pointer] = details[name] + # Newest pointers are also useful while a stage is in progress. Revision + # checks prevent old retained artifacts from masquerading as new results. + for pointer in ('case', 'case_interpretation', 'hypotheses', 'investigation_state', 'diagnosis', 'change_request', 'failure_report', 'evidence_index'): + ref = reader.manifest['artifacts'].get(pointer) + if ref and matches(read_json(reader, ref), reader.manifest, revision): + result[pointer] = ref + return result + + +def bounded(value, limit, depth=0): + """Bound display excerpts, never the underlying case; report truncation.""" + if depth > 8 or limit[1] <= 0: + limit[0] = True + return [] if isinstance(value, list) else {} if isinstance(value, dict) else '…' + if isinstance(value, str): + data = value.encode('utf-8') + maximum = min(48000, limit[1]) + limit[1] -= min(len(data), maximum) + if len(value) > 12000 or len(data) > maximum: + limit[0] = True + return data[:maximum].decode('utf-8', errors='ignore')[:12000] + '…' + return value + if isinstance(value, list): + if len(value) > 100: + limit[0] = True + return [bounded(v, limit, depth+1) for v in value[:100]] + if isinstance(value, dict): + if len(value) > 100: + limit[0] = True + result = {} + for k,v in list(value.items())[:100]: + limit[1] -= len(k.encode('utf-8')) + 8 + result[k] = bounded(v, limit, depth+1) + return result + return value + + +def hypotheses_view(reader, docs): + state = docs.get('investigation_state', {}).get('hypotheses', {}) + proposals = {row['id']: row for row in docs.get('hypotheses', {}).get('hypotheses', [])} + result = [] + for hid in sorted(set(state) | set(proposals), key=lambda s: int(s[1:]) if s[1:].isdigit() else 0): + current, initial = state.get(hid, {}), proposals.get(hid, {}) + proposal = current.get('proposal', initial.get('proposal', {})) + brief_doc = read_json(reader, current.get('working_brief_ref')) + brief = brief_doc.get('brief', {}) + if brief_doc and (not matches(brief_doc, reader.manifest, docs['investigation_state']['analysis_revision']) + or brief_doc.get('hypothesis_id') != hid): + brief = {} + # Findings are model interpretations with evidence references, not new observations. + findings = brief.get('findings', []) + review = next((n['causal_review'] for n in reversed(current.get('notes', [])) if 'causal_review' in n), {}) + result.append({'id':hid, 'title':proposal.get('title', hid), + 'confidence':current.get('confidence', initial.get('confidence', 'unassessed')), + 'disposition':current.get('disposition', initial.get('status', 'open')), + 'proposal': select(proposal, ('mechanism','rationale','expected_evidence','falsifying_evidence','open_questions')), + 'support':[select(f, ('text','scope','evidence_refs')) for f in findings if f.get('direction') == 'support'], + 'against':[select(f, ('text','scope','evidence_refs')) for f in findings if f.get('direction') == 'against'], + 'review':select(review, ('effect','reason','support','against')), + 'uncertainties':brief.get('uncertainties', []), + 'questions':[select(q, ('id','text','resolved')) for q in current.get('questions', [])], + 'evidence_ids':current.get('evidence_refs', []), 'iterations':current.get('iterations', 0)}) + return result + + +def next_actions(state, reason, worker): + if state in FINISHED: + return [{'kind':'download','text':'Die Analyse ist abgeschlossen. Aufträge und Fallakte an die nächste Instanz übergeben.'}, + {'kind':'return','text':'Nach gescheitertem Patch oder neuen Beobachtungen die unveränderte Akte mit Rückmeldung übergeben.'}] + help_ = { + 'AWAITING_TEST_FEEDBACK': 'Die angeforderte Testerantwort muss über den vorgesehenen Feedbackablauf bestätigt werden. Keine neue Analyse-Revision eröffnen.', + 'RESEARCH_UNAVAILABLE': 'Datenbank-/Repository-Dienst prüfen. Nach Behebung ist eine gezielte Betreiberfreigabe über den Worker möglich.', + 'INVALID_LLM_RESPONSES': 'Die Antworten wurden fachlich oder technisch zurückgewiesen. Fehlerdiagnostik prüfen; keine Budgets zurücksetzen.', + 'LLM_CONFIGURATION': 'Providerkonfiguration und Zugangsdaten durch den Betreiber prüfen.', + 'LLM_UNAVAILABLE': 'Provider-Erreichbarkeit und ausgeschöpfte Wiederholungen prüfen. Eine neue Versuchskennung ist keine automatische Fehlerbehebung.', + } + if state == 'BLOCKED': + return [{'kind':'operator','text':help_.get(reason, 'Sperrgrund durch den Betreiber prüfen. Der Fall wird nicht unkontrolliert erneut gestartet.')}] + if worker.get('status') == 'ATTENTION': + return [{'kind':'operator','text':'Der Worker benötigt eine Betreiberprüfung. Nach Behebung nur die zulässige lokale Freigabe verwenden.'}] + return [{'kind':'wait','text':'Die Analyse läuft oder wartet auf den nächsten Worker-Schritt.'}] + + +class Cases: + def __init__(self, service): + self.service = service + + def head(self, handle, client): + if not re.fullmatch(r'[0-9a-f]{64}\.[0-9a-f]{64}', handle): + raise APIError('API_NOT_FOUND', 404) + case_hash, package_hash = handle.split('.') + folder = self.service.workspace.cases / case_hash / package_hash + if (not folder.is_dir() or folder.is_symlink() or folder.parent.is_symlink() + or getattr(folder.lstat(), 'st_file_attributes', 0) & 0x400 + or getattr(folder.parent.lstat(), 'st_file_attributes', 0) & 0x400): + raise APIError('API_NOT_FOUND', 404) + paths = sorted(folder.glob('g*.zip'), key=lambda p: int(p.stem[1:])) + if not paths or paths[-1].is_symlink() or getattr(paths[-1].lstat(), 'st_file_attributes', 0) & 0x400: + raise APIError('API_NOT_FOUND', 404) + with PackageReader(paths[-1]) as reader: + manifest = reader.manifest + if (manifest['project'].get('id') not in client.projects + or case_key(manifest['case_id'], manifest['package_id']) != handle): + raise APIError('API_NOT_FOUND', 404) + return self.service.store.latest(manifest['case_id'], manifest['package_id']) + + def detail(self, handle, client, requested_revision=None): + path = self.head(handle, client) + with PackageReader(path) as reader: + m = reader.manifest + chain = checkpoint_chain(reader) + revisions = {} + for _, cp in reversed(chain): + revisions[cp['analysis_revision']] = {'revision':cp['analysis_revision'], 'state':cp['state'], 'updated_at':cp['created_at']} + revisions.setdefault(m['analysis_revision'], {'revision':m['analysis_revision'],'state':m['state'],'updated_at':m['updated_at']}) + revision = m['analysis_revision'] if requested_revision is None else requested_revision + if revision not in revisions: + raise APIError('API_REVISION_NOT_FOUND', 404) + refs = references(reader, chain, revision) + docs = {p:read_json(reader, ref) for p,ref in refs.items() if p != 'human'} + docs = {p:d for p,d in docs.items() if p == 'decision' or matches(d, m, revision)} + case = docs.get('case', {}) + reports = [select(r, ('title','summary')) for r in case.get('bug', {}).get('reports', [])] + observations = case.get('human_observations', {}) + if not case and revision == m['analysis_revision']: + index = read_json(reader, m['artifacts'].get('case_inputs')) + source_reports = [read_json(reader, ref) for ref in index.get('reports', [])[:100]] + reports = [select(r, ('title','summary')) for r in source_reports] + observations = {name:[item for report in source_reports for item in report.get(name, [])] + for name in ('observations','tester_hypotheses')} + interpretation = docs.get('case_interpretation', {}).get('interpretation', {}) + coding = docs.get('change_request', {}) + human = docs.get('decision', {}).get('human_dev') + human_text = None + if not human and refs.get('human') in reader.files: + if reader._archive.getinfo(refs['human']).file_size <= 512*1024: + human_text = reader.read_bytes(refs['human']).decode('utf-8') + evidence = [] + for entry in docs.get('evidence_index', {}).get('entries', [])[:100]: + record = read_json(reader, entry.get('record_ref')) + if not matches(record, m, revision): + continue + content = read_json(reader, record.get('content_ref')) + evidence.append(select(record, ('id','hypothesis_id','operation','duplicate_of')) | + select(content, ('kind','coverage','scope'))) + feedback = [] + for _, cp in reversed(chain): + d = cp['details'] + if d.get('operation') != 'api-case-return' or cp['analysis_revision'] > revision: + continue + request = read_json(reader, f"intake/submissions/{d['submission_id']}/request.json") + value = request.get('metadata', {}).get('feedback') + if value: + feedback.append({'revision':cp['analysis_revision'],'received_at':cp['created_at'], + **select(value, ('outcome','summary','observations','patch_id','tested_build'))}) + selected = next((cp for _, cp in chain if cp['analysis_revision'] == revision), None) + selected_state = revisions[revision]['state'] + reason = selected['routing'].get('reason') if selected else m['routing'].get('reason') + worker = {} + ledger = self.service.workspace.runtime / 'worker/cases' / (key(m['case_id']+':'+m['package_id'])+'.json') + if ledger.is_file(): + try: + value = json.loads(ledger.read_bytes()) + except (OSError, ValueError): + value = {} + if value.get('analysis_revision') == revision: + worker = select(value, ('status','code','retry_not_before')) + result = {'key':handle, 'case_id':m['case_id'],'package_id':m['package_id'], + 'project_id':m['project'].get('id'),'revision':revision,'current_revision':m['analysis_revision'], + 'package_generation':m['package_generation'],'state':selected_state, 'reason':reason, + 'revisions':sorted(revisions.values(), key=lambda r:r['revision'], reverse=True), + 'reports':reports, 'observations':observations.get('observations', []), + 'tester_assumptions':observations.get('tester_hypotheses', []), + 'expected_behavior':case.get('expected_behavior', {}), + 'interpretation':interpretation,'hypotheses':hypotheses_view(reader,docs), + 'evidence':evidence, 'diagnosis':docs.get('diagnosis', {}).get('diagnosis'), + 'review':select(docs.get('failure_report', {}), ('reason','remaining_questions','recommended_human_actions')), + 'coding':select(coding, ('assignment','human_dev_assigned','request','diagnostic_caveats')), + 'human':human, 'human_text':human_text, 'feedback':feedback, 'worker':worker, + 'actions':next_actions(selected_state,reason,worker)} + limited=[len(docs.get('evidence_index', {}).get('entries', [])) > 100, 2*1024**2] + result=bounded(result,limited) + result['truncated']=limited[0] + return result diff --git a/src/viano_bug_analysis/api/dashboard.py b/src/viano_bug_analysis/api/dashboard.py index a37f378..82c6db2 100644 --- a/src/viano_bug_analysis/api/dashboard.py +++ b/src/viano_bug_analysis/api/dashboard.py @@ -100,6 +100,7 @@ def case_view(reader): calls.append({'bucket': bucket if bucket in LABELS else 'unknown', 'phase': phase(call, prompt), 'category': category, 'day': cp['created_at'][:10], **usage}) return {k: manifest[k] for k in ('case_id', 'package_id', 'analysis_revision', 'package_generation', 'state')} | { + 'key':key(manifest['case_id']) + '.' + key(manifest['package_id']), 'project_id': manifest['project'].get('id'), 'title': title, 'progress': round(sum(complete) / len(complete) * 100), 'calls': calls, 'updated_at': manifest['updated_at']} diff --git a/src/viano_bug_analysis/web/case-detail.js b/src/viano_bug_analysis/web/case-detail.js new file mode 100644 index 0000000..fb3b998 --- /dev/null +++ b/src/viano_bug_analysis/web/case-detail.js @@ -0,0 +1,114 @@ +'use strict'; +let selectedCase=null, detailLoading=false, detailHash=null; +const words={observed_behavior:'Beobachtetes Verhalten',expected_behavior:'Erwartetes Verhalten',tester_assumptions:'Testerannahmen',confirmed_technical_facts:'Bestätigte technische Fakten',behavior_gap:'Abweichung',first_known_divergence:'Erste bekannte Abweichung',failure_stage:'Fehlerstelle',contradictions:'Widersprüche',unknowns:'Offene Fragen',relevant_entities:'Relevante Entitäten',investigation_direction:'Untersuchungsrichtung',interpretation_summary:'Einordnung',described_behavior:'Beschriebenes Verhalten',provisional_behavior_gap:'Vorläufige Abweichung',mechanism:'Vermuteter Mechanismus',rationale:'Begründung',expected_evidence:'Erwartete Belege',falsifying_evidence:'Mögliche Gegenbelege',open_questions:'Offene Fragen',text:'Aussage',scope:'Geltungsbereich',reason:'Begründung',status:'Status',confidence:'Confidence',selected_hypothesis:'Gewählte Hypothese',root_cause:'Fehlerursache',evaluations:'Bewertungen der Hypothesen',remaining_uncertainty:'Verbleibende Unsicherheit',recommended_human_actions:'Empfohlene menschliche Maßnahmen',decision_basis:'Entscheidungsgrundlage',causal_claim:'Kausale Aussage',supporting_evidence:'Stützende Belege',counter_evidence:'Gegenbelege',scope_limits:'Geltungsgrenzen',title:'Aufgabe',instructions:'Anweisungen',expected_results:'Erwartetes Ergebnis',problem_summary:'Problem',required_change:'Erforderliche Änderung',implementation_intent:'Umsetzungsziel',affected_scope:'Betroffener Bereich',implementation_guidance:'Hinweise zur Umsetzung',validation_requirements:'Validierung',constraints:'Vorgaben',preserve:'Beibehalten',forbidden_changes:'Unzulässige Änderungen',do_not_fix_by:'Zu vermeidende Lösungswege',missing_information:'Fehlende Informationen',placeholders:'Platzhalter',diagnostic_caveats:'Diagnostische Einschränkungen',coverage:'Abdeckung',kind:'Art',operation:'Werkzeug',duplicate_of:'Bereits erfasster Beleg',hypothesis_id:'Hypothese',resolved:'Beantwortet',author:'Beobachter',patch_id:'Patch',tested_build:'Getesteter Build',summary:'Zusammenfassung',observations:'Beobachtungen',outcome:'Rückmeldung',entity_id:'Entität',required:'Erforderlich',description:'Beschreibung',value:'Wert',column:'Spalte',table:'Tabelle',source_ref:'Quelle',evidence_refs:'Belegreferenzen',source_ids:'Quellen',interpretation_ids:'Interpretationsreferenzen'}; +const values={unassessed:'Unbewertet',weak:'Schwach',plausible:'Plausibel',supported:'Gestützt',strongly_supported:'Stark gestützt',weakened:'Geschwächt',confirmed:'Bestätigt',inconclusive:'Nicht abschließend geklärt',assigned:'Auftrag vorhanden',none:'Kein Auftrag',ready:'Bereit',ready_with_placeholders:'Vorlage mit Platzhaltern',refuted:'Verworfen',finished:'Abgeschlossen',open:'Offen',investigation_finished:'Untersuchung abgeschlossen',patch_failed:'Patch behebt den Fehler nicht',validation_failed:'Validierung fehlgeschlagen',new_evidence:'Neue Belege',review_feedback:'Review-Rückmeldung'}; +function friendly(value){return typeof value==='string'?(values[value]||value):value===true?'Ja':value===false?'Nein':String(value);} +Object.assign(words,{completeness:'Vollständigkeit',statements:'Aussagen',observed_at:'Beobachtet am',received_at:'Eingegangen am',revision:'Revision',effect:'Wirkung',support:'Dafür',against:'Dagegen'}); +Object.assign(values,{missing:'Nicht erfasst',complete:'Vollständig',partial:'Teilweise erfasst'}); +function semantic(value,depth=0){ + if(value===null||value===undefined)return text('p','Nicht angegeben.','hint'); + if(Array.isArray(value)){ + if(!value.length)return text('p','Keine Angaben.','hint'); + const list=text('div','','semantic-list');value.forEach(v=>{const item=text('div','','semantic-item');item.append(semantic(v,depth+1));list.append(item);});return list; + } + if(typeof value!=='object')return text('p',friendly(value),'semantic-text'); + if(typeof value.text==='string'){ + const block=text('div','','statement');block.append(text('p',value.text,'semantic-text')); + for(const field of ['scope','author','observed_at'])if(value[field])block.append(text('small',`${words[field]||field}: ${value[field]}`)); + for(const field of ['evidence_refs','source_ids'])if(value[field]?.length)block.append(text('div',value[field].join(' · '),'reference-list')); + return block; + } + const dl=text('dl','','semantic-fields');Object.entries(value).forEach(([key,child])=>{ + if(key==='provenance'||key==='origin')return; + dl.append(text('dt',words[key]||key.replaceAll('_',' ')));const dd=document.createElement('dd');dd.append(semantic(child,depth+1));dl.append(dd); + });return dl; +} +function panel(title,content,expanded=true){ + const card=text('details','','panel detail-panel');card.open=expanded;card.append(text('summary',title)); + const body=text('div','','detail-body');body.append(content);card.append(body);return card; +} +function renderArchive(){ + if(!overview)return; + const project=$('result-project').value,search=$('result-search').value.trim().toLocaleLowerCase('de-DE'); + const list=overview.cases.filter(c=>finished.has(c.state)&&(!project||project===c.project_id)&&(!search||`${c.case_id} ${c.title}`.toLocaleLowerCase('de-DE').includes(search))).sort((a,b)=>b.updated_at.localeCompare(a.updated_at)); + const body=$('result-rows');body.replaceChildren(); + list.forEach(c=>{const row=document.createElement('tr'),name=document.createElement('td'),link=text('a',c.case_id,'case-link');link.href=`#case/${c.key}`; + name.append(link,text('small',c.title));row.append(name,text('td',c.project_id),text('td',({OUTPUT_READY:'Ergebnis bereit',HANDED_OFF:'Übergeben',FAILED_TO_ANALYZE:'Menschliches Review'})[c.state]),text('td',String(c.analysis_revision)),text('td',new Date(c.updated_at).toLocaleString('de-DE')));body.append(row);}); + if(!list.length){const row=document.createElement('tr'),cell=text('td','Keine abgeschlossenen Fälle für diese Auswahl.','empty');cell.colSpan=5;row.append(cell);body.append(row);} +} +function clearDetails(){selectedCase=null;detailHash=null;detailLoading=false;$('result-search').value='';} +async function downloadCase(data,button){ + const generation=session,credential=token;button.disabled=true;error(); + try{const response=await fetch(`/v1/cases/${data.key}/package`,{headers:{Authorization:`Bearer ${credential}`},cache:'no-store'}); + if(!response.ok){const issue=await response.json();if(response.status===401&&credential===token)logout();throw new Error(issue.code||`HTTP ${response.status}`);} + const blob=await response.blob();if(generation!==session)return; + const url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download=`${data.case_id.replace(/[^A-Za-z0-9_.-]/g,'_')}-Fallakte.zip`;document.body.append(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(url),30000); + }catch(e){if(generation===session)error(`Download fehlgeschlagen: ${e.message}`);}finally{button.disabled=false;} +} +function renderDetail(data){ + const root=$('case-detail');root.replaceChildren(); + const head=text('section','','panel detail-header'),top=text('div','','section-head'),info=document.createElement('div'); + info.append(text('div',`${data.project_id} · ${data.case_id}`,'eyebrow')); + const title=data.reports.find(r=>r.title)?.title||data.case_id;info.append(text('h2',title)); + info.append(text('p',`${names[data.state]||({OUTPUT_READY:'Ergebnis bereit',HANDED_OFF:'Übergeben',FAILED_TO_ANALYZE:'Menschliches Review'})[data.state]||data.state} · Revision ${data.revision}`)); + const controls=text('div','','detail-controls'),label=text('label','Analyse-Revision'),select=document.createElement('select');select.id='revision-select';select.setAttribute('aria-label','Analyse-Revision'); + data.revisions.forEach(r=>select.add(new Option(`Revision ${r.revision}${r.revision===data.current_revision?' · aktuell':''}`,r.revision))); + select.value=String(data.revision);select.addEventListener('change',()=>location.hash=`#case/${data.key}/revision/${select.value}`);label.append(select);controls.append(label); + const download=text('button','Fallakte herunterladen');download.type='button';download.addEventListener('click',()=>downloadCase(data,download));controls.append(download); + top.append(info,controls);head.append(top,text('p','Der Download enthält die vollständige aktuelle Akte einschließlich aller erhaltenen Revisionen.','hint')); + if(data.revision!==data.current_revision)head.append(text('p','Historische Ansicht: Ergebnisse anderer Revisionen werden nicht beigemischt.','history-note')); + if(data.truncated)head.append(text('p','Die Anzeige ist gekürzt. Die vollständigen Inhalte bleiben in der herunterladbaren Fallakte erhalten.','history-note')); + root.append(head); + const actions=text('div','','next-actions'); + if(data.reason)actions.append(text('p',`Sperr-/Reviewgrund: ${data.reason}`)); + if(data.worker.code)actions.append(text('p',`Workerhinweis: ${data.worker.code}`)); + data.actions.forEach(a=>actions.append(text('p',a.text)));root.append(panel('Zustand und nächste Schritte',actions)); + root.append(panel('Testerbeobachtungen',semantic({observations:data.observations,tester_assumptions:data.tester_assumptions,expected_behavior:data.expected_behavior}))); + root.append(panel('Caseinterpretation',Object.keys(data.interpretation).length?semantic(data.interpretation):text('p','Für diese Revision liegt noch keine akzeptierte Interpretation vor.','hint'),false)); + const hypotheses=text('div','','hypothesis-list'); + data.hypotheses.forEach(h=>{const card=text('article','','hypothesis-card'),line=text('div','','hypothesis-heading');line.append(text('h3',`${h.id} · ${h.title}`),text('span',`Confidence: ${friendly(h.confidence)}`,'badge')); + card.append(line,text('p',`Status: ${friendly(h.disposition)} · Untersuchungsschritte: ${h.iterations}`,'hint'),semantic(h.proposal)); + const evidence=text('div','','evidence-columns');for(const [key,title] of [['support','Befunde dafür'],['against','Befunde dagegen']]){const column=document.createElement('div');column.append(text('h4',title),semantic(h[key]));evidence.append(column);}card.append(evidence,text('p','Diese Befunde sind KI-Bewertungen der referenzierten Belege, keine zusätzlichen Messungen.','hint')); + if(Object.keys(h.review).length)card.append(panel('Letzte kausale Bewertung',semantic(h.review),false)); + if(h.uncertainties.length)card.append(panel('Offene Unsicherheiten',semantic(h.uncertainties),false)); + if(h.evidence_ids.length)card.append(text('p',`Belege: ${h.evidence_ids.join(' · ')}`,'reference-list')); + hypotheses.append(card);}); + if(!data.hypotheses.length)hypotheses.append(text('p','Noch keine akzeptierten Hypothesen in dieser Revision.','hint')); + root.append(panel('Hypothesen und Bewertung',hypotheses)); + root.append(panel('Belegverzeichnis',data.evidence.length?semantic(data.evidence):text('p','Noch keine erfassten Belege.','hint'),false)); + root.append(panel('Diagnose',data.diagnosis?semantic(data.diagnosis):text('p','Noch keine finale Diagnose in dieser Revision.','hint'))); + if(Object.keys(data.review).length)root.append(panel('Menschliches Review',semantic(data.review))); + const coding=document.createElement('div'); + if(!Object.keys(data.coding).length)coding.append(text('p','Für diese Revision wurde noch kein Codingauftragsdokument erstellt.','hint')); + else{coding.append(text('p',data.coding.assignment==='none'?'Kein Codingauftrag vorhanden.':'Codingauftrag vorhanden.','assignment-status'));coding.append(semantic(data.coding.request||{}));if(data.coding.diagnostic_caveats)coding.append(panel('Diagnostische Einschränkungen',semantic(data.coding.diagnostic_caveats),false));} + root.append(panel('Codingauftrag',coding)); + const human=document.createElement('div'); + if(data.human&&['none','assigned'].includes(data.human.status)){human.append(text('p',data.human.status==='none'?'Kein Human-Dev-Auftrag vorhanden.':'Human-Dev-Auftrag vorhanden.','assignment-status'),semantic(data.human));} + else if(data.human_text)human.append(text('pre',data.human_text,'human-document')); + else human.append(text('p','Für diese Revision liegt noch kein Human-Dev-Auftragsdokument vor.','hint')); + root.append(panel('Human-Dev-Auftrag',human)); + root.append(panel('Patch-Rückmeldungen und neue Beobachtungen',data.feedback.length?semantic(data.feedback):text('p','Bis zu dieser Revision liegen keine über die Übergabe-API eingereichten Rückmeldungen vor.','hint'),false)); +} +async function loadDetail(hash){ + const match=hash.match(/^#case\/([a-f0-9]{64}\.[a-f0-9]{64})(?:\/revision\/([0-9]+))?$/);if(!match||!token)return; + const generation=session;detailLoading=true;detailHash=hash; + if(!selectedCase||selectedCase.key!==match[1]||String(selectedCase.revision)!==match[2])$('case-detail').replaceChildren(text('p','Fallakte wird geladen …','panel')); + try{const data=await api(`/v1/cases/${match[1]}${match[2]!==undefined?'?revision='+match[2]:''}`); + if(generation!==session||location.hash!==hash)return;selectedCase=data;renderDetail(data); + }catch(e){if(generation===session&&location.hash===hash){$('case-detail').replaceChildren(text('p',`Fallakte nicht verfügbar: ${e.message}`,'panel'));}} + finally{if(generation===session&&detailHash===hash)detailLoading=false;} +} +function syncDetail(){ + if(!token||!location.hash.startsWith('#case/'))return; + const current=overview?.cases.find(c=>c.key===selectedCase?.key); + if(!detailLoading&&(!selectedCase||detailHash!==location.hash||(!location.hash.includes('/revision/')&¤t?.package_generation!==selectedCase.package_generation)))loadDetail(location.hash); +} +function route(){ + const detail=location.hash.startsWith('#case/'),results=location.hash==='#results',submissionPage=location.hash==='#submit'; + $('overview-page').hidden=detail||results||submissionPage;$('results').hidden=!results;$('case-detail').hidden=!detail;$('submit').hidden=detail||results; + document.querySelector('header h1').textContent=detail?'Die Fallakte.':results?'Abgeschlossene Fälle.':submissionPage?'Auftrag übergeben.':'Analyse im Blick.'; + document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('selected',detail?a.hash==='#cases':a.hash===(results?'#results':submissionPage?'#submit':location.hash==='#cases'?'#cases':'#overview'))); + if(detail)loadDetail(location.hash);if(results)renderArchive(); +} +$('result-project').addEventListener('change',renderArchive);$('result-search').addEventListener('input',renderArchive); +window.addEventListener('hashchange',route);route(); diff --git a/src/viano_bug_analysis/web/dashboard.css b/src/viano_bug_analysis/web/dashboard.css index 7b404f2..a1280df 100644 --- a/src/viano_bug_analysis/web/dashboard.css +++ b/src/viano_bug_analysis/web/dashboard.css @@ -1,3 +1,5 @@ :root{--bg:#f4f5f8;--ink:#202536;--muted:#727a8c;--line:#e7e9f0;--accent:#5364de;--orange:#e9954c}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 system-ui,-apple-system,Segoe UI,sans-serif}.rail{position:fixed;inset:0 auto 0 0;width:210px;background:#202637;color:#a9b1c6;padding:36px 24px;display:flex;flex-direction:column}.brand{color:white;font-size:30px;font-weight:800;letter-spacing:-2px;text-decoration:none}.brand span{color:#a6afff}.brand small{display:block;font-size:12px;letter-spacing:2px;color:#9aa4bf;font-weight:500}nav{display:grid;gap:10px;margin-top:58px}nav a{color:#aeb7cc;text-decoration:none;padding:12px;border-radius:8px}nav a:hover,nav .selected{background:#323b56;color:white}.rail-bottom{margin-top:auto;font-size:10px;letter-spacing:1px;color:#7f8aa5}.rail-bottom span{font-size:12px;color:#c2cae0;letter-spacing:0}main{margin-left:210px;padding:42px 42px 30px;max-width:1660px}header{display:flex;align-items:center;justify-content:space-between;margin-bottom:30px}h1{font-size:36px;letter-spacing:-1.4px;line-height:1.2;margin:8px 0}h2{font-size:19px;letter-spacing:-.4px;margin:4px 0}p{color:var(--muted);margin:6px 0 12px}.eyebrow{font-size:10px;letter-spacing:1.8px;font-weight:750;color:#8b93a7}.live{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:8px}.dot{width:7px;height:7px;background:#8e98ad;border-radius:50%}.connected .dot{background:#3db28a}.panel{background:#fff;border:1px solid var(--line);border-radius:14px;padding:25px;margin-bottom:24px;box-shadow:0 3px 14px #17243c03}.section-head{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:22px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:24px}.stats article{background:#fff;border:1px solid var(--line);padding:22px;border-radius:12px}.stats span{color:var(--muted);font-size:12px}.stats strong{display:block;font-size:30px;font-weight:650;letter-spacing:-1px;margin:8px 0}.stats small{color:#969daf;font-size:10px}.badge{background:#eef0fd;color:#6572cb;border-radius:20px;padding:5px 12px;font-size:11px}.worker{border-top:1px solid var(--line);padding:18px 0}.worker:first-child{border-top:0;padding-top:0}.worker-top{display:flex;align-items:center;gap:9px;font-weight:600}.worker-top i{width:8px;height:8px;background:#42b98e;border-radius:50%}.worker-case{display:flex;justify-content:space-between;gap:15px;margin:12px 0 8px;color:#646d82}.progress{height:8px;background:#edf0f7;border-radius:8px;overflow:hidden}.progress-fill{height:100%;background:var(--accent);border-radius:8px;transition:width .5s}.hint{font-size:11px;color:#929aab;margin:14px 0 0}.charts{display:grid;grid-template-columns:1fr 1fr;gap:24px}.charts .panel{min-width:0}.pie-layout{display:grid;grid-template-columns:minmax(170px,1fr) 1fr;align-items:center;gap:20px}canvas{display:block;width:100%;max-width:250px;height:auto;margin:auto}.legend{padding:0;margin:0;list-style:none}.legend li{display:grid;grid-template-columns:9px 1fr auto;gap:8px;font-size:11px;align-items:center;margin:10px 0}.swatch{height:8px;width:8px;border-radius:2px}.legend b{font-size:11px;font-weight:600}.keys{display:flex;gap:20px;color:var(--muted);font-size:11px;margin-bottom:18px}.keys span:before{content:'';display:inline-block;width:8px;height:8px;background:#8e9aed;border-radius:2px;margin-right:6px}.keys .output-key:before{background:#303f96}.bar-row{margin:14px 0}.bar-label{display:flex;justify-content:space-between;font-size:11px;margin-bottom:5px;color:#68728a}.bar-track{height:12px;background:#f1f3f8;border-radius:3px;display:flex;overflow:hidden}.bar-input{background:#8e9aed}.bar-output{background:#303f96}.bar-row.correction .bar-input{background:#f2c397}.bar-row.correction .bar-output{background:var(--orange)}table{width:100%;border-collapse:collapse;font-size:12px;text-align:left}th{color:#929aaa;font-size:10px;letter-spacing:.5px;font-weight:600;padding:12px}td{padding:15px 12px;border-top:1px solid var(--line)}td small{display:block;color:var(--muted)}td .progress{width:110px;height:5px}label{display:block;color:#788297;font-size:11px;margin-bottom:14px}input,select,textarea{display:block;width:100%;font:inherit;color:var(--ink);border:1px solid #dfe3ed;background:white;border-radius:7px;padding:10px 12px;margin-top:5px}input:focus,textarea:focus,select:focus{outline:2px solid #aab3fa;outline-offset:1px}input[type=file]{font-size:11px;padding:8px}.filter{display:flex;align-items:center;gap:9px;margin:0}.filter select{width:auto;max-width:240px;margin:0}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 22px}button{font:inherit;background:var(--accent);color:white;border:0;border-radius:7px;padding:11px 18px;cursor:pointer}button:hover{background:#4654be}button:disabled{opacity:.5;cursor:wait}#logout{font-size:11px;padding:6px 10px;margin-top:12px;background:#353f58}.inline{display:flex;gap:10px}.inline input{margin:0}.login{display:flex;gap:40px;align-items:center}.login form{flex:1;min-width:240px}.error{background:#fff0eb;color:#b04c34;padding:12px 16px;border-radius:8px;font-size:12px}.empty{padding:20px 0;color:#9099ad;font-size:13px}.table-wrap{overflow:auto}footer{font-size:10px;color:#9299aa;padding:0 0 20px}[hidden]{display:none!important}@media(min-width:1550px){main{margin-right:auto}}@media(max-width:1150px){main{padding:28px 24px}.rail{width:170px;padding:28px 18px}main{margin-left:170px}.charts{grid-template-columns:1fr}.stats{grid-template-columns:1fr 1fr}}@media(max-width:700px){.rail{position:static;width:100%;padding:16px 20px;flex-direction:row;align-items:center;gap:20px}.brand{font-size:23px}.brand small,.rail-bottom{display:none}nav{display:flex;margin:0;gap:0;font-size:11px}nav a{padding:7px}main{margin:0;padding:24px 16px}header{align-items:flex-start;gap:12px}h1{font-size:29px}.live{font-size:10px;max-width:100px}.stats{gap:10px}.stats article{padding:15px}.stats strong{font-size:25px}.panel{padding:19px}.section-head{flex-wrap:wrap}.form-grid{grid-template-columns:1fr}.pie-layout{gap:12px;grid-template-columns:1fr 1fr}.login{display:block}.legend li{font-size:10px}.inline{flex-wrap:wrap}} .worker-case span:last-child{white-space:nowrap;flex-shrink:0} + +.case-link{color:#4f61c7;font-weight:650;text-decoration:none}.case-link:hover{text-decoration:underline}.detail-panel>summary{font-size:18px;font-weight:650;cursor:pointer}.detail-body{margin-top:20px}.semantic-text{color:#45516a;white-space:pre-wrap;overflow-wrap:anywhere;margin:5px 0 12px}.semantic-fields{display:grid;grid-template-columns:minmax(130px,190px) minmax(0,1fr);gap:12px 22px;margin:0}.semantic-fields dt{font-size:12px;color:#7c8599;font-weight:600}.semantic-fields dd{margin:0;min-width:0}.semantic-fields .semantic-fields{grid-template-columns:130px minmax(0,1fr)}.semantic-list{display:grid;gap:10px}.semantic-item{border-left:2px solid #e6eaf6;padding-left:13px}.statement small{display:block;color:#9299a8}.reference-list{color:#7785ae;font-size:11px;overflow-wrap:anywhere}.hypothesis-card{border:1px solid #e6e9f1;border-radius:9px;padding:20px;margin-bottom:16px}.hypothesis-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}.hypothesis-heading h3{font-size:16px;margin:0}.hypothesis-heading .badge{flex-shrink:0}.hypothesis-card .detail-panel{margin:16px 0 0;padding:14px;box-shadow:none}.hypothesis-card .detail-panel>summary{font-size:13px}.evidence-columns{display:grid;grid-template-columns:1fr 1fr;gap:26px;margin-top:18px}.evidence-columns h4{font-size:12px;color:#68758c}.evidence-columns>div{background:#f7f8fc;padding:16px;border-radius:7px;min-width:0}.detail-controls{display:flex;align-items:center;gap:16px}.detail-controls label{margin:0;min-width:170px}.detail-controls button{margin-top:20px}.history-note{background:#f1f3fd;color:#6472ae;padding:12px;border-radius:6px;font-size:12px}.assignment-status{font-weight:650;color:#34436a}.human-document{white-space:pre-wrap;overflow-wrap:anywhere;font:13px/1.7 system-ui}.next-actions p{color:#5f6d85}#case-detail .panel{scroll-margin-top:20px}@media(max-width:900px){.semantic-fields,.semantic-fields .semantic-fields{grid-template-columns:1fr;gap:4px}.semantic-fields dd{margin-bottom:12px}.detail-controls{flex-wrap:wrap}}@media(max-width:700px){.hypothesis-heading{align-items:flex-start;flex-direction:column}.evidence-columns{grid-template-columns:1fr}.detail-controls{width:100%}.detail-controls button{margin-top:0}.hypothesis-card{padding:14px}.rail{flex-wrap:wrap}.rail nav{flex-wrap:wrap}} diff --git a/src/viano_bug_analysis/web/dashboard.js b/src/viano_bug_analysis/web/dashboard.js index ce679f1..3d5a736 100644 --- a/src/viano_bug_analysis/web/dashboard.js +++ b/src/viano_bug_analysis/web/dashboard.js @@ -24,7 +24,8 @@ function logout() { $('logout').hidden=true; $('connection').textContent='Nicht verbunden'; document.body.classList.remove('connected'); $('updated').textContent='Warte auf Verbindung'; overview=null;projects=[];submission=null; $('submit-form').reset();$('receipt').textContent='';$('new-files').hidden=false;$('return-files').hidden=true; - ['workers','case-rows','bars','legend','project','project-filter'].forEach(id=>$(id).replaceChildren()); + ['workers','case-rows','bars','legend','project','project-filter','result-project','result-rows','case-detail'].forEach(id=>$(id).replaceChildren()); + if(typeof clearDetails==='function')clearDetails(); ['active-count','token-count','correction-count','finished-count'].forEach(id=>$(id).textContent='—'); $('pie').getContext('2d').clearRect(0,0,360,360); } @@ -94,9 +95,12 @@ function render() { $('coverage').textContent=`${buckets.reduce((n,b)=>n+b.calls,0)} erfasste Aufrufe. ${missing?`${missing} Aufrufe mit fehlenden Tokenmesswerten; die Diagramme zeigen nur bekannte Werte.`:'Alle erfassten Aufrufe mit vollständigen Eingabe-/Ausgabewerten.'}${unassigned?.calls?` ${unassigned.calls} ältere Aufrufe ohne sichere Korrekturzuordnung.`:''}`; const root=$('case-rows');root.replaceChildren(); active.forEach(c=>{const row=document.createElement('tr'), title=document.createElement('td'); - title.append(text('strong',c.case_id),text('small',c.title));row.append(title,text('td',c.project_id),text('td',names[c.state]||c.state),text('td',String(c.analysis_revision))); + const link=text('a',c.case_id,'case-link');link.href=`#case/${c.key}`; + title.append(link,text('small',c.title));row.append(title,text('td',c.project_id),text('td',names[c.state]||c.state),text('td',String(c.analysis_revision))); const cell=document.createElement('td');cell.append(progress(c.progress,`Fortschritt ${c.case_id}`));row.append(cell);root.append(row);}); if(!active.length){const row=document.createElement('tr'),cell=text('td','Keine offenen Fälle.','empty');cell.colSpan=5;row.append(cell);root.append(row);} + if(typeof renderArchive==='function')renderArchive(); + if(typeof syncDetail==='function')syncDetail(); } async function refresh() { clearTimeout(timer); if(!token)return;const generation=session; @@ -108,10 +112,10 @@ async function refresh() { $('login-form').addEventListener('submit',async e=>{ e.preventDefault();const generation=++session;token=$('token').value.trim();error(); try {const response=await api('/v1/profiles');if(generation!==session)return;projects=response.projects; - $('project').replaceChildren();$('project-filter').replaceChildren(new Option('Alle freigegebenen Projekte','')); - projects.forEach(p=>{$('project').add(new Option(p.project_id,p.project_id));$('project-filter').add(new Option(p.project_id,p.project_id));}); + $('project').replaceChildren();$('project-filter').replaceChildren(new Option('Alle freigegebenen Projekte',''));$('result-project').replaceChildren(new Option('Alle freigegebenen Projekte','')); + projects.forEach(p=>{$('project').add(new Option(p.project_id,p.project_id));$('project-filter').add(new Option(p.project_id,p.project_id));$('result-project').add(new Option(p.project_id,p.project_id));}); $('login').hidden=true;$('content').hidden=false;$('logout').hidden=false;$('token').value=''; - $('connection').textContent='Verbunden';document.body.classList.add('connected');await refresh(); + $('connection').textContent='Verbunden';document.body.classList.add('connected');if(typeof route==='function')route();await refresh(); } catch(e) {if(generation===session){token='';error(`Verbindung fehlgeschlagen: ${e.message}`);}else if(!token)error('Verbindung fehlgeschlagen: API-Zugangsschlüssel prüfen.');} }); $('logout').addEventListener('click',logout);$('project-filter').addEventListener('change',render); diff --git a/src/viano_bug_analysis/web/index.html b/src/viano_bug_analysis/web/index.html index e9332e4..4a2c1d5 100644 --- a/src/viano_bug_analysis/web/index.html +++ b/src/viano_bug_analysis/web/index.html @@ -2,10 +2,11 @@ BugAnalysis · Übersicht - + +
BETRIEBSÜBERSICHT

Analyse im Blick.

Aktuelle Arbeit und messbarer Verbrauch.

Warte auf Verbindung
@@ -13,6 +14,7 @@