mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-04 11:53:44 +00:00
feat(dashboard): UGC list shows a model's given name, and Took/CPU/RAM columns
The models list's Took, CPU and RAM (est.) are separate columns, each sortable (sort=slowest|cpu|memory). The File column shows the name a player gave the model where it is placed, with the upload's extension (the upload's file name is its tooltip), and the name sort uses it (case-insensitive, so SQLite and MySQL agree). The config layer test no longer assumes the build's sharedconfig.ini has no mysql_host; it checks that the database-supplied value isn't used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,7 +191,7 @@ namespace {
|
||||
return { { "id", std::to_string(entry.id) }, { "characterId", std::to_string(entry.characterId) }, { "characterName", entry.characterName },
|
||||
{ "accountId", entry.accountId }, { "accountName", entry.accountName }, { "state", IUgc::ProcessStateName(entry.state) }, { "attempts", entry.attempts },
|
||||
{ "processedAt", entry.processedAt }, { "error", entry.error }, { "bakeAo", entry.bakeAo }, { "processAfter", entry.processAfter },
|
||||
{ "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles }, { "processMs", entry.processMs }, { "processCpuMs", entry.processCpuMs }, { "processMemoryKb", entry.processMemoryKb } };
|
||||
{ "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles }, { "processMs", entry.processMs }, { "processCpuMs", entry.processCpuMs }, { "processMemoryKb", entry.processMemoryKb }, { "modelName", entry.modelName } };
|
||||
}
|
||||
|
||||
// Web thread: every car and rocket build (the assemblies are made from them)
|
||||
@@ -241,7 +241,7 @@ namespace UgcRoutes {
|
||||
"A page of player models (kind=model) or of car and rocket assemblies (kind=modular: one per combination of modules, however many builds use "
|
||||
"it). Query: q= (\"state:\", \"kind:\"/\"type:\" (a build type, e.g. build6), \"owner:\", \"account:\", \"property:\", \"name:\", "
|
||||
"\"lot:\"/\"module:\" (a LOT or a module's name), \"id:\", or plain text across names, owners and ids), state=, type=, sort=newest|oldest|owner|name|"
|
||||
"bricks|triangles|slowest|made (models) or newest|oldest|references|name (assemblies), reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, "
|
||||
"bricks|triangles|slowest|made|cpu|memory (models) or newest|oldest|references|name (assemblies), reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, "
|
||||
"kinds, ugcPublicUrl, canManage}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
||||
@@ -260,7 +260,7 @@ namespace UgcRoutes {
|
||||
query.search = parsed.search;
|
||||
query.state = parsed.state;
|
||||
static const std::map<std::string, IUgcLookup::eSort> SORTS = { { "newest", IUgcLookup::eSort::NEWEST }, { "oldest", IUgcLookup::eSort::OLDEST },
|
||||
{ "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES }, { "slowest", IUgcLookup::eSort::SLOWEST }, { "made", IUgcLookup::eSort::MADE } };
|
||||
{ "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES }, { "slowest", IUgcLookup::eSort::SLOWEST }, { "made", IUgcLookup::eSort::MADE }, { "cpu", IUgcLookup::eSort::CPU }, { "memory", IUgcLookup::eSort::MEMORY } };
|
||||
if (const auto it = SORTS.find(sortText); it != SORTS.end()) query.sort = it->second;
|
||||
query.reverse = reverse;
|
||||
query.offset = page * size;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] };
|
||||
var SORTS = {
|
||||
model: [['newest', 'Newest'], ['oldest', 'Oldest'], ['bricks', 'Most bricks'], ['triangles', 'Most triangles'], ['made', 'Recently made'], ['slowest', 'Slowest to make'], ['owner', 'Owner'], ['name', 'File name']],
|
||||
model: [['newest', 'Newest'], ['oldest', 'Oldest'], ['bricks', 'Most bricks'], ['triangles', 'Most triangles'], ['made', 'Recently made'], ['slowest', 'Slowest to make'], ['cpu', 'Most CPU'], ['memory', 'Most RAM'], ['owner', 'Owner'], ['name', 'File name']],
|
||||
modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['name', 'Name']]
|
||||
};
|
||||
// The list's state, as in the address: ?kind=&q=&state=&type=&sort=&page= (from 1)&view=, and the open item (&item=, &build=)
|
||||
@@ -110,6 +110,13 @@
|
||||
if (!i.processedAt) return '';
|
||||
return fmt.unix(i.processedAt) + (costText(i) ? ' \u00b7 ' + costText(i) : '');
|
||||
}
|
||||
// A model's file as the player named it: its name with the upload's extension (".lxfml"), else the upload's name
|
||||
function fileName(i) {
|
||||
var file = i.detail || '';
|
||||
if (!i.modelName) return file;
|
||||
var dot = file.lastIndexOf('.');
|
||||
return i.modelName + (dot > 0 ? file.slice(dot) : '');
|
||||
}
|
||||
function waitBadge(i) {
|
||||
return i.state === 'pending' && i.processAfter > Date.now() / 1000 ? ' <span class="small text-body-secondary" title="Waits for the owner to stop saving">after ' + esc(fmt.unix(i.processAfter)) + '</span>' : '';
|
||||
}
|
||||
@@ -142,9 +149,11 @@
|
||||
return badge(i.state) + (i.attempts ? ' <span class="small text-body-secondary">' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '</span>' : '') + waitBadge(i);
|
||||
}),
|
||||
column('Made', function (i) { return '<span class="small">' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + '</span>'; }, 'made', true),
|
||||
column('Took', function (i) { return '<span class="small">' + esc(costText(i).replace(/^took /, '')) + '</span>'; }, 'slowest', true),
|
||||
column('Took', function (i) { return '<span class="small">' + (i.processMs ? esc(duration(i.processMs)) : '') + '</span>'; }, 'slowest', true),
|
||||
column('CPU', function (i) { return '<span class="small">' + (i.processCpuMs ? esc(duration(i.processCpuMs)) : '') + '</span>'; }, 'cpu', true),
|
||||
column('RAM (est.)', function (i) { return '<span class="small">' + (i.processMemoryKb ? '~' + esc(megabytes(i.processMemoryKb)) : '') + '</span>'; }, 'memory', true),
|
||||
column('Size', function (i) { return '<span class="small">' + (i.bricks ? esc(i.bricks) + ' bricks<br>' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '</span>'; }, 'bricks', true),
|
||||
column('File', function (i) { return '<span class="small">' + esc(i.detail || '') + '</span>' + errorText(i); }, 'name'),
|
||||
column('File', function (i) { return '<span class="small" title="' + esc(i.detail || '') + '">' + esc(fileName(i)) + '</span>' + errorText(i); }, 'name'),
|
||||
column('', function (i) { return '<div class="text-end text-nowrap">' + actions(i, i.id) + '</div>'; })
|
||||
],
|
||||
modular: [
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<div id="gallery" class="ugc-gallery mb-2"></div>
|
||||
<div class="card d-none" id="listCard"><div class="card-body p-2">
|
||||
<div id="modelTableBox"><table class="table table-sm align-middle mb-0 w-100 table-stack" id="ugcModelTable">
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>Size</th><th>File</th><th></th></tr></thead></table></div>
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>CPU</th><th>RAM (est.)</th><th>Size</th><th>File</th><th></th></tr></thead></table></div>
|
||||
<div id="assemblyTableBox" class="d-none"><table class="table table-sm align-middle mb-0 w-100 table-stack" id="ugcAssemblyTable">
|
||||
<thead><tr><th>Icon</th><th>Newest build</th><th>Type</th><th>Modules</th><th>State</th><th>Builds</th><th>Owners</th><th></th></tr></thead></table></div>
|
||||
</div></div>
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
uint32_t processMs{}; // how long the last successful make took (0: not made, or made before it was timed)
|
||||
uint32_t processCpuMs{}; // the worker thread's CPU time for it
|
||||
uint32_t processMemoryKb{}; // the memory the UGC server estimated for it (not measured)
|
||||
std::string modelName; // models: the name a player gave it where it is placed (empty: none)
|
||||
};
|
||||
|
||||
// What SearchUgc matches. A number (when set) is matched against ids; text against names
|
||||
@@ -65,6 +66,8 @@ public:
|
||||
TRIANGLES, // models: the most triangles first
|
||||
SLOWEST, // the longest last make first (process_ms)
|
||||
MADE, // the most recently made (or attempted) first (processed_at)
|
||||
CPU, // the most CPU time in the last make first (process_cpu_ms)
|
||||
MEMORY, // the most estimated memory in the last make first (process_memory_kb)
|
||||
};
|
||||
|
||||
// A page of one kind: all of them or those matching the search (the same matching as SearchUgc), in a state or any
|
||||
|
||||
@@ -31,10 +31,12 @@ namespace UgcLookupSql {
|
||||
return (modular
|
||||
? "SELECT b.ugc_id AS id, b.character_id, c.name AS character_name, COALESCE(c.account_id, 0) AS account_id, a.name AS account_name, "
|
||||
"b.is_optimized, b.process_error, b.ldf_config AS detail, b.process_attempts, b.processed_at, 0 AS process_after, 0 AS bake_ao, "
|
||||
"0 AS brick_count, 0 AS triangle_count, b.process_ms, b.process_cpu_ms, b.process_memory_kb "
|
||||
"0 AS brick_count, 0 AS triangle_count, b.process_ms, b.process_cpu_ms, b.process_memory_kb, '' AS model_name "
|
||||
: "SELECT u.id, u.character_id, c.name AS character_name, u.account_id, a.name AS account_name, "
|
||||
"u.is_optimized, u.process_error, u.filename AS detail, u.process_attempts, u.processed_at, u.process_after, u.bake_ao, "
|
||||
"u.brick_count, u.triangle_count, u.process_ms, u.process_cpu_ms, u.process_memory_kb ") + From(modular);
|
||||
"u.brick_count, u.triangle_count, u.process_ms, u.process_cpu_ms, u.process_memory_kb, "
|
||||
// The name a player gave the model where it is placed (empty: none)
|
||||
"COALESCE((SELECT pc.model_name FROM properties_contents AS pc WHERE pc.ugc_id = u.id AND pc.model_name <> '' LIMIT 1), '') AS model_name ") + From(modular);
|
||||
}
|
||||
|
||||
// Whether a search has anything to match (else a list is of everything)
|
||||
@@ -55,9 +57,12 @@ namespace UgcLookupSql {
|
||||
switch (query.sort) {
|
||||
case eSort::OLDEST: return "ORDER BY " + id + dir(false) + " ";
|
||||
case eSort::OWNER: return "ORDER BY c.name" + std::string(dir(false)) + ", " + id + " DESC ";
|
||||
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "u.filename") + dir(false) + ", " + id + " DESC ";
|
||||
// Models: the name the player gave it, else the upload's file name
|
||||
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "LOWER(COALESCE(NULLIF(model_name, ''), u.filename))") + dir(false) + ", " + id + " DESC ";
|
||||
case eSort::BRICKS: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.brick_count" + std::string(dir(true)) + ", u.id DESC ";
|
||||
case eSort::TRIANGLES: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.triangle_count" + std::string(dir(true)) + ", u.id DESC ";
|
||||
case eSort::CPU: return "ORDER BY " + std::string(modular ? "b" : "u") + ".process_cpu_ms" + dir(true) + ", " + id + " DESC ";
|
||||
case eSort::MEMORY: return "ORDER BY " + std::string(modular ? "b" : "u") + ".process_memory_kb" + dir(true) + ", " + id + " DESC ";
|
||||
case eSort::MADE: return "ORDER BY " + std::string(modular ? "b" : "u") + ".processed_at" + dir(true) + ", " + id + " DESC ";
|
||||
case eSort::SLOWEST: return "ORDER BY " + std::string(modular ? "b" : "u") + ".process_ms" + dir(true) + ", " + id + " DESC ";
|
||||
default: return "ORDER BY " + id + dir(true) + " ";
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace {
|
||||
entry.processMs = static_cast<uint32_t>(result->getInt64("process_ms"));
|
||||
entry.processCpuMs = static_cast<uint32_t>(result->getInt64("process_cpu_ms"));
|
||||
entry.processMemoryKb = static_cast<uint32_t>(result->getInt64("process_memory_kb"));
|
||||
entry.modelName = result->getString("model_name").c_str();
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace {
|
||||
entry.processMs = static_cast<uint32_t>(result.getInt64Field("process_ms"));
|
||||
entry.processCpuMs = static_cast<uint32_t>(result.getInt64Field("process_cpu_ms"));
|
||||
entry.processMemoryKb = static_cast<uint32_t>(result.getInt64Field("process_memory_kb"));
|
||||
entry.modelName = result.getStringField("model_name", "");
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ TEST(ConfigLayerTests, DatabaseValuesLayerAroundFilesAndEnvironment) {
|
||||
config.SetDatabaseValues({ { "zz_override", "web" }, { "mysql_host", "evil" } }, { { "zz_fallback", "web" }, { "zz_both", "web" } });
|
||||
EXPECT_EQ(config.GetValue("zz_override"), "web");
|
||||
EXPECT_EQ(config.GetValue("zz_fallback"), "web");
|
||||
EXPECT_EQ(config.GetValue("mysql_host"), ""); // connection settings never come from the database
|
||||
EXPECT_NE(config.GetValue("mysql_host"), "evil"); // connection settings never come from the database (the build's own sharedconfig.ini may set one)
|
||||
EXPECT_EQ(config.GetValue("zz_missing"), "");
|
||||
|
||||
// The environment beats a fallback but not an override
|
||||
|
||||
@@ -173,15 +173,19 @@ TEST_F(UgcLookupSqlTests, ListsPagesWithCounts) {
|
||||
query.reverse = true; // the fewest bricks first
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.reverse = false;
|
||||
query.sort = IUgcLookup::eSort::NAME; // boat before tower
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.reverse = true; // tower before boat
|
||||
query.sort = IUgcLookup::eSort::NAME; // "Big tower" (its placed name) before boat.lxfml, whatever the case
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1000, 1001 }));
|
||||
query.reverse = true;
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.reverse = false;
|
||||
query.sort = IUgcLookup::eSort::NEWEST;
|
||||
query.reverse = true; // the oldest first
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1000, 1001 }));
|
||||
query.reverse = false;
|
||||
query.sort = IUgcLookup::eSort::CPU; // none recorded: ties go newest first
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.sort = IUgcLookup::eSort::MEMORY;
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.sort = IUgcLookup::eSort::MADE; // neither has been made: ties go newest first
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.sort = IUgcLookup::eSort::SLOWEST; // the tower took 5.2 s; the boat isn't made
|
||||
@@ -193,7 +197,7 @@ TEST_F(UgcLookupSqlTests, ListsPagesWithCounts) {
|
||||
query.limit = 1;
|
||||
query.offset = 1;
|
||||
std::tie(ids, total) = List(query, false);
|
||||
EXPECT_EQ(ids, (std::vector<int64_t>{ 1000 }));
|
||||
EXPECT_EQ(ids, (std::vector<int64_t>{ 1001 }));
|
||||
EXPECT_EQ(total, 2); // the count is of every match, not the page
|
||||
query = {};
|
||||
query.state = IUgc::eProcessState::DONE;
|
||||
|
||||
Reference in New Issue
Block a user