97 lines
10 KiB
HTML
97 lines
10 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{font-family:Inter,system-ui,sans-serif;color-scheme:dark;background:#0b1020;color:#e5e7eb}body{margin:0}.wrap{max-width:1500px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;align-items:center;gap:20px}.badge{padding:6px 10px;border-radius:999px;background:#1f2937;font-size:12px}.warn{background:#713f12}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin:24px 0}.card{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:24px;font-weight:700;margin-top:8px;overflow-wrap:anywhere}.ok{color:#86efac}.bad{color:#fca5a5}.neutral{color:#e5e7eb}table{width:100%;border-collapse:collapse;background:#111827;border-radius:14px;overflow:hidden}th,td{text-align:left;vertical-align:top;padding:12px;border-bottom:1px solid #273244;font-size:13px}th{color:#9ca3af}.muted{color:#9ca3af}.pill{display:inline-block;padding:3px 7px;border-radius:999px;background:#1f2937}.pill-ok{background:#14532d;color:#bbf7d0}.pill-warn{background:#713f12;color:#fde68a}.pill-bad{background:#7f1d1d;color:#fecaca}.decision{line-height:1.5;min-width:250px}.decision strong{color:#f3f4f6}.sub{color:#9ca3af;font-size:12px;margin-top:3px}.reason{max-width:520px;line-height:1.45}.policy{margin-top:7px;color:#cbd5e1}.knowledge{margin-top:7px;color:#93c5fd}h1{margin:0;font-size:25px}@media(max-width:900px){.wrap{padding:16px}.hide-md{display:none}}@media(max-width:650px){.hide-sm{display:none}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div class="top">
|
||
<div><h1>GLPI AI Agent</h1><div class="muted">Status & Audit Dashboard</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="cards" class="grid"></div>
|
||
<h2>Letzte Verarbeitungen</h2>
|
||
<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>
|
||
<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}`;
|
||
function categoryDecision(x){
|
||
const code=x.category_decision||'';
|
||
const ai=x.ai_recommended_category_id?cat(x.ai_recommended_category_name,x.ai_recommended_category_id):'keine Kategorie';
|
||
const conf=pct(x.ai_category_confidence), threshold=pct(x.category_threshold);
|
||
const current=cat(x.category_before_name||((x.category_before||0)===0?'Nicht gesetzt':''),x.category_before||0);
|
||
let label='Keine Aktion', cls='pill-warn', detail=code||'keine Policy-Information';
|
||
if(code==='category_written'){label='Geändert';cls='pill-ok';detail=`KI ${conf} ≥ ${threshold}`}
|
||
else if(code==='category_accepted_dry_run'){label='Würde ändern';cls='pill-ok';detail=`DRY RUN · KI ${conf} ≥ ${threshold}`}
|
||
else if(code==='category_accepted'){label='Freigegeben';cls='pill-ok';detail=`KI ${conf} ≥ ${threshold}`}
|
||
else if(code==='category_already_correct'){label='Bereits korrekt';cls='pill-ok';detail=`KI ${conf}`}
|
||
else if(code==='category_confidence_below_threshold'){label='Blockiert';cls='pill-warn';detail=`KI ${conf} < Schwellwert ${threshold}`}
|
||
else if(code==='category_unknown'){label='Blockiert';cls='pill-bad';detail='KI-ID ist nicht in der GLPI-Kategorieliste'}
|
||
else if(code==='category_no_recommendation'){label='Keine Empfehlung';cls='pill-warn';detail='KI hat Kategorie-ID 0 geliefert'}
|
||
else if(code==='category_auto_disabled'){label='Auto-Kategorie aus';cls='pill-warn';detail=`KI-Empfehlung wird nicht geschrieben`}
|
||
else if(code==='category_ticket_changed_before_write'){label='Abgebrochen';cls='pill-warn';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)} · ${esc(conf)}</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></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';detail=`KI ${conf} ≥ ${threshold}`}
|
||
else if(code==='reply_auto_disabled'){detail='AUTO_REPLY=false'}
|
||
else if(code==='reply_no_knowledge_candidates'){detail='Keine Knowledge-Treffer vorhanden'}
|
||
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='KI hat keinen Knowledge-Eintrag 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='Von KI 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 zur Kommunikationspolicy'}
|
||
else if(code==='reply_style_mismatch'){detail='Knowledge-Stil passt nicht zur Kommunikationspolicy'}
|
||
else if(code==='reply_knowledge_auto_reply_disabled'){detail='Knowledge-Eintrag ist 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='Knowledge-Eintrag enthält keinen Antworttext'}
|
||
else if(code==='reply_category_not_allowed'){detail='Knowledge-Eintrag ist für die Zielkategorie nicht 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='';
|
||
if(x.knowledge_top_id){knowledge=`<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>`}
|
||
else knowledge='<div class="knowledge">Knowledge: keine Treffer</div>';
|
||
return `<div class="decision"><div><strong>KI empfiehlt:</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>`;
|
||
}
|
||
async function refresh(){
|
||
try{
|
||
const [s,r]=await Promise.all([fetch('/api/status').then(x=>x.json()),fetch('/api/runs?limit=50').then(x=>x.json())]);
|
||
const cards=[
|
||
['GLPI',s.glpi_ok?'OK':'Fehler',s.glpi_ok],['Ollama',s.ollama_ok?'OK':'Fehler',s.ollama_ok],['Verarbeitet',s.processed,true],['Übersprungen',s.skipped,true],['Fehler',s.errors,s.errors===0],['Antworten',s.replies,true],['Kategorien',s.category_changes,true],['Queue',s.queue_depth,s.queue_depth<20],['Knowledge',s.knowledge_docs,true],['Kategorie-Schwelle',pct(s.category_confidence),true],['Reply-Schwelle',pct(s.reply_confidence),true],['RAG-Schwelle',pct(s.knowledge_min_score),true],['Sprache',s.communication_language,true],['Stil',s.communication_style,true],['Quellen',s.knowledge_allowed_sources.join(', '),true],['Reply-Quellen',s.knowledge_auto_reply_sources.join(', ')||'keine',true],['Context',s.context_enabled?'aktiv':'aus',true],['Context-Fehler',s.context_errors,s.context_errors===0],['Changes',s.change_calendar_enabled?'an':'aus',true],['Major Incidents',s.major_incidents_enabled?'an':'aus',true],['Benutzer/Geräte',s.user_device_context_enabled?'an':'aus',true],['Uptime Kuma',s.uptime_kuma_enabled?(s.uptime_kuma_status_pages.join(', ')||s.uptime_kuma_mode||'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('#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>';
|
||
}catch(e){console.error(e)}
|
||
}
|
||
refresh();setInterval(refresh,5000);
|
||
</script>
|
||
</body>
|
||
</html>
|