mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
feat(dashboard): reprocess or remove every model on a property
The property page gets a Reprocess all models button (ugc_manage): every
model placed on the property goes back to the UGC server's queue, made
again with the current settings (/api/ugc/reprocess with {property}).
Import models gets Remove all models (properties_import): deletes every
model placed on the property after the property id is typed in, refused
while the property is loaded in a world, as import is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -281,4 +281,23 @@ void RegisterMaintenanceRoutes() {
|
||||
BroadcastTableChanged("properties", std::to_string(*propertyId));
|
||||
JsonSuccess(reply, { {"models", created}, {"message", "Imported " + std::to_string(created.size()) + " model(s)"} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::POST, "/api/properties/:id/remove_models", Perm("properties_import"),
|
||||
"Delete every model placed on a property (they don't go back to the owner). Body: {confirm: the property ID}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto propertyId = PathId<LWOOBJID>(context.path, 2);
|
||||
const auto body = ParseBody(context);
|
||||
if (!propertyId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid ID");
|
||||
if (!body || body->value("confirm", std::string()) != std::to_string(*propertyId)) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Confirm with the property ID");
|
||||
const auto property = Database::Get()->GetPropertyInfo(*propertyId);
|
||||
if (!property) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "Property not found");
|
||||
// A loaded property keeps its own list of models and saves it back
|
||||
if (PropertyInstanceRunning(property->cloneId)) return JsonError(reply, eHTTPStatusCode::CONFLICT, "This property is loaded in a world right now; try again when nobody is on it");
|
||||
|
||||
const auto models = Database::Get()->GetPropertyModels(*propertyId);
|
||||
for (const auto& model : models) Database::Get()->RemoveModel(model.id);
|
||||
Audit(context, "remove_property_models", std::to_string(models.size()) + " model(s) from property " + std::to_string(*propertyId));
|
||||
BroadcastTableChanged("properties", std::to_string(*propertyId));
|
||||
JsonSuccess(reply, { {"removed", models.size()}, {"message", "Removed " + std::to_string(models.size()) + " model(s)"} });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -682,11 +682,19 @@ namespace UgcRoutes {
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::POST, "/api/ugc/reprocess", Perm("ugc_manage"),
|
||||
"Have the UGC server make items again. Body: {kind: model|modular, id} for one, {kind, failedOnly: true} for the failed ones, {kind} for all",
|
||||
"Have the UGC server make items again. Body: {kind: model|modular, id} for one, {kind: model, property} for every model placed on a property, {kind, failedOnly: true} for the failed ones, {kind} for all",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto body = ParseBody(context);
|
||||
if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON");
|
||||
const bool modular = body->value("kind", "model") == "modular";
|
||||
if (!modular && body->contains("property")) {
|
||||
const auto property = GeneralUtils::TryParse<LWOOBJID>((*body)["property"].is_string() ? (*body)["property"].get<std::string>() : (*body)["property"].dump());
|
||||
if (!property) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid property");
|
||||
const auto changed = Database::Get()->ResetPropertyUgcModelProcessing(*property);
|
||||
Audit(context, "ugc_reprocess", "Queued " + std::to_string(changed) + " model(s) on property " + std::to_string(*property) + " to be made again");
|
||||
BroadcastTableChanged("ugc");
|
||||
return JsonSuccess(reply, { { "message", std::to_string(changed) + " model" + (changed == 1 ? "" : "s") + " will be made again" } });
|
||||
}
|
||||
std::optional<LWOOBJID> id;
|
||||
if (body->contains("id")) {
|
||||
id = BodyId(*body);
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="rejectProperty()">Reject</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary" href="/properties/{{ property_id }}/3d">Open 3D view</a>
|
||||
{% if can.ugc_manage %}
|
||||
<button class="btn btn-sm btn-outline-secondary" id="reprocessModelsBtn" title="Have the UGC server make every model on this property again (mesh and icon), with the current settings">Reprocess all models</button>
|
||||
{% endif %}
|
||||
{% if can.reports_view %}
|
||||
<a class="btn btn-sm btn-outline-secondary d-none" id="propEconomy" href="#">Economy & map</a>
|
||||
{% endif %}
|
||||
@@ -88,6 +91,11 @@
|
||||
</div>
|
||||
<div class="col-md-2"><button class="btn btn-sm btn-primary w-100" id="importBtn">Import</button></div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<button class="btn btn-sm btn-outline-danger" id="removeModelsBtn">Remove all models</button>
|
||||
<span class="small text-body-secondary">Deletes every model placed on this property; they don't go back to the owner. Not possible while the property is loaded in a world.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -264,6 +272,21 @@ $('#modelsTable').DataTable({
|
||||
language: { emptyTable: 'Nothing placed on this property' }
|
||||
});
|
||||
|
||||
const reprocessModelsBtn = document.getElementById('reprocessModelsBtn');
|
||||
if (reprocessModelsBtn) reprocessModelsBtn.addEventListener('click', () => {
|
||||
reprocessModelsBtn.disabled = true;
|
||||
api.action('/api/ugc/reprocess', { kind: 'model', property: propertyId }).then((d) => { toast(d.message, 'success'); load(); })
|
||||
.catch(() => {}).finally(() => { reprocessModelsBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const removeModelsBtn = document.getElementById('removeModelsBtn');
|
||||
if (removeModelsBtn) removeModelsBtn.addEventListener('click', () => {
|
||||
if (window.prompt('This deletes every model on the property. Type the property ID (' + propertyId + ') to confirm:') !== propertyId) return;
|
||||
removeModelsBtn.disabled = true;
|
||||
api.action('/api/properties/' + propertyId + '/remove_models', { confirm: propertyId }).then((d) => { toast(d.message, 'success'); load(); })
|
||||
.catch(() => {}).finally(() => { removeModelsBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const importBtn = document.getElementById('importBtn');
|
||||
if (importBtn) importBtn.addEventListener('click', async () => {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
|
||||
@@ -110,6 +110,9 @@ public:
|
||||
// with `failedOnly`). Returns how many rows changed.
|
||||
virtual uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) = 0;
|
||||
|
||||
// Sets every model placed on a property back to pending with no attempts. Returns how many rows changed.
|
||||
virtual uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) = 0;
|
||||
|
||||
// A page of models (all, or those in `state`), the newest first; `search` (when not empty) matches the model's id
|
||||
// exactly or part of its owner's name
|
||||
virtual std::vector<ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) = 0;
|
||||
|
||||
@@ -437,6 +437,7 @@ public:
|
||||
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override;
|
||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
|
||||
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
|
||||
uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override;
|
||||
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override;
|
||||
std::vector<std::pair<IUgc::eProcessState, uint64_t>> GetUgcProcessCounts() override;
|
||||
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsToProcess(const uint32_t limit) override;
|
||||
|
||||
@@ -144,6 +144,11 @@ uint64_t MySQLDatabase::ResetUgcModelProcessing(const std::optional<LWOOBJID> id
|
||||
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0;");
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) {
|
||||
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0 "
|
||||
"WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", propertyId);
|
||||
}
|
||||
|
||||
std::vector<IUgc::ProcessInfo> MySQLDatabase::GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) {
|
||||
const std::string select =
|
||||
"SELECT u.id, u.character_id, c.name AS character_name, u.is_optimized, u.process_attempts, u.processed_at, u.process_error, u.bake_ao, u.process_after "
|
||||
|
||||
@@ -421,6 +421,7 @@ public:
|
||||
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override;
|
||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
|
||||
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
|
||||
uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override;
|
||||
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override;
|
||||
std::vector<std::pair<IUgc::eProcessState, uint64_t>> GetUgcProcessCounts() override;
|
||||
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsToProcess(const uint32_t limit) override;
|
||||
|
||||
@@ -145,6 +145,11 @@ uint64_t SQLiteDatabase::ResetUgcModelProcessing(const std::optional<LWOOBJID> i
|
||||
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0;");
|
||||
}
|
||||
|
||||
uint64_t SQLiteDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) {
|
||||
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0 "
|
||||
"WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", propertyId);
|
||||
}
|
||||
|
||||
std::vector<IUgc::ProcessInfo> SQLiteDatabase::GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) {
|
||||
const std::string select =
|
||||
"SELECT u.id, u.character_id, c.name AS character_name, u.is_optimized, u.process_attempts, u.processed_at, u.process_error, u.bake_ao, u.process_after "
|
||||
|
||||
@@ -419,6 +419,7 @@ class TestSQLDatabase : public GameDatabase {
|
||||
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override { return {}; }
|
||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override { return {}; }
|
||||
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override { return 0; }
|
||||
uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override { return 0; }
|
||||
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override { return {}; }
|
||||
std::vector<std::pair<IUgc::eProcessState, uint64_t>> GetUgcProcessCounts() override { return {}; }
|
||||
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsToProcess(const uint32_t limit) override { return {}; }
|
||||
|
||||
@@ -756,6 +756,13 @@ TEST_F(ParitySeeded, UgcModel) {
|
||||
db.SetUgcModelProcessed(1152921510000500001LL, IUgc::eProcessState::DONE, 1, "", true);
|
||||
return json{ changed, infoJson(db.GetUgcProcessInfo(1152921510000500001LL)), infoJson(db.GetUgcProcessInfo(1)) };
|
||||
});
|
||||
Both("ResetPropertyUgcModelProcessing", [&](GameDatabase& db) {
|
||||
IPropertyContents::Model model;
|
||||
model.id = 1152921510000400009LL; model.lot = 14; model.ugcId = 1152921510000500001LL;
|
||||
db.InsertNewPropertyModel(PROP1, model, "Objects_14_name");
|
||||
const auto changed = db.ResetPropertyUgcModelProcessing(PROP1);
|
||||
return json{ changed, infoJson(db.GetUgcProcessInfo(1152921510000500001LL)), db.ResetPropertyUgcModelProcessing(1) };
|
||||
});
|
||||
Both("Modular build processing", [&](GameDatabase& db) {
|
||||
db.InsertUgcBuild("1:4713+1:4714+1:4715", 1152921510000500002LL, CHAR_ALICE);
|
||||
json out = json::array();
|
||||
|
||||
Reference in New Issue
Block a user