fix(diarization-ui): background jobs page auto-refresh with runtime/cancel/delete actions

This commit is contained in:
2026-03-21 15:07:31 +01:00
parent 648a24303a
commit 7397dcaf4c

116
app.py
View File

@@ -10,7 +10,7 @@ from typing import Optional
import markdown as md
import requests
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
from fastapi.responses import HTMLResponse, PlainTextResponse, Response, JSONResponse
API_BASE = os.getenv("API_BASE", "http://gx10.aquantico.lan:8093").rstrip("/")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://gx10.aquantico.lan:11434").rstrip("/")
@@ -224,6 +224,11 @@ def get_prompts():
return c.execute("SELECT id,name,prompt FROM prompts ORDER BY name").fetchall()
def _job_get(job_id: int):
with db() as c:
return c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
def _job_set(job_id: int, **fields):
if not fields:
return
@@ -234,13 +239,17 @@ def _job_set(job_id: int, **fields):
def _process_upload_job(job_id: int):
with db() as c:
j = c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
j = _job_get(job_id)
if not j:
return
if j["status"] == "cancelled":
return
_job_set(job_id, status="running", started_at=now_iso())
try:
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
p = Path(j["file_path"])
data = p.read_bytes()
filename = p.name
@@ -249,6 +258,10 @@ def _process_upload_job(job_id: int):
r.raise_for_status()
payload = r.json()
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
content_md = payload.get("formatted_text", "")
doc_title = (j["title"] or "").strip() or filename
with db() as c:
@@ -264,13 +277,17 @@ def _process_upload_job(job_id: int):
def _process_analysis_job(job_id: int):
with db() as c:
j = c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
j = _job_get(job_id)
if not j:
return
if j["status"] == "cancelled":
return
_job_set(job_id, status="running", started_at=now_iso())
try:
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
with db() as c:
doc = c.execute("SELECT * FROM documents WHERE id=?", (j["document_id"],)).fetchone()
prm = c.execute("SELECT * FROM prompts WHERE id=?", (j["prompt_id"],)).fetchone()
@@ -291,6 +308,10 @@ def _process_analysis_job(job_id: int):
r.raise_for_status()
answer = r.json().get("response", "")
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
with db() as c:
cur = c.execute(
"""
@@ -707,8 +728,7 @@ def prompt_delete(prompt_id: int):
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
@app.get("/jobs", response_class=HTMLResponse)
def jobs_page(queued: Optional[int] = None):
def _jobs_payload(limit: int = 200):
with db() as c:
jobs = c.execute(
"""
@@ -717,27 +737,81 @@ def jobs_page(queued: Optional[int] = None):
LEFT JOIN projects p ON p.id=j.project_id
LEFT JOIN documents d ON d.id=j.document_id
LEFT JOIN prompts pr ON pr.id=j.prompt_id
ORDER BY j.id DESC LIMIT 200
"""
ORDER BY j.id DESC LIMIT ?
""",
(limit,),
).fetchall()
return [dict(j) for j in jobs]
rows = "".join([
f"<div class='card'><b>Job #{j['id']}</b> [{j['kind']}] · <b>{j['status']}</b><br>"
f"<small>{j['created_at']}</small><br>"
f"{('Projekt: '+str(j['project_name'])+'<br>') if j['project_name'] else ''}"
f"{('Dokument: '+str(j['document_title'])+'<br>') if j['document_title'] else ''}"
f"{('Prompt: '+str(j['prompt_name'])+'<br>') if j['prompt_name'] else ''}"
f"{('Fehler: <pre>'+str(j['error']).replace('<','&lt;')+'</pre>') if j['error'] else ''}"
f"{(f'<a href=\'/document/{j['result_document_id']}\'>Ergebnis öffnen</a>') if j['result_document_id'] else ''}"
f"</div>" for j in jobs
])
@app.get("/jobs/data")
def jobs_data(limit: int = 200):
return {"items": _jobs_payload(limit)}
@app.post("/jobs/{job_id}/cancel")
def jobs_cancel(job_id: int):
j = _job_get(job_id)
if not j:
raise HTTPException(404, "job not found")
if j["status"] in ("done", "error", "cancelled"):
return {"ok": True, "status": j["status"]}
_job_set(job_id, status="cancelled", finished_at=now_iso(), error="Cancelled by user")
return {"ok": True, "status": "cancelled"}
@app.post("/jobs/{job_id}/delete")
def jobs_delete(job_id: int):
with db() as c:
c.execute("DELETE FROM jobs WHERE id=?", (job_id,))
return {"ok": True}
@app.get("/jobs", response_class=HTMLResponse)
def jobs_page(queued: Optional[int] = None):
notice = f"<p><b>Job #{queued} wurde eingereiht.</b></p>" if queued else ""
body = f"""
<h2>Hintergrundverarbeitung</h2>
<p class='hint'>Maximal 2 Jobs laufen gleichzeitig.</p>
<p class='hint'>Maximal 2 Jobs gleichzeitig. Seite aktualisiert automatisch.</p>
{notice}
{rows or '<p>Keine Jobs.</p>'}
<div id='jobs-root'></div>
<script>
function since(ts) {{
if(!ts) return '-';
const s = Math.floor((Date.now()-Date.parse(ts))/1000);
if (s<60) return s+'s';
const m = Math.floor(s/60); if (m<60) return m+'m '+(s%60)+'s';
const h = Math.floor(m/60); return h+'h '+(m%60)+'m';
}}
async function post(url) {{
const r = await fetch(url, {{method:'POST'}});
if(!r.ok) alert('Fehler '+r.status);
}}
async function renderJobs() {{
const r = await fetch('/jobs/data');
const j = await r.json();
const root = document.getElementById('jobs-root');
root.innerHTML = '';
if(!j.items.length) {{ root.innerHTML = '<p>Keine Jobs.</p>'; return; }}
for(const it of j.items) {{
const d = document.createElement('div'); d.className='card';
const actions = [];
if(!['done','error','cancelled'].includes(it.status)) actions.push("<a href='#' class='iconbtn' onclick=\"cancelJob("+it.id+");return false;\">⛔</a>");
actions.push("<a href='#' class='iconbtn' onclick=\"deleteJob("+it.id+");return false;\">🗑️</a>");
const result = it.result_document_id ? ("<a href='/document/"+it.result_document_id+"'>Ergebnis öffnen</a>") : '';
const err = it.error ? ("<pre>"+String(it.error).replaceAll('<','&lt;')+"</pre>") : '';
d.innerHTML = "<b>Job #"+it.id+"</b> ["+it.kind+"] · <b>"+it.status+"</b> · läuft: "+since(it.started_at || it.created_at)+"<br><small>"+it.created_at+"</small><br>"
+(it.project_name?('Projekt: '+it.project_name+'<br>'):'')
+(it.document_title?('Dokument: '+it.document_title+'<br>'):'')
+(it.prompt_name?('Prompt: '+it.prompt_name+'<br>'):'')
+"<div class='row' style='margin-top:8px'>"+actions.join(' ')+" "+result+"</div>"+err;
root.appendChild(d);
}}
}}
async function cancelJob(id) {{ if(!confirm('Job abbrechen?')) return; await post('/jobs/'+id+'/cancel'); renderJobs(); }}
async function deleteJob(id) {{ if(!confirm('Job löschen?')) return; await post('/jobs/'+id+'/delete'); renderJobs(); }}
renderJobs(); setInterval(renderJobs, 5000);
</script>
"""
return layout("Jobs", body)