Files
DarkflameServer/dDashboardServer/templates/guilds.jinja2
Aaron Kimbrell f3a49991d4 feat(dashboard): guild, team and whisper chat histories
Guild chat per guild (/chat_log/guild/<id>, from the guild's card), team chat by
team (/chat_log/teams) and a character's whispers by conversation
(/characters/<id>/whispers, chat_dms; opening one is audited), paged on the
server. The Chat Log filters by account and time range, shows the conversation
around a message, links to its history and marks flagged messages; messages can
be picked (shift-click for a range) to flag. The character page links to all of
them.

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

158 lines
7.4 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>
{% if can.chat_private %}<a class="btn btn-sm btn-outline-secondary me-1" id="guildChat" href="#">Guild chat</a>{% endif %}
<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 + ')';
var chatLink = document.getElementById('guildChat');
if (chatLink) chatLink.href = '/chat_log/guild/' + encodeURIComponent(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 () {});
});
// #guild=<id> opens that guild (links from its chat history)
var fromHash = (location.hash.match(/guild=(\d+)/) || [])[1];
if (fromHash) show(fromHash);
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 %}