Files
DarkflameServer/dDashboardServer/templates/moderation.jinja2
Aaron Kimbrell 431eddd5bd feat(dashboard): guilds page, guild name review and guild chat in the chat log
A Guilds page (Moderation, permission guilds_manage, GM 5 by default): every guild with its name status, member count,
leader and age, a pending-only switch, and per guild its members (with ranks) and history (guild_events). Staff can
approve a guild name that waits for review, reject it (the guild becomes "Guild <id>"), rename a guild (same name rules
as the game, unique without regard to case), remove a member (a leader's guild goes to the next member, the last
member's guild is deleted) and disband a guild. Every change is audited and added to the guild's history, and the chat
server is asked (GUILD_CHANGED through master) to tell the online members. Pending guild names also show in the Review
Queue. Guild chat ("guild" in the chat log) counts as private chat like whispers and team chat (chat_private), with its
own channel filter and Prometheus counter.

Check on the dashboard: /guilds lists a guild made in game; approve/reject/rename/remove/disband update the in-game guild
window and name billboard of online members; the Review Queue shows a guild whose name waits for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:48:40 -05:00

153 lines
7.9 KiB
Django/Jinja

{% extends "base.jinja2" %}
{% block title %}Moderation - DarkflameServer{% endblock %}
{% block content %}
<h2 class="mb-1">Moderation Queue</h2>
<p class="text-body-secondary mb-4">Review the names and properties players are waiting on. The queues update live.</p>
<p class="small text-body-secondary d-none" id="aiBudget"></p>
{% if can.moderate_names %}
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Pending Character Names</h5></div>
<div class="card-body">
<table id="pendingNamesTable" class="table table-striped table-hover table-stack">
<thead><tr><th>Current Name</th><th>Requested Name</th><th>Account</th><th>Actions</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
{% endif %}
{% if can.moderate_pet_names %}
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Pending Pet Names</h5></div>
<div class="card-body">
<table id="pendingPetsTable" class="table table-striped table-hover table-stack">
<thead><tr><th>ID</th><th>Pet</th><th>Pet Name</th><th>Owner</th><th>Actions</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
{% endif %}
{% if can.guilds_manage %}
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Pending Guild Names</h5></div>
<div class="card-body">
<table id="pendingGuildsTable" class="table table-striped table-hover table-stack">
<thead><tr><th>ID</th><th>Guild Name</th><th>Leader</th><th>Members</th><th>Actions</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
{% endif %}
{% if can.moderate_properties %}
<div class="card">
<div class="card-header"><h5 class="mb-0">Pending Properties</h5></div>
<div class="card-body">
<table id="pendingPropsTable" class="table table-striped table-hover table-stack">
<thead><tr><th>ID</th><th>Name</th><th>Owner</th><th>Models</th><th>Reputation</th><th>Zone</th><th>Actions</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
// Owners come from a background scan of inventories; until it has run they show as pending
function petOwner(d, t, row) {
return row.owner_id ? fmt.character(row.owner_id, row.owner_name) : '<span class="text-body-secondary small">looking up&hellip;</span>';
}
// characterId: whose account a strike would go on
function actionButtons(kind, id, characterId, subject) {
var about = ' data-id="' + esc(id) + '" data-character="' + esc(characterId || '') + '" data-subject="' + esc(subject || '') + '"';
return '<button class="btn btn-sm btn-outline-success me-1" data-kind="' + kind + '" data-action="approve"' + about + '>Approve</button>' +
'<button class="btn btn-sm btn-outline-danger" data-kind="' + kind + '" data-action="reject"' + about + '>Reject</button>' +
(kind === 'property' ? '' : AiSuggest.button(kind === 'pet' ? 'pet_name' : 'name', id, 'ms-1'));
}
var endpoints = {
name: { approve: function (id) { return '/api/characters/' + id + '/approve_name'; }, reject: function (id) { return '/api/characters/' + id + '/reject_name'; } },
pet: { approve: function (id) { return '/api/pet_names/' + id + '/approve'; }, reject: function (id) { return '/api/pet_names/' + id + '/reject'; } },
property: { approve: function (id) { return '/api/properties/' + id + '/approve'; }, reject: function (id) { return '/api/properties/' + id + '/reject'; } }
};
$(function () {
var small = { dataTable: { pageLength: 10, searching: false, order: [] } };
if (DASH.can('moderate_names')) serverTable('#pendingNamesTable', '/api/tables/pending_names', [
{ data: 'name', orderable: false, render: function (d, t, row) { return fmt.link('/characters/' + row.id, d); } },
{ data: 'pending_name', orderable: false },
{ data: 'account_name', orderable: false, render: function (d, t, row) { return fmt.link('/accounts/' + row.account_id, d); } },
{ data: 'id', orderable: false, render: function (d, t, row) { return actionButtons('name', d, row.id, row.pending_name); } }
], $.extend({ liveTable: 'pending_names' }, small));
if (DASH.can('moderate_pet_names')) {
serverTable('#pendingPetsTable', '/api/tables/pending_pet_names', [
{ data: 'id', orderable: false },
{ data: 'lot', orderable: false, render: function (d, t, row) { return fmt.pet(row.lot, row.kind); } },
{ data: 'pet_name', orderable: false },
{ data: 'owner_id', orderable: false, defaultContent: '', render: petOwner },
{ data: 'id', orderable: false, render: function (d, t, row) { return actionButtons('pet', d, row.owner_id, row.pet_name); } }
], $.extend({ liveTable: 'pet_names' }, small));
}
if (DASH.can('guilds_manage')) {
serverTable('#pendingGuildsTable', '/api/tables/guilds', [
{ data: 'id', orderable: false },
{ data: 'name', orderable: false, render: function (d) { return fmt.link('/guilds', d); } },
{ data: 'leader_id', orderable: false, render: function (d, t, row) { return d ? fmt.character(d, row.leader_name) : ''; } },
{ data: 'member_count', orderable: false },
{ data: 'id', orderable: false, render: function (d, t, row) {
return '<button class="btn btn-sm btn-outline-success me-1" data-guild-action="approve" data-id="' + esc(d) + '">Approve</button>' +
'<button class="btn btn-sm btn-outline-danger" data-guild-action="reject" data-id="' + esc(d) + '" data-name="' + esc(row.name) + '">Reject</button>';
} }
], $.extend({ liveTable: 'guilds', extra: function () { return { pending: true }; } }, small));
$('#pendingGuildsTable').on('click', '[data-guild-action]', function () {
var id = this.getAttribute('data-id');
if (this.getAttribute('data-guild-action') === 'approve') { api.action('/api/guilds/' + id + '/approve', {}, 'Approved').catch(function () {}); return; }
var reason = prompt('Reject the guild name “' + this.getAttribute('data-name') + '”? The guild is renamed “Guild ' + id + '”. Reason (optional, kept in its history):', '');
if (reason === null) return;
api.action('/api/guilds/' + id + '/reject', { reason: reason }, 'Rejected').catch(function () {});
});
}
if (DASH.can('moderate_properties')) {
serverTable('#pendingPropsTable', '/api/tables/pending_properties', [
{ data: 'id', orderable: false },
{ data: 'name', orderable: false, render: function (d, t, row) { return fmt.property(row.id, d); } },
{ data: 'owner_id', orderable: false, render: function (d, t, row) { return fmt.character(d, row.owner_name); } },
// Nothing built yet is worth knowing before approving it for everyone to visit
{ data: 'models', orderable: false, render: function (d) { return d ? esc(d) : fmt.badge('Empty', 'secondary'); } },
{ data: 'reputation', orderable: false },
{ data: 'zone_id', orderable: false, render: function (d, t, row) { return fmt.zone(d, row.zone_name); } },
{ data: 'id', orderable: false, render: function (d, t, row) { return actionButtons('property', d, row.owner_id, row.name); } }
], $.extend({ liveTable: 'properties' }, small));
}
$(document).on('click', '[data-kind][data-action]', function () {
var kind = this.getAttribute('data-kind');
var action = this.getAttribute('data-action');
var url = endpoints[kind][action](this.getAttribute('data-id'));
// Tables refresh through the table_changed event
if (action === 'approve') { api.action(url, {}, 'Approved').catch(function () {}); return; }
var what = { name: 'the name', pet: 'the pet name', property: 'the property' }[kind];
Decide({
title: 'Reject ' + what + (this.getAttribute('data-subject') ? ' “' + this.getAttribute('data-subject') + '”' : ''),
reasonLabel: kind === 'property' ? 'Reason (shown to the player)' : 'Reason (optional, shown to the player on the dashboard)',
reasonRequired: kind === 'property',
characterId: this.getAttribute('data-character')
}).then(function (choice) {
if (!choice) return;
api.action(url, { reason: choice.reason, strike: choice.strike }, choice.strike ? 'Rejected, with a strike' : 'Rejected').catch(function () {});
});
});
});
</script>
{% endblock %}