fix(diarization-ui): background jobs page auto-refresh with runtime/cancel/delete actions
This commit is contained in:
116
app.py
116
app.py
@@ -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('<','<')+'</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('<','<')+"</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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user