From 1b2883e92e8583fbf476a74621f9bdfc8532065f Mon Sep 17 00:00:00 2001
From: Migratingcocofruit
<69551563+Migratingcocofruit@users.noreply.github.com>
Date: Sat, 1 Nov 2025 16:06:34 +0200
Subject: [PATCH] Records unhandled in-game bug reports in a DB table and loads
the reports from it for the next round (#30710)
* initial table setup
* adds helpers to get full byond versions as numbers and adds those to the table as well
* reorder bug report new() proc and init bug_report_data as empty list instead of null
* more table changes. move adding the metadata to its own proc
* record unsent bug reports into the DB table
* refers to the correct index in the bug report data for the commit
* flip user and server byond versions
* jsonify bug report contents and metadata
* makes a bug report subsystem and moves recording to it
* Implements loading bug reports from the DB at shift start. Also removes handled bug reports from the DB directly
* Update SSbugreports.dm
* Update SSbugreports.dm
* scopes the bug report recording proc to the subsystem
---
SQL/paradise_schema.sql | 14 ++++
SQL/updates/70-71.sql | 11 +++
code/__DEFINES/misc_defines.dm | 2 +-
code/controllers/subsystem/SSbugreports.dm | 40 +++++++++++
code/datums/bug_report.dm | 80 +++++++++++++++-------
code/modules/admin/verbs/debug.dm | 2 +-
config/example/config.toml | 2 +-
paradise.dme | 1 +
8 files changed, 125 insertions(+), 27 deletions(-)
create mode 100644 SQL/updates/70-71.sql
create mode 100644 code/controllers/subsystem/SSbugreports.dm
diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql
index 3c16d6ae650..8bab7b207f7 100644
--- a/SQL/paradise_schema.sql
+++ b/SQL/paradise_schema.sql
@@ -666,3 +666,17 @@ CREATE TABLE `json_datum_saves` (
UNIQUE INDEX `ckey_unique` (`ckey`, `slotname`) USING BTREE,
INDEX `ckey` (`ckey`) USING BTREE
) COLLATE = 'utf8mb4_general_ci' ENGINE = InnoDB;
+
+--
+-- Table structure for table 'bug_reports'
+--
+DROP TABLE IF EXISTS `bug_reports`;
+CREATE TABLE `bug_reports` (
+ `db_uid` BIGINT(32) NOT NULL,
+ `author_ckey` varchar(32) NOT NULL,
+ `title` MEDIUMTEXT COLLATE 'utf8mb4_general_ci',
+ `round_id` int(11),
+ `contents_json` LONGTEXT,
+ CONSTRAINT bug_key PRIMARY KEY (`db_uid`,`author_ckey`) USING BTREE
+
+) COLLATE = 'utf8mb4_general_ci' ENGINE = INNODB;
diff --git a/SQL/updates/70-71.sql b/SQL/updates/70-71.sql
new file mode 100644
index 00000000000..a2e704286ea
--- /dev/null
+++ b/SQL/updates/70-71.sql
@@ -0,0 +1,11 @@
+# Updating SQL from 70 to 71 -MigratingCocofruit
+# Adding new table for bug reports
+CREATE TABLE `bug_reports` (
+ `db_uid` INT(32) NOT NULL,
+ `author_ckey` varchar(32) NOT NULL,
+ `title` MEDIUMTEXT COLLATE 'utf8mb4_general_ci',
+ `round_id` int(11),
+ `contents_json` LONGTEXT,
+ CONSTRAINT bug_key PRIMARY KEY (`db_uid`,`author_ckey`) USING BTREE
+
+) COLLATE = 'utf8mb4_general_ci' ENGINE = INNODB;
diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm
index 66c3218bd6d..0edeabbc864 100644
--- a/code/__DEFINES/misc_defines.dm
+++ b/code/__DEFINES/misc_defines.dm
@@ -440,7 +440,7 @@
#define INVESTIGATE_DEATHS "deaths"
// The SQL version required by this version of the code
-#define SQL_VERSION 70
+#define SQL_VERSION 71
// Vending machine stuff
#define CAT_NORMAL (1<<0)
diff --git a/code/controllers/subsystem/SSbugreports.dm b/code/controllers/subsystem/SSbugreports.dm
new file mode 100644
index 00000000000..8d14496a94b
--- /dev/null
+++ b/code/controllers/subsystem/SSbugreports.dm
@@ -0,0 +1,40 @@
+SUBSYSTEM_DEF(bugreports)
+ name = "Bug Reports"
+ flags = SS_NO_FIRE
+
+/datum/controller/subsystem/bugreports/Initialize()
+ // We just want to load all bug reports into the
+ var/datum/db_query/query_bug_reports = SSdbcore.NewQuery("SELECT * FROM bug_reports")
+ if(!query_bug_reports.warn_execute())
+ log_debug("Failed to load stored bug reports from DB")
+ qdel(query_bug_reports)
+ return
+ while(query_bug_reports.NextRow())
+ // Create a new bug report form an load the details from the current row into it
+ var/datum/tgui_bug_report_form/bug_report = new()
+ GLOB.bug_reports += bug_report
+ bug_report.db_uid = query_bug_reports.item[1]
+ bug_report.initial_key = query_bug_reports.item[2]
+ bug_report.bug_report_data = json_decode(query_bug_reports.item[5])
+ bug_report.awaiting_approval = TRUE
+
+/datum/controller/subsystem/bugreports/Shutdown()
+ record_bug_reports()
+
+/datum/controller/subsystem/bugreports/proc/record_bug_reports()
+ for(var/datum/tgui_bug_report_form/bug_report in GLOB.bug_reports)
+ var/datum/db_query/bug_query = SSdbcore.NewQuery({"
+ INSERT IGNORE INTO bug_reports (db_uid, author_ckey, title, round_id, contents_json) VALUES (:db_uid, :author_ckey, :title, :round_id, :contents_json)
+ "},
+ list(
+ "db_uid" = bug_report.db_uid,
+ "author_ckey" = bug_report.initial_key,
+ "title" = bug_report.bug_report_data["title"],
+ "round_id" = bug_report.bug_report_data["round_id"],
+ "contents_json" = json_encode(bug_report.bug_report_data),
+ )
+ )
+ bug_query.warn_execute()
+ qdel(bug_query)
+ GLOB.bug_reports -= bug_report
+ qdel(bug_report)
diff --git a/code/datums/bug_report.dm b/code/datums/bug_report.dm
index f2f7aa49549..8946a6e608f 100644
--- a/code/datums/bug_report.dm
+++ b/code/datums/bug_report.dm
@@ -6,11 +6,12 @@ GLOBAL_LIST_EMPTY(bug_report_time)
/datum/tgui_bug_report_form
/// contains all the body text for the bug report.
- var/list/bug_report_data = null
+ var/list/bug_report_data = list()
/// client of the bug report author, needed to create the ticket
var/initial_user_uid = null
- // ckey of the author
+
+ /// ckey of the author
var/initial_key = null // just incase they leave after creating the bug report
/// client of the admin/dev who is accessing the report, we don't want multiple people unknowingly making changes at the same time.
@@ -22,24 +23,27 @@ GLOBAL_LIST_EMPTY(bug_report_time)
/// for garbage collection purposes.
var/selected_confirm = FALSE
- /// byond version of the user, so we still have the byond version if the user logs out
- var/user_byond_version
-
- /// Current Server commit
- var/local_commit
-
- /// Current test merges formatted for the bug report
- var/test_merges
+ /// UID for DB stuff
+ var/db_uid
/datum/tgui_bug_report_form/New(mob/user)
- local_commit = GLOB.revision_info.commit_hash
- initial_user_uid = user.client.UID()
- initial_key = user.client.key
- user_byond_version = "[user.client.byond_version].[user.client.byond_build]"
- if(length(GLOB.revision_info.origin_commit))
- local_commit = GLOB.revision_info.origin_commit
+ if(user)
+ initial_user_uid = user.client.UID()
+ initial_key = user.client.key
+
+/datum/tgui_bug_report_form/proc/add_metadata(mob/user)
+ bug_report_data["server_byond_version"] = "[world.byond_version].[world.byond_build]"
+ bug_report_data["user_byond_version"] = "[user.client.byond_version].[user.client.byond_build]"
+
+ bug_report_data["local_commit"] = "No Commit Data"
+ if(GLOB.revision_info.commit_hash)
+ bug_report_data["local_commit"] = GLOB.revision_info.commit_hash
+ if(GLOB.revision_info?.origin_commit && length(GLOB.revision_info.origin_commit))
+ bug_report_data["local_commit"] = GLOB.revision_info.origin_commit
+
for(var/datum/tgs_revision_information/test_merge/tm in GLOB.revision_info.testmerges)
- test_merges += "#[tm.number] at [tm.head_commit]\n"
+ bug_report_data["test_merges"] += "#[tm.number] at [tm.head_commit]\n"
+ bug_report_data["round_id"] = GLOB.round_id
/datum/tgui_bug_report_form/proc/external_link_prompt(client/user)
@@ -118,11 +122,11 @@ GLOBAL_LIST_EMPTY(bug_report_time)
## Additional details
- Author: [initial_key]
- Approved By: [approving_user]
-- Round ID: [GLOB.round_id ? GLOB.round_id : "N/A"]
-- Client BYOND Version: [user_byond_version]
-- Server BYOND Version: [world.byond_version].[world.byond_build]
-- Server commit: [local_commit]
-- Active Test Merges: [test_merges ? test_merges : "None"]
+- Round ID: [bug_report_data["round_id"] ? bug_report_data["round_id"] : "N/A"]
+- Client BYOND Version: [bug_report_data["user_byond_version"]]
+- Server BYOND Version: [bug_report_data["server_byond_version"]]
+- Server commit: [bug_report_data["local_commit"]]
+- Active Test Merges: [bug_report_data["test_merges"] ? bug_report_data["test_merges"] : "None"]
- Note: [bug_report_data["approver_note"] ? bug_report_data["approver_note"] : "None"]
"}
@@ -156,6 +160,19 @@ GLOBAL_LIST_EMPTY(bug_report_time)
request.prepare(RUSTLIBS_HTTP_METHOD_POST, url, json_encode(payload), headers)
request.begin_async()
+
+ // Report has been handled so we can remove it from the DB.
+ // If the request fails the user is prompted to open an issue on Github, so we consider it handled as well.
+ var/datum/db_query/query_delete_bug_report = SSdbcore.NewQuery(
+ "DELETE FROM bug_reports WHERE (db_uid=:db_uid AND author_ckey=:author_ckey)",
+ list(
+ "db_uid" = db_uid,
+ "author_ckey" = initial_key,
+ )
+ )
+ query_delete_bug_report.warn_execute()
+ qdel(query_delete_bug_report)
+
var/start_time = world.time
UNTIL(request.is_complete() || (world.time > start_time + 5 SECONDS))
if(!request.is_complete() && world.time > start_time + 5 SECONDS)
@@ -168,14 +185,18 @@ GLOBAL_LIST_EMPTY(bug_report_time)
else
var/client/initial_user = locateUID(initial_user_uid)
message_admins("[user.ckey] has approved a bug report from [initial_key] titled [bug_report_data["title"]] at [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")].")
- to_chat(initial_user, "An admin has successfully submitted your report and it should now be visible on GitHub. Thanks again!")
- qdel(src)// approved and submitted, we no longer need the datum.
+ if(initial_user)
+ to_chat(initial_user, "An admin has successfully submitted your report and it should now be visible on GitHub. Thanks again!")
+ // approved and submitted, we no longer need the datum.
+ qdel(src)
// proc that creates a ticket for an admin to approve or deny a bug report request
/datum/tgui_bug_report_form/proc/bug_report_request()
var/client/initial_user = locateUID(initial_user_uid)
if(initial_user)
to_chat(initial_user, "Your bug report has been submitted, thank you!")
+ if(!db_uid)
+ db_uid = world.realtime
GLOB.bug_reports += src
var/general_message = "[initial_key] has created a bug report which is now pending approval. The report can be viewed using \"View Bug Reports\" in the debug tab. "
@@ -192,6 +213,7 @@ GLOBAL_LIST_EMPTY(bug_report_time)
to_chat(user, "You have already approved this submission, please wait a moment for the API to process your submission.")
return
bug_report_data = sanitize_payload(params)
+ add_metadata(user)
selected_confirm = TRUE
// bug report request is now waiting for admin approval
if(!awaiting_approval)
@@ -218,5 +240,15 @@ GLOBAL_LIST_EMPTY(bug_report_time)
var/client/initial_user = locateUID(initial_user_uid)
if(initial_user)
to_chat(initial_user, "A staff member has rejected your bug report, this can happen for several reasons. They will most likely get back to you shortly regarding your issue.")
+ // Report has been handled so we can remove it from the DB
+ var/datum/db_query/query_delete_bug_report = SSdbcore.NewQuery(
+ "DELETE FROM bug_reports WHERE (db_uid=:db_uid AND author_ckey=:author_ckey)",
+ list(
+ "db_uid" = db_uid,
+ "author_ckey" = initial_key,
+ )
+ )
+ query_delete_bug_report.warn_execute()
+ qdel(query_delete_bug_report)
#undef STATUS_SUCCESS
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 9213fe72adb..1067496b9d1 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -937,7 +937,7 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention)
for(var/datum/tgui_bug_report_form/report in GLOB.bug_reports)
bug_report_selection["[report.initial_key] - [report.bug_report_data["title"]]"] = report
var/datum/tgui_bug_report_form/form = bug_report_selection[tgui_input_list(usr, "Select a report to view:", "Bug Reports", bug_report_selection)]
- if(!form.assign_approver(usr))
+ if(!form?.assign_approver(usr))
return
form.ui_interact(usr)
return
diff --git a/config/example/config.toml b/config/example/config.toml
index 43a484e58e9..598bee10690 100644
--- a/config/example/config.toml
+++ b/config/example/config.toml
@@ -180,7 +180,7 @@ fluff_undershirts = [
# Enable/disable the database on a whole
sql_enabled = false
# SQL version. If this is a mismatch, round start will be delayed
-sql_version = 70
+sql_version = 71
# SQL server address. Can be an IP or DNS name
sql_address = "127.0.0.1"
# SQL server port
diff --git a/paradise.dme b/paradise.dme
index a29f112cf22..5b13b27782c 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -323,6 +323,7 @@
#include "code\controllers\subsystem\SSair.dm"
#include "code\controllers\subsystem\SSambience.dm"
#include "code\controllers\subsystem\SSblackbox.dm"
+#include "code\controllers\subsystem\SSbugreports.dm"
#include "code\controllers\subsystem\SScamera.dm"
#include "code\controllers\subsystem\SSchat.dm"
#include "code\controllers\subsystem\SScleanup.dm"