SysOps: deploy-all — 2026-06-09 10:41 UTC
This commit is contained in:
+221
-49
@@ -1,8 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block extra_head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/hermes.css" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=5" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
|
||||
<link rel="stylesheet" href="/static/css/agents-mesh.css" />
|
||||
<link rel="stylesheet" href="/static/css/agent-characters.css?v=3" />
|
||||
<script src="/static/js/agent-avatars.js?v=3"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="hm-neo" x-data="agentsSoulHub()" x-init="init()">
|
||||
@@ -11,6 +14,15 @@
|
||||
<p class="hm-sub">Animated team · duidelijke rollen · soul.md · live activiteit</p>
|
||||
</header>
|
||||
|
||||
<div id="viz-toolbar" class="viz-toolbar"></div>
|
||||
<div class="hm-chart-panel page-viz-panel viz-mode-chart agents-viz-compact" style="margin-bottom:1rem">
|
||||
<div class="hm-chart-head"><h4>Agent activiteit (events)</h4><small>5 visualisatie-modi</small></div>
|
||||
<div class="page-viz-body">
|
||||
<div class="hm-chart-wrap page-viz-canvas"><canvas id="chart-agents-viz"></canvas></div>
|
||||
<div id="viz-agents-alt" class="viz-alt"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vtabs-layout" style="--vt-color:#ffd700" x-ref="vtabsWrap">
|
||||
<nav class="vtabs-nav" x-ref="vtabsNav" :style="'--vt-indicator-top:'+tabIndicatorTop+'px;--vt-indicator-h:'+tabIndicatorH+'px'">
|
||||
<button class="vtab-btn" :class="{active: tab==='souls'}" @click="setTab('souls', $el)">
|
||||
@@ -19,6 +31,10 @@
|
||||
<button class="vtab-btn" :class="{active: tab==='mesh'}" @click="setTab('mesh', $el)">
|
||||
<span class="vtab-icon">🕸️</span><span>Netwerk</span>
|
||||
</button>
|
||||
<button class="vtab-btn" :class="{active: tab==='approvals'}" @click="setTab('approvals', $el)">
|
||||
<span class="vtab-icon">✓</span><span>Goedkeuringen</span>
|
||||
<span class="vtab-badge" x-show="pendingCount" x-text="pendingCount"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="vtabs-content">
|
||||
@@ -27,54 +43,41 @@
|
||||
<div class="hm-neo-kpi"><div class="hm-neo-kpi-top"><div class="hm-neo-ring" style="--ring-color:#00e5ff;--ring-pct:80%"><div class="hm-neo-ring-inner">🤖</div></div><div><div class="hm-neo-kpi-label">Agents</div><div class="hm-neo-kpi-sub">geregistreerd</div></div></div><div class="hm-neo-kpi-value" x-text="souls.length">0</div><div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div></div>
|
||||
<div class="hm-neo-kpi"><div class="hm-neo-kpi-top"><div class="hm-neo-ring" style="--ring-color:#b8ff3c;--ring-pct:70%"><div class="hm-neo-ring-inner">⚡</div></div><div><div class="hm-neo-kpi-label">Events</div><div class="hm-neo-kpi-sub">totaal gelogd</div></div></div><div class="hm-neo-kpi-value" x-text="totalEvents">0</div><div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div></div>
|
||||
<div class="hm-neo-kpi"><div class="hm-neo-kpi-top"><div class="hm-neo-ring" style="--ring-color:#ff9f43;--ring-pct:50%"><div class="hm-neo-ring-inner">✓</div></div><div><div class="hm-neo-kpi-label">Actief</div><div class="hm-neo-kpi-sub">agents online</div></div></div><div class="hm-neo-kpi-value" x-text="activeCount">0</div><div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div></div>
|
||||
<div class="hm-neo-kpi"><div class="hm-neo-kpi-top"><div class="hm-neo-ring" style="--ring-color:#ffd700;--ring-pct:90%"><div class="hm-neo-ring-inner">👑</div></div><div><div class="hm-neo-kpi-label">Herman</div><div class="hm-neo-kpi-sub">orchestrator</div></div></div><div class="hm-neo-kpi-value">CEO</div><div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div></div>
|
||||
<div class="hm-neo-kpi"><div class="hm-neo-kpi-top"><div class="hm-neo-ring" style="--ring-color:#ffd700;--ring-pct:90%"><div class="hm-neo-ring-inner">👑</div></div><div><div class="hm-neo-kpi-label">Herman</div><div class="hm-neo-kpi-sub">Co-CEO · takenverdeler</div></div></div><div class="hm-neo-kpi-value" style="font-size:1.1rem">Co-CEO</div><div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div></div>
|
||||
</div>
|
||||
|
||||
<div class="agents-neo-grid">
|
||||
<template x-for="s in souls" :key="s.agent_key">
|
||||
<article class="agent-soul-card" :class="{ selected: selected?.agent_key === s.agent_key }" @click="select(s.agent_key)">
|
||||
<div class="hm-neo-kpi-top">
|
||||
<div class="agent-avatar-wrap">
|
||||
<div class="agent-avatar" :style="'--av-color:' + (s.avatar_color || '#00e5ff')">
|
||||
<div class="agent-face" :class="[avatarStyle(s.agent_key), 'mood-' + (s.avatar_mood || 'happy')]">
|
||||
<span class="hair" x-show="hasHair(s.agent_key)"></span>
|
||||
<span class="glasses" x-show="hasGlasses(s.agent_key)"></span>
|
||||
<span class="eye left"></span>
|
||||
<span class="eye right"></span>
|
||||
<span class="mouth"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="agent-status-dot" x-show="s.is_active !== false"></span>
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<strong x-text="s.display_name"></strong>
|
||||
<div class="hm-neo-kpi-sub" x-text="s.role_title"></div>
|
||||
<span class="agent-tag" x-text="s.agent_key"></span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="agent-role-desc" x-text="s.responsibilities || 'Geen beschrijving'"></p>
|
||||
<div class="agent-char-wrap" :class="s.agent_key"
|
||||
:data-agent-avatar="s.agent_key"
|
||||
:data-agent-color="s.avatar_color || '#1e3a5f'"
|
||||
x-init="$nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, s.agent_key, s.avatar_color))"></div>
|
||||
<strong x-text="s.display_name"></strong>
|
||||
<div class="hm-neo-kpi-sub" x-text="s.role_title"></div>
|
||||
<span class="agent-status-pill" :class="statusClass(s)">
|
||||
<span class="dot"></span>
|
||||
<span x-text="statusLabel(s)"></span>
|
||||
</span>
|
||||
<p class="agent-current-task" x-text="s.current_task || 'Wacht op taak…'"></p>
|
||||
<p class="agent-role-desc" x-text="(s.responsibilities || '').substring(0,120)"></p>
|
||||
<div class="agent-meta-row">
|
||||
<small x-text="(s.event_count||0) + ' events'"></small>
|
||||
<small x-text="s.last_event_at ? 'Laatst: '+s.last_event_at.substring(0,10) : 'Nog geen activiteit'"></small>
|
||||
<button type="button" class="agent-events-link"
|
||||
@click.stop="openEvents(s.agent_key, s.display_name)"
|
||||
x-text="(s.event_count||0) + ' events'"
|
||||
:title="'Bekijk wat ' + s.display_name + ' heeft gedaan'"></button>
|
||||
<span class="agent-tag" x-text="s.agent_key"></span>
|
||||
</div>
|
||||
<div class="hm-eq" style="margin-top:0.5rem"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="agent-soul-editor" x-show="selected" x-cloak>
|
||||
<div class="agent-editor-header">
|
||||
<div class="agent-avatar-wrap">
|
||||
<div class="agent-avatar" :style="'--av-color:' + (selected?.avatar_color || '#00e5ff')">
|
||||
<div class="agent-face" :class="[avatarStyle(selected?.agent_key), 'mood-' + (selected?.avatar_mood || 'happy')]">
|
||||
<span class="hair" x-show="hasHair(selected?.agent_key)"></span>
|
||||
<span class="glasses" x-show="hasGlasses(selected?.agent_key)"></span>
|
||||
<span class="eye left"></span>
|
||||
<span class="eye right"></span>
|
||||
<span class="mouth"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-char-wrap large" :class="selected?.agent_key"
|
||||
:data-agent-avatar="selected?.agent_key"
|
||||
:data-agent-color="selected?.avatar_color || '#1e3a5f'"
|
||||
x-effect="selected && $nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, selected.agent_key, selected.avatar_color))"></div>
|
||||
<div style="flex:1">
|
||||
<h2><span x-text="selected?.display_name"></span> — <span x-text="selected?.role_title"></span></h2>
|
||||
<p style="margin:0.25rem 0 0;color:#94a3b8;font-size:0.85rem" x-text="selected?.responsibilities"></p>
|
||||
@@ -87,9 +90,16 @@
|
||||
<label>Rol titel<input class="form-input" x-model="form.role_title"></label>
|
||||
<label>Functie & werkzaamheden<textarea class="form-input" rows="3" x-model="form.responsibilities"></textarea></label>
|
||||
<label>Soul.md (karakter, tone of voice, regels)<textarea x-model="form.soul_md"></textarea></label>
|
||||
<h3 style="font-size:0.75rem;text-transform:uppercase;color:#64748b;margin-top:1rem">Recente activiteit</h3>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:1rem">
|
||||
<h3 style="font-size:0.75rem;text-transform:uppercase;color:#64748b;margin:0">Recente activiteit</h3>
|
||||
<button type="button" class="agent-events-link" x-show="selected" @click="openEvents(selected.agent_key, selected.display_name)">Alle events →</button>
|
||||
</div>
|
||||
<template x-for="ev in (selected?.recent_events||[])" :key="ev.id">
|
||||
<div class="hub-feed-item"><span x-text="ev.created_at?.substring(0,16)"></span> <span x-text="ev.title||ev.event_type"></span></div>
|
||||
<button type="button" class="hub-feed-item agent-event-clickable" @click="openEvents(selected.agent_key, selected.display_name, ev.id)">
|
||||
<span class="agent-event-time" x-text="formatEventTime(ev.created_at)"></span>
|
||||
<span class="agent-event-title" x-text="ev.title||ev.event_type"></span>
|
||||
<span class="badge badge-sm" x-text="ev.status||'—'"></span>
|
||||
</button>
|
||||
</template>
|
||||
<p x-show="!(selected?.recent_events||[]).length" class="muted">Nog geen events voor deze agent.</p>
|
||||
</div>
|
||||
@@ -98,7 +108,7 @@
|
||||
<section x-show="tab==='mesh'" x-cloak>
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0;">Netwerk</h2>
|
||||
<p class="page-tip" style="margin-top:.25rem;">Communication mesh met Herman als centrale Co-CEO hub.</p>
|
||||
<p class="page-tip" style="margin-top:.25rem;">Agents onderaan → Herman (Co-CEO) → CEO & CTO. Pulse = actieve communicatie.</p>
|
||||
<div class="mesh-wrap">
|
||||
<svg id="agents-mesh-canvas" class="mesh-svg" viewBox="0 0 1100 640"></svg>
|
||||
</div>
|
||||
@@ -110,11 +120,82 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-show="tab==='approvals'" x-cloak x-data="approvalsPanel()" x-init="load()">
|
||||
<div class="panel">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.75rem;margin-bottom:1rem">
|
||||
<div>
|
||||
<h2 style="margin:0;">Approval queue</h2>
|
||||
<p class="page-tip">Agents dienen queries in → jij keurt goed → pas daarna mag de actie uitgevoerd worden.</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-pulse" @click="load()">↻ Verversen</button>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>#</th><th>Agent</th><th>Actie</th><th>Titel</th><th>Status</th><th>Tijd</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<template x-for="r in items" :key="r.id">
|
||||
<tr>
|
||||
<td x-text="r.id"></td>
|
||||
<td x-text="r.agent_key"></td>
|
||||
<td x-text="r.action_type"></td>
|
||||
<td x-text="r.title"></td>
|
||||
<td><span class="badge" :class="r.status==='pending'?'badge-warn':'badge-positive'" x-text="r.status"></span></td>
|
||||
<td x-text="(r.created_at||'').substring(0,16).replace('T',' ')"></td>
|
||||
<td>
|
||||
<template x-if="r.status==='pending'">
|
||||
<span>
|
||||
<button class="btn btn-sm btn-approve" @click="approve(r.id)">✓</button>
|
||||
<button class="btn btn-sm btn-reject" @click="reject(r.id)">✗</button>
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<p x-show="!items.length" class="muted">Geen approval requests.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-events-backdrop" x-show="eventsOpen" x-cloak @click="closeEvents()"></div>
|
||||
<aside class="agent-events-drawer" :class="{ open: eventsOpen }" x-cloak>
|
||||
<header class="agent-events-head">
|
||||
<div>
|
||||
<h3 x-text="eventsAgentName || 'Agent'"></h3>
|
||||
<p class="muted" x-text="eventsList.length + ' activiteiten gelogd'"></p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm" @click="closeEvents()">✕</button>
|
||||
</header>
|
||||
<div class="agent-events-body" x-show="!eventsLoading">
|
||||
<template x-for="ev in eventsList" :key="ev.id">
|
||||
<article class="agent-event-card" :class="{ expanded: selectedEvent?.id === ev.id }" @click="toggleEventDetail(ev)">
|
||||
<div class="agent-event-card-top">
|
||||
<time x-text="formatEventTime(ev.created_at)"></time>
|
||||
<span class="badge badge-sm" :class="eventStatusClass(ev.status)" x-text="ev.status || 'completed'"></span>
|
||||
</div>
|
||||
<strong x-text="ev.title || ev.event_type"></strong>
|
||||
<div class="agent-event-meta">
|
||||
<span x-text="ev.event_type"></span>
|
||||
<span x-show="ev.channel" x-text="'· ' + ev.channel"></span>
|
||||
</div>
|
||||
<div class="agent-event-detail" x-show="selectedEvent?.id === ev.id" x-cloak>
|
||||
<p x-show="ev.body" x-text="ev.body"></p>
|
||||
<pre x-show="!ev.body && ev.metadata && Object.keys(ev.metadata||{}).length" x-text="JSON.stringify(ev.metadata, null, 2)"></pre>
|
||||
<p x-show="!ev.body && !(ev.metadata && Object.keys(ev.metadata||{}).length)" class="muted">Geen extra details voor dit event.</p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
<p x-show="!eventsList.length && !eventsLoading" class="muted" style="padding:1rem">Nog geen events voor deze agent.</p>
|
||||
</div>
|
||||
<p x-show="eventsLoading" class="muted" style="padding:1.5rem">Events laden…</p>
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/dashboard-layout.js?v=2"></script>
|
||||
<script src="/static/js/page-viz.js?v=1"></script>
|
||||
<script src="/static/js/agents-mesh.js"></script>
|
||||
<script>
|
||||
function agentsSoulHub() {
|
||||
@@ -123,22 +204,67 @@ function agentsSoulHub() {
|
||||
tabIndicatorTop: 0,
|
||||
tabIndicatorH: 40,
|
||||
meshLoaded: false,
|
||||
pendingCount: 0,
|
||||
souls: [], selected: null, form: {}, saving: false,
|
||||
eventsOpen: false, eventsAgentKey: null, eventsAgentName: '', eventsList: [], eventsLoading: false, selectedEvent: null,
|
||||
get totalEvents() { return this.souls.reduce((a,s) => a + (s.event_count||0), 0); },
|
||||
get activeCount() { return this.souls.filter(s => s.is_active !== false).length; },
|
||||
avatarStyle(key) {
|
||||
const m = {
|
||||
herman: 'style-male-glasses', design: 'style-female-long', marketing: 'style-female-bob',
|
||||
retail: 'style-male-short', research: 'style-male-glasses', sales: 'style-male-curl',
|
||||
hermes: 'style-male-short', finance: 'style-male-glasses', ops: 'style-male-short',
|
||||
quality: 'style-female-bob', logistics: 'style-male-curl', legal: 'style-male-glasses',
|
||||
};
|
||||
return m[key] || 'style-male-short';
|
||||
statusClass(s) {
|
||||
const st = (s.current_status || '').toLowerCase();
|
||||
if (st === 'needs_approval' || st === 'pending') return 'busy';
|
||||
if (st === 'error' || st === 'rejected') return 'error';
|
||||
if (st === 'completed' || (s.events_6h > 0)) return 'active';
|
||||
return 'idle';
|
||||
},
|
||||
formatEventTime(ts) {
|
||||
return (ts || '').substring(0, 16).replace('T', ' ');
|
||||
},
|
||||
eventStatusClass(status) {
|
||||
const s = (status || '').toLowerCase();
|
||||
if (s === 'pending' || s === 'needs_approval') return 'badge-warn';
|
||||
if (s === 'error' || s === 'rejected') return 'badge-negative';
|
||||
if (s === 'approved' || s === 'completed') return 'badge-positive';
|
||||
return '';
|
||||
},
|
||||
async openEvents(key, name, focusId) {
|
||||
if (!key) return;
|
||||
this.eventsAgentKey = key;
|
||||
this.eventsAgentName = name || key;
|
||||
this.eventsOpen = true;
|
||||
this.eventsLoading = true;
|
||||
this.selectedEvent = null;
|
||||
try {
|
||||
const r = await fetch('/api/agents/souls/' + encodeURIComponent(key) + '/events?limit=50').then(x => x.json());
|
||||
this.eventsList = r.items || [];
|
||||
if (focusId) {
|
||||
const hit = this.eventsList.find(e => e.id === focusId);
|
||||
if (hit) this.selectedEvent = hit;
|
||||
}
|
||||
} catch (e) { this.eventsList = []; }
|
||||
this.eventsLoading = false;
|
||||
},
|
||||
closeEvents() {
|
||||
this.eventsOpen = false;
|
||||
this.selectedEvent = null;
|
||||
},
|
||||
toggleEventDetail(ev) {
|
||||
this.selectedEvent = this.selectedEvent?.id === ev.id ? null : ev;
|
||||
},
|
||||
statusLabel(s) {
|
||||
const st = (s.current_status || '').toLowerCase();
|
||||
if (st === 'needs_approval') return 'Wacht op goedkeuring';
|
||||
if (st === 'pending') return 'In wachtrij';
|
||||
if (st === 'completed') return 'Klaar';
|
||||
if (st === 'error') return 'Fout';
|
||||
if (s.current_task) return 'Bezig';
|
||||
return 'Standby';
|
||||
},
|
||||
hasHair(key) { return !!key; },
|
||||
hasGlasses(key) { return ['herman','research','finance','legal'].includes(key); },
|
||||
async init() {
|
||||
await this.load();
|
||||
await this.loadPendingCount();
|
||||
if (window.PageViz) {
|
||||
window.PageViz.init('agents', (mode) => this.renderViz(mode));
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
const activeBtn = this.$refs.vtabsNav?.querySelector('.vtab-btn.active');
|
||||
if (activeBtn) this.updateTabIndicator(activeBtn);
|
||||
@@ -159,6 +285,13 @@ function agentsSoulHub() {
|
||||
url.searchParams.set('tab', next);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
if (next === 'mesh') this.mountMesh();
|
||||
if (next === 'approvals') this.loadPendingCount();
|
||||
},
|
||||
async loadPendingCount() {
|
||||
try {
|
||||
const r = await fetch('/api/agents/approvals?status=pending').then(x => x.json());
|
||||
this.pendingCount = (r.items || []).length;
|
||||
} catch (e) { this.pendingCount = 0; }
|
||||
},
|
||||
mountMesh() {
|
||||
if (this.meshLoaded || !window.AgentsMesh) return;
|
||||
@@ -169,6 +302,22 @@ function agentsSoulHub() {
|
||||
const r = await fetch('/api/agents/souls').then(x => x.json());
|
||||
this.souls = r.items || [];
|
||||
if (this.souls.length && !this.selected) this.select(this.souls[0].agent_key);
|
||||
this.renderViz(window.__vizMode || 'neo-bars');
|
||||
this.$nextTick(() => window.AgentAvatars && AgentAvatars.paintAll(this.$root));
|
||||
},
|
||||
renderViz(mode) {
|
||||
if (!window.VizEngine) return;
|
||||
const rows = (this.souls || []).map(s => ({
|
||||
label: s.agent_key,
|
||||
value: s.event_count || 0,
|
||||
raw: s.event_count || 0,
|
||||
})).sort((a, b) => b.raw - a.raw).slice(0, 12);
|
||||
if (!rows.length) rows.push({ label: 'geen', value: 0, raw: 0 });
|
||||
VizEngine.renderFromRows(mode,
|
||||
document.getElementById('chart-agents-viz'),
|
||||
document.getElementById('viz-agents-alt'),
|
||||
'agents-page', 'Events per agent', rows, 'hm-eq-lime'
|
||||
);
|
||||
},
|
||||
async select(key) {
|
||||
const r = await fetch('/api/agents/souls/' + key).then(x => x.json());
|
||||
@@ -188,5 +337,28 @@ function agentsSoulHub() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function approvalsPanel() {
|
||||
return {
|
||||
items: [],
|
||||
async load() {
|
||||
try {
|
||||
const r = await fetch('/api/agents/approvals?limit=50').then(x => x.json());
|
||||
this.items = r.items || [];
|
||||
} catch (e) { this.items = []; }
|
||||
},
|
||||
async approve(id) {
|
||||
await Cockpit.api('/api/agents/approvals/' + id + '/approve', { method: 'POST', body: '{}' });
|
||||
Cockpit.toast('Goedgekeurd — agent mag query uitvoeren', 'success');
|
||||
await this.load();
|
||||
},
|
||||
async reject(id) {
|
||||
const reason = prompt('Reden afwijzing (optioneel):') || '';
|
||||
await Cockpit.api('/api/agents/approvals/' + id + '/reject', { method: 'POST', body: JSON.stringify({ reason }) });
|
||||
Cockpit.toast('Afgewezen', 'info');
|
||||
await this.load();
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user