110 lines
22 KiB
HTML
110 lines
22 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>GLPI AI Agent</title>
|
||
<style>
|
||
:root{color-scheme:dark;font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif;background:#0b1020;color:#e5e7eb}*{box-sizing:border-box}body{margin:0}.wrap{max-width:1600px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:20px}.badge,.pill{display:inline-block;padding:4px 8px;border-radius:999px;background:#1f2937;font-size:12px}.badge.warn,.pill-warn{background:#713f12;color:#fde68a}.pill-ok{background:#14532d;color:#bbf7d0}.pill-bad{background:#7f1d1d;color:#fecaca}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:16px 0 28px}.card,.panel{background:#111827;border:1px solid #273244;border-radius:14px;padding:16px}.k{color:#9ca3af;font-size:11px;text-transform:uppercase;letter-spacing:.08em}.v{font-size:23px;font-weight:700;margin-top:7px;overflow-wrap:anywhere}.ok{color:#86efac}.bad{color:#fca5a5}.muted,.sub{color:#9ca3af}.sub{font-size:12px;margin-top:3px}h1{margin:0;font-size:25px}h2{margin-top:30px}h3{margin:0 0 14px}table{width:100%;border-collapse:collapse;background:#111827;border-radius:14px;overflow:hidden}th,td{text-align:left;vertical-align:top;padding:11px;border-bottom:1px solid #273244;font-size:13px}th{color:#9ca3af}.decision{line-height:1.5;min-width:245px}.decision strong{color:#f3f4f6}.reason{max-width:520px;line-height:1.45}.policy{margin-top:7px;color:#cbd5e1}.knowledge{margin-top:7px;color:#93c5fd}.actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:8px}button{cursor:pointer;border:1px solid #374151;background:#1f2937;color:#f3f4f6;border-radius:8px;padding:7px 10px}button:hover{background:#374151}button.primary{background:#1d4ed8;border-color:#2563eb}button.danger{background:#7f1d1d;border-color:#991b1b}button:disabled{opacity:.5;cursor:not-allowed}input,textarea,select{width:100%;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:8px;padding:9px}textarea{min-height:110px;resize:vertical}.formgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.span2{grid-column:span 2}.span4{grid-column:1/-1}.field label{display:block;font-size:12px;color:#9ca3af;margin-bottom:5px}.check{display:flex;align-items:center;gap:8px}.check input{width:auto}.twocol{display:grid;grid-template-columns:1.15fr .85fr;gap:16px}.msg{display:none;margin:12px 0;padding:10px 12px;border-radius:8px;background:#1f2937}.msg.show{display:block}.msg.err{background:#7f1d1d}.msg.good{background:#14532d}.modal{display:none;position:fixed;inset:0;background:#0009;align-items:center;justify-content:center;padding:20px;z-index:20}.modal.show{display:flex}.modalbox{width:min(600px,100%);background:#111827;border:1px solid #374151;border-radius:14px;padding:20px}.kb-title{font-weight:700}.scroll{overflow:auto}@media(max-width:1100px){.twocol{grid-template-columns:1fr}.formgrid{grid-template-columns:repeat(2,1fr)}.span4{grid-column:1/-1}}@media(max-width:760px){.wrap{padding:14px}.hide-md{display:none}.formgrid{grid-template-columns:1fr}.span2,.span4{grid-column:1}.top{align-items:flex-start;flex-direction:column}}@media(max-width:560px){.hide-sm{display:none}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div class="top">
|
||
<div><h1>GLPI AI Agent</h1><div class="muted">Status, Audit, Lernen & Knowledge Base</div></div>
|
||
<div>{{if .DryRun}}<span class="badge warn">DRY RUN</span>{{else}}<span class="badge">LIVE</span>{{end}} {{if .AutoReply}}<span class="badge">Auto-Reply an</span>{{else}}<span class="badge">Auto-Reply aus</span>{{end}}</div>
|
||
</div>
|
||
<div id="globalMsg" class="msg"></div>
|
||
<div id="cards" class="grid"></div>
|
||
|
||
<h2>Letzte Verarbeitungen</h2>
|
||
<div class="scroll"><table>
|
||
<thead><tr><th>Zeit</th><th>Ticket</th><th>Ergebnis</th><th>Kategorieentscheidung</th><th>Antwortentscheidung</th><th class="hide-md">Kontext</th><th class="hide-sm">Begründung / Policy</th></tr></thead>
|
||
<tbody id="runs"><tr><td colspan="7">Lade…</td></tr></tbody>
|
||
</table></div>
|
||
|
||
<div class="twocol">
|
||
<section>
|
||
<h2>Interne Knowledge Base</h2>
|
||
<div class="panel">
|
||
<h3 id="kbFormTitle">Knowledge-Eintrag anlegen</h3>
|
||
<div id="kbDisabled" class="msg"></div>
|
||
<form id="kbForm">
|
||
<div class="formgrid">
|
||
<div class="field"><label>ID</label><input id="kbId" required placeholder="KB-AD-001"></div>
|
||
<div class="field span2"><label>Titel</label><input id="kbTitle" required placeholder="Benutzerkonto gesperrt"></div>
|
||
<div class="field"><label>Quelle</label><select id="kbSource"></select></div>
|
||
<div class="field"><label>Sprache</label><input id="kbLanguage" value="de-DE"></div>
|
||
<div class="field"><label>Stil</label><select id="kbStyle"><option value="formal">formal</option><option value="neutral">neutral</option><option value="informal">informal</option></select></div>
|
||
<div class="field"><label>Min. RAG-Score</label><input id="kbScore" type="number" min="0" max="1" step="0.01" value="0.88"></div>
|
||
<div class="field span2"><label>GLPI-Kategorien (leer = alle)</label><select id="kbCategories" multiple size="6"></select></div>
|
||
<div class="field span2"><label>Keywords</label><input id="kbKeywords" placeholder="Konto gesperrt, Login, Passwort"></div>
|
||
<div class="field span2"><label>Source URI (optional)</label><input id="kbUri" placeholder="kb://identity/account-locked"></div>
|
||
<div class="field span4"><label>Wissen / Diagnosekontext</label><textarea id="kbText" required></textarea></div>
|
||
<div class="field span4"><label>Freigegebener Antworttext</label><textarea id="kbAnswer" placeholder="Nur der fachliche Text; Anrede und Signatur ergänzt der Agent."></textarea></div>
|
||
<div class="field span4 check"><input id="kbAutoReply" type="checkbox"><label for="kbAutoReply">Für automatische Antwort grundsätzlich freigeben</label></div>
|
||
</div>
|
||
<div class="actions"><button class="primary" type="submit">Speichern</button><button type="button" onclick="resetKBForm()">Neu / Zurücksetzen</button></div>
|
||
</form>
|
||
</div>
|
||
<div class="scroll" style="margin-top:12px"><table><thead><tr><th>ID / Titel</th><th>Quelle</th><th>Auto-Reply</th><th>Kategorien</th><th>Aktionen</th></tr></thead><tbody id="kbRows"><tr><td colspan="5">Lade…</td></tr></tbody></table></div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Bestätigtes Lernen</h2>
|
||
<div class="panel"><div class="muted">Nur von Ihnen bestätigte Zuordnungen werden als Beispiele an die KI weitergegeben. KI-Entscheidungen lernen niemals automatisch aus sich selbst.</div></div>
|
||
<div class="scroll" style="margin-top:12px"><table><thead><tr><th>Beispiel</th><th>Kategorie</th><th>Zeit</th><th></th></tr></thead><tbody id="learningRows"><tr><td colspan="4">Lade…</td></tr></tbody></table></div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="learnModal" class="modal" onclick="if(event.target===this)closeLearn()">
|
||
<div class="modalbox"><h3>Kategorie als Lernbeispiel bestätigen</h3><p id="learnTicket" class="muted"></p><div class="field"><label>Korrekte GLPI-Kategorie</label><select id="learnCategory"></select></div><div class="actions"><button class="primary" onclick="saveLearning()">Bestätigen</button><button onclick="closeLearn()">Abbrechen</button></div></div>
|
||
</div>
|
||
|
||
<script>
|
||
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const pct=v=>`${Math.round(Number(v||0)*100)} %`;
|
||
const cat=(name,id)=>name?`${name} (#${id})`:`#${id||0}`;
|
||
let categories=[], kbDocs=[], runsData=[], currentLearnRun='';
|
||
function msg(text,kind='good'){const x=document.querySelector('#globalMsg');x.textContent=text;x.className=`msg show ${kind}`;setTimeout(()=>x.className='msg',5000)}
|
||
async function api(url,opt={}){opt.headers={...(opt.headers||{}),'X-Requested-With':'GLPI-AI-Agent'};if(opt.body)opt.headers['Content-Type']='application/json';const r=await fetch(url,opt);if(!r.ok){throw new Error((await r.text()).trim()||`HTTP ${r.status}`)};if(r.status===204)return null;return r.json()}
|
||
function categoryDecision(x){
|
||
const code=x.category_decision||''; const current=cat(x.category_before_name||((x.category_before||0)===0?'Nicht gesetzt':''),x.category_before||0); const threshold=pct(x.category_threshold);
|
||
let ai=x.ai_recommended_category_id?`${cat(x.ai_recommended_category_name,x.ai_recommended_category_id)} · ${pct(x.ai_category_confidence)}`:`Keine passende Kategorie · Sicherheit ${pct(x.ai_category_confidence)}`;
|
||
let label='Keine Aktion',cls='pill-warn',detail=code||'keine Policy-Information';
|
||
if(code==='category_written'){label='Geändert';cls='pill-ok';detail=`KI ${pct(x.ai_category_confidence)} ≥ ${threshold}`}
|
||
else if(code==='category_accepted_dry_run'){label='Würde ändern';cls='pill-ok';detail=`DRY RUN · KI ${pct(x.ai_category_confidence)} ≥ ${threshold}`}
|
||
else if(code==='category_accepted'){label='Freigegeben';cls='pill-ok';detail=`KI ${pct(x.ai_category_confidence)} ≥ ${threshold}`}
|
||
else if(code==='category_already_correct'){label='Bereits korrekt';cls='pill-ok';detail=`KI ${pct(x.ai_category_confidence)}`}
|
||
else if(code==='category_confidence_below_threshold'){label='Blockiert';detail=`KI ${pct(x.ai_category_confidence)} < Schwellwert ${threshold}`}
|
||
else if(code==='category_unknown'){label='Blockiert';cls='pill-bad';detail='Kategorie-ID ist nicht in GLPI bekannt'}
|
||
else if(code==='category_no_recommendation'){label='Keine Empfehlung';detail='Keine angebotene Kategorie fachlich ausreichend'}
|
||
else if(code==='category_auto_disabled'){label='Auto-Kategorie aus';detail='Empfehlung wird nur protokolliert'}
|
||
else if(code==='category_ticket_changed_before_write'){label='Abgebrochen';detail='Ticket wurde während der Analyse verändert'}
|
||
else if(code==='category_write_failed'){label='Schreibfehler';cls='pill-bad';detail='GLPI-Kategorie konnte nicht geschrieben werden'}
|
||
return `<div class="decision"><div><strong>Aktuell:</strong> ${esc(current)}</div><div><strong>KI:</strong> ${esc(ai)}</div><div class="sub">Schwellwert ${esc(threshold)}</div><div style="margin-top:6px"><span class="pill ${cls}">${esc(label)}</span> <span class="sub">${esc(detail)}</span></div>${(!x.error&&x.outcome==='processed')?`<div class="actions"><button onclick="openLearn('${esc(x.run_id)}',${Number(x.ai_recommended_category_id||x.category_before||0)})">✓ Kategorie bestätigen/korrigieren</button></div>`:''}</div>`;
|
||
}
|
||
function replyDecision(x){
|
||
const code=x.reply_decision||'';const conf=pct(x.ai_reply_confidence),threshold=pct(x.reply_threshold);let label='Keine Antwort',cls='pill-warn',detail=code||'keine Policy-Information';
|
||
if(code==='reply_written'){label='Geschrieben';cls='pill-ok';detail=`KI ${conf} ≥ ${threshold}`} else if(code==='reply_accepted_dry_run'){label='Würde antworten';cls='pill-ok';detail=`DRY RUN · KI ${conf} ≥ ${threshold}`} else if(code==='reply_accepted'){label='Freigegeben';cls='pill-ok'} else if(code==='reply_auto_disabled'){detail='AUTO_REPLY=false'} else if(code==='reply_no_knowledge_candidates'){detail='Keine freigegebene Wissensquelle gefunden'} else if(code==='reply_model_not_recommended'){detail='KI empfiehlt keine automatische Antwort'} else if(code==='reply_confidence_below_threshold'){detail=`KI ${conf} < Schwellwert ${threshold}`} else if(code==='reply_no_knowledge_selected'){detail='Keine Knowledge-ID ausgewählt'} else if(code==='reply_context_incomplete'){detail='Kontextquelle unvollständig / nicht erreichbar'} else if(code==='reply_relevant_incident'){detail='Relevanter Major Incident oder Service-Ausfall'} else if(code==='reply_existing_followup'){detail='Ticket hatte bereits ein Followup'} else if(code==='reply_followup_appeared_before_write'){detail='Während der Analyse ist ein Followup hinzugekommen'} else if(code==='reply_knowledge_not_found'){detail='Gewählte Knowledge-ID nicht gefunden'} else if(code==='reply_source_not_allowed'){detail='Knowledge-Quelle nicht erlaubt'} else if(code==='reply_source_not_allowed_for_auto_reply'){detail='Quelle darf nicht automatisch antworten'} else if(code==='reply_language_mismatch'){detail='Knowledge-Sprache passt nicht'} else if(code==='reply_style_mismatch'){detail='Knowledge-Stil passt nicht'} else if(code==='reply_knowledge_auto_reply_disabled'){detail='KB nicht für Auto-Reply freigegeben'} else if(code==='reply_knowledge_score_below_threshold'){detail='Knowledge-Ähnlichkeit unter Schwellwert'} else if(code==='reply_knowledge_answer_empty'){detail='Kein freigegebener Antworttext'} else if(code==='reply_category_not_allowed'){detail='KB nicht für Zielkategorie freigegeben'} else if(code==='reply_ticket_changed_before_write'){detail='Ticket wurde während der Analyse verändert'} else if(code==='reply_write_failed'){label='Schreibfehler';cls='pill-bad';detail='Followup konnte nicht geschrieben werden'}
|
||
const ai=x.ai_reply_recommended?`Ja · ${conf}`:`Nein · ${conf}`;let knowledge=x.knowledge_top_id?`<div class="knowledge">Top-KB: ${esc(x.knowledge_top_title||x.knowledge_top_id)} (${esc(x.knowledge_top_id)}) · ${esc(pct(x.knowledge_score))}</div>`:'<div class="knowledge">Knowledge: keine Treffer</div>';
|
||
return `<div class="decision"><div><strong>Lösungsvorschlag KI:</strong> ${esc(ai)}</div><div class="sub">Reply-Schwellwert ${esc(threshold)}${x.ai_knowledge_id?` · KB ${esc(x.ai_knowledge_id)}`:''}</div><div style="margin-top:6px"><span class="pill ${cls}">${esc(label)}</span> <span class="sub">${esc(detail)}</span></div>${knowledge}</div>`;
|
||
}
|
||
function renderRuns(r){runsData=r;document.querySelector('#runs').innerHTML=r.length?r.map(x=>{const aiReason=x.ai_reason||x.reason||'–';const policy=x.policy_reason||[x.category_decision,x.reply_decision].filter(Boolean).join('; ')||'–';return `<tr><td>${esc(new Date(x.finished_at).toLocaleString('de-DE'))}</td><td>#${esc(x.ticket_id)} ${esc(x.ticket_name)}</td><td><span class="pill">${esc(x.outcome)}</span>${x.dry_run?'<div class="sub">Dry Run</div>':''}</td><td>${categoryDecision(x)}</td><td>${replyDecision(x)}</td><td class="hide-md">C:${esc(x.context_changes||0)} I:${esc(x.context_incidents||0)} U:${esc(x.context_issues||0)} D:${esc(x.context_devices||0)}${(x.context_warnings||[]).length?' ⚠':''}</td><td class="hide-sm reason"><div><strong>KI:</strong> ${esc(aiReason)}</div><div class="policy"><strong>Policy:</strong> ${esc(policy)}</div>${x.error?`<div class="bad"><strong>Fehler:</strong> ${esc(x.error)}</div>`:''}</td></tr>`}).join(''):'<tr><td colspan="7">Noch keine Verarbeitung.</td></tr>'}
|
||
function renderKB(){document.querySelector('#kbRows').innerHTML=kbDocs.length?kbDocs.map(d=>`<tr><td><div class="kb-title">${esc(d.id)}</div>${esc(d.title)}</td><td>${esc(d.source)}<div class="sub">${d.managed?'Web-verwaltet':(d.origin&&d.origin!=='static'?`${d.origin} / read-only`:'statisch / read-only')}</div></td><td>${d.auto_reply?'<span class="pill pill-ok">ja</span>':'<span class="pill">nein</span>'}</td><td>${esc((d.categories||[]).join(', ')||'alle')} ${(d.source_category_ids||[]).length?`<div class="sub">GLPI-KB-Kategorien: ${esc(d.source_category_ids.join(', '))}</div>`:''}</td><td><div class="actions">${d.managed?`<button onclick="editKB('${esc(d.id)}')">Bearbeiten</button><button class="danger" onclick="deleteKB('${esc(d.id)}')">Löschen</button>`:'<span class="sub">über Git/Datei verwalten</span>'}</div></td></tr>`).join(''):'<tr><td colspan="5">Noch keine Knowledge-Einträge.</td></tr>'}
|
||
function renderLearning(rows){document.querySelector('#learningRows').innerHTML=rows.length?rows.map(x=>`<tr><td><strong>#${esc(x.ticket_id)} ${esc(x.subject)}</strong><div class="sub">${esc((x.text||'').slice(0,180))}</div></td><td>${esc(x.category_name)} (#${esc(x.category_id)})<div class="sub">${x.correction?'Korrektur':'Bestätigung'}${x.ai_recommended_category_id?` · KI #${esc(x.ai_recommended_category_id)} ${esc(pct(x.ai_confidence))}`:''}</div></td><td>${esc(new Date(x.created_at).toLocaleString('de-DE'))}</td><td><button class="danger" onclick="deleteLearning('${esc(x.id)}')">Löschen</button></td></tr>`).join(''):'<tr><td colspan="4">Noch keine bestätigten Beispiele.</td></tr>'}
|
||
async function refresh(){try{const [s,r,c,k,l]=await Promise.all([api('/api/status'),api('/api/runs?limit=50'),api('/api/categories'),api('/api/knowledge'),api('/api/learning')]);categories=c;kbDocs=k;const srcSel=document.querySelector('#kbSource');const oldSource=srcSel.value;srcSel.innerHTML=(s.knowledge_allowed_sources||[]).map(x=>`<option value="${esc(x)}">${esc(x)}</option>`).join('');if(oldSource&&[...srcSel.options].some(o=>o.value===oldSource))srcSel.value=oldSource;else if([...srcSel.options].some(o=>o.value==='internal-kb'))srcSel.value='internal-kb';const kbSel=document.querySelector('#kbCategories');const selected=new Set([...kbSel.selectedOptions].map(o=>Number(o.value)));kbSel.innerHTML=categories.map(x=>`<option value="${Number(x.id)}">${esc(x.completename||x.name)} (#${Number(x.id)})</option>`).join('');[...kbSel.options].forEach(o=>o.selected=selected.has(Number(o.value)));const cards=[['GLPI',s.glpi_ok?'OK':'Fehler',s.glpi_ok],['Ollama',s.ollama_ok?'OK':'Fehler',s.ollama_ok],['Verarbeitet',s.processed,true],['Fehler',s.errors,s.errors===0],['Knowledge',s.knowledge_docs,true],['Lernbeispiele',s.learning_examples,true],['Kategorie-Schwelle',pct(s.category_confidence),true],['Reply-Schwelle',pct(s.reply_confidence),true],['Sprache',s.communication_language,true],['Stil',s.communication_style,true],['KB-Editor',s.knowledge_edit_enabled?'aktiv':'aus',s.knowledge_edit_enabled],['GLPI-KB',s.glpi_kb_enabled?(s.glpi_kb_ok?`${s.glpi_kb_documents} synchronisiert`:'Fehler'):'aus',!s.glpi_kb_enabled||s.glpi_kb_ok],['Uptime Kuma',s.uptime_kuma_enabled?'an':'aus',true]];document.querySelector('#cards').innerHTML=cards.map(c=>`<div class="card"><div class="k">${esc(c[0])}</div><div class="v ${c[2]?'ok':'bad'}">${esc(c[1])}</div></div>`).join('');document.querySelector('#kbForm').querySelectorAll('input,textarea,select,button').forEach(x=>x.disabled=!s.knowledge_edit_enabled);const kd=document.querySelector('#kbDisabled');if(!s.knowledge_edit_enabled){kd.textContent='KB-Bearbeitung ist deaktiviert. Setzen Sie KNOWLEDGE_WEB_EDIT_ENABLED=true (nur mit authentifiziertem Dashboard).';kd.className='msg show'}else{kd.className='msg'}renderRuns(r);renderKB();renderLearning(l)}catch(e){msg(e.message,'err')}}
|
||
function resetKBForm(){document.querySelector('#kbForm').reset();if([...document.querySelector('#kbSource').options].some(o=>o.value==='internal-kb'))document.querySelector('#kbSource').value='internal-kb';document.querySelector('#kbLanguage').value='de-DE';document.querySelector('#kbStyle').value='formal';document.querySelector('#kbScore').value='0.88';document.querySelector('#kbFormTitle').textContent='Knowledge-Eintrag anlegen'}
|
||
function editKB(id){const d=kbDocs.find(x=>x.id===id);if(!d)return;document.querySelector('#kbFormTitle').textContent=`Knowledge-Eintrag bearbeiten: ${id}`;document.querySelector('#kbId').value=d.id||'';document.querySelector('#kbTitle').value=d.title||'';document.querySelector('#kbSource').value=d.source||'internal-kb';document.querySelector('#kbLanguage').value=d.language||'de-DE';document.querySelector('#kbStyle').value=d.communication_style||'formal';document.querySelector('#kbScore').value=d.min_score??0.88;[...document.querySelector('#kbCategories').options].forEach(o=>o.selected=(d.categories||[]).includes(Number(o.value)));document.querySelector('#kbKeywords').value=(d.keywords||[]).join(', ');document.querySelector('#kbUri').value=d.source_uri||'';document.querySelector('#kbText').value=d.text||'';document.querySelector('#kbAnswer').value=d.answer||'';document.querySelector('#kbAutoReply').checked=!!d.auto_reply;document.querySelector('#kbForm').scrollIntoView({behavior:'smooth'})}
|
||
document.querySelector('#kbForm').addEventListener('submit',async e=>{e.preventDefault();const strs=v=>v.split(',').map(x=>x.trim()).filter(Boolean);const selectedCats=[...document.querySelector('#kbCategories').selectedOptions].map(o=>Number(o.value));const d={id:document.querySelector('#kbId').value.trim(),title:document.querySelector('#kbTitle').value.trim(),source:document.querySelector('#kbSource').value.trim(),language:document.querySelector('#kbLanguage').value.trim(),communication_style:document.querySelector('#kbStyle').value,text:document.querySelector('#kbText').value.trim(),answer:document.querySelector('#kbAnswer').value.trim(),auto_reply:document.querySelector('#kbAutoReply').checked,min_score:Number(document.querySelector('#kbScore').value||0),categories:selectedCats,keywords:strs(document.querySelector('#kbKeywords').value),source_uri:document.querySelector('#kbUri').value.trim()};try{await api('/api/knowledge',{method:'POST',body:JSON.stringify(d)});msg('Knowledge-Eintrag gespeichert.','good');resetKBForm();await refresh()}catch(e){msg(e.message,'err')}})
|
||
async function deleteKB(id){if(!confirm(`Knowledge-Eintrag ${id} wirklich löschen?`))return;try{await api(`/api/knowledge/${encodeURIComponent(id)}`,{method:'DELETE'});msg('Knowledge-Eintrag gelöscht.','good');await refresh()}catch(e){msg(e.message,'err')}}
|
||
function openLearn(runID,preferred){currentLearnRun=runID;const run=runsData.find(x=>x.run_id===runID);document.querySelector('#learnTicket').textContent=run?`#${run.ticket_id} ${run.ticket_name}`:runID;const sel=document.querySelector('#learnCategory');sel.innerHTML=categories.map(c=>`<option value="${Number(c.id)}">${esc(c.completename||c.name)} (#${Number(c.id)})</option>`).join('');if(preferred>0)sel.value=String(preferred);document.querySelector('#learnModal').classList.add('show')}
|
||
function closeLearn(){document.querySelector('#learnModal').classList.remove('show');currentLearnRun=''}
|
||
async function saveLearning(){const id=Number(document.querySelector('#learnCategory').value);try{await api('/api/learning',{method:'POST',body:JSON.stringify({run_id:currentLearnRun,category_id:id})});msg('Kategorie bestätigt. Das Beispiel wird künftig für ähnliche Tickets verwendet.','good');closeLearn();await refresh()}catch(e){msg(e.message,'err')}}
|
||
async function deleteLearning(id){if(!confirm('Lernbeispiel wirklich löschen?'))return;try{await api(`/api/learning/${encodeURIComponent(id)}`,{method:'DELETE'});msg('Lernbeispiel gelöscht.','good');await refresh()}catch(e){msg(e.message,'err')}}
|
||
refresh();setInterval(async()=>{try{const [s,r]=await Promise.all([api('/api/status'),api('/api/runs?limit=50')]);renderRuns(r)}catch(e){}},5000);
|
||
</script>
|
||
</body>
|
||
</html>
|