Files
DarkflameServer/dDashboardServer/templates/guilds.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

151 lines
7.0 KiB
Django/Jinja

{% extends "base.jinja2" %}
{% block title %}Guilds - DarkflameServer{% endblock %}
{% block content %}
<h2 class="mb-1">Guilds</h2>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-body-secondary mb-0">Every guild. A name the chat filter doesn't allow waits for review; other players see no guild name until it is approved. Rejecting a name renames the guild “Guild &lt;id&gt;”.</p>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pendingOnly" data-pref="guilds.pendingOnly">
<label class="form-check-label" for="pendingOnly">Pending only</label>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<table id="guildsTable" class="table table-striped table-hover table-stack">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Members</th>
<th>Leader</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card d-none" id="guildDetail">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="guildTitle"></h5>
<div>
<button class="btn btn-sm btn-outline-primary me-1" id="guildRename">Rename</button>
<button class="btn btn-sm btn-outline-danger" id="guildDisband">Disband</button>
</div>
</div>
<div class="card-body">
<p class="small text-body-secondary" id="guildInfo"></p>
<div class="row">
<div class="col-lg-6">
<h6>Members</h6>
<table class="table table-sm table-stack">
<thead><tr><th>Character</th><th>Rank</th><th>Joined</th><th></th></tr></thead>
<tbody id="guildMembers"></tbody>
</table>
</div>
<div class="col-lg-6">
<h6>History</h6>
<table class="table table-sm table-stack">
<thead><tr><th>When</th><th>What</th><th>Who</th><th>By</th><th>Detail</th></tr></thead>
<tbody id="guildEvents"></tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function () {
var ranks = { 1: 'Leader', 2: 'Officer', 3: 'Veteran', 4: 'Recruit' };
var pending = document.getElementById('pendingOnly');
var shown = null;
var table = serverTable('#guildsTable', '/api/tables/guilds', [
{ data: 'id', orderable: false },
{ data: 'name', orderable: false },
{ data: 'name_status', orderable: false, render: function (d) { return d === 'approved' ? fmt.badge('Approved', 'success') : fmt.badge('Pending', 'warning'); } },
{ data: 'member_count', orderable: false },
{ data: 'leader_id', orderable: false, render: function (d, t, row) { return d ? fmt.character(d, row.leader_name) : '<span class="text-body-secondary small">-</span>'; } },
{ data: 'created_at', orderable: false, render: function (d) { return fmt.unix(d); } },
{ data: null, orderable: false, render: function (d) {
var buttons = '<button class="btn btn-sm btn-outline-secondary me-1" data-guild-action="view" data-id="' + esc(d.id) + '">View</button>';
if (d.name_status !== 'approved') {
buttons += '<button class="btn btn-sm btn-outline-success me-1" data-guild-action="approve" data-id="' + esc(d.id) + '">Approve</button>' +
'<button class="btn btn-sm btn-outline-danger" data-guild-action="reject" data-id="' + esc(d.id) + '" data-name="' + esc(d.name) + '">Reject</button>';
}
return buttons;
}}
], { liveTable: 'guilds', dataTable: { order: [] }, extra: function () { return { pending: pending.checked }; } });
pending.addEventListener('change', function () { table.ajax.reload(); });
function show(id) {
api.get('/api/guilds/' + id).then(function (d) {
var guild = d.guild;
shown = guild;
document.getElementById('guildDetail').classList.remove('d-none');
document.getElementById('guildTitle').textContent = guild.name + ' (' + guild.id + ')';
document.getElementById('guildInfo').innerHTML = 'Name ' + esc(guild.name_status) + '. Founded ' + esc(fmt.unix(guild.created_at)) +
(guild.founder_name ? ' by ' + fmt.character(guild.founder_id, guild.founder_name) : '') + '.';
document.getElementById('guildMembers').innerHTML = guild.members.map(function (m) {
return '<tr><td>' + fmt.character(m.id, m.name || m.id) + '</td><td>' + esc(ranks[m.rank] || m.rank) + '</td><td>' + esc(fmt.unix(m.joined_at)) +
'</td><td><button class="btn btn-sm btn-outline-danger" data-member="' + esc(m.id) + '" data-name="' + esc(m.name) + '">Remove</button></td></tr>';
}).join('') || '<tr><td colspan="4" class="text-body-secondary">No members</td></tr>';
document.getElementById('guildEvents').innerHTML = guild.events.map(function (e) {
return '<tr><td>' + esc(fmt.unix(e.time)) + '</td><td>' + esc(e.kind) + '</td><td>' + (e.character_id !== '0' ? fmt.character(e.character_id, e.character_name || e.character_id) : '') +
'</td><td>' + esc(e.actor) + '</td><td>' + esc(e.detail) + '</td></tr>';
}).join('');
}).catch(function () {});
}
function refresh() {
table.ajax.reload(null, false);
if (shown) show(shown.id);
}
$('#guildsTable').on('click', '[data-guild-action]', function () {
var action = this.getAttribute('data-guild-action');
var id = this.getAttribute('data-id');
if (action === 'view') return show(id);
if (action === 'approve') { api.action('/api/guilds/' + id + '/approve', {}, 'Approved').then(refresh).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').then(refresh).catch(function () {});
});
document.getElementById('guildRename').addEventListener('click', function () {
if (!shown) return;
var name = prompt('New name for ' + shown.name + ' (3 to 30 letters, digits, spaces, \' - and .):', shown.name);
if (name === null || name === shown.name) return;
api.action('/api/guilds/' + shown.id + '/rename', { name: name }, 'Renamed').then(refresh).catch(function () {});
});
document.getElementById('guildDisband').addEventListener('click', function () {
if (!shown) return;
var typed = prompt('This removes every member of ' + shown.name + ' and deletes the guild (its history stays). Type the guild ID (' + shown.id + ') to confirm:');
if (typed !== shown.id) return;
var id = shown.id;
api.action('/api/guilds/' + id + '/disband', {}, 'Disbanded').then(function () {
shown = null;
document.getElementById('guildDetail').classList.add('d-none');
table.ajax.reload(null, false);
}).catch(function () {});
});
document.getElementById('guildMembers').addEventListener('click', function (event) {
var button = event.target.closest('[data-member]');
if (!button || !shown) return;
if (!confirm('Remove ' + button.getAttribute('data-name') + ' from ' + shown.name + '?')) return;
api.action('/api/guilds/' + shown.id + '/members/' + button.getAttribute('data-member') + '/remove', {}, 'Removed').then(refresh).catch(function () {});
});
});
</script>
{% endblock %}