feat(dashboard): show each pet's icon and kind when moderating pet names

The pet name tables (Pet Names page and the review queue) get the pet's
LOT from its owner's save and its CDClient name, shown with the icon in
a new Pet column. Each owner on the page is read once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 00:38:34 -05:00
parent 27c562e242
commit 931277e76c
5 changed files with 44 additions and 5 deletions

View File

@@ -12,6 +12,7 @@
#include "BehaviorXml.h"
#include "CharacterTools.h"
#include "ClientAssets.h"
#include "CharacterXml.h"
#include "Scenery.h"
#include "Workers.h"
#include "LiveWorld.h"
@@ -88,6 +89,30 @@ namespace {
return json.dump();
}
/**
* Adds each pet's LOT and kind (its CDClient name) to pet_names rows. pet_names doesn't hold the LOT; the owner's
* save does (<pet><p id l/>), so each owner on the page is read once. Pets whose owner is unknown, or who have
* left the owner's save, get lot 0.
*/
std::string WithPetKinds(const std::string& raw) {
auto json = nlohmann::json::parse(raw, nullptr, false);
if (json.is_discarded() || !json.contains("data") || !json["data"].is_array()) return raw;
std::map<LWOOBJID, std::map<LWOOBJID, LOT>> owners;
for (auto& row : json["data"]) {
const auto owner = GeneralUtils::TryParse<LWOOBJID>(row.value("owner_id", std::string{})).value_or(0);
const auto pet = GeneralUtils::TryParse<LWOOBJID>(row.value("id", std::string{})).value_or(0);
LOT lot = 0;
if (owner && pet) {
auto [it, added] = owners.try_emplace(owner);
if (added) for (const auto& held : CharacterXml::Pets(Database::Get()->GetCharacterXml(owner))) it->second[held.id] = held.lot;
if (const auto found = it->second.find(pet); found != it->second.end()) lot = found->second;
}
row["lot"] = lot;
row["kind"] = lot > 0 ? ClientAssets::ItemName(lot) : "";
}
return json.dump();
}
// Register a DataTables endpoint. The fetcher returns the DB layer's JSON string. Access: a GM level or a Perm.
template<typename Access>
void TableRoute(const std::string& path, const Access& access, const std::string& description, TableFetcher fetcher) {
@@ -1290,12 +1315,14 @@ namespace {
}
void RegisterPetRoutes() {
TableRoute("/api/tables/pet_names", Perm("moderate_pet_names"), "Pet names (DataTables)", [](const DataTablesRequest& r, const nlohmann::json& body) {
return Database::Get()->GetPetNamesTable(r.start, r.length, r.search, r.orderColumn, r.orderAsc, body.value("pending", false));
TableRoute("/api/tables/pet_names", Perm("moderate_pet_names"), "Pet names (DataTables); rows include the pet's lot and kind. Order columns: 0 id, 2 name, 3 status, 4 owner", [](const DataTablesRequest& r, const nlohmann::json& body) {
// The page shows the pet's kind (not sortable) as column 1; the database numbers its columns without it
const uint32_t column = r.orderColumn >= 2 ? r.orderColumn - 1 : 0;
return WithPetKinds(Database::Get()->GetPetNamesTable(r.start, r.length, r.search, column, r.orderAsc, body.value("pending", false)));
});
TableRoute("/api/tables/pending_pet_names", Perm("moderate_pet_names"), "Pet names awaiting review (DataTables)", [](const DataTablesRequest& r, const nlohmann::json&) {
return Database::Get()->GetPetNamesTable(r.start, r.length, r.search, r.orderColumn, r.orderAsc, true);
TableRoute("/api/tables/pending_pet_names", Perm("moderate_pet_names"), "Pet names awaiting review (DataTables); rows include the pet's lot and kind", [](const DataTablesRequest& r, const nlohmann::json&) {
return WithPetKinds(Database::Get()->GetPetNamesTable(r.start, r.length, r.search, r.orderColumn, r.orderAsc, true));
});
Route(eHTTPMethod::POST, "/api/pet_names/:id/approve", Perm("moderate_pet_names"), "Approve a pet name",

View File

@@ -135,6 +135,12 @@
if (!id || id === '0') return '<span class="text-body-secondary">-</span>';
return name ? fmt.link('/characters/' + id, name) : '<span class="text-body-secondary" title="Character not found">Unknown (' + esc(id) + ')</span>';
},
// A pet's kind: its icon and CDClient name (rows from the pet name tables carry lot and kind)
pet: function (lot, kind) {
if (!lot) return '<span class="text-body-secondary small" title="The owner\'s save doesn\'t hold this pet">Unknown</span>';
return '<span class="text-nowrap"><img src="/api/icon/' + esc(lot) + '" width="28" height="28" class="me-1 align-middle" alt="" loading="lazy">' +
esc(kind || ('LOT ' + lot)) + ' <span class="small text-body-secondary">(' + esc(lot) + ')</span></span>';
},
zone: function (id, name) {
return esc(name || ('Zone ' + id)) + ' <span class="small text-body-secondary">(' + esc(id) + ')</span>';
}

View File

@@ -24,7 +24,7 @@
<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 Name</th><th>Owner</th><th>Actions</th></tr></thead>
<thead><tr><th>ID</th><th>Pet</th><th>Pet Name</th><th>Owner</th><th>Actions</th></tr></thead>
<tbody></tbody>
</table>
</div>
@@ -78,6 +78,7 @@ $(function () {
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); } }

View File

@@ -18,6 +18,7 @@
<thead>
<tr>
<th>ID</th>
<th>Pet</th>
<th>Pet Name</th>
<th>Status</th>
<th>Owner</th>
@@ -36,6 +37,7 @@ $(function () {
var pending = document.getElementById('pendingOnly');
var table = serverTable('#petNamesTable', '/api/tables/pet_names', [
{ data: 'id' },
{ data: 'lot', orderable: false, render: function (d, t, row) { return fmt.pet(row.lot, row.kind); } },
{ data: 'pet_name' },
// 1 = pending moderation, 2 = approved (rejected names are deleted)
{ data: 'approved', render: function (d) { return d === 2 ? fmt.badge('Approved', 'success') : fmt.badge('Pending', 'warning'); } },

View File

@@ -721,6 +721,9 @@ tells the player in game if they're online. Rejecting asks for an optional reaso
account page next to what they asked for. The queues show only what the game itself would still review: rejected
properties and names whose player must pick a new one no longer appear.
Pet names show what kind of pet each one is (its icon and name from the CDClient), read from the pet's entry in its
owner's save. A pet whose owner is unknown or no longer has it shows as Unknown.
### Moderation history, warnings and bans
Each account page has a **Moderation history** (GM 2+, `accounts_notes`): notes and warnings staff write, plus every