From 58deaee5ce92bc10223685e66241cf9546562463 Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 5 Jul 2026 21:59:12 +0100 Subject: [PATCH] feat: enhance error handling with submission error overlay and logging level controls --- css/main.css | 74 +++++++++++++++- index.html | 24 +++++ js/interact.js | 115 ++++++++++++++---------- js/main.js | 236 +++++++++++++++++++++++++++++++------------------ 4 files changed, 317 insertions(+), 132 deletions(-) diff --git a/css/main.css b/css/main.css index c76ecaa..4ff4ae0 100644 --- a/css/main.css +++ b/css/main.css @@ -1138,7 +1138,7 @@ select option:disabled { } } -#submit-error-overlay, #view-answers-overlay { +#view-answers-overlay { opacity: 0.9; position: absolute; top: 0; @@ -1151,6 +1151,78 @@ select option:disabled { z-index: 5000; } +#submit-error-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + user-select: text; + -moz-user-select: text; + -webkit-user-select: text; + overflow-y: auto; + padding: 20px; + box-sizing: border-box; +} + +#submit-error-overlay .error-card { + background-color: #1a1a1a; + border: 1px solid #c62828; + border-radius: 8px; + max-width: 600px; + width: 100%; + padding: 25px; + box-shadow: 0 10px 25px rgba(0,0,0,0.5); + color: #eee; + font-family: sans-serif; +} + +#submit-error-overlay .error-card h3 { + margin-top: 0; + color: #ef5350; + font-size: 20px; + border-bottom: 1px solid #333; + padding-bottom: 10px; + margin-bottom: 15px; +} + +#submit-error-overlay .error-card button { + cursor: pointer; + background-color: #311B92; + color: white; + border: none; + padding: 10px 18px; + font-size: 14px; + border-radius: 4px; + transition: background-color 0.2s; + font-weight: bold; +} + +#submit-error-overlay .error-card button:hover { + background-color: #4527A0; +} + +#submit-error-overlay .error-card button.btn-secondary { + background-color: #444; +} + +#submit-error-overlay .error-card button.btn-secondary:hover { + background-color: #555; +} + +#submit-error-overlay .error-card button.btn-danger { + background-color: #c62828; +} + +#submit-error-overlay .error-card button.btn-danger:hover { + background-color: #d32f2f; +} + .cache-out-of-date { color: red; } diff --git a/index.html b/index.html index 2cba4dd..dcd462e 100644 --- a/index.html +++ b/index.html @@ -255,6 +255,16 @@ View local saved answers

