450 lines
30 KiB
HTML
450 lines
30 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Einstellungen · {{ workspace.name }}{% endblock %}
|
|
{% block content %}
|
|
{% set wbase = '/workspaces/' ~ workspace.id %}
|
|
<div class="stack stack-sm">
|
|
<h1>Einstellungen</h1>
|
|
</div>
|
|
|
|
<nav aria-label="Bereiche"><ul class="tab-list">
|
|
{% for key, label in tabs.items() %}<li><a href="{{ wbase }}/settings?tab={{ key }}" {% if tab == key %}class="active" aria-current="page"{% endif %}>{{ label }}</a></li>{% endfor %}
|
|
</ul></nav>
|
|
|
|
{% if saved %}<div class="alert alert-success" role="status"><div class="alert-content"><p>Gespeichert.</p></div></div>{% endif %}
|
|
{% set labels = {'allow': 'erlaubt', 'require_approval': 'Freigabe nötig', 'deny': 'gesperrt'} %}
|
|
|
|
{% if tab == 'general' %}
|
|
<section class="card" id="general">
|
|
<div class="card-header"><h2 class="text-lg">Name und Beschreibung</h2></div>
|
|
<div class="card-body">
|
|
<form method="post" action="{{ wbase }}/settings" class="stack stack-sm aiw-form">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<div class="form-group"><label for="ws-name">Name</label><input id="ws-name" name="name" required maxlength="200" value="{{ workspace.name }}"></div>
|
|
<div class="form-group"><label for="ws-description">Beschreibung</label><textarea id="ws-description" name="description" maxlength="2000" rows="3">{{ workspace.description }}</textarea>
|
|
<p class="form-hint">Erscheint in der Übersicht. Die KI sieht Name und Beschreibung als Kontext.</p></div>
|
|
<p class="text-sm text-muted">Team: {{ team.name }} (lässt sich nicht ändern)</p>
|
|
<div><button class="btn btn-primary btn-sm" type="submit">Speichern</button></div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card" id="examples">
|
|
<div class="card-header"><h2 class="text-lg">Beispielfragen</h2></div>
|
|
<div class="card-body">
|
|
<form method="post" action="{{ wbase }}/examples" class="stack stack-sm aiw-form">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<div class="form-group"><label for="ws-examples">Eine Frage je Zeile</label>
|
|
<textarea id="ws-examples" name="examples" rows="5" placeholder="Wie ist das Projekt aufgebaut? Was hat sich diese Woche im Repository geändert?">{{ (workspace.settings or {}).get('examples', [])|join('\n') }}</textarea>
|
|
<p class="form-hint">Erscheinen in der Übersicht und in neuen Chats; ein Klick stellt die Frage. Zeigt neuen Mitgliedern, was hier geht. Höchstens 8.</p></div>
|
|
<div><button class="btn btn-primary btn-sm" type="submit">Speichern</button></div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card" id="answers">
|
|
<div class="card-header"><h2 class="text-lg">Antworten</h2></div>
|
|
<div class="card-body">
|
|
<form method="post" action="{{ wbase }}/tool-rounds" class="stack stack-sm aiw-form">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<div class="form-group"><label for="ws-rounds">Tool-Schritte je Antwort</label>
|
|
<input id="ws-rounds" name="value" type="number" min="4" max="{{ rounds_limit }}" placeholder="{{ rounds_default }} (Standard)" value="{{ workspace.settings.get('max_tool_rounds', '') if workspace.settings else '' }}">
|
|
<p class="form-hint">Aktuell {{ tool_rounds }}. Mehr Schritte erlauben größere Aufgaben in einer Antwort (viel lesen, mehrere Commits), kosten aber mehr. Ist das Limit erreicht, fasst die KI den Stand zusammen, und es geht mit „Weiterarbeiten“ weiter. Höchstens {{ rounds_limit }}; leer = Standard.</p></div>
|
|
<div><button class="btn btn-primary btn-sm" type="submit">Speichern</button></div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card" id="chat-only">
|
|
<div class="card-header"><h2 class="text-lg">Ohne Datenquellen chatten</h2></div>
|
|
<div class="card-body">
|
|
<form method="post" action="{{ wbase }}/chat-only" class="stack stack-sm aiw-form">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<label class="cluster"><input type="checkbox" name="enabled" value="1" {% if workspace.settings and workspace.settings.get('chat_only') %}checked{% endif %}>
|
|
<span>Chat auch ohne verbundene Quelle erlauben</span></label>
|
|
<p class="form-hint">Normalerweise startet der Chat erst, wenn eine Quelle verbunden ist, denn ohne Kontext antwortet die KI nur aus allgemeinem Wissen. Für einen reinen Chat (Texte formulieren, allgemeine Fragen) oder um den Unterschied zu zeigen, hier einschalten. Verbundene Quellen nutzt die KI trotzdem.</p>
|
|
<div><button class="btn btn-primary btn-sm" type="submit">Speichern</button></div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card" id="approvals">
|
|
<div class="card-header"><h2 class="text-lg">Freigaben</h2></div>
|
|
<div class="card-body stack stack-sm">
|
|
<form method="post" action="{{ wbase }}/approval-mode" class="stack stack-sm aiw-form">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<label class="cluster"><input type="radio" name="mode" value="risk" {% if workspace.approval_mode != 'all_writes' %}checked{% endif %}>
|
|
<span><strong>Nach Einstufung</strong> <span class="text-sm text-muted">(Standard)</span><br>
|
|
<span class="text-sm text-muted">Eine Freigabe braucht, was der Plattform-Admin oder dieser Workspace unter <a href="{{ wbase }}/settings?tab=tools">Tools und Regeln</a> auf „Freigabe nötig“ stellt.</span></span></label>
|
|
<label class="cluster"><input type="radio" name="mode" value="all_writes" {% if workspace.approval_mode == 'all_writes' %}checked{% endif %}>
|
|
<span><strong>Jede Schreibaktion in ein Fremdsystem</strong><br>
|
|
<span class="text-sm text-muted">Tools der Klasse <code>write_external</code> (z. B. schreibende MCP-Tools) brauchen immer eine Freigabe, auch wenn sie erlaubt sind. Commits auf den Chat-Branch und das Gedächtnis sind ausgenommen; nach außen pushen ohnehin nur Menschen.</span></span></label>
|
|
<div><button class="btn btn-primary btn-sm" type="submit">Speichern</button></div>
|
|
</form>
|
|
<p class="text-xs text-muted">Entscheiden dürfen Admins und Freigeber dieses Workspace. Lockern lässt sich die zentrale Einstufung hier nicht.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card" id="model">
|
|
<div class="card-header"><h2 class="text-lg">Modell</h2></div>
|
|
<div class="card-body stack">
|
|
{% if effective_model %}
|
|
<p>Aktuell: <strong>{{ effective_model.label }}</strong> <code class="text-sm">{{ effective_model.name }}</code>
|
|
{% if not workspace.model_id %}<span class="text-muted text-sm">(Standard der Plattform)</span>{% endif %}</p>
|
|
{% else %}
|
|
<div class="alert alert-warning" role="status"><div class="alert-content"><p>Es ist noch kein Modell eingerichtet. Das macht der Plattform-Admin unter „Modelle und Zugänge“.</p></div></div>
|
|
{% endif %}
|
|
{% if models %}
|
|
<form method="post" action="{{ wbase }}/model" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<select name="model_id" aria-label="Modell">
|
|
<option value="">Standard der Plattform</option>
|
|
{% for m in models %}<option value="{{ m.id }}" {% if workspace.model_id == m.id %}selected{% endif %}>{{ m.label }}</option>{% endfor %}
|
|
</select>
|
|
<button class="btn btn-secondary btn-sm" type="submit">Übernehmen</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card" id="notifications">
|
|
<div class="card-header"><h2 class="text-lg">Benachrichtigungen</h2></div>
|
|
<div class="card-body stack stack-sm">
|
|
<p class="text-sm text-muted">Wartet ein Antrag auf Freigabe, bekommen die Admins und Freigeber des Workspace eine E-Mail (wenn im Profil eine Adresse steht). Zusätzlich kann eine Nachricht in einen Teams- oder Slack-Kanal gehen: dafür dort einen eingehenden Webhook anlegen und die Adresse hier eintragen. Sie wird verschlüsselt gespeichert.</p>
|
|
{% if workspace.webhook %}<p class="text-sm">Webhook eingerichtet: <code>{{ workspace.webhook_hint }}</code></p>{% endif %}
|
|
<form method="post" action="{{ wbase }}/webhook" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input name="url" type="password" autocomplete="off" class="flex-1" placeholder="{{ 'neue Adresse (leer = unverändert)' if workspace.webhook else 'https://…' }}" aria-label="Webhook-Adresse">
|
|
{% if workspace.webhook %}<label class="cluster text-sm"><input type="checkbox" name="remove" value="1"> entfernen</label>{% endif %}
|
|
<button class="btn btn-secondary btn-sm" type="submit">Speichern</button>
|
|
</form>
|
|
{% if workspace.webhook %}
|
|
<form method="post" action="{{ wbase }}/webhook/test">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<button class="btn btn-ghost btn-sm" type="submit">Test-Nachricht senden</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card aiw-danger-zone" id="delete">
|
|
<div class="card-header"><h2 class="text-lg">Workspace löschen</h2></div>
|
|
<div class="card-body stack stack-sm">
|
|
<p class="text-sm">Der Workspace kommt in den Papierkorb und ist sofort für alle unsichtbar. Workspace-Admins können ihn dort {{ trash_days }} Tage lang wiederherstellen; danach wird er endgültig gelöscht, mit Quellen, Chats, Freigabeanträgen, Uploads und Tool-Vorgaben.</p>
|
|
<p class="text-sm text-muted">Repositories und Datenbanken bleiben unberührt, ebenso Branches und Pull Requests, die schon erstellt wurden. Audit-Log und Verbrauch bleiben erhalten.{% if memory_enabled %} Beim endgültigen Löschen wird der Munin-Key entfernt; Projekt und Key müssen danach in Munin gelöscht oder widerrufen werden.{% endif %}</p>
|
|
<form method="post" action="{{ wbase }}/delete" class="cluster" data-confirm="Workspace {{ workspace.name }} in den Papierkorb legen?">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input name="confirmation" required autocomplete="off" placeholder="{{ workspace.name }}" aria-label="Name des Workspace zur Bestätigung">
|
|
<button class="btn btn-danger btn-sm" type="submit">In den Papierkorb</button>
|
|
</form>
|
|
<p class="text-xs text-muted">Zur Bestätigung den Namen „{{ workspace.name }}“ eingeben.</p>
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if tab == 'members' %}
|
|
<section class="card" id="members">
|
|
<div class="card-header"><h2 class="text-lg">Mitglieder</h2></div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">Mitglieder sehen alle Chats, Quellen und Dateien und können chatten. Admins verwalten Quellen, Mitglieder und Tools und entscheiden Freigaben.</p>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>Name</th><th>Nutzername</th><th>Rolle</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for u, role in members %}
|
|
<tr>
|
|
<td>{{ u.display_name }}{% if u.id == p.user_id %} <span class="text-xs text-muted">(du)</span>{% endif %}</td>
|
|
<td><code class="text-sm">{{ u.username }}</code></td>
|
|
<td>
|
|
<form method="post" action="{{ wbase }}/members/{{ u.id }}/role" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<select name="role" aria-label="Rolle von {{ u.display_name }}">
|
|
{% for value, label in role_labels|dictsort|reverse %}<option value="{{ value }}" {% if role == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
|
|
</select>
|
|
<button class="btn btn-ghost btn-sm" type="submit">Ändern</button>
|
|
</form>
|
|
</td>
|
|
<td>
|
|
<form method="post" action="{{ wbase }}/members/{{ u.id }}/remove" data-confirm="{{ u.display_name }} aus dem Workspace entfernen?">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}"><button class="btn btn-ghost btn-sm" type="submit">Entfernen</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<h3 class="text-base">Mitglied hinzufügen</h3>
|
|
<form method="post" action="{{ wbase }}/members" class="cluster" hx-post="{{ wbase }}/members" hx-target="#member-result">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input name="username" placeholder="Nutzername" required aria-label="Nutzername">
|
|
<select name="role" aria-label="Rolle">{% for value, label in role_labels|dictsort|reverse %}<option value="{{ value }}">{{ label }}</option>{% endfor %}</select>
|
|
<button class="btn btn-secondary btn-sm" type="submit">Hinzufügen</button>
|
|
</form>
|
|
<p class="text-xs text-muted">Nur Mitglieder des Teams {{ team.name }} kommen in Frage. Freigeber entscheiden Freigabeanträge wie Admins, verwalten den Workspace aber nicht.</p>
|
|
<div id="member-result"></div>
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if tab == 'tools' %}
|
|
<section class="card" id="tools">
|
|
<div class="card-header"><h2 class="text-lg">Tools</h2></div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">Was das Modell in diesem Workspace darf. Die Einstufung legt der Plattform-Admin fest; hier lässt sie sich nur verschärfen („wie global“ übernimmt die Einstufung unverändert). Aufrufe von Tools mit „Freigabe nötig“ warten auf die Entscheidung eines Workspace-Admins oder Freigebers.</p>
|
|
{% if saved_tool %}<div class="alert alert-success" role="status"><div class="alert-content"><p>Gespeichert: <code>{{ saved_tool.definition.name }}</code> ist jetzt {{ labels[saved_tool.verdict.decision] }} ({{ 'Vorgabe des Workspace' if saved_tool.override else 'wie global' }}).</p></div></div>{% endif %}
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Tool</th><th>Wirksam</th><th>Workspace-Vorgabe</th></tr></thead>
|
|
<tbody>
|
|
{% for t in tools %}
|
|
<tr>
|
|
<td><code>{{ t.definition.name }}</code>{% if not t.available %} <span class="text-xs text-muted">{% if t.definition.kind.startswith('mcp:') %}(<a href="{{ wbase }}/settings?tab=mcp">MCP-Server im Workspace nicht freigeschaltet</a>){% elif t.definition.kind.startswith('hugin') %}(<a href="{{ wbase }}/settings?tab=hugin">keine passenden Pakete aus Hugin</a>){% else %}(keine passende Quelle){% endif %}</span>{% endif %}</td>
|
|
<td><span class="badge {{ 'badge-success' if t.verdict.decision == 'allow' and t.available else 'badge-default' if t.verdict.decision == 'allow' else 'badge-warning' if t.verdict.decision == 'require_approval' else 'badge-danger' }}">{{ labels[t.verdict.decision] }}</span>
|
|
<span class="text-xs text-muted">{{ t.verdict.reason }}</span>
|
|
{% if t.verdict.level == 'global_approver' %}<p class="text-xs text-muted">Freigabe durch globalen Approver</p>{% endif %}
|
|
{% if t.verdict.four_eyes %}<p class="text-xs text-muted">Vier-Augen-Prinzip</p>{% endif %}</td>
|
|
<td>
|
|
<form method="post" action="{{ wbase }}/tools/{{ t.definition.name }}" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<select name="decision" aria-label="Vorgabe">
|
|
<option value="">wie global</option>
|
|
<option value="require_approval" {% if t.override == 'require_approval' %}selected{% endif %}>Freigabe nötig</option>
|
|
<option value="deny" {% if t.override == 'deny' %}selected{% endif %}>sperren</option>
|
|
</select>
|
|
<label class="cluster text-sm" title="Niemand darf seinen eigenen Antrag freigeben"><input type="checkbox" name="four_eyes" value="1" {% if t.four_eyes_override %}checked{% endif %}> Vier Augen</label>
|
|
<button class="btn btn-ghost btn-sm" type="submit">Setzen</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
</div>
|
|
</section>
|
|
|
|
{% set rule_sources = sources|selectattr('kind', 'in', ['git'])|list %}
|
|
<section class="card" id="source-rules">
|
|
<div class="card-header"><h2 class="text-lg">Regeln je Quelle</h2></div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">Verschärft ein Tool nur für ein bestimmtes Repository, z. B. <code>commit_change</code> auf dem Produktiv-Repository nur mit Freigabe. Datenbanken und andere MCP-Verbindungen haben eigene Tools (z. B. <code>shop__query</code>); dort gilt die Vorgabe oben unter „Tools“ direkt für diese Verbindung. Auch lesende Tools können so eine Freigabe verlangen; die KI erhält das Ergebnis dann erst nach der Entscheidung.</p>
|
|
{% if not rule_sources %}<p class="text-muted text-sm">Es gibt noch keine Quelle, für die Regeln möglich sind.</p>{% else %}
|
|
{% set source_names = {} %}{% for s in sources %}{% set _ = source_names.update({s.id: s.name}) %}{% endfor %}
|
|
{% if source_rules %}
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Quelle</th><th>Tool</th><th>Regel</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for r in source_rules %}
|
|
<tr>
|
|
<td>{{ source_names.get(r.source_id, r.source_id) }}</td>
|
|
<td><code>{{ r.tool_name }}</code></td>
|
|
<td>{% if r.decision %}<span class="badge {{ 'badge-warning' if r.decision == 'require_approval' else 'badge-danger' }}">{{ labels[r.decision] }}</span>{% endif %}
|
|
{% if r.four_eyes %}<span class="badge badge-info">Vier Augen</span>{% endif %}</td>
|
|
<td><form method="post" action="{{ wbase }}/source-rules">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input type="hidden" name="rule" value="{{ r.source_id }}:{{ r.tool_name }}">
|
|
<button class="btn btn-ghost btn-sm" type="submit">Entfernen</button>
|
|
</form></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
{% endif %}
|
|
<form method="post" action="{{ wbase }}/source-rules" class="cluster aiw-filters">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<label class="text-sm">Quelle und Tool
|
|
<select name="rule" required>
|
|
<option value="">bitte wählen</option>
|
|
{% for s in rule_sources %}
|
|
<optgroup label="{{ s.name }}">
|
|
{% for t in tools if t.definition.kind == s.kind %}<option value="{{ s.id }}:{{ t.definition.name }}">{{ t.definition.name }}</option>{% endfor %}
|
|
</optgroup>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<label class="text-sm">Regel
|
|
<select name="decision">
|
|
<option value="require_approval">Freigabe nötig</option>
|
|
<option value="deny">sperren</option>
|
|
<option value="">wie Workspace (nur Vier Augen)</option>
|
|
</select>
|
|
</label>
|
|
<label class="cluster text-sm"><input type="checkbox" name="four_eyes" value="1"> Vier Augen</label>
|
|
<button class="btn btn-secondary btn-sm" type="submit">Regel setzen</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if tab == 'mcp' %}
|
|
<section class="card" id="mcp">
|
|
<div class="card-header"><h2 class="text-lg">Zentrale Server</h2></div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">Dienste, die der Plattform-Admin registriert hat. Erst freigeschaltet bieten sie ihre eingestuften Tools in diesem Workspace an. Für Munin das Token des Projekts eintragen; es bestimmt, in welches Gedächtnis geschrieben wird.</p>
|
|
{% set waiting = mcp_servers|selectattr('1', 'none')|selectattr('0.active')|list %}
|
|
{% if waiting %}<div class="alert alert-info" role="status"><div class="alert-content"><p>{{ waiting|length }} Server {{ 'ist' if waiting|length == 1 else 'sind' }} verfügbar, aber in diesem Workspace noch nicht freigeschaltet.</p></div></div>{% endif %}
|
|
{% if mcp_servers %}
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Server</th><th>Status</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for s, link in mcp_servers %}
|
|
<tr>
|
|
<td>{{ s.label }} <code class="text-xs">{{ s.name }}</code>{% if s.role == 'memory' %} <span class="badge badge-info">Gedächtnis</span>{% endif %}</td>
|
|
<td>{% if link and s.active %}<span class="badge badge-success">freigeschaltet</span>{% if link.secret_hint %} <span class="text-xs text-muted">eigenes Token …{{ link.secret_hint }}</span>{% endif %}{% elif link %}<span class="badge badge-default">Server inaktiv</span>{% else %}<span class="badge badge-default">aus</span>{% endif %}</td>
|
|
<td>
|
|
{% if link %}
|
|
<form method="post" action="{{ wbase }}/mcp/{{ s.id }}/disable">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<button class="btn btn-ghost btn-sm" type="submit">Ausschalten</button>
|
|
</form>
|
|
{% elif s.active %}
|
|
<form method="post" action="{{ wbase }}/mcp/{{ s.id }}" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input name="token" type="password" autocomplete="off" placeholder="eigenes Token (optional)" aria-label="Eigenes Token">
|
|
<button class="btn btn-secondary btn-sm" type="submit">Freischalten</button>
|
|
</form>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
{% else %}<p class="text-muted text-sm">Es ist noch kein MCP-Server registriert.</p>{% endif %}
|
|
</div>
|
|
</section>
|
|
{% set own = connections|rejectattr('template_id')|list %}
|
|
{% set dbs = connections|selectattr('template_id')|list %}
|
|
{% macro connection_rows(items) %}
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Verbindung</th><th>Status</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for s in items %}
|
|
<tr>
|
|
<td>{{ s.label }} <code class="text-xs">{{ s.name }}__…</code>
|
|
<p class="text-xs text-muted">{% if s.template_id %}{{ s.config.user }}@{{ s.config.host }}{% if s.config.port %}:{{ s.config.port }}{% endif %}/{{ s.config.database }}{% else %}<code>{{ s.url }}</code>{% if s.secret_hint %} · Token …{{ s.secret_hint }}{% endif %}{% if s.header_names %} · Header: {{ s.header_names|join(', ') }}{% endif %}{% endif %}</p></td>
|
|
<td>{% if s.approval == 'pending' %}<span class="badge badge-warning">wartet auf Freigabe</span>{% elif s.approval == 'rejected' %}<span class="badge badge-danger">abgelehnt</span>{% elif not s.active %}<span class="badge badge-default">inaktiv</span>{% elif s.status == 'ok' %}<span class="badge badge-success">ok</span>{% elif s.status == 'error' %}<span class="badge badge-danger">Fehler</span>{% else %}<span class="badge badge-default">neu</span>{% endif %}
|
|
{% if s.status_message %}<p class="text-xs text-muted">{{ s.status_message }}</p>{% endif %}</td>
|
|
<td class="cluster">
|
|
{% if s.approval == 'approved' %}
|
|
<form method="post" action="/connections/{{ s.id }}/test">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<button class="btn btn-ghost btn-sm" type="submit">Testen</button>
|
|
</form>
|
|
{% endif %}
|
|
<a class="btn btn-ghost btn-sm" href="/connections/{{ s.id }}/edit">Bearbeiten</a>
|
|
<form method="post" action="/connections/{{ s.id }}/delete" data-confirm="Verbindung {{ s.label }} entfernen? Ihre Tools und Regeln gibt es dann in diesem Workspace nicht mehr.">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<button class="btn btn-ghost btn-sm" type="submit">Entfernen</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
{% endmacro %}
|
|
<section class="card" id="connections">
|
|
<div class="card-header cluster"><h2 class="text-lg flex-1">Eigene Verbindungen</h2>
|
|
<a class="btn btn-secondary btn-sm" href="{{ wbase }}/connections/new">MCP-Server hinzufügen</a></div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">MCP-Server nur für diesen Workspace, mit eigenem Token und eigenen Headern. Nutzt die Plattform die Adresse schon, ist die Verbindung sofort aktiv; sonst wartet sie auf die Freigabe eines Plattform-Admins. Neue Tools sind gesperrt, bis sie eingestuft sind; gleiche Tools desselben Dienstes übernehmen die Einstufung.</p>
|
|
{% if own %}{{ connection_rows(own) }}{% else %}<p class="text-muted text-sm">Noch keine eigene Verbindung.</p>{% endif %}
|
|
</div>
|
|
</section>
|
|
<section class="card" id="databases">
|
|
<div class="card-header cluster"><h2 class="text-lg flex-1">Datenbanken</h2>
|
|
{% if db_drivers %}<a class="btn btn-secondary btn-sm" href="{{ wbase }}/databases/new">Datenbank hinzufügen</a>{% endif %}</div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">Lesender Zugriff auf eine Datenbank. Den Treiber je Datenbanktyp stellt die Plattform; Host, Nutzer und Passwort trägst du hier ein. Die Zugangsdaten liegen verschlüsselt in Bifröst; das Modell sieht sie nie. Die Tools heißen nach dem Kurznamen, z. B. <code>shop__query</code>; Vorgaben dafür unter „Tools“ gelten nur für diese Datenbank.</p>
|
|
{% if not db_drivers %}<p class="text-muted text-sm">Es gibt noch keinen Datenbank-Treiber; den richtet ein Plattform-Admin unter Administration → Datenbank-Treiber ein.</p>{% endif %}
|
|
{% if dbs %}{{ connection_rows(dbs) }}{% elif db_drivers %}<p class="text-muted text-sm">Noch keine Datenbank.</p>{% endif %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
{% if tab == 'hugin' %}{% set h = hugin %}
|
|
<section class="card" id="hugin">
|
|
<div class="card-header cluster"><h2 class="text-lg flex-1">Pakete aus Hugin</h2>
|
|
{% if h.host and h.access %}<span class="cluster"><a class="btn btn-ghost btn-sm" href="{{ wbase }}/hugin/import">Aus .hugin.yaml</a>
|
|
<a class="btn btn-secondary btn-sm" href="{{ wbase }}/hugin/catalog">Katalog durchsuchen</a></span>{% endif %}</div>
|
|
<div class="card-body stack">
|
|
<p class="text-sm text-muted">Hugin ist das Register für geprüfte KI-Erweiterungen. Regeln gelten in jedem Chat dieses Workspace, Skills lädt die KI bei Bedarf, Befehle ruft man im Chat mit <code>/name</code> auf. Neue Versionen kommen erst, wenn du abgleichst und übernimmst; Pflichtpakete eures Tenants kommen immer mit. Inhalte aus Hugin verleihen keine Rechte.</p>
|
|
{% if not h.host %}<p class="text-muted text-sm">Hugin ist auf dieser Plattform noch nicht eingerichtet; den Host trägt ein Plattform-Admin unter Administration → Hugin ein.</p>
|
|
{% else %}
|
|
<h3 class="text-base">Zugang</h3>
|
|
<p class="text-sm text-muted">Integrations-Key (<code>hi_…</code>) eures Tenants in Hugin unter <code>{{ h.host.url }}</code>, dort unter <em>Integrationen</em> mit den Rollen Lesen und Ausliefern. Der Tenant bestimmt, welche Pakete und Pflichtpakete hier gelten. Der Key liegt verschlüsselt in Bifröst.</p>
|
|
{% set a = h.access %}
|
|
{% if a %}
|
|
<p class="cluster text-sm">Key …{{ a.hint }}
|
|
{% if a.status == 'ok' %}<span class="badge badge-success">ok</span>{% elif a.status == 'error' %}<span class="badge badge-danger">Fehler</span>{% else %}<span class="badge badge-default">ungeprüft</span>{% endif %}
|
|
{% if a.tenant %}<span>Tenant <code>{{ a.tenant }}</code>{% if a.key_name %}, Key „{{ a.key_name }}“{% endif %}</span>{% endif %}
|
|
{% if a.status_message %}<span class="text-xs text-muted">{{ a.status_message }}</span>{% endif %}</p>
|
|
<div class="cluster">
|
|
<form method="post" action="{{ wbase }}/hugin/access/test">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}"><button class="btn btn-ghost btn-sm" type="submit">Testen</button>
|
|
</form>
|
|
<form method="post" action="{{ wbase }}/hugin/access/remove" data-confirm="Hugin-Key entfernen? Festgeschriebene Pakete gelten weiter, Abgleichen und Rückmeldungen gehen nicht mehr.">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}"><button class="btn btn-ghost btn-sm" type="submit">Entfernen</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
<form method="post" action="{{ wbase }}/hugin/access" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input name="key" type="password" autocomplete="off" required placeholder="hi_…" aria-label="Integrations-Key">
|
|
<button class="btn btn-secondary btn-sm" type="submit">{{ 'Key ersetzen' if a else 'Speichern und testen' }}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if h.host and h.access %}
|
|
{% if h.findings %}
|
|
<div class="alert alert-info" role="status"><div class="alert-content">
|
|
<p>Abgleich vom {{ h.access.synced.strftime('%d.%m.%Y %H:%M') }}:</p>
|
|
<ul>{% for line in h.findings %}<li>{{ line }}</li>{% endfor %}</ul>
|
|
<p>Mit <em>Abgleichen</em> ansehen und übernehmen; automatisch übernommen wird nichts.</p>
|
|
</div></div>
|
|
{% elif h.access.synced %}<p class="text-xs text-muted">Zuletzt abgeglichen {{ h.access.synced.strftime('%d.%m.%Y %H:%M') }}: alles aktuell.</p>{% endif %}
|
|
<h3 class="text-base">Gewählt</h3>
|
|
{% if h.subscriptions %}
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Paket</th><th>Version</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for s in h.subscriptions %}
|
|
<tr><td><code>{{ s.package_id }}</code></td><td>{{ s.spec }}</td>
|
|
<td><form method="post" action="{{ wbase }}/hugin/packages/{{ s.package_id }}/remove">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}"><button class="btn btn-ghost btn-sm" type="submit">Entfernen</button></form></td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
{% else %}<p class="text-muted text-sm">Noch kein Paket gewählt.</p>{% endif %}
|
|
<form method="post" action="{{ wbase }}/hugin/packages" class="cluster">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<input name="package" required placeholder="z. B. pub-release-notiz@^1" aria-label="Paket">
|
|
<button class="btn btn-secondary btn-sm" type="submit">Wählen</button>
|
|
</form>
|
|
<form method="post" action="{{ wbase }}/hugin/check">
|
|
<input type="hidden" name="_csrf" value="{{ p.csrf }}">
|
|
<button class="btn btn-primary btn-sm" type="submit">Abgleichen</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
<section class="card" id="hugin-locks">
|
|
<div class="card-header"><h2 class="text-lg">Festgeschrieben</h2></div>
|
|
<div class="card-body stack">
|
|
{% if h.locks %}
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Paket</th><th>Art</th><th>Version</th><th>Stand</th></tr></thead>
|
|
<tbody>
|
|
{% for l in h.locks %}
|
|
<tr>
|
|
<td><code>{% if l.kind == 'command' %}/{% endif %}{{ l.package_id }}</code>{% if l.mandatory %} <span class="badge badge-info">Pflicht</span>{% endif %}{% if l.deprecated %} <span class="badge badge-warning">veraltet</span>{% endif %}
|
|
{% if l.description %}<p class="text-xs text-muted">{{ l.description }}</p>{% endif %}</td>
|
|
<td>{{ h.kind_names.get(l.kind, l.kind) }}</td>
|
|
<td>{{ l.version }}</td>
|
|
<td>{% if l.unsupported %}<span class="badge badge-default">nicht angeboten</span><p class="text-xs text-muted">{{ l.unsupported }}</p>{% else %}<span class="badge badge-success">aktiv</span>{% endif %}
|
|
<p class="text-xs text-muted">{{ l.locked.strftime('%d.%m.%Y %H:%M') }}</p></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table></div>
|
|
{% else %}<p class="text-muted text-sm">Noch nichts übernommen.</p>{% endif %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
{% endblock %}
|