Files
glpi-ai-agent/internal/web/templates/dashboard.html
T
jbergner 1c12e3ed60
release-tag / release-image (push) Successful in 1m41s
Update
2026-07-27 21:27:42 +02:00

97 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>