+
+ + +
@@ -358,6 +368,20 @@
+ +
diff --git a/js/interact.js b/js/interact.js index 310f8f0..34088df 100644 --- a/js/interact.js +++ b/js/interact.js @@ -91,74 +91,97 @@ export function postAnswers(ans, URLS, exam_details) { alert(`Answers sucessfully submitted.`); } } else { - submissionError(data, ans, exam_details); + submissionError(data, ans, exam_details, URLS); } }) .fail((e) => { // Will occur with server error such as 500 console.debug("error", e); - submissionError(e, ans, exam_details); + submissionError(e, ans, exam_details, URLS); }); // $.post( "http://localhost:8000/submit_answers", JSON.stringify(ans)); } -export function submissionError(data, answer_json, exam_details) { - // error will not be defined with server errors - if (data.error != undefined) { - alert(`Error submitting answers: ${data.error}`); - } else { - alert(`Error submitting answers`); - } - var docHeight = $(document).height(); +export function submissionError(data, answer_json, exam_details, URLS) { + // Clear any existing overlay + $("#submit-error-overlay").remove(); - let answers = JSON.parse(answer_json.answers) + let answers = JSON.parse(answer_json.answers); + let answer_map = {}; - let answer_map = {} - - answers.forEach((ans, n) => { + answers.forEach((ans) => { if (!answer_map.hasOwnProperty(ans.qid)) { answer_map[ans.qid] = []; } - answer_map[ans.qid].push(ans); - }); - let html = $("
    "); - - exam_details.question_order.forEach((i, j) => { - console.debug(i, answer_map) - if (i in answer_map) { - console.debug("YES", i, answer_map) - let ans_array = answer_map[i]; - ans_array.forEach((x, y) => { - $(html).append(`
  • Question ${j+1}.${y}: ${x.ans}
  • `); + let $list = $("
      "); + exam_details.question_order.forEach((qid, idx) => { + if (qid in answer_map) { + let ans_array = answer_map[qid]; + ans_array.forEach((ans_obj, sub_idx) => { + $list.append(`
    1. Question ${idx + 1}.${sub_idx + 1}: ${ans_obj.ans}
    2. `); }); } + }); - }) + let rawJsonStr = JSON.stringify(answer_json, null, 2); - console.debug(exam_details.question_order); - console.debug(answer_map); - console.debug(html); - - if ($("#submit-error-overlay").length < 1) { - $("body").append( - `

      An error has occurred when submitting your answers. A copy of your answers are displayed below, you may wish to save a copy if subsequent submissions still fail. Please try submitting again or refresh this page to continue (answers will be saved locally in the browser unless you clear local storage).

      ${html.get(0).innerHTML}

      ${JSON.stringify( - answer_json - )}

      ` - ); - - $("#btn-submit-nav").prependTo("#submit-error-overlay"); - - $("#submit-error-overlay").height(docHeight); - - let file = new Blob([JSON.stringify(answer_json)], {type: "application/json"}) - - let link = document.getElementById("save-failed-answers"); - link.href = URL.createObjectURL(file); - link.download = "answers.json"; + let errorMsg = "An error occurred while submitting your answers."; + if (data && data.error) { + errorMsg = `Server returned error: ${data.error}`; + } else if (data && data.statusText) { + errorMsg = `Network error: ${data.statusText} (${data.status})`; } + + let $overlay = $(` +
      +
      +

      Submission Failed

      +

      ${errorMsg}

      +

      + Your answers are saved locally in the browser's database and will not be lost unless you clear your local data. + Please try to submit again, or save a backup file of your answers and contact support. +

      + +
      +

      Submitted Answers:

      +
      + ${$list.prop("outerHTML")} +
      +
      + +
      + View Raw Submission JSON +
      ${rawJsonStr}
      +
      + +
      + + + +
      +
      +
      + `); + + $("body").append($overlay); + + // Setup download backup link + let fileBlob = new Blob([JSON.stringify(answer_json)], {type: "application/json"}); + let backupUrl = URL.createObjectURL(fileBlob); + $overlay.find("#btn-err-backup").attr("href", backupUrl).attr("download", `answers_backup_${exam_details.eid.replace(/\//g, "_")}.json`); + + // Close handler + $overlay.find("#btn-err-close").on("click", function() { + $("#submit-error-overlay").remove(); + }); + + // Retry handler + $overlay.find("#btn-err-retry").on("click", function() { + postAnswers(answer_json, URLS, exam_details); + }); } export function getQuestion(url, question_number, question_total) { diff --git a/js/main.js b/js/main.js index 109d86d..7587521 100644 --- a/js/main.js +++ b/js/main.js @@ -7,8 +7,42 @@ import { db, question_db } from "./db.js"; import { initSync, triggerImmediateSync, updateSyncUI } from "./sync.js"; // const { v4: uuidv4 } = require('uuid'); -log.setDefaultLevel("warn") -log.setDefaultLevel("debug") +function initLogging() { + try { + const urlParams = new URLSearchParams(window.location.search); + let logLevel = urlParams.get("log"); + + if (logLevel !== null) { + if (logLevel === "1" || logLevel === "true") { + logLevel = "debug"; + } else if (logLevel === "0" || logLevel === "false") { + logLevel = "silent"; + } + localStorage.setItem("rts.logLevel", logLevel); + } else { + logLevel = localStorage.getItem("rts.logLevel"); + } + + if (!logLevel || !["silent", "error", "warn", "info", "debug"].includes(logLevel)) { + logLevel = "warn"; + } + + log.setLevel(logLevel); + + // Redirect console methods through loglevel so they can be silenced consistently + console.debug = log.debug.bind(log); + console.log = log.info.bind(log); + console.warn = log.warn.bind(log); + console.error = log.error.bind(log); + + log.info(`RTS logging level initialized to: ${logLevel}`); + } catch (e) { + log.setLevel("warn"); + console.warn("Failed to initialize custom log level, falling back to warn:", e); + } +} + +initLogging(); let URLS = {}; @@ -309,7 +343,7 @@ function showLoadingState($btn, $targetContainer) { } async function retrievePacketList(target = "all", onComplete = null) { - log.debug(`Load users from database...`) + log.info(`Load users from database...`) let cid = localStorage.getItem("cid.cid") let passcode = localStorage.getItem("cid.passcode") @@ -327,7 +361,7 @@ async function retrievePacketList(target = "all", onComplete = null) { // Update the candidate details display (only shows when both values exist) showCandidateDetails(); } else { - log.debug("Try and set CID / Passcode from current URL") + log.info("Try and set CID / Passcode from current URL") try { const params = new URLSearchParams(window.location.search); const cidParam = params.get("cid"); @@ -337,8 +371,8 @@ async function retrievePacketList(target = "all", onComplete = null) { if (cidParam !== null && passParam !== null && cidParam !== "" && passParam !== "") { cid = cidParam; passcode = passParam; - log.debug("CID and passcode extracted from URL", cid, passcode); - log.debug("adding to local db"); + log.info("CID and passcode extracted from URL", cid, passcode); + log.info("adding to local db"); localStorage.setItem("cid.cid", cid); localStorage.setItem("cid.passcode", passcode); @@ -348,10 +382,10 @@ async function retrievePacketList(target = "all", onComplete = null) { showCandidateDetails(); } else { - log.debug("CID or passcode missing in URL; not setting local storage"); + log.info("CID or passcode missing in URL; not setting local storage"); } } catch (e) { - log.debug("Error parsing URL params for CID/passcode", e); + log.error("Error parsing URL params for CID/passcode", e); } } @@ -385,7 +419,7 @@ async function retrievePacketList(target = "all", onComplete = null) { } }, error: function(jqXHR, textStatus, errorThrown) { - log.debug("Unable to load packets, trying legacy '/packets'"); + log.info("Unable to load packets, trying legacy '/packets'"); $.getJSON("packets", function(data) { if (!data.hasOwnProperty("exams")) { loadPacketList(data); @@ -410,7 +444,7 @@ async function retrievePacketList(target = "all", onComplete = null) { } } } catch (e) { - log.debug("Unable to set exam query url", e); + log.warn("Unable to set exam query url", e); } if (examUrl) { @@ -506,17 +540,17 @@ async function retrievePacketList(target = "all", onComplete = null) { async function loadExamList(data, clearExistingList = true, target = "all") { // If exam mode was enabled via URL param, hide packets by default if (typeof url_exam_mode !== "undefined" && url_exam_mode) { - log.debug("URL exam mode active - hiding packets list"); + log.info("URL exam mode active - hiding packets list"); try { $(".packet-wrapper").hide(); } catch (e) { - log.debug("Unable to hide packet UI elements", e); + log.warn("Unable to hide packet UI elements", e); } } - log.debug(`Load exam list: ${data}`) - log.debug("Loading saved sessions") + log.info(`Load exam list: ${data}`) + log.info("Loading saved sessions") let sessions = await db.session.toArray().catch(function(error) { - log.debug("Error loading session", error); + log.error("Error loading session", error); $("#database-error").text( "Error loading the database, schema has probably changed and needs updating. You will probably need to delete the local database." ); @@ -539,7 +573,7 @@ async function loadExamList(data, clearExistingList = true, target = "all") { let exams_started = []; let exams_completed = []; - log.debug("Check status of saved exams") + log.info("Check status of saved exams") for (const s of sessions) { log.debug(`checking status of`, s) if (s.status == "active") { @@ -569,7 +603,7 @@ async function loadExamList(data, clearExistingList = true, target = "all") { $("#cache-details ul").empty(); } - log.debug("Loop available exams") + log.info("Loop available exams") for (const exam of exam_list) { log.debug(exam) const name = exam["name"]; @@ -804,10 +838,10 @@ async function loadExamList(data, clearExistingList = true, target = "all") { async function loadPacketList(data) { // NOTE: currently the packet id is not available prior ot downloading the packet // this severly limits what we can do with it (but to require would limit the utility) - log.debug(`loadPacketList`); - log.debug("Loading saved sessions") + log.info(`loadPacketList`); + log.info("Loading saved sessions") let sessions = await db.session.toArray().catch(function(error) { - log.debug(`Error loading session ${error}`); + log.error(`Error loading session ${error}`); $("#database-error").text( "Error loading the database, schema has probably changed and needs updating. You will probably need to delete the local database." ); @@ -826,7 +860,7 @@ async function loadPacketList(data) { // .equals() let packets_started = []; let packets_completed = []; - log.debug("Check status of saved packets") + log.info("Check status of saved packets") sessions.forEach((s) => { log.debug(`checking status of`, s) if (s.status == "active") { @@ -836,16 +870,16 @@ async function loadPacketList(data) { } }); - log.debug("Packets started") + log.info("Packets started") log.debug(packets_started) - log.debug("Packets completed") + log.info("Packets completed") log.debug(packets_completed) let packet_list = []; if (data != null) { packet_list = data.packets; } else { - log.debug("data == null") + log.warn("data == null") } log.debug(packet_list) @@ -1146,40 +1180,38 @@ function setUpPacket(data, path) { } function loadSession() { - console.debug("load session", exam_details, db.session); + log.info("load session", exam_details, db.session); // Either continue session or create a new one db.session - // .where("status") - // .equals("active") .where("[eid+aid]") .between([exam_details.eid, Dexie.minKey], [exam_details.eid, Dexie.maxKey]) .toArray() .then((sessions) => { - //console.debug("sessions", sessions); - - // let active_sessions = []; - - // sessions.forEach((s) => { - // if (s.status == "active") { - // active_sessions.push(s); - // } - // }); - - let load_previous = false; - - // Start new session if no session found if (sessions.length < 1) { exam_details.aid = uuidv4(); date_started = Date.now(); - } else { - let text = "Select session to continue (leave blank to create new):"; - if (exam_details.exam_mode) { - text = "Session will be continued"; + exam_details.start_time = date_started / 1000; + setUpQuestions(false); + if (!review_mode) { + initSync(exam_details, URLS); } - //console.debug(sessions.date); + } else { + // Show resume dialog + $("#resume-sessions-list").empty(); + + let textPrompt = "Select a session to continue:"; + if (exam_details.exam_mode) { + textPrompt = "An exam session is already in progress. You must continue the existing session:"; + $("#btn-resume-start-new").hide(); + } else { + $("#btn-resume-start-new").show(); + } + + $("#resume-text").text(textPrompt); - for (let i = 0; i < sessions.length; i++) { - let d = new Date(sessions[i].date); + // Populate session items + sessions.forEach((session, i) => { + let d = new Date(session.date); let formatted_date = d.getFullYear() + "-" + @@ -1193,58 +1225,83 @@ function loadSession() { ":" + d.getSeconds(); - if (sessions[i].status == "active") { - text = text + `\r${i}: ${formatted_date} [In progress]`; + let label = `${formatted_date}`; + if (session.status == "active") { + label += " [In progress]"; } else { if (!exam_details.exam_mode) { - text = - text + - `\r${i}: ${formatted_date} [Complete - score ${sessions[i].score}/${sessions[i].max_score}]`; + label += ` [Complete - score ${session.score}/${session.max_score}]`; } else { - text = - text + - `\r${i}: ${formatted_date} [Complete]`; + label += " [Complete]"; } } - } - // If in exam mode we don't want to allow multiple sessions - let s; + let $item = $(` +
      + + +
      + `); + + $item.on("click", function() { + $(this).find("input[type=radio]").prop("checked", true); + $(".resume-session-item").css("border-color", "#444"); + $(this).css("border-color", "#311B92"); + $("#btn-resume-continue").prop("disabled", false); + }); + + $("#resume-sessions-list").append($item); + }); + + // Auto-select the first one by default if exam_mode is active if (exam_details.exam_mode) { - s = "0"; - alert(text); + $(".resume-session-item").first().click(); } else { - s = prompt(text, "0"); + $("#btn-resume-continue").prop("disabled", true); } - if (s != null && s != "") { - load_previous = true; + // Open the dialog + $("#resume-dialog").modal({ + escapeClose: false, + clickClose: false, + showClose: false + }); - exam_details.aid = sessions[parseInt(s)].aid; - exam_details.cid = sessions[parseInt(s)].cid; - date_started = sessions[parseInt(s)].date; - - let time_left = sessions[parseInt(s)].time_left; - - exam_time = time_left; - - exam_details.question_order = sessions[parseInt(s)].question_order; - - if (sessions[parseInt(s)].status == "complete") { - review_mode = true; - } - } else { - // If cancel is pressed or input is blank + // Unbind previous click events to prevent duplicates + $("#btn-resume-start-new").off("click").on("click", function() { + $.modal.close(); exam_details.aid = uuidv4(); date_started = Date.now(); - //console.debug("new date", date_started); - } - exam_details.start_time = date_started / 1000; - } + exam_details.start_time = date_started / 1000; + setUpQuestions(false); + if (!review_mode) { + initSync(exam_details, URLS); + } + }); - setUpQuestions(load_previous); - if (!review_mode) { - initSync(exam_details, URLS); + $("#btn-resume-continue").off("click").on("click", function() { + let selectedIdx = parseInt($("input[name=resume-session-select]:checked").val()); + if (isNaN(selectedIdx)) return; + + $.modal.close(); + let selectedSession = sessions[selectedIdx]; + + exam_details.aid = selectedSession.aid; + exam_details.cid = selectedSession.cid; + date_started = selectedSession.date; + exam_time = selectedSession.time_left; + exam_details.question_order = selectedSession.question_order; + + if (selectedSession.status == "complete") { + review_mode = true; + } + + exam_details.start_time = date_started / 1000; + setUpQuestions(true); + if (!review_mode) { + initSync(exam_details, URLS); + } + }); } }); } @@ -3287,6 +3344,15 @@ $("#btn-reset-local").click(function(evt) { localStorage.removeItem("cid.passcode") } else {} }); +// Initialize logging controls +$("#select-log-level").val(localStorage.getItem("rts.logLevel") || "warn"); +$("#select-log-level").on("change", function() { + let level = $(this).val(); + log.setLevel(level); + localStorage.setItem("rts.logLevel", level); + log.info(`Log level changed to: ${level}`); +}); + $("#btn-delete-current").click(function(evt) { if (exam_details.question_order.length < 1) {