feat(ugc): models with no bricks are "empty", not failed

A model with no bricks has nothing to make: it gets its own state
(is_optimized = 3), isn't counted as a failure or retried, shows as Empty
on the UGC page (with its own filter and count), in the status and in
Prometheus, and its downloads answer 404 like HKX. State names come from
the enum (magic_enum). Migrations dlu/mysql/87 and dlu/sqlite/70 move the
rows that failed only because they had no bricks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 14:52:03 -05:00
parent 585839a539
commit 1309000460
17 changed files with 90 additions and 29 deletions

View File

@@ -223,10 +223,9 @@ namespace {
ok &= Timed(w, "ugc", [&] {
w.Declare("darkflame_ugc_items", "Player models (model) and cars and rockets (modular) by UGC processing state", "gauge");
const auto add = [&](const char* kind, const std::vector<std::pair<IUgc::eProcessState, uint64_t>>& rows) {
std::map<std::string, uint64_t> states{ {"pending", 0}, {"done", 0}, {"failed", 0} };
for (const auto& [state, count] : rows) {
states[state == IUgc::eProcessState::PENDING ? "pending" : state == IUgc::eProcessState::DONE ? "done" : "failed"] += count;
}
std::map<std::string, uint64_t> states;
for (const auto state : magic_enum::enum_values<IUgc::eProcessState>()) states[IUgc::ProcessStateName(state)] = 0;
for (const auto& [state, count] : rows) states[IUgc::ProcessStateName(state)] += count;
for (const auto& [state, count] : states) w.Add("darkflame_ugc_items", "", "gauge", { {"kind", kind}, {"state", state} }, static_cast<double>(count));
};
add("model", db->GetUgcProcessCounts());

View File

@@ -65,9 +65,10 @@ namespace {
// The UGC server's queue in the database (its work list), from the model and modular build tables
nlohmann::json UgcCounts() {
const auto counts = [](const std::vector<std::pair<IUgc::eProcessState, uint64_t>>& rows) {
nlohmann::json out = { {"pending", 0}, {"done", 0}, {"failed", 0} };
nlohmann::json out = nlohmann::json::object();
for (const auto state : magic_enum::enum_values<IUgc::eProcessState>()) out[IUgc::ProcessStateName(state)] = 0;
for (const auto& [state, count] : rows) {
out[state == IUgc::eProcessState::PENDING ? "pending" : state == IUgc::eProcessState::DONE ? "done" : "failed"] = count;
out[IUgc::ProcessStateName(state)] = count;
}
return out;
};

View File

@@ -28,21 +28,10 @@ using namespace RouteUtils;
namespace {
constexpr uint32_t PAGE_SIZE = 50;
const char* StateName(IUgc::eProcessState state) {
switch (state) {
case IUgc::eProcessState::PENDING: return "pending";
case IUgc::eProcessState::DONE: return "done";
case IUgc::eProcessState::FAILED: return "failed";
}
return "unknown";
}
std::string StateName(IUgc::eProcessState state) { return IUgc::ProcessStateName(state); }
std::optional<IUgc::eProcessState> ParseState(const std::string& text) { return IUgc::ParseProcessState(text); }
std::optional<IUgc::eProcessState> ParseState(const std::string& text) {
if (text == "pending") return IUgc::eProcessState::PENDING;
if (text == "done") return IUgc::eProcessState::DONE;
if (text == "failed") return IUgc::eProcessState::FAILED;
return std::nullopt;
}
/**
* The dashboard's own way to the UGC server (ugc_internal_url, normally the same machine): its status and the files
@@ -235,7 +224,8 @@ namespace {
}
nlohmann::json Counts(const std::vector<std::pair<IUgc::eProcessState, uint64_t>>& counts) {
nlohmann::json out = { { "pending", 0 }, { "done", 0 }, { "failed", 0 } };
nlohmann::json out = nlohmann::json::object();
for (const auto state : magic_enum::enum_values<IUgc::eProcessState>()) out[IUgc::ProcessStateName(state)] = 0;
for (const auto& [state, count] : counts) out[StateName(state)] = count;
return out;
}

View File

@@ -9,7 +9,7 @@
var kind = 'model', state = '', search = '', page = 0, view = 'gallery', canManage = false, publicUrl = '';
var nifViewer = null, lxfmlViewer = null, current = null;
var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'] };
var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] };
var PAGE_SIZE = { gallery: 48, list: 50 };
try { view = localStorage.getItem('ugcView') === 'list' ? 'list' : 'gallery'; } catch (e) { /* storage blocked */ }
@@ -25,7 +25,7 @@
function countCard(title, c) {
return '<div class="col-md-6"><div class="card"><div class="card-body py-2"><div class="small text-body-secondary">' + esc(title) + '</div>' +
fmt.badge(c.done + ' made', 'success') + ' ' + fmt.badge(c.pending + ' waiting', 'secondary') + ' ' + fmt.badge(c.failed + ' failed', c.failed ? 'danger' : 'secondary') +
'</div></div></div>';
(c.empty ? ' ' + fmt.badge(c.empty + ' empty', 'light') : '') + '</div></div></div>';
}
function owner(i) {

View File

@@ -41,7 +41,7 @@
<button type="button" class="btn btn-outline-primary" data-kind="modular">Cars and rockets</button>
</div>
<select class="form-select form-select-sm w-auto" id="stateFilter" aria-label="State">
<option value="">Every state</option><option value="pending">Waiting</option><option value="done">Made</option><option value="failed">Failed</option>
<option value="">Every state</option><option value="pending">Waiting</option><option value="done">Made</option><option value="failed">Failed</option><option value="empty">Empty (no bricks)</option>
</select>
<input type="search" class="form-control form-control-sm w-auto" id="search" placeholder="ID or owner" aria-label="Search by ID or owner">
<div class="btn-group btn-group-sm" role="group" id="viewButtons">