This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.
+
+
+
@ViewData["Title"]
+ @RenderBody()
+
+
+
+
+
diff --git a/src/SMAPI.Web/Views/_ViewStart.cshtml b/src/SMAPI.Web/Views/_ViewStart.cshtml
new file mode 100644
index 00000000..a5f10045
--- /dev/null
+++ b/src/SMAPI.Web/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "_Layout";
+}
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index fa8ce71a..87c35ca9 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -1,4 +1,13 @@
-{
+/*
+
+
+ This file is committed to source control with the default settings, but added to .gitignore to
+ avoid accidentally committing login details.
+
+
+
+*/
+{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
@@ -6,5 +15,14 @@
"System": "Information",
"Microsoft": "Information"
}
+ },
+ "ModUpdateCheck": {
+ "GitHubUsername": null,
+ "GitHubPassword": null
+ },
+ "LogParser": {
+ "SectionUrl": "http://localhost:59482/log/",
+ "PastebinUserKey": null,
+ "PastebinDevKey": null
}
}
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 852f6f71..eb6ecc9b 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -1,3 +1,11 @@
+/*
+
+
+ This contains the default settings for the web app. Login credentials and contextual settings are
+ configured via appsettings.Development.json locally, or environment properties in AWS.
+
+
+*/
{
"Logging": {
"IncludeScopes": false,
@@ -19,12 +27,19 @@
"GitHubBaseUrl": "https://api.github.com",
"GitHubReleaseUrlFormat": "repos/{0}/releases/latest",
"GitHubAcceptHeader": "application/vnd.github.v3+json",
- "GitHubUsername": null, /* set via environment properties */
- "GitHubPassword": null, /* set via environment properties */
+ "GitHubUsername": null, // see top note
+ "GitHubPassword": null, // see top note
"NexusKey": "Nexus",
"NexusUserAgent": "Nexus Client v0.63.15",
"NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
"NexusModUrlFormat": "mods/{0}"
+ },
+ "LogParser": {
+ "SectionUrl": null, // see top note
+ "PastebinBaseUrl": "https://pastebin.com/",
+ "PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
+ "PastebinUserKey": null, // see top note
+ "PastebinDevKey": null // see top note
}
}
diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
new file mode 100644
index 00000000..975e9c2e
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
@@ -0,0 +1,348 @@
+.mod-repeat {
+ font-size: 8pt;
+}
+
+.template {
+ display: none;
+}
+
+.popup, #uploader {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, .33);
+ z-index: 2;
+ display: none;
+ padding: 5px;
+}
+
+#upload-button {
+ background: #ccf;
+ border: 1px solid #000088;
+}
+
+#upload-button {
+ background: #eef;
+}
+
+
+#uploader:after {
+ content: attr(data-text);
+ display: block;
+ width: 100px;
+ height: 24px;
+ line-height: 25px;
+ border: 1px solid #000;
+ background: #fff;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -12px -50px 0 0;
+ font-size: 18px;
+ font-weight: bold;
+ text-align: center;
+ border-radius: 5px;
+}
+
+.popup h1 {
+ position: absolute;
+ top: 10%;
+ left: 50%;
+ margin-left: -150px;
+ text-align: center;
+ width: 300px;
+ border: 1px solid #008;
+ border-radius: 5px;
+ background: #fff;
+ font-family: sans-serif;
+ font-size: 40px;
+ margin-top: -25px;
+ z-index: 10;
+ border-bottom: 0;
+}
+
+.frame {
+ margin: auto;
+ margin-top: 25px;
+ padding: 2em;
+ position: absolute;
+ top: 10%;
+ left: 10%;
+ right: 10%;
+ bottom: 10%;
+ padding-bottom: 30px;
+ background: #FFF;
+ border-radius: 5px;
+ border: 1px solid #008;
+}
+
+input[type="button"] {
+ font-size: 20px;
+ border-radius: 5px;
+ outline: none;
+ box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 0, .2);
+ cursor: pointer;
+}
+
+#input[type="button"]:hover {
+ background-color: #fee;
+}
+
+#cancel, #closeraw {
+ border: 1px solid #880000;
+ background-color: #fcc;
+}
+
+#submit {
+ border: 1px solid #008800;
+ background-color: #cfc;
+}
+
+#submit:hover {
+ background-color: #efe;
+}
+
+#input, #dataraw {
+ width: 100%;
+ height: 30em;
+ max-height: 70%;
+ margin: auto;
+ box-sizing: border-box;
+ border-radius: 5px;
+ border: 1px solid #000088;
+ outline: none;
+ box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2);
+}
+
+.color-red {
+ color: red;
+}
+
+.color-green {
+ color: green;
+}
+
+#tabs {
+ border-bottom: 0;
+ display: block;
+ margin: 0;
+ padding: 0;
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(210, 235, 249, 1) 100%);
+}
+
+#tabs li {
+ margin: 5px 1px 0 0;
+ height: 25px;
+ display: inline-block;
+ width: 75px;
+ border: 1px solid #000000;
+ border-bottom: 0;
+ border-radius: 5px 5px 0 0;
+ text-align: center;
+ font-family: monospace;
+ font-size: 18px;
+ cursor: pointer;
+ font-weight: bold;
+ color: #000;
+ text-shadow: 0px 0px 2px #fff;
+ border-color: #880000;
+ background-color: #fcc;
+ box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 0, .2);
+}
+
+#tabs li:hover {
+ background-color: #fee;
+}
+
+#tabs li:first-child {
+ margin-left: 5px;
+}
+
+#tabs li.active {
+ background: #cfc;
+ border-color: #008800;
+}
+
+#tabs li.active:hover {
+ background: #efe;
+}
+
+#tabs li.notice {
+ color: #000000;
+ background: transparent;
+ border: 0;
+ padding-top: 1px;
+ font-size: 13px;
+ font-weight: normal;
+ width: auto;
+ margin-left: 5px;
+ cursor: default;
+ box-shadow: none;
+ font-style: italic;
+}
+
+#output {
+ border-top: 1px solid #888;
+ padding: 10px;
+ overflow: auto;
+ font-family: monospace;
+}
+
+#output > * {
+ display: block;
+}
+
+#output.trace .trace,
+#output.debug .debug,
+#output.info .info,
+#output.alert .alert,
+#output.warn .warn,
+#output.error .error {
+ display: none;
+}
+
+#output .trace {
+ color: #999;
+}
+
+#output .debug {
+ color: #595959;
+}
+
+#output .info {
+ color: #000
+}
+
+#output .alert {
+ color: #b0b;
+}
+
+#output .warn {
+ color: #f80
+}
+
+#output .error {
+ color: #f00
+}
+
+#output .always {
+ font-weight: bold;
+ border-bottom: 1px dashed #888888;
+ padding-bottom: 10px;
+ margin-bottom: 5px;
+}
+
+caption {
+ text-align: left;
+ padding-top: 2px;
+}
+
+#log {
+ border-spacing: 0;
+}
+
+#log tr {
+ background: #fff;
+}
+
+#log td {
+ padding: 0 1px;
+ background: inherit;
+ border-bottom: 1px dotted #ccc;
+ border-top: 2px solid #fff;
+ vertical-align: top;
+}
+
+#log td:not(:last-child) {
+ max-width: 175px;
+ padding: 0 4px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+#log td[data-title]:hover {
+ font-size: 1px;
+ overflow: inherit;
+ position: relative;
+}
+
+#log td:nth-child(3):hover:after {
+ content: attr(data-title);
+ display: block;
+ position: absolute;
+ border-radius: 4px;
+ box-shadow: 1px 1px 2px #ccc;
+ background: inherit;
+ border: 1px solid #ccc;
+ background: #efefef;
+ padding: 1px 1px 0 1px;
+ font-size: 10pt;
+ top: -2px;
+ left: 2px;
+ color: #000;
+}
+
+#log td:last-child {
+ width: 100%;
+}
+
+table#gameinfo,
+table#modslist {
+ border: 1px solid #000000;
+ background: #ffffff;
+ border-radius: 5px;
+ border-spacing: 1px;
+ overflow: hidden;
+ cursor: default;
+ box-shadow: 1px 1px 1px 1px #dddddd;
+}
+
+#modslist {
+ min-width: 400px;
+}
+
+#gameinfo td:first-child {
+ padding-right: 5px;
+}
+
+#gameinfo tr,
+#modslist tr {
+ background: #eee
+}
+
+#gameinfo tr:nth-child(even),
+#modslist tr:nth-child(even) {
+ background: #fff
+}
+
+#modslist tr {
+ cursor: pointer;
+}
+
+span.notice {
+ font-weight: normal;
+ font-size: 11px;
+ position: relative;
+ top: -1px;
+ display: none;
+}
+
+span.notice.btn {
+ cursor: pointer;
+ border: 1px solid #000;
+ border-radius: 5px;
+ position: relative;
+ top: -1px;
+ padding: 0 2px;
+ background: #eee;
+}
+
+#output:not(.modfilter) span.notice.txt {
+ display: inline-block;
+}
+
+#output.modfilter span.notice.btn {
+ display: inline-block;
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css
new file mode 100644
index 00000000..d1fa49e0
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/css/main.css
@@ -0,0 +1,107 @@
+/* tags */
+html {
+ height: 100%;
+}
+
+body {
+ height: 100%;
+ font-family: sans-serif;
+}
+
+h1, h2, h3 {
+ font-weight: bold;
+ margin: 0.2em 0 0.1em 0;
+ padding-top: .5em;
+}
+
+h1 {
+ font-size: 1.5em;
+ color: #888;
+ margin-bottom: 0;
+}
+
+h2 {
+ font-size: 1.5em;
+ border-bottom: 1px solid #AAA;
+}
+
+h3 {
+ font-size: 1.2em;
+ border-bottom: 1px solid #AAA;
+ width: 50%;
+}
+
+a {
+ color: #006;
+}
+
+/* content */
+#content-column {
+ position: absolute;
+ top: 1em;
+ left: 10em;
+}
+
+#content {
+ min-height: 140px;
+ padding: 0 1em 1em 1em;
+ border-left: 1px solid #CCC;
+ background: #FFF;
+ font-size: 0.9em;
+}
+
+#content p {
+ max-width: 55em;
+}
+
+.section {
+ border: 1px solid #CCC;
+ padding: 0.5em;
+ margin-bottom: 1em;
+}
+
+/* sidebar */
+#sidebar {
+ margin-top: 3em;
+ min-height: 75%;
+ width: 12em;
+ background: url("../images/sidebar-bg.gif") no-repeat top right;
+ color: #666;
+}
+
+#sidebar h4 {
+ margin: 0 0 0.2em 0;
+ width: 10em;
+ border-bottom: 1px solid #CCC;
+ font-size: 0.8em;
+ font-weight: normal;
+}
+
+#sidebar a {
+ color: #77B;
+ border: 0;
+}
+
+#sidebar ul, #sidebar li {
+ margin: 0;
+ padding: 0;
+ list-style: none none;
+ font-size: 0.9em;
+ color: #888;
+}
+
+#sidebar li {
+ margin-left: 1em;
+}
+
+/* footer */
+#footer {
+ margin: 1em;
+ padding: 1em;
+ font-size: 0.6em;
+ color: gray;
+}
+
+#footer a {
+ color: #669;
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif b/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif
new file mode 100644
index 00000000..48e9af5a
Binary files /dev/null and b/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif differ
diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
new file mode 100644
index 00000000..8e30ae12
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
@@ -0,0 +1,278 @@
+/* globals $ */
+
+var smapi = smapi || {};
+smapi.logParser = function(sectionUrl, pasteID) {
+ /*********
+ ** Initialisation
+ *********/
+ var stage,
+ flags = $("#modflags"),
+ output = $("#output"),
+ filters = 0,
+ memory = "",
+ versionInfo,
+ modInfo,
+ modMap,
+ modErrors,
+ logInfo,
+ templateBody = $("#template-body").text(),
+ templateModentry = $("#template-modentry").text(),
+ templateCss = $("#template-css").text(),
+ templateLogentry = $("#template-logentry").text(),
+ templateLognotice = $("#template-lognotice").text(),
+ regexInfo = /\[[\d\:]+ INFO SMAPI] SMAPI (.*?) with Stardew Valley (.*?) on (.*?)\n/g,
+ regexMods = /\[[^\]]+\] Loaded \d+ mods:(?:\n\[[^\]]+\] .+)+/g,
+ regexLog = /\[([\d\:]+) (TRACE|DEBUG|INFO|WARN|ALERT|ERROR) ? ([^\]]+)\] ?((?:\n|.)*?)(?=(?:\[\d\d:|$))/g,
+ regexMod = /\[(?:.*?)\] *(.*?) (\d+\.?(?:.*?))(?: by (.*?))? \|(?:.*?)$/gm,
+ regexDate = /\[\d{2}:\d{2}:\d{2} TRACE SMAPI\] Log started at (.*?) UTC/g,
+ regexPath = /\[\d{2}:\d{2}:\d{2} DEBUG SMAPI\] Mods go here: (.*?)(?:\n|$)/g;
+
+ $("#tabs li:not(.notice)").on("click", function(evt) {
+ var t = $(evt.currentTarget);
+ t.toggleClass("active");
+ $("#output").toggleClass(t.text().toLowerCase());
+ });
+ $("#upload-button").on("click", function() {
+ memory = $("#input").val() || "";
+ $("#input").val("");
+ $("#popup-upload").fadeIn();
+ });
+ $("#popup-upload").on({
+ 'dragover dragenter': function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ 'drop': function(e) {
+ $("#uploader").attr("data-text", "Reading...");
+ $("#uploader").show();
+ var dataTransfer = e.originalEvent.dataTransfer;
+ if (dataTransfer && dataTransfer.files.length) {
+ e.preventDefault();
+ e.stopPropagation();
+ var file = dataTransfer.files[0];
+ var reader = new FileReader();
+ reader.onload = $.proxy(function(file, $input, event) {
+ $input.val(event.target.result);
+ $("#uploader").fadeOut();
+ $("#submit").click();
+ }, this, file, $("#input"));
+ reader.readAsText(file);
+ }
+ }
+ });
+
+ $("#submit").on("click", function() {
+ $("#popup-upload").fadeOut();
+ var paste = $("#input").val();
+ if (paste) {
+ memory = "";
+ $("#uploader").attr("data-text", "Saving...");
+ $("#uploader").fadeIn();
+ $
+ .ajax({
+ type: "POST",
+ url: sectionUrl + "/save",
+ data: JSON.stringify(paste),
+ contentType: "application/json" // sent to API
+ })
+ .fail(function(xhr, textStatus) {
+ $("#uploader").fadeOut();
+ $("#output").html('Parsing failed!
Parsing of the log failed, details follow.
Stage: Upload
Error: ' + textStatus + ': ' + xhr.responseText + "
" + $("#input").val() + "
");
+ })
+ .then(function(data) {
+ $("#uploader").fadeOut();
+ if (!data.success)
+ $("#output").html('Parsing failed!
Parsing of the log failed, details follow.
Stage: Upload
Error: ' + data.error + "
" + $("#input").val() + "
");
+ else
+ location.href = (sectionUrl.replace(/\/$/, "") + "/" + data.id);
+ });
+ } else {
+ alert("Unable to parse log, the input is empty!");
+ $("#uploader").fadeOut();
+ }
+ });
+ $("#cancel").on("click", function() {
+ $("#popup-upload").fadeOut(400, function() {
+ $("#input").val(memory);
+ memory = "";
+ });
+ });
+ $("#closeraw").on("click", function() {
+ $("#popup-raw").fadeOut(400);
+ });
+ if (pasteID) {
+ getData(pasteID);
+ }
+ else
+ $("#popup-upload").fadeIn();
+
+
+ /*********
+ ** Helpers
+ *********/
+ function modClicked(evt) {
+ var id = $(evt.currentTarget).attr("id").split("-")[1],
+ cls = "mod-" + id;
+ if (output.hasClass(cls))
+ filters--;
+ else
+ filters++;
+ output.toggleClass(cls);
+ if (filters === 0) {
+ output.removeClass("modfilter");
+ } else {
+ output.addClass("modfilter");
+ }
+ }
+
+ function removeFilter() {
+ for (var c = 0; c < modInfo.length; c++) {
+ output.removeClass("mod-" + c);
+ }
+ filters = 0;
+ output.removeClass("modfilter");
+ }
+
+ function selectAll() {
+ for (var c = 0; c < modInfo.length; c++) {
+ output.addClass("mod-" + c);
+ }
+ filters = modInfo.length;
+ output.addClass("modfilter");
+ }
+
+ function parseData() {
+ stage = "parseData.pre";
+ var data = $("#input").val();
+ if (!data) {
+ stage = "parseData.checkNullData";
+ throw new Error("Field `data` is null");
+
+ }
+ var dataInfo = regexInfo.exec(data) || regexInfo.exec(data) || regexInfo.exec(data),
+ dataMods = regexMods.exec(data) || regexMods.exec(data) || regexMods.exec(data),
+ dataDate = regexDate.exec(data) || regexDate.exec(data) || regexDate.exec(data),
+ dataPath = regexPath.exec(data) || regexPath.exec(data) || regexPath.exec(data),
+ match;
+ stage = "parseData.doNullCheck";
+ if (!dataInfo)
+ throw new Error("Field `dataInfo` is null");
+ if (!dataMods)
+ throw new Error("Field `dataMods` is null");
+ if (!dataPath)
+ throw new Error("Field `dataPath` is null");
+ dataMods = dataMods[0];
+ stage = "parseData.setupDefaults";
+ modMap = {
+ "SMAPI": 0
+ };
+ modErrors = {
+ "SMAPI": 0,
+ "Console.Out": 0
+ };
+ logInfo = [];
+ modInfo = [
+ ["SMAPI", dataInfo[1], "Zoryn, CLxS & Pathoschild"]
+ ];
+ stage = "parseData.parseInfo";
+ var date = dataDate ? new Date(dataDate[1] + "Z") : null;
+ versionInfo = [dataInfo[1], dataInfo[2], dataInfo[3], date ? date.getFullYear() + "-" + ("0" + date.getMonth().toString()).substr(-2) + "-" + ("0" + date.getDay().toString()).substr(-2) + " at " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + " " + date.toLocaleTimeString("en-us", { timeZoneName: "short" }).split(" ")[2] : "No timestamp found", dataPath[1]];
+ stage = "parseData.parseMods";
+ while ((match = regexMod.exec(dataMods))) {
+ modErrors[match[1]] = 0;
+ modMap[match[1]] = modInfo.length;
+ modInfo.push([match[1], match[2], match[3] ? ("by " + match[3]) : "Unknown author"]);
+ }
+ stage = "parseData.parseLog";
+ while ((match = regexLog.exec(data))) {
+ if (match[2] === "ERROR")
+ modErrors[match[3]]++;
+ logInfo.push([match[1], match[2], match[3], match[4]]);
+ }
+ stage = "parseData.post";
+ modMap["Console.Out"] = modInfo.length;
+ modInfo.push(["Console.Out", "", ""]);
+ }
+
+ function renderData() {
+ stage = "renderData.pre";
+ output.html(prepare(templateBody, versionInfo));
+ var modslist = $("#modslist"), log = $("#log"), modCache = [], y = 0;
+ for (; y < modInfo.length; y++) {
+ var errors = modErrors[modInfo[y][0]],
+ err, cls = "color-red";
+ if (errors === 0) {
+ err = "No Errors";
+ cls = "color-green";
+ } else if (errors === 1)
+ err = "1 Error";
+ else
+ err = errors + " Errors";
+ modCache.push(prepare(templateModentry, [y, modInfo[y][0], modInfo[y][1], modInfo[y][2], cls, err]));
+ }
+ modslist.append(modCache.join(""));
+ for (var z = 0; z < modInfo.length; z++)
+ $("#modlink-" + z).on("click", modClicked);
+ var flagCache = [];
+ for (var c = 0; c < modInfo.length; c++)
+ flagCache.push(prepare(templateCss, [c]));
+ flags.html(flagCache.join(""));
+ var logCache = [], dupeCount = 0, dupeMemory = "|||";
+ for (var x = 0; x < logInfo.length; x++) {
+ var dm = logInfo[x][1] + "|" + logInfo[x][2] + "|" + logInfo[x][3];
+ if (dupeMemory !== dm) {
+ if (dupeCount > 0)
+ logCache.push(prepare(templateLognotice, [logInfo[x - 1][1].toLowerCase(), modMap[logInfo[x - 1][2]], dupeCount]));
+ dupeCount = 0;
+ dupeMemory = dm;
+ logCache.push(prepare(templateLogentry, [logInfo[x][1].toLowerCase(), modMap[logInfo[x][2]], logInfo[x][0], logInfo[x][1], logInfo[x][2], logInfo[x][3].split(" ").join("  ").replace(//g, ">").replace(/\n/g, "