diff --git a/config/api.example.toml b/config/api.example.toml index 4aad101..67bbc91 100644 --- a/config/api.example.toml +++ b/config/api.example.toml @@ -14,6 +14,7 @@ planning_mode = "joint" # project_config = "project.local.toml" # llm_config = "llm.early-review.local.toml" # investigation_config = "investigation.bounded.toml" +# budget_config = "budget.human-tests.toml" [[clients]] id = "wow-capture-worker" @@ -26,3 +27,4 @@ projects = ["viano-core"] # token_env = "VBA_OPERATOR_TOKEN" # projects = ["viano-core"] # operator = true +# tester = true diff --git a/config/budget.human-tests.toml b/config/budget.human-tests.toml new file mode 100644 index 0000000..9682890 --- /dev/null +++ b/config/budget.human-tests.toml @@ -0,0 +1,5 @@ +# Human requests are available when an authenticated web feedback adapter exists. +# Frozen per new analysis revision; existing revisions retain their policy. +[budget] +[budget.action_costs] +"testing.request_human" = 40 diff --git a/docs/async-tests-v1.md b/docs/async-tests-v1.md index e0e86f3..59d482a 100644 --- a/docs/async-tests-v1.md +++ b/docs/async-tests-v1.md @@ -1,8 +1,9 @@ # Asynchronous test handoff v1 -This addition prepares the existing project for externally performed human and -automated tests. No web UI, HTTP feedback endpoint, notifications, bot steering, -client automation, Tool Router or Investigation Loop is implemented here. +This contract supports externally performed human and automated tests. The human +HTTP adapter and dedicated web page are now documented in +[Web console](web-console.md#menschliche-testanfragen). Automated execution, +notifications, bot steering and client automation are not provided by that page. `docs/architecture.md` remains unchanged and authoritative: VianoBugAnalysis does not steer labs. It requests observations through a future external adapter. @@ -10,8 +11,8 @@ does not steer labs. It requests observations through a future external adapter. `AsyncTestCoordinator` is imported from `viano_bug_analysis.lifecycle.async_tests`. It is a trusted controller service, not a callable operation exposed directly to -an LLM. The future router must validate requests before calling it. The future -web adapter must authenticate/authorize users and confirm feedback before calling +an LLM. The investigation controller validates requests before calling it. The +web adapter authenticates/authorizes users and confirms feedback before calling `confirm_feedback`; a payload claiming `confirmed = true` is not authentication. User-supplied observations are untrusted data, not executable instructions. diff --git a/docs/test-results/human-test-requests-20261022.md b/docs/test-results/human-test-requests-20261022.md new file mode 100644 index 0000000..62d0cb9 --- /dev/null +++ b/docs/test-results/human-test-requests-20261022.md @@ -0,0 +1,36 @@ +# Menschliche Rückfragen im Web – 2026-10-22 + +Ziel: Die vorhandene menschliche Testanforderung bedienbar und für neue +Dienstanalysen freigegeben machen. Eigene Seite mit offenen Anfragen, Projekt, +Bug, Zeit, Projektfilter und Sortierungen; Anzeige des Testauftrags und bestätigtes +Textfeedback. Separate Testerrechte bzw. Betreiberrechte erforderlich. + +Neue Budgetdatei `config/budget.human-tests.toml`: menschliche Tests 40 fachliche +Einheiten, keine Freigabe automatisierter Tests. Die ignorierte lokale +API-Konfiguration verweist darauf. Vorhandene eingefrorene Revisionen unverändert. +Antworten benutzen den bestehenden AsyncTestCoordinator und dessen Budget-/Identitäts- +und Resume-Prüfungen, keine eigene Zustandsabkürzung. Serverseitige Bestätigung +unter der authentifizierten Clientkennung. Keine kostenpflichtigen LLM-Aufrufe. + +Prüfung mit synthetischen Daten: + +- API-/Coordinator-/Dashboard-/Worker-Tests: beide Antwortausgänge, + Projektberechtigung, fehlende Rechte, falsche Anfrage, fehlende Bestätigung, + identische bzw. widersprüchliche Wiederholung, unveränderte Budgetkonten und + Fortsetzung in derselben Revision. + Zusammen 51 Tests bestanden. +- Schemavertrag: ohne Budgetfreigabe kein Testzweig, bei menschlicher Freigabe + ausschließlich `human` als Testexecutor. +- Echter Headless-Edge: eigene Seite, Fall-/Projektanzeige, Sortierauswahl, + Auftragsanzeige und Antwortformular, Mobilansicht ohne horizontalen Überlauf, + Datenentfernung bei Abmeldung. Feedbackwirkung separat mit echter Coordinator- + und Budget-Fixture getestet, UI-Anfrage ist ausdrücklich eine Anzeige-Fixture. +- Vor Tests Test-Workspaces 16.939.129.428 Bytes; kleine Fixtures und temporäre + pytest-/Browserdaten bleiben mit Reserve innerhalb des 20-GB-Budgets. +- Breitere Untersuchungstests: 72 bestanden, 26 alte Tests in + `test_investigator.py` scheitern am vorhandenen `INVESTIGATION_PROMPT_LIMIT`. + Vollständige Gegenprobe dieses Moduls mit deaktivierter neuer Schemafilterung: + dieselben 26 Fehler, 6 bestanden. Keine Erweiterung der Zeichengrenze vorgenommen. + +Grenzen: Textfeedback; Dateianhänge und automatisierte Testausführung sind keine +Bestandteile dieser Oberfläche. Alte Revisionen benötigen ihre ursprüngliche Policy. diff --git a/docs/web-console.md b/docs/web-console.md index f70656a..67ae730 100644 --- a/docs/web-console.md +++ b/docs/web-console.md @@ -126,6 +126,54 @@ Protokoll/Workerzustand. `POST /v1/operations` benötigt Betreiberrechte und JSO Bei gleichzeitigem Zugriff kann `API_CONTROL_BUSY` auftreten; nach Aktualisierung erneut versuchen. Der Worker muss nach diesem Softwareupdate neu gestartet werden. +## Menschliche Testanfragen + +Die eigene Seite **Testanfragen** listet offene menschliche Rückfragen mit +Eingangszeit, Projekt, Bug-ID, Falltitel und Frage. Projektfilter sowie Sortierung +„Älteste zuerst“, „Neueste zuerst“ und „Projekt, dann älteste zuerst“ ermöglichen +chronologisches oder projektweises Abarbeiten. Die Liste aktualisiert sich mit der +Übersicht; eine noch offene Eingabe wird dabei nicht überschrieben. + +„Ansehen / beantworten“ zeigt Umgebung, Voraussetzungen, Testschritte, benötigte +Beobachtungen und angefragten Termin. Die Antwort erfasst die tatsächliche Umgebung, +Beobachtungen und „Test durchgeführt“ oder „Nicht durchführbar“. Die ausdrückliche +Bestätigung ist erforderlich. Textdaten bleiben untrusted observations, keine +auszuführenden Befehle. Diese erste Oberfläche nimmt Text, keine Dateianhänge an. + +Clients brauchen `tester = true` oder Betreiberrechte und Projektzugriff, um eine +Antwort zu bestätigen. Ein reiner Uploadclient erhält diese Rechte nicht automatisch. +Beispiel eines separaten menschlichen Clients in der API-Konfiguration: + +```toml +[[clients]] +id = "human-dev" +token_env = "VBA_HUMAN_TOKEN" +projects = ["viano-core"] +tester = true +``` + +Eigenen API-Token über diese Umgebungsvariable bereitstellen und API neu starten. +Die bestätigende Identität und Zeit stammen vom Server, nicht aus frei editierbaren +Formularfeldern. `GET /v1/test-requests` liefert ausschließlich freigegebene Projekte; +`POST /v1/test-requests/{case-key}` bestätigt die konkrete Anfrage/Revision. +Kollisionen, veraltete Anfragen und widersprüchliche Wiederholungen werden abgewiesen. +Eine identische erneute Übermittlung desselben Clients bleibt ohne doppelte Wirkung. + +Eine bestätigte Antwort setzt `AWAITING_TEST_FEEDBACK` über den vorhandenen +Coordinator auf `INVESTIGATING_HYPOTHESES` zurück. Revision, Budget und ursprüngliche +Belege bleiben erhalten. Der laufende Worker verarbeitet die Rückmeldung regulär; +eine besondere Betreiberfreigabe ist dafür nicht erforderlich. Der Termin führt +auch nach Ablauf zu keiner automatischen Fortsetzung und verwirft die Anfrage nicht. + +Die Werkzeugfreigabe erfolgt über `budget_config = "budget.human-tests.toml"` in +den vertrauenswürdigen Projekteinstellungen. Die lokale Dienstkonfiguration wurde +entsprechend ergänzt. Diese Policy erlaubt `testing.request_human` für 40 fachliche +Einheiten, automatisierte Tests bleiben ohne Eintrag gesperrt. Bei Trial-Läufen +die Datei ausdrücklich mit `--budget-config config/budget.human-tests.toml` angeben. +Bestehende Revisionen behalten ihre eingefrorene Policy; sie werden nicht migriert. +Nach Konfigurationsänderung API und Worker neu starten. Keine Modellanfrage wird +allein durch Öffnen oder Beantworten der Seite gestartet; der Worker läuft separat. + `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. diff --git a/src/viano_bug_analysis/analysis/investigation_prompt.py b/src/viano_bug_analysis/analysis/investigation_prompt.py index 248f925..fbc2c07 100644 --- a/src/viano_bug_analysis/analysis/investigation_prompt.py +++ b/src/viano_bug_analysis/analysis/investigation_prompt.py @@ -7,6 +7,29 @@ PROMPT_VERSION="investigation_v1" def output_schema(context=None, *, strict=False): + schema=_output_schema(context,strict=strict) + if context and 'budget_policy' in context: + costs=context['budget_policy']['action_costs'] + allowed=[e for e in ('human','automated') if 'testing.request_'+e in costs] + def restrict(value): + if isinstance(value,list): + return [restrict(v) for v in value if not (isinstance(v,dict) + and v.get('properties',{}).get('kind',{}).get('const')=='test' and not allowed)] + if not isinstance(value,dict):return value + value={k:restrict(v) for k,v in value.items()} + properties=value.get('properties',{}) + if properties.get('kind',{}).get('const')=='test': + properties['executor']['enum']=allowed + if not allowed: + for field in ('kind','intent'): + if 'enum' in properties.get(field,{}): + properties[field]['enum']=[v for v in properties[field]['enum'] if v!='test'] + return value + schema=restrict(schema) + return schema + + +def _output_schema(context=None, *, strict=False): if context and context.get("startup_explore"): from .investigation_startup import explore_schema return explore_schema(context) diff --git a/src/viano_bug_analysis/api/app.py b/src/viano_bug_analysis/api/app.py index 7d556a5..421861a 100644 --- a/src/viano_bug_analysis/api/app.py +++ b/src/viano_bug_analysis/api/app.py @@ -21,6 +21,7 @@ from .service import IntakeService from .dashboard import Dashboard from .cases import Cases from .operations import Operations +from .test_requests import TestRequests from viano_bug_analysis.lifecycle.errors import LockBusy from viano_bug_analysis.lifecycle.errors import LifecycleError from viano_bug_analysis.package import PackageReadError @@ -32,6 +33,7 @@ def create_app(config): dashboard = Dashboard(service) cases = Cases(service) operations = Operations(service) + test_requests = TestRequests(service) tokens = config.tokens() service.recover() active = 0 @@ -119,14 +121,40 @@ def create_app(config): raise APIError('API_CONTROL_UNAVAILABLE',503) from None return JSONResponse(result,headers={'Cache-Control':'no-store'}) + async def tests(request): + client=authenticate(request) + try: + if request.method=='GET': + result=await run_in_threadpool(test_requests.listing,client) + else: + if not (client.operator or client.tester): + raise APIError('API_FORBIDDEN',403) + if request.headers.get('content-type','').split(';')[0]!='application/json': + raise APIError('API_CONTENT_TYPE',415) + body=bytearray() + async for chunk in request.stream(): + body.extend(chunk) + if len(body)>64*1024: + raise APIError('API_TEST_FEEDBACK_INVALID',413) + try: + data=json.loads(body) + except ValueError: + raise APIError('API_TEST_FEEDBACK_INVALID',422) from None + result=await run_in_threadpool(test_requests.answer,client,request.path_params['handle'],data) + except LockBusy: + raise APIError('API_TEST_BUSY',409) from None + except (LifecycleError,PackageReadError) as exc: + raise APIError(getattr(exc,'code','API_CASE_UNAVAILABLE'),409) from None + return JSONResponse(result,headers={'Cache-Control':'no-store'}) + 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', 'case-detail.js','operations.js'}: + if name not in {'index.html', 'dashboard.js', 'dashboard.css', 'case-detail.js','operations.js','test-requests.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', 'case-detail.js':'text/javascript','operations.js':'text/javascript'}[name]) + 'dashboard.css': 'text/css', 'case-detail.js':'text/javascript','operations.js':'text/javascript','test-requests.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 @@ -201,6 +229,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/operations',operation,methods=['GET','POST']), + Route('/v1/test-requests',tests),Route('/v1/test-requests/{handle}',tests,methods=['POST']), 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)], diff --git a/src/viano_bug_analysis/api/cases.py b/src/viano_bug_analysis/api/cases.py index 3230304..f114b43 100644 --- a/src/viano_bug_analysis/api/cases.py +++ b/src/viano_bug_analysis/api/cases.py @@ -112,7 +112,7 @@ def next_actions(state, reason, worker): 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.', + 'AWAITING_TEST_FEEDBACK': 'Die angeforderte Testerantwort auf der Seite Testanfragen bestätigen. Die Untersuchung wird in derselben Revision fortgesetzt.', '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.', diff --git a/src/viano_bug_analysis/api/config.py b/src/viano_bug_analysis/api/config.py index c1850ed..974b8be 100644 --- a/src/viano_bug_analysis/api/config.py +++ b/src/viano_bug_analysis/api/config.py @@ -67,6 +67,7 @@ class Client: token_env: str projects: tuple[str, ...] operator: bool = False + tester: bool = False @dataclass(frozen=True) @@ -86,7 +87,7 @@ class APIConfig: or not isinstance(self.host, str) or not self.host): raise APIError('API_CONFIG', 500) for c in self.clients: - if (type(c.operator) is not bool or not isinstance(c.id, str) or not IDENTIFIER.fullmatch(c.id) + if (type(c.operator) is not bool or type(c.tester) is not bool or not isinstance(c.id, str) or not IDENTIFIER.fullmatch(c.id) or not isinstance(c.token_env, str) or not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', c.token_env) or not c.projects or not set(c.projects) <= ids): diff --git a/src/viano_bug_analysis/api/test_requests.py b/src/viano_bug_analysis/api/test_requests.py new file mode 100644 index 0000000..26caae4 --- /dev/null +++ b/src/viano_bug_analysis/api/test_requests.py @@ -0,0 +1,84 @@ +"""Authenticated human observations resume the existing case-local test contract.""" +from .config import APIError +from .cases import Cases, case_key +from .dashboard import read_json +from viano_bug_analysis.lifecycle.async_tests import AsyncTestCoordinator +from viano_bug_analysis.lifecycle.checkpoints import checkpoint_chain +from viano_bug_analysis.lifecycle.locking import FileLock +from viano_bug_analysis.lifecycle.storage import key +from viano_bug_analysis.lifecycle.job import timestamp +from viano_bug_analysis.package import PackageReader + + +class TestRequests: + def __init__(self,service): + self.service=service + self.cases=Cases(service) + + def request(self,reader): + m=reader.manifest + value=read_json(reader,m['artifacts'].get('async_test_request')) + if (value.get('executor')!='human' or any(value.get(k)!=m[k] + for k in ('case_id','package_id','analysis_revision'))): + return None + return value + + def listing(self,client): + rows=[];errors=0 + for folder in self.service.workspace.cases.glob('*/*'): + try: + handle=folder.parent.name+'.'+folder.name + path=self.cases.head(handle,client) + with PackageReader(path) as reader: + m=reader.manifest + if m['state']!='BLOCKED' or m['routing']['reason']!='AWAITING_TEST_FEEDBACK': + continue + request=self.request(reader) + if not request: + continue + ref=m['artifacts']['async_test_request'] + cp=next(cp for _,cp in checkpoint_chain(reader) if cp['details'].get('operation')=='async-test-wait' + and cp['details'].get('request_ref')==ref) + case=read_json(reader,m['artifacts'].get('case')) + title=next((r.get('title') for r in case.get('bug',{}).get('reports',[]) if r.get('title')),m['case_id']) + rows.append({'key':handle,'case_id':m['case_id'],'project_id':m['project']['id'], + 'title':title,'received_at':cp['created_at'],'request':request}) + except APIError as error: + if error.status!=404: + errors+=1 + except Exception: + errors+=1 + return {'can_answer':client.operator or client.tester,'requests':sorted(rows,key=lambda r:(r['received_at'],r['key'])), + 'read_errors':errors} + + def answer(self,client,handle,data): + if not (client.operator or client.tester): + raise APIError('API_FORBIDDEN',403) + fields={'request_id','analysis_revision','outcome','observations','environment','confirmed'} + if (not isinstance(data,dict) or set(data)!=fields or data['confirmed'] is not True + or type(data['analysis_revision']) is not int + or data['outcome'] not in ('completed','unable_to_test') + or not isinstance(data['environment'],str) or not data['environment'].strip() + or len(data['environment'])>4000 or not isinstance(data['observations'],list) + or not 1<=len(data['observations'])<=100 + or any(not isinstance(s,str) or not s.strip() or len(s)>12000 for s in data['observations'])): + raise APIError('API_TEST_FEEDBACK_INVALID',422) + path=self.cases.head(handle,client) + with PackageReader(path) as reader: + case_id=reader.manifest['case_id'] + with FileLock(self.service.workspace.runtime/'locks'/('trial-'+key(case_id)+'.lock')): + path=self.cases.head(handle,client) + with PackageReader(path) as reader: + request=self.request(reader) + if not request or (request['request_id'],request['analysis_revision'])!=(data['request_id'],data['analysis_revision']): + raise APIError('API_TEST_REQUEST_STALE',409) + feedback={k:request[k] for k in ('case_id','package_id','analysis_revision','request_id','hypothesis_id','action_id','executor','format_version')} + feedback.update(format='viano-async-test-feedback',outcome=data['outcome'],observations=data['observations'], + environment=data['environment'],tester_id=client.id,attachments=[], + confirmation={'confirmed':True,'confirmed_by':client.id,'confirmed_at':timestamp()}) + saved=read_json(reader,reader.manifest['artifacts'].get('async_test_feedback')) + # Network retries preserve the original server confirmation timestamp. + if saved and all(saved.get(k)==v for k,v in feedback.items() if k!='confirmation') and saved.get('confirmation',{}).get('confirmed_by')==client.id: + return {'status':'RESUMED','reused':True} + result=AsyncTestCoordinator(self.service.store).confirm_feedback(feedback) + return {'status':'RESUMED','reused':result.reused} diff --git a/src/viano_bug_analysis/lifecycle/async_tests.py b/src/viano_bug_analysis/lifecycle/async_tests.py index c25e601..f7f7e19 100644 --- a/src/viano_bug_analysis/lifecycle/async_tests.py +++ b/src/viano_bug_analysis/lifecycle/async_tests.py @@ -29,7 +29,7 @@ class AsyncTestResult: class AsyncTestCoordinator: """Trusted controller API, never a public HTTP endpoint or direct LLM tool. - A future authenticated web adapter calls confirm_feedback after approval. + The authenticated web adapter calls confirm_feedback after confirmation. No messages are sent and no clients/bots/labs are controlled here. """ diff --git a/src/viano_bug_analysis/web/case-detail.js b/src/viano_bug_analysis/web/case-detail.js index 516b98b..8104d9d 100644 --- a/src/viano_bug_analysis/web/case-detail.js +++ b/src/viano_bug_analysis/web/case-detail.js @@ -63,6 +63,7 @@ function renderDetail(data){ 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))); + if(data.reason==='AWAITING_TEST_FEEDBACK'){const link=text('a','Offene Testanfragen öffnen','case-link');link.href='#tests';actions.append(link);} if(typeof addRetryControl==='function')addRetryControl(data,actions); 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}))); @@ -106,6 +107,8 @@ function syncDetail(){ if(!detailLoading&&(!selectedCase||detailHash!==location.hash||(!location.hash.includes('/revision/')&¤t?.package_generation!==selectedCase.package_generation)))loadDetail(location.hash); } function route(){ + if(typeof testRoute==='function'&&testRoute())return; + if($('test-page'))$('test-page').hidden=true; 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.'; diff --git a/src/viano_bug_analysis/web/dashboard.js b/src/viano_bug_analysis/web/dashboard.js index 299f0c7..65af892 100644 --- a/src/viano_bug_analysis/web/dashboard.js +++ b/src/viano_bug_analysis/web/dashboard.js @@ -27,6 +27,7 @@ function logout() { ['workers','case-rows','bars','legend','project','project-filter','result-project','result-rows','case-detail'].forEach(id=>$(id).replaceChildren()); if(typeof clearDetails==='function')clearDetails(); if(typeof clearOperations==='function')clearOperations(); + if(typeof clearTests==='function')clearTests(); ['active-count','token-count','correction-count','finished-count'].forEach(id=>$(id).textContent='—'); $('pie').getContext('2d').clearRect(0,0,360,360); } @@ -108,6 +109,7 @@ async function refresh() { try {const response=await api('/v1/overview');if(generation!==session)return;overview=response;render();$('updated').textContent=`Aktualisiert ${new Date().toLocaleTimeString('de-DE')}`; error(overview.read_errors?'Einige Akten konnten nicht gelesen werden. Die Verbrauchsdaten können unvollständig sein.':''); if(typeof refreshOperations==='function')await refreshOperations(); + if(typeof refreshTests==='function')await refreshTests(); } catch(e) {if(generation===session)error(`Aktualisierung fehlgeschlagen: ${e.message}`);} if(token && generation===session)timer=setTimeout(refresh,4000); } diff --git a/src/viano_bug_analysis/web/index.html b/src/viano_bug_analysis/web/index.html index ea87b61..560aab8 100644 --- a/src/viano_bug_analysis/web/index.html +++ b/src/viano_bug_analysis/web/index.html @@ -3,10 +3,10 @@ BugAnalysis · Übersicht - +
BETRIEBSÜBERSICHT

Analyse im Blick.

Aktuelle Arbeit und messbarer Verbrauch.

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