ITSM/templates/aes_dashboard.html

279 lines
19 KiB
HTML
Executable File

{% extends "base.html" %}
{% block content %}
{% include "aes_nav.html" %}
{% if !configured %}
<div class="panel"><p class="err">{{ error }}</p></div>
{% else %}
{% if !error.is_empty() %}<div class="panel"><p class="err">{{ error }}</p></div>{% endif %}
{% if !note.is_empty() %}<div class="panel"><p class="sz">{{ note }}</p></div>{% endif %}
{% if section == "dashboard" %}
<div style="display:flex;flex-wrap:wrap;gap:10px">
{% for c in health %}
<div class="card" style="flex:1;min-width:200px">
<div class="svc-card"><div><b>{{ c.label }}</b><div class="sz" style="margin-top:4px">{{ c.sub }}</div></div><span class="badge {{ c.class }}">{{ c.value }}</span></div>
</div>
{% endfor %}
</div>
<div style="display:flex;flex-wrap:wrap;gap:10px;margin-top:10px">
{% for k in kpis %}
<div class="card" style="flex:1;min-width:150px">
<div class="sz">{{ k.label }}</div>
<div style="font-size:26px;font-weight:700;margin:2px 0">{{ k.value }}</div>
<div class="sz">{{ k.sub }}</div>
</div>
{% endfor %}
</div>
<h2 class="page-title" style="font-size:18px;margin-top:18px">Projekte</h2>
{% if projects.is_empty() %}<p class="sz">Keine Projekte.</p>{% else %}
<table class="tickets"><thead><tr><th>Projekt</th><th>Letzter Lauf</th></tr></thead><tbody>
{% for p in projects %}<tr><td><a href="/erweiterungen/aes/projekte/{{ p.name }}"><b>{{ p.name }}</b></a></td><td><span class="badge {{ p.status_class }}">{{ p.status }}</span></td></tr>{% endfor %}
</tbody></table>{% endif %}
<div style="display:flex;flex-wrap:wrap;gap:14px;margin-top:18px">
<div style="flex:1;min-width:300px">
<h3 style="margin:0 0 6px">Live Activity</h3>
{% if activity.is_empty() %}<p class="sz">Keine Aktivitaet.</p>{% else %}
<div class="panel">{% for a in activity %}<div class="sz" style="border-bottom:1px solid var(--border);padding:5px 0"><span class="badge {{ a.class }}" style="margin-right:6px">&bull;</span>{{ a.text }}<span style="float:right;opacity:.7">{{ a.when }}</span></div>{% endfor %}</div>{% endif %}
</div>
<div style="flex:1;min-width:300px">
<h3 style="margin:0 0 6px">Infrastruktur</h3>
<p class="sz">{{ bal_summary }}</p>
{% if backends.is_empty() %}<p class="sz">Keine Backends gemeldet.</p>{% endif %}
{% for b in backends %}<div class="card"><div class="svc-card"><div><b>{{ b.url }}</b><div class="sz" style="margin-top:4px">CPU {{ b.cpu }}% &middot; RAM {{ b.ram }}% &middot; VRAM {{ b.vram }}% &middot; {{ b.latency }}ms</div></div><span class="badge {{ b.badge_class }}">{{ b.badge }}</span></div></div>{% endfor %}
</div>
</div>
{% else if section == "projekte" %}
<h1 class="page-title">Projekte</h1>
{% if projects.is_empty() %}<p class="sz">Noch keine Projekte.</p>{% else %}
<table class="tickets"><thead><tr><th>Projekt</th><th></th></tr></thead><tbody>
{% for p in projects %}<tr><td><b>{{ p.name }}</b></td><td><a class="btn ghost mini" href="/erweiterungen/aes/projekte/{{ p.name }}">Oeffnen &rarr;</a></td></tr>{% endfor %}
</tbody></table>{% endif %}
<div class="panel" style="margin-top:14px">
<h3 style="margin-top:0">Projekt anlegen</h3>
<form method="post" action="/erweiterungen/aes/projekte">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<div class="formrow"><input type="text" name="name" placeholder="Projektname (a-z, 0-9, - , _)" pattern="[A-Za-z0-9_-]+" required></div>
<button class="btn" type="submit">Anlegen</button>
</form>
</div>
{% else if section == "forge" %}
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
<h1 class="page-title" style="margin:0">Forge</h1>
<a class="btn ghost mini" href="/erweiterungen/aes/einstellungen?comp=forge" title="Forge-Einstellungen">&#9881; Einstellungen</a>
</div>
<p class="sz">Git-Server der AES-Suite. Zugriff erfolgt serverseitig mit dem AES-Token &mdash; kein eigenes Token noetig.</p>
{% if repos.is_empty() %}<p class="sz">Keine Repositories.</p>{% else %}
<table class="tickets"><thead><tr><th>Repository</th><th>Branch</th><th>Sichtbarkeit</th><th></th></tr></thead><tbody>
{% for r in repos %}<tr>
<td><a href="/erweiterungen/aes/forge/{{ r.label }}"><b>{{ r.label }}</b></a></td>
<td class="sz">{{ r.value }}</td>
<td><span class="badge {{ r.class }}">{{ r.sub }}</span></td>
<td>
<form method="post" action="/erweiterungen/aes/forge/{{ r.label }}/delete" class="inline-form" data-confirm="Repository {{ r.label }} wirklich loeschen?">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<button class="btn ghost mini" type="submit">Loeschen</button>
</form>
</td>
</tr>{% endfor %}
</tbody></table>{% endif %}
<div class="panel" style="margin-top:14px">
<h3 style="margin-top:0">Repository anlegen</h3>
<form method="post" action="/erweiterungen/aes/forge">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<div class="formrow"><input type="text" name="name" placeholder="repo-name (a-z, 0-9, - , _)" pattern="[A-Za-z0-9_.-]+" required></div>
<button class="btn" type="submit">Anlegen (privat)</button>
</form>
</div>
{% else if section == "runner" %}
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
<h1 class="page-title" style="margin:0">Runner</h1>
<a class="btn ghost mini" href="/erweiterungen/aes/einstellungen?comp=runner" title="Runner-Einstellungen">&#9881; Einstellungen</a>
</div>
{% if !runner_available %}
<div class="panel"><p class="sz">Runner-Status nicht verfuegbar. {{ runner_hint }}</p></div>
{% else %}
<div style="display:flex;flex-wrap:wrap;gap:10px">
<div class="card" style="flex:1;min-width:180px"><div class="sz">Version</div><div style="font-size:20px;font-weight:700">v{{ runner_version }}</div></div>
<div class="card" style="flex:1;min-width:180px"><div class="sz">Poll-Intervall</div><div style="font-size:20px;font-weight:700">{{ runner_poll }}s</div></div>
<div class="card" style="flex:2;min-width:220px"><div class="sz">Zuletzt aktiv</div><div style="font-size:15px;font-weight:600;margin-top:4px">{{ runner_updated }}</div></div>
</div>
<div class="sz" style="margin-top:10px">Forge/Gitea: <b>{{ runner_instance }}</b> &middot; Self-Update-Repo: <b>{{ runner_update_repo }}</b></div>
<h3 style="margin:16px 0 6px">Beobachtete Repositories</h3>
{% if runner_watched.is_empty() %}<p class="sz">Keine.</p>{% else %}
<div style="display:flex;flex-wrap:wrap;gap:6px">{% for w in runner_watched %}<span class="badge ok">{{ w }}</span>{% endfor %}</div>
{% endif %}
<h3 style="margin:16px 0 6px">Letzte CI-Laeufe</h3>
{% if runner_runs.is_empty() %}<p class="sz">Noch keine Laeufe aufgezeichnet (erscheinen beim naechsten Commit auf einem beobachteten Repo).</p>{% else %}
<table class="tickets"><thead><tr><th>Repository</th><th>Branch</th><th>Commit</th><th>Ergebnis</th><th>Beendet</th></tr></thead><tbody>
{% for r in runner_runs %}<tr><td class="sz">{{ r.repo }}</td><td class="sz">{{ r.branch }}</td><td class="sz" style="font-family:monospace">{{ r.sha }}</td><td><span class="badge {{ r.ok_class }}">{{ r.ok_label }}</span></td><td class="sz">{{ r.finished_at }}</td></tr>{% endfor %}
</tbody></table>{% endif %}
{% endif %}
{% else if section == "agenten" %}
<h1 class="page-title">Agenten</h1>
<p class="sz">Agenten sind <code>llm-agent</code>-Instanzen (je LLM-Host, mit lokalem Ollama), die sich beim Balancer anmelden und Inferenz + Host-Metriken liefern.</p>
<p class="sz">{{ bal_summary }}</p>
{% if backends.is_empty() %}
<div class="panel"><p class="sz">Kein Agent online. Starte einen Agenten auf einem GPU/CPU-Host: <code>aes-suite agent --token &lt;AGENT_TOKEN&gt;</code> (Ollama vorausgesetzt) und trage seine URL im Balancer (OLLAMA_HOSTS) ein.</p></div>
{% else %}
<table class="tickets"><thead><tr><th>Host</th><th>Agent</th><th>Backend</th><th>CPU</th><th>RAM</th><th>VRAM</th><th>Latenz</th></tr></thead><tbody>
{% for b in backends %}<tr><td class="sz">{{ b.url }}</td><td><span class="badge {% if b.agent == "online" %}ok{% else %}err{% endif %}">{{ b.agent }}</span></td><td><span class="badge {{ b.badge_class }}">{{ b.badge }}</span></td><td class="sz">{{ b.cpu }}%</td><td class="sz">{{ b.ram }}%</td><td class="sz">{{ b.vram }}%</td><td class="sz">{{ b.latency }}ms</td></tr>{% endfor %}
</tbody></table>{% endif %}
{% else if section == "llm" %}
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
<h1 class="page-title" style="margin:0">LLM &amp; Verbindungen</h1>
<a class="btn ghost mini" href="/erweiterungen/aes/einstellungen?comp=llm" title="Balancer-/Cloud-Einstellungen">&#9881; Einstellungen</a>
</div>
{% if !hinweis.is_empty() %}<div class="panel"><p class="sz">{{ hinweis }}</p></div>{% endif %}
{% if !fehler.is_empty() %}<div class="panel"><p class="err">{{ fehler }}</p></div>{% endif %}
<p class="sz">Der Balancer proxyt Inferenz OpenAI-/Ollama-kompatibel auf die Verbindungen; ohne gesundes Ollama-Backend greift der passende Cloud-/OpenAI-Provider.</p>
<div class="panel">
<h3 style="margin-top:0">LLM-Verbindungen (Provider)</h3>
<p class="sz">Ollama-Hosts und OpenAI-kompatible Provider (OpenAI, InferX, Together, Groq, Infercom, …). <b>base_url</b> = OpenAI-Basis (…/v1). API-Key leer lassen = unveraendert. Speichern startet die aes-suite neu.</p>
<p class="sz" style="opacity:.75">Presets base_url: InferX <code>https://model.inferx.net/&lt;endpoint&gt;/v1</code> &middot; Together <code>https://api.together.xyz/v1</code> &middot; Groq <code>https://api.groq.com/openai/v1</code> &middot; OpenAI <code>https://api.openai.com/v1</code></p>
<form method="post" action="/erweiterungen/aes/llm/connections">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<input type="hidden" name="rows" value="{{ conn_rows }}">
<div style="overflow-x:auto"><table class="tickets"><thead><tr><th>Aktiv</th><th>Name</th><th>Typ</th><th>URL (ollama) / base_url (cloud)</th><th>Agent-URL</th><th>Modell</th><th>API-Key</th></tr></thead><tbody>
{% for c in conns %}
<tr>
<td><input type="checkbox" name="enabled_{{ c.idx }}"{% if c.enabled %} checked{% endif %}></td>
<td><input type="text" name="name_{{ c.idx }}" value="{{ c.name }}" placeholder="Name" style="width:8em"></td>
<td><select name="type_{{ c.idx }}">
<option value="ollama"{% if c.kind == "ollama" %} selected{% endif %}>ollama</option>
<option value="openai"{% if c.kind == "openai" %} selected{% endif %}>openai-kompat.</option>
<option value="anthropic"{% if c.kind == "anthropic" %} selected{% endif %}>anthropic</option>
</select></td>
<td><input type="text" name="url_{{ c.idx }}" value="{{ c.url }}" placeholder="http://host:11434" style="width:12em">&nbsp;<input type="text" name="base_url_{{ c.idx }}" value="{{ c.base_url }}" placeholder="https://…/v1" style="width:12em"></td>
<td><input type="text" name="agent_url_{{ c.idx }}" value="{{ c.agent_url }}" placeholder="(optional)" style="width:9em"></td>
<td><input type="text" name="model_{{ c.idx }}" value="{{ c.model }}" placeholder="Modell" style="width:9em"></td>
<td><input type="password" name="api_key_{{ c.idx }}" placeholder="{% if c.key_set %}gesetzt{% else %}—{% endif %}" style="width:8em"></td>
</tr>
{% endfor %}
</tbody></table></div>
<button class="btn" type="submit" style="margin-top:8px">Verbindungen speichern &amp; neu starten</button>
</form>
</div>
<div class="panel">
<h3 style="margin-top:0">Inferenz testen</h3>
<form method="post" action="/erweiterungen/aes/llm/infer">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<div class="formrow"><input type="text" name="model" placeholder="Modell (z.B. llama3.2 oder claude-sonnet-4-6)" value="{{ infer_model }}" required></div>
<div class="formrow"><textarea name="prompt" rows="3" placeholder="Prompt" required>{{ infer_prompt }}</textarea></div>
<button class="btn" type="submit">Senden</button>
</form>
{% if infer_shown %}
<h4 style="margin:12px 0 4px">Antwort{% if !infer_model.is_empty() %} ({{ infer_model }}){% endif %}</h4>
{% if infer_result.is_empty() %}<p class="sz">Keine Antwort &mdash; Ursache siehe Fehlermeldung oben (kein gesundes Backend und kein Cloud-Fallback aktiv).</p>
{% else %}<div class="panel" style="white-space:pre-wrap">{{ infer_result }}</div>{% endif %}
{% endif %}
</div>
<div class="panel" style="margin-top:14px">
<h3 style="margin-top:0">Modelle verwalten</h3>
<form method="post" action="/erweiterungen/aes/llm/pull" style="margin-bottom:10px">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<div class="formrow"><input type="text" name="model" placeholder="Modellname laden, z.B. llama3.2" required></div>
<button class="btn" type="submit">Laden (pull)</button>
</form>
{% if models.is_empty() %}<p class="sz">Keine Modelle geladen (Backend offline oder leer).</p>{% else %}
<table class="tickets"><thead><tr><th>Modell</th><th></th></tr></thead><tbody>
{% for m in models %}<tr><td class="sz">{{ m }}</td><td>
<form method="post" action="/erweiterungen/aes/llm/delete" class="inline-form" data-confirm="Modell {{ m }} loeschen?">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<input type="hidden" name="model" value="{{ m }}">
<button class="btn ghost mini" type="submit">Loeschen</button>
</form></td></tr>{% endfor %}
</tbody></table>{% endif %}
</div>
<h3 style="margin:16px 0 6px">Backends</h3>
<p class="sz">{{ bal_summary }}</p>
{% if backends.is_empty() %}<p class="sz">Keine Backends. Sobald ein llm-agent online ist, erscheinen hier Auslastung und Modelle.</p>{% else %}
<table class="tickets"><thead><tr><th>Backend</th><th>Status</th><th>CPU</th><th>RAM</th><th>VRAM</th><th>Latenz</th><th>Modelle</th></tr></thead><tbody>
{% for b in backends %}<tr><td class="sz">{{ b.url }}</td><td><span class="badge {{ b.badge_class }}">{{ b.badge }}</span></td><td class="sz">{{ b.cpu }}%</td><td class="sz">{{ b.ram }}%</td><td class="sz">{{ b.vram }}%</td><td class="sz">{{ b.latency }}ms</td><td class="sz">{{ b.models }}</td></tr>{% endfor %}
</tbody></table>{% endif %}
{% else if section == "monitoring" %}
<h1 class="page-title">Monitoring</h1>
<p class="sz">Live-Ansicht aller Dienste (aktualisiert sich automatisch). Externe Anbindung siehe Admin &rarr; Metrics.</p>
<div id="aes-monitor" data-src="/erweiterungen/aes/monitor.json">
<p class="sz">Lade Live-Daten&hellip; (aktiviere JavaScript fuer die Live-Ansicht)</p>
</div>
<script src="/static/aes-monitor.js"></script>
<h3 style="margin:16px 0 4px">Logs (Events)</h3>
{% if logs.is_empty() %}<p class="sz">Keine Events.</p>{% else %}
<div class="panel" style="overflow-x:auto;max-height:320px;overflow-y:auto">{% for l in logs %}<div class="sz" style="font-family:monospace;white-space:pre-wrap;border-bottom:1px solid var(--border);padding:3px 0">{{ l }}</div>{% endfor %}</div>{% endif %}
{% else if section == "admin" %}
<h1 class="page-title">Admin</h1>
<h3 style="margin:6px 0 6px">Dienste &amp; Modul</h3>
<table class="tickets"><tbody>
{% for r in admin_rows %}<tr><td class="sz">{{ r.k }}</td><td>{% if r.class.is_empty() %}<span class="sz">{{ r.v }}</span>{% else %}<span class="badge {{ r.class }}">{{ r.v }}</span>{% endif %}</td></tr>{% endfor %}
</tbody></table>
<h3 style="margin:16px 0 6px">Externe Monitoring-Schnittstelle</h3>
<p class="sz">Zum Anbinden eines externen Monitorings (Prometheus/Grafana o.ae.). Aufruf mit Header <code>Authorization: Bearer &lt;ITSM_AES_METRICS_TOKEN&gt;</code>.</p>
<table class="tickets"><tbody>
{% for r in metrics_rows %}<tr><td class="sz">{{ r.k }}</td><td><span class="badge {{ r.class }}">{{ r.v }}</span></td></tr>{% endfor %}
</tbody></table>
{% else if section == "einstellungen" %}
<h1 class="page-title">Einstellungen{% if !comp.is_empty() %} &middot; {{ comp }}{% endif %}</h1>
{% if !comp.is_empty() %}<p class="sz"><a href="/erweiterungen/aes/einstellungen">&larr; alle Einstellungen</a></p>{% endif %}
{% if !hinweis.is_empty() %}<div class="panel"><p class="sz">{{ hinweis }}</p></div>{% endif %}
{% if !fehler.is_empty() %}<div class="panel"><p class="err">{{ fehler }}</p></div>{% endif %}
<p class="sz">Secrets werden nie im Klartext angezeigt; Feld leer lassen = unveraendert. Speichern startet den betroffenen Dienst automatisch neu.</p>
{% if comp != "runner" %}
<form method="post" action="/erweiterungen/aes/einstellungen/save" class="panel">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<input type="hidden" name="file" value="aes">
<h3 style="margin-top:0">AES-Suite / Balancer / Cloud / Forge</h3>
{% for g in sett_aes %}
{% if comp.is_empty() || comp == "all" || (comp == "llm" && g.name == "LLM-Balancer / Cloud") || (comp == "forge" && g.name == "Forge") || (comp == "forge" && g.name == "AES-Kern") %}
<h4 style="margin:12px 0 4px">{{ g.name }}</h4>
{% for it in g.items %}
<div class="formrow">
<label class="sz" style="display:block">{{ it.label }} <span style="opacity:.6">({{ it.key }})</span></label>
{% if it.secret %}<input type="password" name="{{ it.key }}" placeholder="{% if it.set %}gesetzt &mdash; neuen Wert eingeben zum Aendern{% else %}nicht gesetzt{% endif %}">
{% else %}<input type="text" name="{{ it.key }}" value="{{ it.value }}">{% endif %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
<button class="btn" type="submit">Speichern &amp; aes-suite neu starten</button>
</form>
{% endif %}
{% if comp.is_empty() || comp == "all" || comp == "runner" %}
<form method="post" action="/erweiterungen/aes/einstellungen/save" class="panel" style="margin-top:14px">
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<input type="hidden" name="file" value="runner">
<h3 style="margin-top:0">Runner</h3>
{% for g in sett_runner %}
{% for it in g.items %}
<div class="formrow">
<label class="sz" style="display:block">{{ it.label }} <span style="opacity:.6">({{ it.key }})</span></label>
{% if it.secret %}<input type="password" name="{{ it.key }}" placeholder="{% if it.set %}gesetzt &mdash; neuen Wert eingeben zum Aendern{% else %}nicht gesetzt{% endif %}">
{% else %}<input type="text" name="{{ it.key }}" value="{{ it.value }}">{% endif %}
</div>
{% endfor %}
{% endfor %}
<button class="btn" type="submit">Speichern &amp; aes-runner neu starten</button>
</form>
{% endif %}
{% endif %}
{% endif %}
{% endblock %}