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 markdown as md
import requests import requests
from fastapi import FastAPI, File, Form, HTTPException, UploadFile 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("/") 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("/") 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() 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): def _job_set(job_id: int, **fields):
if not fields: if not fields:
return return
@@ -234,13 +239,17 @@ def _job_set(job_id: int, **fields):
def _process_upload_job(job_id: int): def _process_upload_job(job_id: int):
with db() as c: j = _job_get(job_id)
j = c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
if not j: if not j:
return return
if j["status"] == "cancelled":
return
_job_set(job_id, status="running", started_at=now_iso()) _job_set(job_id, status="running", started_at=now_iso())
try: try:
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
p = Path(j["file_path"]) p = Path(j["file_path"])
data = p.read_bytes() data = p.read_bytes()
filename = p.name filename = p.name
@@ -249,6 +258,10 @@ def _process_upload_job(job_id: int):
r.raise_for_status() r.raise_for_status()
payload = r.json() payload = r.json()
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
content_md = payload.get("formatted_text", "") content_md = payload.get("formatted_text", "")
doc_title = (j["title"] or "").strip() or filename doc_title = (j["title"] or "").strip() or filename
with db() as c: with db() as c:
@@ -264,13 +277,17 @@ def _process_upload_job(job_id: int):
def _process_analysis_job(job_id: int): def _process_analysis_job(job_id: int):
with db() as c: j = _job_get(job_id)
j = c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
if not j: if not j:
return return
if j["status"] == "cancelled":
return
_job_set(job_id, status="running", started_at=now_iso()) _job_set(job_id, status="running", started_at=now_iso())
try: try:
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
with db() as c: with db() as c:
doc = c.execute("SELECT * FROM documents WHERE id=?", (j["document_id"],)).fetchone() doc = c.execute("SELECT * FROM documents WHERE id=?", (j["document_id"],)).fetchone()
prm = c.execute("SELECT * FROM prompts WHERE id=?", (j["prompt_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() r.raise_for_status()
answer = r.json().get("response", "") answer = r.json().get("response", "")
j = _job_get(job_id)
if not j or j["status"] == "cancelled":
return
with db() as c: with db() as c:
cur = c.execute( cur = c.execute(
""" """
@@ -707,8 +728,7 @@ def prompt_delete(prompt_id: int):
return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>") return HTMLResponse("<meta http-equiv='refresh' content='0; url=/prompts'>")
@app.get("/jobs", response_class=HTMLResponse) def _jobs_payload(limit: int = 200):
def jobs_page(queued: Optional[int] = None):
with db() as c: with db() as c:
jobs = c.execute( 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 projects p ON p.id=j.project_id
LEFT JOIN documents d ON d.id=j.document_id LEFT JOIN documents d ON d.id=j.document_id
LEFT JOIN prompts pr ON pr.id=j.prompt_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() ).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 "" notice = f"<p><b>Job #{queued} wurde eingereiht.</b></p>" if queued else ""
body = f""" body = f"""
<h2>Hintergrundverarbeitung</h2> <h2>Hintergrundverarbeitung</h2>
<p class='hint'>Maximal 2 Jobs laufen gleichzeitig.</p> <p class='hint'>Maximal 2 Jobs gleichzeitig. Seite aktualisiert automatisch.</p>
{notice} {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) return layout("Jobs", body)