Files
DarkflameServer/dDashboardServer/templates/webhooks.jinja2
Aaron Kimbrell e213a7aeff feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:43 -05:00

145 lines
8.2 KiB
Django/Jinja

{% extends "base.jinja2" %}
{% block title %}Webhooks - DarkflameServer{% endblock %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-3">
<div>
<h2 class="mb-1">Webhooks</h2>
<p class="text-body-secondary mb-0">Send alerts to Discord, Slack or your own service when things happen on the server.</p>
</div>
<button class="btn btn-primary" id="addWebhook">Add webhook</button>
</div>
<div class="card mb-4">
<div class="card-body table-responsive">
<table class="table table-hover align-middle mb-0">
<thead><tr><th>Name</th><th>Destination</th><th>Events</th><th>Last delivery</th><th class="text-end">Actions</th></tr></thead>
<tbody id="webhookRows"><tr><td colspan="5" class="text-body-secondary">Loading&hellip;</td></tr></tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header"><h5 class="mb-0">Formats</h5></div>
<div class="card-body small">
<p class="mb-2"><strong>Discord</strong> and <strong>Slack</strong>: paste the channel's incoming webhook URL.</p>
<p class="mb-0"><strong>JSON</strong> posts <code>{event, title, description, fields, url, time}</code>. With a secret, each request carries
<code>X-DLU-Signature: sha256=&lt;hex HMAC-SHA256 of the body&gt;</code> so your service can check it came from here.
Links point into the dashboard when <code>dashboard_url</code> is set.</p>
</div>
</div>
<div class="modal fade" id="webhookModal" tabindex="-1" aria-labelledby="webhookModalTitle" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content" id="webhookForm">
<div class="modal-header"><h5 class="modal-title" id="webhookModalTitle">Webhook</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>
<div class="modal-body">
<div class="mb-3"><label class="form-label" for="whName">Name</label><input class="form-control" id="whName" maxlength="64" required></div>
<div class="mb-3"><label class="form-label" for="whUrl">URL</label><input class="form-control" id="whUrl" type="url" placeholder="https://discord.com/api/webhooks/...">
<div class="form-text" id="whUrlHelp"></div></div>
<div class="mb-3"><label class="form-label" for="whFormat">Format</label>
<select class="form-select" id="whFormat"><option value="discord">Discord</option><option value="slack">Slack</option><option value="json">JSON</option></select></div>
<fieldset class="mb-3"><legend class="form-label fs-6">Events</legend><div id="whEvents"></div></fieldset>
<div class="mb-3"><label class="form-label" for="whSecret">Signing secret <span class="text-body-secondary small">(JSON only, optional)</span></label>
<input class="form-control" id="whSecret" maxlength="128" autocomplete="off"><div class="form-text" id="whSecretHelp"></div></div>
<div class="form-check form-switch"><input class="form-check-input" type="checkbox" role="switch" id="whEnabled" checked><label class="form-check-label" for="whEnabled">Enabled</label></div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Save</button></div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
var webhooks = [], events = [], editing = null;
var modal = new bootstrap.Modal(document.getElementById('webhookModal'));
function lastDelivery(w) {
if (!w.lastSentAt) return '<span class="text-body-secondary">Never</span>';
var ok = w.lastStatus >= 200 && w.lastStatus < 300 && !w.lastError;
return (ok ? fmt.badge('OK', 'success') : '<span class="badge text-bg-danger"><i class="bi bi-x-octagon me-1" aria-hidden="true"></i>Failed</span>') +
' <span class="small text-body-secondary">' + esc(fmt.unix(w.lastSentAt)) + '</span>' +
(w.lastError ? '<div class="small text-danger">' + esc(w.lastError) + '</div>' : '');
}
function render() {
var rows = document.getElementById('webhookRows');
if (!webhooks.length) { rows.innerHTML = '<tr><td colspan="5" class="text-body-secondary">No webhooks yet.</td></tr>'; return; }
rows.innerHTML = webhooks.map(function (w) {
return '<tr><td>' + esc(w.name) + (w.enabled ? '' : ' ' + fmt.badge('Disabled', 'secondary')) + '</td>' +
'<td><span class="small">' + esc(w.format) + '</span><div class="small text-body-secondary text-break">' + esc(w.url) + '</div></td>' +
'<td class="small">' + esc(w.events === '*' ? 'Everything' : w.events.split(',').join(', ')) + '</td>' +
'<td>' + lastDelivery(w) + '</td>' +
'<td class="text-end text-nowrap"><button class="btn btn-sm btn-outline-secondary me-1" data-test="' + w.id + '">Test</button>' +
'<button class="btn btn-sm btn-outline-primary me-1" data-edit="' + w.id + '">Edit</button>' +
'<button class="btn btn-sm btn-outline-danger" data-delete="' + w.id + '">Remove</button></td></tr>';
}).join('');
}
function load() {
return api.get('/api/webhooks').then(function (r) { webhooks = r.webhooks || []; events = r.events || []; render(); });
}
function open(w) {
editing = w;
document.getElementById('webhookModalTitle').textContent = w ? 'Edit webhook' : 'Add webhook';
document.getElementById('whName').value = w ? w.name : '';
document.getElementById('whUrl').value = '';
document.getElementById('whUrl').required = !w;
document.getElementById('whUrlHelp').textContent = w ? 'Leave empty to keep ' + w.url : '';
document.getElementById('whFormat').value = w ? w.format : 'discord';
document.getElementById('whSecret').value = '';
document.getElementById('whSecretHelp').textContent = w && w.hasSecret ? 'A secret is set. Leave empty to keep it.' : '';
document.getElementById('whEnabled').checked = w ? w.enabled : true;
var selected = w ? w.events.split(',') : ['bug_report', 'moderation', 'security', 'economy_flag', 'server'];
document.getElementById('whEvents').innerHTML = [{ name: '*', description: 'Everything, including events added later' }].concat(events).map(function (e) {
var id = 'ev_' + e.name.replace('*', 'all');
return '<div class="form-check"><input class="form-check-input" type="checkbox" id="' + id + '" value="' + esc(e.name) + '"' +
(selected.indexOf(e.name) >= 0 || (w && w.events === '*' && e.name === '*') ? ' checked' : '') + '>' +
'<label class="form-check-label" for="' + id + '">' + esc(e.name === '*' ? 'All events' : e.name.replace(/_/g, ' ')) +
' <span class="small text-body-secondary">' + esc(e.description) + '</span></label></div>';
}).join('');
modal.show();
}
document.getElementById('addWebhook').addEventListener('click', function () { open(null); });
document.getElementById('webhookForm').addEventListener('submit', function (e) {
e.preventDefault();
var picked = Array.prototype.map.call(document.querySelectorAll('#whEvents input:checked'), function (i) { return i.value; });
var body = {
name: document.getElementById('whName').value.trim(),
format: document.getElementById('whFormat').value,
events: picked.indexOf('*') >= 0 ? '*' : picked,
enabled: document.getElementById('whEnabled').checked
};
var url = document.getElementById('whUrl').value.trim();
if (url) body.url = url;
var secret = document.getElementById('whSecret').value;
body.secret = secret ? secret : (editing ? null : '');
api.action(editing ? '/api/webhooks/' + editing.id : '/api/webhooks', body).then(function (r) {
toast(r.message, 'success');
modal.hide();
load();
}).catch(function () {});
});
document.getElementById('webhookRows').addEventListener('click', function (e) {
var button = e.target.closest('button');
if (!button) return;
var id = button.getAttribute('data-test') || button.getAttribute('data-edit') || button.getAttribute('data-delete');
var w = webhooks.filter(function (x) { return String(x.id) === id; })[0];
if (button.hasAttribute('data-test')) api.action('/api/webhooks/' + id + '/test', {}, 'Test sent, result shows here in a moment').catch(function () {});
if (button.hasAttribute('data-edit')) open(w);
if (button.hasAttribute('data-delete') && confirm('Remove the webhook "' + w.name + '"?')) api.action('/api/webhooks/' + id + '/delete').then(load).catch(function () {});
});
if (window.Live) Live.on('webhooks', Live.throttle(load, 500));
load();
})();
</script>
{% endblock %}