From d939d03690b38ced23261b49cc010d76ef75d267 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Wed, 1 Jun 2022 23:44:10 +0100
Subject: [PATCH 2/9] Added JSDoc for src/components/settings/*

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 src/components/settings/Backup.vue         | 9 +++++++++
 src/components/settings/General.vue        | 2 ++
 src/components/settings/MonitorHistory.vue | 4 ++++
 src/components/settings/ReverseProxy.vue   | 3 +++
 src/components/settings/Security.vue       | 3 +++
 5 files changed, 21 insertions(+)

diff --git a/src/components/settings/Backup.vue b/src/components/settings/Backup.vue
index ba0f92f0e..685a4c6ba 100644
--- a/src/components/settings/Backup.vue
+++ b/src/components/settings/Backup.vue
@@ -133,10 +133,15 @@ export default {
     },
 
     methods: {
+        /**
+         * Show the confimation dialog confirming the configuration
+         * be imported
+         */
         confirmImport() {
             this.$refs.confirmImport.show();
         },
 
+        /** Download a backup of the configuration */
         downloadBackup() {
             let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
             let fileName = `Uptime_Kuma_Backup_${time}.json`;
@@ -157,6 +162,10 @@ export default {
             downloadItem.click();
         },
 
+        /**
+         * Import the specified backup file
+         * @returns {?string}
+         */
         importBackup() {
             this.processing = true;
             let uploadItem = document.getElementById("import-backend").files;
diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue
index a175988b9..19dc8077e 100644
--- a/src/components/settings/General.vue
+++ b/src/components/settings/General.vue
@@ -178,10 +178,12 @@ export default {
     },
 
     methods: {
+        /** Save the settings */
         saveGeneral() {
             localStorage.timezone = this.$root.userTimezone;
             this.saveSettings();
         },
+        /** Get the base URL of the application */
         autoGetPrimaryBaseURL() {
             this.settings.primaryBaseURL = location.protocol + "//" + location.host;
         },
diff --git a/src/components/settings/MonitorHistory.vue b/src/components/settings/MonitorHistory.vue
index 0092727f4..c78c6aaf7 100644
--- a/src/components/settings/MonitorHistory.vue
+++ b/src/components/settings/MonitorHistory.vue
@@ -90,6 +90,7 @@ export default {
     },
 
     methods: {
+        /** Get the current size of the database */
         loadDatabaseSize() {
             log.debug("monitorhistory", "load database size");
             this.$root.getSocket().emit("getDatabaseSize", (res) => {
@@ -102,6 +103,7 @@ export default {
             });
         },
 
+        /** Request that the database is shrunk */
         shrinkDatabase() {
             this.$root.getSocket().emit("shrinkDatabase", (res) => {
                 if (res.ok) {
@@ -113,10 +115,12 @@ export default {
             });
         },
 
+        /** Show the dialog to confirm clearing stats */
         confirmClearStatistics() {
             this.$refs.confirmClearStatistics.show();
         },
 
+        /** Send the request to clear stats */
         clearStatistics() {
             this.$root.clearStatistics((res) => {
                 if (res.ok) {
diff --git a/src/components/settings/ReverseProxy.vue b/src/components/settings/ReverseProxy.vue
index 97db4d597..e93bd0ef2 100644
--- a/src/components/settings/ReverseProxy.vue
+++ b/src/components/settings/ReverseProxy.vue
@@ -120,14 +120,17 @@ export default {
         this.$root.getSocket().emit(prefix + "leave");
     },
     methods: {
+        /** Start the Cloudflare tunnel */
         start() {
             this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
         },
+        /** Stop the Cloudflare tunnel */
         stop() {
             this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
                 this.$root.toastRes(res);
             });
         },
+        /** Remove the token for the Cloudflare tunnel */
         removeToken() {
             this.$root.getSocket().emit(prefix + "removeToken");
             this.cloudflareTunnelToken = "";
diff --git a/src/components/settings/Security.vue b/src/components/settings/Security.vue
index a5d42f82b..60e72bda2 100644
--- a/src/components/settings/Security.vue
+++ b/src/components/settings/Security.vue
@@ -303,6 +303,7 @@ export default {
     },
 
     methods: {
+        /** Check new passwords match before saving them */
         savePassword() {
             if (this.password.newPassword !== this.password.repeatNewPassword) {
                 this.invalidPassword = true;
@@ -320,6 +321,7 @@ export default {
             }
         },
 
+        /** Disable authentication for web app access */
         disableAuth() {
             this.settings.disableAuth = true;
 
@@ -332,6 +334,7 @@ export default {
             }, this.password.currentPassword);
         },
 
+        /** Enable authentication for web app access */
         enableAuth() {
             this.settings.disableAuth = false;
             this.saveSettings();

From 2b42c3c828b4292fd64f0428c9a676c1bc4d6df7 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 00:32:05 +0100
Subject: [PATCH 3/9] Added JSDoc for src/components/*

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 src/components/CertificateInfo.vue    |  2 ++
 src/components/CertificateInfoRow.vue |  7 +++++
 src/components/Confirm.vue            |  7 +++++
 src/components/CopyableInput.vue      | 15 ++++++++++
 src/components/CountUp.vue            |  6 ++--
 src/components/Datetime.vue           |  2 ++
 src/components/HeartbeatBar.vue       |  9 ++++++
 src/components/HiddenInput.vue        |  8 ++++++
 src/components/Login.vue              |  1 +
 src/components/MonitorList.vue        |  8 ++++++
 src/components/NotificationDialog.vue |  9 ++++++
 src/components/PingChart.vue          |  1 +
 src/components/ProxyDialog.vue        |  7 +++++
 src/components/PublicGroupList.vue    | 12 ++++++++
 src/components/Status.vue             |  1 +
 src/components/Tag.vue                |  6 ++++
 src/components/TagsManager.vue        | 41 +++++++++++++++++++++++++++
 src/components/ToggleSection.vue      |  2 ++
 src/components/TwoFADialog.vue        |  8 ++++++
 src/components/Uptime.vue             |  3 ++
 20 files changed, 152 insertions(+), 3 deletions(-)

diff --git a/src/components/CertificateInfo.vue b/src/components/CertificateInfo.vue
index bb10f158d..cb1a82917 100644
--- a/src/components/CertificateInfo.vue
+++ b/src/components/CertificateInfo.vue
@@ -25,10 +25,12 @@ export default {
         CertificateInfoRow,
     },
     props: {
+        /** Object representing certificate */
         certInfo: {
             type: Object,
             required: true,
         },
+        /** Is the TLS certificate valid? */
         valid: {
             type: Boolean,
             required: true,
diff --git a/src/components/CertificateInfoRow.vue b/src/components/CertificateInfoRow.vue
index 3ac22f3b6..f02d1d7b9 100644
--- a/src/components/CertificateInfoRow.vue
+++ b/src/components/CertificateInfoRow.vue
@@ -56,12 +56,19 @@ export default {
         Datetime,
     },
     props: {
+        /** Object representing certificate */
         cert: {
             type: Object,
             required: true,
         },
     },
     methods: {
+        /**
+         * Format the subject of the certificate
+         * @param {Object} subject Object representing the certificates
+         * subject
+         * @returns {string}
+         */
         formatSubject(subject) {
             if (subject.O && subject.CN && subject.C) {
                 return `${subject.CN} - ${subject.O} (${subject.C})`;
diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue
index 1bfe7fe4a..d369e0b5b 100644
--- a/src/components/Confirm.vue
+++ b/src/components/Confirm.vue
@@ -29,14 +29,17 @@ import { Modal } from "bootstrap";
 
 export default {
     props: {
+        /** Style of button */
         btnStyle: {
             type: String,
             default: "btn-primary",
         },
+        /** Text to use as yes */
         yesText: {
             type: String,
             default: "Yes",     // TODO: No idea what to translate this
         },
+        /** Text to use as no */
         noText: {
             type: String,
             default: "No",
@@ -50,9 +53,13 @@ export default {
         this.modal = new Modal(this.$refs.modal);
     },
     methods: {
+        /** Show the confirm dialog */
         show() {
             this.modal.show();
         },
+        /**
+         * @emits string A string that simply says "yes"
+         */
         yes() {
             this.$emit("yes");
         },
diff --git a/src/components/CopyableInput.vue b/src/components/CopyableInput.vue
index 1bccfa2ce..2e1dee766 100644
--- a/src/components/CopyableInput.vue
+++ b/src/components/CopyableInput.vue
@@ -25,33 +25,41 @@ let timeout;
 
 export default {
     props: {
+        /** ID of this input */
         id: {
             type: String,
             default: ""
         },
+        /** Type of input */
         type: {
             type: String,
             default: "text"
         },
+        /** The value of the input */
         modelValue: {
             type: String,
             default: ""
         },
+        /** A placeholder to use */
         placeholder: {
             type: String,
             default: ""
         },
+        /** Should the field auto complete */
         autocomplete: {
             type: String,
             default: undefined,
         },
+        /** Is the input required? */
         required: {
             type: Boolean
         },
+        /** Should the input be read only? */
         readonly: {
             type: String,
             default: undefined,
         },
+        /** Is the input disabled? */
         disabled: {
             type: String,
             default: undefined,
@@ -79,14 +87,21 @@ export default {
     },
     methods: {
 
+        /** Show the input */
         showInput() {
             this.visibility = "text";
         },
 
+        /** Hide the input */
         hideInput() {
             this.visibility = "password";
         },
 
+        /**
+         * Copy the provided text to the users clipboard
+         * @param {string} textToCopy
+         * @returns {Promise<void>}
+         */
         copyToClipboard(textToCopy) {
             this.icon = "check";
 
diff --git a/src/components/CountUp.vue b/src/components/CountUp.vue
index df1d1ac6c..9ce68507f 100644
--- a/src/components/CountUp.vue
+++ b/src/components/CountUp.vue
@@ -10,6 +10,7 @@ import { sleep } from "../util.ts";
 export default {
 
     props: {
+        /** Value to count */
         value: {
             type: [ String, Number ],
             default: 0,
@@ -18,6 +19,7 @@ export default {
             type: Number,
             default: 0.3,
         },
+        /** Unit of the value */
         unit: {
             type: String,
             default: "ms",
@@ -43,9 +45,7 @@ export default {
             let frames = 12;
             let step = Math.floor(diff / frames);
 
-            if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) {
-                // Lazy to NOT this condition, hahaha.
-            } else {
+            if (! (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0)) {
                 for (let i = 1; i < frames; i++) {
                     this.output += step;
                     await sleep(15);
diff --git a/src/components/Datetime.vue b/src/components/Datetime.vue
index fa68d02c7..3c66f5d6e 100644
--- a/src/components/Datetime.vue
+++ b/src/components/Datetime.vue
@@ -13,10 +13,12 @@ dayjs.extend(relativeTime);
 
 export default {
     props: {
+        /** Value of data time */
         value: {
             type: String,
             default: null,
         },
+        /** Should only the date be displayed? */
         dateOnly: {
             type: Boolean,
             default: false,
diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue
index ce888a989..83923dd23 100644
--- a/src/components/HeartbeatBar.vue
+++ b/src/components/HeartbeatBar.vue
@@ -17,14 +17,17 @@
 
 export default {
     props: {
+        /** Size of the heart beat bar */
         size: {
             type: String,
             default: "big",
         },
+        /** ID of the monitor */
         monitorId: {
             type: Number,
             required: true,
         },
+        /** Array of the monitors heart beats */
         heartbeatList: {
             type: Array,
             default: null,
@@ -160,12 +163,18 @@ export default {
         this.resize();
     },
     methods: {
+        /** Resize the heartbeat bar */
         resize() {
             if (this.$refs.wrap) {
                 this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
             }
         },
 
+        /**
+         * Get the title of the beat
+         * @param {Object} beat Beat to get title from
+         * @returns {string}
+         */
         getBeatTitle(beat) {
             return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
         },
diff --git a/src/components/HiddenInput.vue b/src/components/HiddenInput.vue
index d2327b9df..8fd68d4c4 100644
--- a/src/components/HiddenInput.vue
+++ b/src/components/HiddenInput.vue
@@ -24,25 +24,31 @@
 <script>
 export default {
     props: {
+        /** The value of the input */
         modelValue: {
             type: String,
             default: ""
         },
+        /** A placeholder to use */
         placeholder: {
             type: String,
             default: ""
         },
+        /** Maximum lenght of the input */
         maxlength: {
             type: Number,
             default: 255
         },
+        /** Should the field auto complete */
         autocomplete: {
             type: String,
             default: undefined,
         },
+        /** Is the input required? */
         required: {
             type: Boolean
         },
+        /** Should the input be read only? */
         readonly: {
             type: String,
             default: undefined,
@@ -68,9 +74,11 @@ export default {
 
     },
     methods: {
+        /** Show users input in plain text */
         showInput() {
             this.visibility = "text";
         },
+        /** Censor users input */
         hideInput() {
             this.visibility = "password";
         },
diff --git a/src/components/Login.vue b/src/components/Login.vue
index 987651e23..3a821881d 100644
--- a/src/components/Login.vue
+++ b/src/components/Login.vue
@@ -55,6 +55,7 @@ export default {
         };
     },
     methods: {
+        /** Submit the user details and attempt to log in */
         submit() {
             this.processing = true;
 
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index 7aeadd1e1..1d48af710 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -58,6 +58,7 @@ export default {
         Tag,
     },
     props: {
+        /** Should the scrollbar be shown */
         scrollbar: {
             type: Boolean,
         },
@@ -124,6 +125,7 @@ export default {
         window.removeEventListener("scroll", this.onScroll);
     },
     methods: {
+        /** Called when the user scrolls */
         onScroll() {
             if (window.top.scrollY <= 133) {
                 this.windowTop = window.top.scrollY;
@@ -131,9 +133,15 @@ export default {
                 this.windowTop = 133;
             }
         },
+        /**
+         * Get URL of monitor
+         * @param {number} id ID of monitor
+         * @returns {string} Relative URL of monitor
+         */
         monitorURL(id) {
             return getMonitorRelativeURL(id);
         },
+        /** Clear the search bar */
         clearSearchText() {
             this.searchText = "";
         }
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index 7a1f1a100..0ca95c222 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -125,11 +125,16 @@ export default {
     },
     methods: {
 
+        /** Show dialog to confirm deletion */
         deleteConfirm() {
             this.modal.hide();
             this.$refs.confirmDelete.show();
         },
 
+        /**
+         * Show settings for specified notification
+         * @param {number} notificationID ID of notification to show
+         */
         show(notificationID) {
             if (notificationID) {
                 this.id = notificationID;
@@ -152,6 +157,7 @@ export default {
             this.modal.show();
         },
 
+        /** Submit the form to the server */
         submit() {
             this.processing = true;
             this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
@@ -170,6 +176,7 @@ export default {
             });
         },
 
+        /** Test the notification endpoint */
         test() {
             this.processing = true;
             this.$root.getSocket().emit("testNotification", this.notification, (res) => {
@@ -178,6 +185,7 @@ export default {
             });
         },
 
+        /** Delete the notification endpoint */
         deleteNotification() {
             this.processing = true;
             this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
@@ -190,6 +198,7 @@ export default {
             });
         },
         /**
+         * Get a unique default name for the notification
          * @param {keyof NotificationFormList} notificationKey
          * @return {string}
          */
diff --git a/src/components/PingChart.vue b/src/components/PingChart.vue
index 67bdc00c4..e472d898a 100644
--- a/src/components/PingChart.vue
+++ b/src/components/PingChart.vue
@@ -35,6 +35,7 @@ Chart.register(LineController, BarController, LineElement, PointElement, TimeSca
 export default {
     components: { LineChart },
     props: {
+        /** ID of monitor */
         monitorId: {
             type: Number,
             required: true,
diff --git a/src/components/ProxyDialog.vue b/src/components/ProxyDialog.vue
index 3070925c1..e52698f9f 100644
--- a/src/components/ProxyDialog.vue
+++ b/src/components/ProxyDialog.vue
@@ -130,11 +130,16 @@ export default {
     },
 
     methods: {
+        /** Show dialog to confirm delection */
         deleteConfirm() {
             this.modal.hide();
             this.$refs.confirmDelete.show();
         },
 
+        /**
+         * Show settings for specified proxy
+         * @param {number} proxyID ID of proxy to show
+         */
         show(proxyID) {
             if (proxyID) {
                 this.id = proxyID;
@@ -163,6 +168,7 @@ export default {
             this.modal.show();
         },
 
+        /** Submit form data for saving */
         submit() {
             this.processing = true;
             this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
@@ -180,6 +186,7 @@ export default {
             });
         },
 
+        /** Delete this proxy */
         deleteProxy() {
             this.processing = true;
             this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue
index df94eec98..137f734d1 100644
--- a/src/components/PublicGroupList.vue
+++ b/src/components/PublicGroupList.vue
@@ -72,10 +72,12 @@ export default {
         Tag,
     },
     props: {
+        /** Are we in edit mode? */
         editMode: {
             type: Boolean,
             required: true,
         },
+        /** Should tags be shown? */
         showTags: {
             type: Boolean,
         }
@@ -94,10 +96,20 @@ export default {
 
     },
     methods: {
+        /**
+         * Remove the specified group
+         * @param {number} index Index of group to remove
+         */
         removeGroup(index) {
             this.$root.publicGroupList.splice(index, 1);
         },
 
+        /**
+         * Remove a monitor from a group
+         * @param {number} groupIndex Index of group to remove monitor
+         * from
+         * @param {number} index Index of monitor to remove
+         */
         removeMonitor(groupIndex, index) {
             this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
         },
diff --git a/src/components/Status.vue b/src/components/Status.vue
index 1985d8518..11897047a 100644
--- a/src/components/Status.vue
+++ b/src/components/Status.vue
@@ -5,6 +5,7 @@
 <script>
 export default {
     props: {
+        /** Current status of monitor */
         status: {
             type: Number,
             default: 0,
diff --git a/src/components/Tag.vue b/src/components/Tag.vue
index 3c9668502..0325d9900 100644
--- a/src/components/Tag.vue
+++ b/src/components/Tag.vue
@@ -20,14 +20,20 @@
 <script>
 export default {
     props: {
+        /** Object representing tag */
         item: {
             type: Object,
             required: true,
         },
+        /** Function to remove tag */
         remove: {
             type: Function,
             default: null,
         },
+        /**
+         * Size of tag
+         * @values normal, small
+         */
         size: {
             type: String,
             default: "normal",
diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue
index 1a9212ca5..10f6a6f7d 100644
--- a/src/components/TagsManager.vue
+++ b/src/components/TagsManager.vue
@@ -139,6 +139,7 @@ export default {
         VueMultiselect,
     },
     props: {
+        /** Array of tags to be pre-selected */
         preSelectedTags: {
             type: Array,
             default: () => [],
@@ -241,9 +242,11 @@ export default {
         this.getExistingTags();
     },
     methods: {
+        /** Show the add tag dialog */
         showAddDialog() {
             this.modal.show();
         },
+        /** Get all existing tags */
         getExistingTags() {
             this.$root.getSocket().emit("getTags", (res) => {
                 if (res.ok) {
@@ -253,6 +256,10 @@ export default {
                 }
             });
         },
+        /**
+         * Delete the specified tag
+         * @param {Object} tag Object representing tag to delete
+         */
         deleteTag(item) {
             if (item.new) {
                 // Undo Adding a new Tag
@@ -262,6 +269,13 @@ export default {
                 this.deleteTags.push(item);
             }
         },
+        /**
+         * Set colour of text
+         * @param {Object} option Object representing color choice. If
+         * option.color is set, the text color will be white, else it
+         * be chosen based upon application theme
+         * @returns string
+         */
         textColor(option) {
             if (option.color) {
                 return "white";
@@ -269,6 +283,7 @@ export default {
                 return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
             }
         },
+        /** Add a draft tag */
         addDraftTag() {
             console.log("Adding Draft Tag: ", this.newDraftTag);
             if (this.newDraftTag.select != null) {
@@ -296,6 +311,7 @@ export default {
             }
             this.clearDraftTag();
         },
+        /** Remove a draft tag */
         clearDraftTag() {
             this.newDraftTag = {
                 name: null,
@@ -307,26 +323,51 @@ export default {
             };
             this.modal.hide();
         },
+        /**
+         * Add a tag asynchronously
+         * @param {Object} newTag Object representing new tag to add
+         * @returns {Promise<void>}
+         */
         addTagAsync(newTag) {
             return new Promise((resolve) => {
                 this.$root.getSocket().emit("addTag", newTag, resolve);
             });
         },
+        /**
+         * Add a tag to a monitor asynchronously
+         * @param {number} tagId ID of tag to add
+         * @param {number} monitorId ID of monitor to add tag to
+         * @param {string} value Value of tag
+         * @returns {Promise<void>}
+         */
         addMonitorTagAsync(tagId, monitorId, value) {
             return new Promise((resolve) => {
                 this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
             });
         },
+        /**
+         * Delete a tag from a monitor asynchronously
+         * @param {number} tagId ID of tag to remove
+         * @param {number} monitorId ID of monitor to remove tag from
+         * @param {string} value Value of tag
+         * @returns {Promise<void>}
+         */
         deleteMonitorTagAsync(tagId, monitorId, value) {
             return new Promise((resolve) => {
                 this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
             });
         },
+        /** Called as user types input */
         onEnter() {
             if (!this.validateDraftTag.invalid) {
                 this.addDraftTag();
             }
         },
+        /**
+         * Submit the form data
+         * @param {number} monitorId ID of monitor this change affects
+         * @returns {void}
+         */
         async submit(monitorId) {
             console.log(`Submitting tag changes for monitor ${monitorId}...`);
             this.processing = true;
diff --git a/src/components/ToggleSection.vue b/src/components/ToggleSection.vue
index daf9c00c2..5dfa3a637 100644
--- a/src/components/ToggleSection.vue
+++ b/src/components/ToggleSection.vue
@@ -29,10 +29,12 @@
 <script>
 export default {
     props: {
+        /** Heading to use */
         heading: {
             type: String,
             default: "",
         },
+        /** Should the selection be open by default? */
         defaultOpen: {
             type: Boolean,
             default: false,
diff --git a/src/components/TwoFADialog.vue b/src/components/TwoFADialog.vue
index 049f7c870..8dc9f9c79 100644
--- a/src/components/TwoFADialog.vue
+++ b/src/components/TwoFADialog.vue
@@ -100,18 +100,22 @@ export default {
         this.getStatus();
     },
     methods: {
+        /** Show the dialog */
         show() {
             this.modal.show();
         },
 
+        /** Show dialog to confirm enabling 2FA */
         confirmEnableTwoFA() {
             this.$refs.confirmEnableTwoFA.show();
         },
 
+        /** Show dialog to confirm disabling 2FA */
         confirmDisableTwoFA() {
             this.$refs.confirmDisableTwoFA.show();
         },
 
+        /** Prepare 2FA configuration */
         prepare2FA() {
             this.processing = true;
 
@@ -126,6 +130,7 @@ export default {
             });
         },
 
+        /** Save the current 2FA configuration */
         save2FA() {
             this.processing = true;
 
@@ -143,6 +148,7 @@ export default {
             });
         },
 
+        /** Disable 2FA for this user */
         disable2FA() {
             this.processing = true;
 
@@ -160,6 +166,7 @@ export default {
             });
         },
 
+        /** Verify the token generated by the user */
         verifyToken() {
             this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
                 if (res.ok) {
@@ -170,6 +177,7 @@ export default {
             });
         },
 
+        /** Get current status of 2FA */
         getStatus() {
             this.$root.getSocket().emit("twoFAStatus", (res) => {
                 if (res.ok) {
diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue
index 487d62b72..8793b7a7b 100644
--- a/src/components/Uptime.vue
+++ b/src/components/Uptime.vue
@@ -5,14 +5,17 @@
 <script>
 export default {
     props: {
+        /** Monitor this represents */
         monitor: {
             type: Object,
             default: null,
         },
+        /** Type of monitor */
         type: {
             type: String,
             default: null,
         },
+        /** Is this a pill */
         pill: {
             type: Boolean,
             default: false,

From 213aca4fc32a373dcb7db1f53f1777f0dbec0e53 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 10:38:17 +0100
Subject: [PATCH 4/9] Added JSDoc for src/mixins/*

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 src/mixins/datetime.js |  23 +++++++++
 src/mixins/lang.js     |   1 +
 src/mixins/mobile.js   |   2 +
 src/mixins/socket.js   | 114 +++++++++++++++++++++++++++++++++++++++++
 src/mixins/theme.js    |   1 +
 5 files changed, 141 insertions(+)

diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js
index 7cef22d2b..698bf9e80 100644
--- a/src/mixins/datetime.js
+++ b/src/mixins/datetime.js
@@ -18,14 +18,31 @@ export default {
     },
 
     methods: {
+        /**
+         * Return a given value in the format YYYY-MM-DD HH:mm:ss
+         * @param {any} value Value to format as date time
+         * @returns {string}
+         */
         datetime(value) {
             return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
         },
 
+        /**
+         * Return a given value in the format YYYY-MM-DD
+         * @param {any} value  Value to format as date
+         * @returns {string}
+         */
         date(value) {
             return this.datetimeFormat(value, "YYYY-MM-DD");
         },
 
+        /**
+         * Return a given value in the format HH:mm or if second is set
+         * to true, HH:mm:ss
+         * @param {any} value Value to format
+         * @param {boolean} second Should seconds be included?
+         * @returns {string}
+         */
         time(value, second = true) {
             let secondString;
             if (second) {
@@ -36,6 +53,12 @@ export default {
             return this.datetimeFormat(value, "HH:mm" + secondString);
         },
 
+        /**
+         * Return a value in a custom format
+         * @param {any} value Value to format
+         * @param {any} format Format to return value in
+         * @returns {string}
+         */
         datetimeFormat(value, format) {
             if (value !== undefined && value !== "") {
                 return dayjs.utc(value).tz(this.timezone).format(format);
diff --git a/src/mixins/lang.js b/src/mixins/lang.js
index 31d5a8e0b..aca951498 100644
--- a/src/mixins/lang.js
+++ b/src/mixins/lang.js
@@ -22,6 +22,7 @@ export default {
     },
 
     methods: {
+        /** Change the application language */
         async changeLang(lang) {
             let message = (await langModules["../languages/" + lang + ".js"]()).default;
             this.$i18n.setLocaleMessage(lang, message);
diff --git a/src/mixins/mobile.js b/src/mixins/mobile.js
index e81ebf45c..ffc50f50c 100644
--- a/src/mixins/mobile.js
+++ b/src/mixins/mobile.js
@@ -12,11 +12,13 @@ export default {
     },
 
     methods: {
+        /** Called when the screen changes size */
         onResize() {
             this.windowWidth = window.innerWidth;
             this.updateBody();
         },
 
+        /** Update the document body */
         updateBody() {
             if (this.isMobile) {
                 document.body.classList.add("mobile");
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index c54b573f3..04785b2de 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -62,6 +62,12 @@ export default {
 
     methods: {
 
+        /**
+         * Initialize connection to socket server
+         * @param {boolean} [bypass = false] Should the check for if we
+         * are on a status page be bypassed?
+         * @returns {(void|null)}
+         */
         initSocketIO(bypass = false) {
             // No need to re-init
             if (this.socket.initedSocketIO) {
@@ -258,10 +264,18 @@ export default {
             socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res);
         },
 
+        /**
+         * The storage currently in use
+         * @returns {Storage}
+         */
         storage() {
             return (this.remember) ? localStorage : sessionStorage;
         },
 
+        /**
+         * Get payload of JWT cookie
+         * @returns {(Object|undefined)}
+         */
         getJWTPayload() {
             const jwtToken = this.$root.storage().token;
 
@@ -271,10 +285,18 @@ export default {
             return undefined;
         },
 
+        /**
+         * Get current socket
+         * @returns {Socket}
+         */
         getSocket() {
             return socket;
         },
 
+        /**
+         * Show success or error toast dependant on response status code
+         * @param {Object} res Response object
+         */
         toastRes(res) {
             if (res.ok) {
                 toast.success(res.msg);
@@ -283,14 +305,35 @@ export default {
             }
         },
 
+        /**
+         * Show a success toast
+         * @param {string} msg Message to show
+         */
         toastSuccess(msg) {
             toast.success(msg);
         },
 
+        /**
+         * Show an error toast
+         * @param {string} msg Message to show
+         */
         toastError(msg) {
             toast.error(msg);
         },
 
+        /**
+         * Callback for login
+         * @callback loginCB
+         * @param {Object} res Response object
+         */
+
+        /**
+         * Send request to log user in
+         * @param {string} username Username to log in with
+         * @param {string} password Password to log in with
+         * @param {string} token User token
+         * @param {loginCB} callback Callback to call with result
+         */
         login(username, password, token, callback) {
             socket.emit("login", {
                 username,
@@ -315,6 +358,10 @@ export default {
             });
         },
 
+        /**
+         * Log in using a token
+         * @param {string} token Token to log in with
+         */
         loginByToken(token) {
             socket.emit("loginByToken", token, (res) => {
                 this.allowLoginDialog = true;
@@ -328,6 +375,7 @@ export default {
             });
         },
 
+        /** Log out of the web application */
         logout() {
             socket.emit("logout", () => { });
             this.storage().removeItem("token");
@@ -337,26 +385,54 @@ export default {
             this.clearData();
         },
 
+        /**
+         * Callback for general socket requests
+         * @callback socketCB
+         * @param {Object} res Result of operation
+         */
+        /** Prepare 2FA configuration */
         prepare2FA(callback) {
             socket.emit("prepare2FA", callback);
         },
 
+        /**
+         * Save the current 2FA configuration
+         * @param {any} secret Unused
+         * @param {socketCB} callback
+         */
         save2FA(secret, callback) {
             socket.emit("save2FA", callback);
         },
 
+        /**
+         * Disable 2FA for this user
+         * @param {socketCB} callback
+         */
         disable2FA(callback) {
             socket.emit("disable2FA", callback);
         },
 
+        /**
+         * Verify the provided 2FA token
+         * @param {string} token Token to verify
+         * @param {socketCB} callback
+         */
         verifyToken(token, callback) {
             socket.emit("verifyToken", token, callback);
         },
 
+        /**
+         * Get current 2FA status
+         * @param {socketCB} callback
+         */
         twoFAStatus(callback) {
             socket.emit("twoFAStatus", callback);
         },
 
+        /**
+         * Get list of monitors
+         * @param {socketCB} callback
+         */
         getMonitorList(callback) {
             if (! callback) {
                 callback = () => { };
@@ -364,36 +440,74 @@ export default {
             socket.emit("getMonitorList", callback);
         },
 
+        /**
+         * Add a monitor
+         * @param {Object} monitor Object representing monitor to add
+         * @param {socketCB} callback
+         */
         add(monitor, callback) {
             socket.emit("add", monitor, callback);
         },
 
+        /**
+         * Delete monitor by ID
+         * @param {number} monitorID ID of monitor to delete
+         * @param {socketCB} callback
+         */
         deleteMonitor(monitorID, callback) {
             socket.emit("deleteMonitor", monitorID, callback);
         },
 
+        /** Clear the hearbeat list */
         clearData() {
             console.log("reset heartbeat list");
             this.heartbeatList = {};
             this.importantHeartbeatList = {};
         },
 
+        /**
+         * Upload the provided backup
+         * @param {string} uploadedJSON JSON to upload
+         * @param {string} importHandle Type of import. If set to
+         * most data in database will be replaced
+         * @param {socketCB} callback
+         */
         uploadBackup(uploadedJSON, importHandle, callback) {
             socket.emit("uploadBackup", uploadedJSON, importHandle, callback);
         },
 
+        /**
+         * Clear events for a specified monitor
+         * @param {number} monitorID ID of monitor to clear
+         * @param {socketCB} callback
+         */
         clearEvents(monitorID, callback) {
             socket.emit("clearEvents", monitorID, callback);
         },
 
+        /**
+         * Clear the heartbeats of a specified monitor
+         * @param {number} monitorID Id of monitor to clear
+         * @param {socketCB} callback
+         */
         clearHeartbeats(monitorID, callback) {
             socket.emit("clearHeartbeats", monitorID, callback);
         },
 
+        /**
+         * Clear all statistics
+         * @param {socketCB} callback
+         */
         clearStatistics(callback) {
             socket.emit("clearStatistics", callback);
         },
 
+        /**
+         * Get monitor beats for a specific monitor in a time range
+         * @param {number} monitorID ID of monitor to fetch
+         * @param {number} period Time in hours from now
+         * @param {socketCB} callback
+         */
         getMonitorBeats(monitorID, period, callback) {
             socket.emit("getMonitorBeats", monitorID, period, callback);
         }
diff --git a/src/mixins/theme.js b/src/mixins/theme.js
index 21ebd0916..58f4d91b9 100644
--- a/src/mixins/theme.js
+++ b/src/mixins/theme.js
@@ -75,6 +75,7 @@ export default {
     },
 
     methods: {
+        /** Update the theme color meta tag */
         updateThemeColorMeta() {
             if (this.theme === "dark") {
                 document.querySelector("#theme-color").setAttribute("content", "#161B22");

From 217022903159e0c010ceda2df1ea589003c70319 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 10:42:37 +0100
Subject: [PATCH 5/9] Improve JSDoc for some components

Apply suggestions from code review

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
---
 src/components/Confirm.vue       | 2 +-
 src/components/HeartbeatBar.vue  | 7 ++++---
 src/components/HiddenInput.vue   | 2 +-
 src/components/TagsManager.vue   | 4 ++--
 src/components/ToggleSection.vue | 4 ++--
 5 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue
index d369e0b5b..258724a39 100644
--- a/src/components/Confirm.vue
+++ b/src/components/Confirm.vue
@@ -58,7 +58,7 @@ export default {
             this.modal.show();
         },
         /**
-         * @emits string A string that simply says "yes"
+         * @emits "yes" - Notify the parent when Yes is pressed
          */
         yes() {
             this.$emit("yes");
diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue
index 83923dd23..028b8e335 100644
--- a/src/components/HeartbeatBar.vue
+++ b/src/components/HeartbeatBar.vue
@@ -17,7 +17,7 @@
 
 export default {
     props: {
-        /** Size of the heart beat bar */
+        /** Size of the heartbeat bar */
         size: {
             type: String,
             default: "big",
@@ -27,7 +27,7 @@ export default {
             type: Number,
             required: true,
         },
-        /** Array of the monitors heart beats */
+        /** Array of the monitors heartbeats */
         heartbeatList: {
             type: Array,
             default: null,
@@ -171,7 +171,8 @@ export default {
         },
 
         /**
-         * Get the title of the beat
+         * Get the title of the beat.
+         * Used as the hover tooltip on the heartbeat bar.
          * @param {Object} beat Beat to get title from
          * @returns {string}
          */
diff --git a/src/components/HiddenInput.vue b/src/components/HiddenInput.vue
index 8fd68d4c4..6287af05c 100644
--- a/src/components/HiddenInput.vue
+++ b/src/components/HiddenInput.vue
@@ -34,7 +34,7 @@ export default {
             type: String,
             default: ""
         },
-        /** Maximum lenght of the input */
+        /** Maximum length of the input */
         maxlength: {
             type: Number,
             default: 255
diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue
index 10f6a6f7d..fd94c93e2 100644
--- a/src/components/TagsManager.vue
+++ b/src/components/TagsManager.vue
@@ -270,7 +270,7 @@ export default {
             }
         },
         /**
-         * Set colour of text
+         * Get colour of text inside the tag
          * @param {Object} option Object representing color choice. If
          * option.color is set, the text color will be white, else it
          * be chosen based upon application theme
@@ -357,7 +357,7 @@ export default {
                 this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
             });
         },
-        /** Called as user types input */
+        /** Handle pressing Enter key when inside the modal */
         onEnter() {
             if (!this.validateDraftTag.invalid) {
                 this.addDraftTag();
diff --git a/src/components/ToggleSection.vue b/src/components/ToggleSection.vue
index 5dfa3a637..47dc21905 100644
--- a/src/components/ToggleSection.vue
+++ b/src/components/ToggleSection.vue
@@ -29,12 +29,12 @@
 <script>
 export default {
     props: {
-        /** Heading to use */
+        /** Heading of the Section */
         heading: {
             type: String,
             default: "",
         },
-        /** Should the selection be open by default? */
+        /** Should the section be open by default? */
         defaultOpen: {
             type: Boolean,
             default: false,

From b0476cfb5b4f5da694e6c6056f2cc37e0246d73e Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 13:46:44 +0100
Subject: [PATCH 6/9] Added JSDoc for src/pages/*

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 src/pages/AddStatusPage.vue    |  1 +
 src/pages/Details.vue          | 15 +++++++++++++
 src/pages/EditMonitor.vue      | 23 ++++++++++++++++----
 src/pages/ManageStatusPage.vue |  5 +++++
 src/pages/NotFound.vue         |  1 +
 src/pages/Settings.vue         | 15 +++++++++++--
 src/pages/Setup.vue            |  4 ++++
 src/pages/StatusPage.vue       | 39 ++++++++++++++++++++++++++++++----
 8 files changed, 93 insertions(+), 10 deletions(-)

diff --git a/src/pages/AddStatusPage.vue b/src/pages/AddStatusPage.vue
index e0200177e..230894ff4 100644
--- a/src/pages/AddStatusPage.vue
+++ b/src/pages/AddStatusPage.vue
@@ -51,6 +51,7 @@ export default {
         };
     },
     methods: {
+        /** Submit form data to add new status page */
         async submit() {
             this.processing = true;
 
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index d40561fe0..f5728d661 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -289,39 +289,47 @@ export default {
 
     },
     methods: {
+        /** Request a test notification be sent for this monitor */
         testNotification() {
             this.$root.getSocket().emit("testNotification", this.monitor.id);
             toast.success("Test notification is requested.");
         },
 
+        /** Show dialog to confirm pause */
         pauseDialog() {
             this.$refs.confirmPause.show();
         },
 
+        /** Resume this monitor */
         resumeMonitor() {
             this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
                 this.$root.toastRes(res);
             });
         },
 
+        /** Request that this monitor is paused */
         pauseMonitor() {
             this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
                 this.$root.toastRes(res);
             });
         },
 
+        /** Show dialog to confirm deletion */
         deleteDialog() {
             this.$refs.confirmDelete.show();
         },
 
+        /** Show dialog to confirm clearing events */
         clearEventsDialog() {
             this.$refs.confirmClearEvents.show();
         },
 
+        /** Show dialog to confirm clearing heartbeats */
         clearHeartbeatsDialog() {
             this.$refs.confirmClearHeartbeats.show();
         },
 
+        /** Request that this monitor is deleted */
         deleteMonitor() {
             this.$root.deleteMonitor(this.monitor.id, (res) => {
                 if (res.ok) {
@@ -333,6 +341,7 @@ export default {
             });
         },
 
+        /** Request that this monitors events are cleared */
         clearEvents() {
             this.$root.clearEvents(this.monitor.id, (res) => {
                 if (! res.ok) {
@@ -341,6 +350,7 @@ export default {
             });
         },
 
+        /** Request that this monitors heartbeats are cleared */
         clearHeartbeats() {
             this.$root.clearHeartbeats(this.monitor.id, (res) => {
                 if (! res.ok) {
@@ -349,6 +359,11 @@ export default {
             });
         },
 
+        /**
+         * Return the correct title for the ping stat
+         * @param {boolean} [average=false] Is the statistic an average?
+         * @returns {string} Title formated dependant on monitor type
+         */
         pingTitle(average = false) {
             let translationPrefix = "";
             if (average) {
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 1cb3684be..0b196ae25 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -522,6 +522,7 @@ export default {
         this.dnsresolvetypeOptions = dnsresolvetypeOptions;
     },
     methods: {
+        /** Initialize the edit monitor form */
         init() {
             if (this.isAdd) {
 
@@ -578,6 +579,10 @@ export default {
 
         },
 
+        /**
+         * Validate form input
+         * @returns {boolean} Is the form input valid?
+         */
         isInputValid() {
             if (this.monitor.body) {
                 try {
@@ -598,6 +603,10 @@ export default {
             return true;
         },
 
+        /**
+         * Submit the form data for processing
+         * @returns {void}
+         */
         async submit() {
             this.processing = true;
 
@@ -642,14 +651,20 @@ export default {
             }
         },
 
-        // Added a Notification Event
-        // Enable it if the notification is added in EditMonitor.vue
+        /**
+         * Added a Notification Event
+         * Enable it if the notification is added in EditMonitor.vue
+         * @param {number} id ID of notification to add
+         */
         addedNotification(id) {
             this.monitor.notificationIDList[id] = true;
         },
 
-        // Added a Proxy Event
-        // Enable it if the proxy is added in EditMonitor.vue
+        /**
+         * Added a Proxy Event
+         * Enable it if the proxy is added in EditMonitor.vue
+         * @param {number} id ID of proxy to add
+         */
         addedProxy(id) {
             this.monitor.proxyId = id;
         },
diff --git a/src/pages/ManageStatusPage.vue b/src/pages/ManageStatusPage.vue
index c3360375c..275fd4900 100644
--- a/src/pages/ManageStatusPage.vue
+++ b/src/pages/ManageStatusPage.vue
@@ -51,6 +51,11 @@ export default {
 
     },
     methods: {
+        /**
+         * Get the correct URL for the icon
+         * @param {string} icon Path for icon
+         * @returns {string} Correctly formatted path including port numbers
+         */
         icon(icon) {
             if (icon === "/icon.svg") {
                 return icon;
diff --git a/src/pages/NotFound.vue b/src/pages/NotFound.vue
index 410c16a81..6fb7e2a77 100644
--- a/src/pages/NotFound.vue
+++ b/src/pages/NotFound.vue
@@ -44,6 +44,7 @@ export default {
 
     },
     methods: {
+        /** Go back 1 in browser history */
         goBack() {
             history.back();
         }
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
index 11d3a1be3..c118226c9 100644
--- a/src/pages/Settings.vue
+++ b/src/pages/Settings.vue
@@ -118,13 +118,17 @@ export default {
 
     methods: {
 
-        // For desktop only, mobile do nothing
+        /**
+         * Load the general settings page
+         * For desktop only, mobile do nothing
+         */
         loadGeneralPage() {
             if (!this.currentPage && !this.$root.isMobile) {
                 this.$router.push("/settings/general");
             }
         },
 
+        /** Load settings from server */
         loadSettings() {
             this.$root.getSocket().emit("getSettings", (res) => {
                 this.settings = res.data;
@@ -149,9 +153,16 @@ export default {
             });
         },
 
+        /**
+         * Callback for saving settings
+         * @callback saveSettingsCB
+         * @param {Object} res Result of operation
+         */
+
         /**
          * Save Settings
-         * @param currentPassword (Optional) Only need for disableAuth to true
+         * @param {saveSettingsCB} [callback]
+         * @param {string} [currentPassword] Only need for disableAuth to true
          */
         saveSettings(callback, currentPassword) {
             this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
diff --git a/src/pages/Setup.vue b/src/pages/Setup.vue
index ab5952167..08347b8e1 100644
--- a/src/pages/Setup.vue
+++ b/src/pages/Setup.vue
@@ -71,6 +71,10 @@ export default {
         });
     },
     methods: {
+        /**
+         * Submit form data for processing
+         * @returns {void}
+         */
         submit() {
             this.processing = true;
 
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index fbcbf9e8e..1d6481417 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -332,6 +332,7 @@ export default {
     },
 
     props: {
+        /** Override for the status page slug */
         overrideSlug: {
             type: String,
             required: false,
@@ -582,10 +583,16 @@ export default {
             }
         },
 
+        /**
+         * Provide syntax highlighting for CSS
+         * @param {string} code Text to highlight
+         * @returns {string}
+         */
         highlighter(code) {
             return highlight(code, languages.css);
         },
 
+        /** Update the heartbeat list and update favicon if neccessary */
         updateHeartbeatList() {
             // If editMode, it will use the data from websocket.
             if (! this.editMode) {
@@ -614,6 +621,7 @@ export default {
             }
         },
 
+        /** Enable editing mode */
         edit() {
             if (this.hasToken) {
                 this.$root.initSocketIO(true);
@@ -622,6 +630,7 @@ export default {
             }
         },
 
+        /** Save the status page */
         save() {
             let startTime = new Date();
             this.config.slug = this.config.slug.trim().toLowerCase();
@@ -649,10 +658,12 @@ export default {
             });
         },
 
+        /** Show dialog confirming deletion */
         deleteDialog() {
             this.$refs.confirmDelete.show();
         },
 
+        /** Request deletion of this status page */
         deleteStatusPage() {
             this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
                 if (res.ok) {
@@ -664,10 +675,16 @@ export default {
             });
         },
 
+        /**
+         * Returns label for a specifed monitor
+         * @param {Object} monitor Object representing monitor
+         * @returns {string}
+         */
         monitorSelectorLabel(monitor) {
             return `${monitor.name}`;
         },
 
+        /** Add a group to the status page */
         addGroup() {
             let groupName = this.$t("Untitled Group");
 
@@ -681,27 +698,32 @@ export default {
             });
         },
 
+        /** Add a domain to the status page */
         addDomainField() {
             this.config.domainNameList.push("");
         },
 
+        /** Discard changes to status page */
         discard() {
             location.href = "/status/" + this.slug;
         },
 
         /**
-         * Crop Success
+         * Set URL of new image after successful crop operation
+         * @param {string} imgDataUrl URL of image in data:// format
          */
         cropSuccess(imgDataUrl) {
             this.imgDataUrl = imgDataUrl;
         },
 
+        /** Show image crop dialog if in edit mode */
         showImageCropUploadMethod() {
             if (this.editMode) {
                 this.showImageCropUpload = true;
             }
         },
 
+        /** Create an incident for this status page */
         createIncident() {
             this.enableEditIncidentMode = true;
 
@@ -716,6 +738,7 @@ export default {
             };
         },
 
+        /** Post the incident to the status page */
         postIncident() {
             if (this.incident.title === "" || this.incident.content === "") {
                 toast.error(this.$t("Please input title and content"));
@@ -735,14 +758,13 @@ export default {
 
         },
 
-        /**
-         * Click Edit Button
-         */
+        /** Click Edit Button */
         editIncident() {
             this.enableEditIncidentMode = true;
             this.previousIncident = Object.assign({}, this.incident);
         },
 
+        /** Cancel creation or editing of incident */
         cancelIncident() {
             this.enableEditIncidentMode = false;
 
@@ -752,16 +774,25 @@ export default {
             }
         },
 
+        /** Unpin the incident */
         unpinIncident() {
             this.$root.getSocket().emit("unpinIncident", this.slug, () => {
                 this.incident = null;
             });
         },
 
+        /**
+         * Get the relative time difference of a date from now
+         * @returns {string}
+         */
         dateFromNow(date) {
             return dayjs.utc(date).fromNow();
         },
 
+        /**
+         * Remove a domain from the status page
+         * @param {number} index Index of domain to remove
+         */
         removeDomain(index) {
             this.config.domainNameList.splice(index, 1);
         },

From c94dcf15335e7c0313ef3236400650ea6b5dcd58 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 14:32:38 +0100
Subject: [PATCH 7/9] Added JSDoc for src/*

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
---
 src/util-frontend.js |  7 ++--
 src/util.js          | 70 ++++++++++++++++++++++++++++++++++++----
 src/util.ts          | 76 ++++++++++++++++++++++++++++++++++++++------
 3 files changed, 135 insertions(+), 18 deletions(-)

diff --git a/src/util-frontend.js b/src/util-frontend.js
index 565b053c4..36dac49f9 100644
--- a/src/util-frontend.js
+++ b/src/util-frontend.js
@@ -26,8 +26,8 @@ function getTimezoneOffset(timeZone) {
 
 /**
 * Returns a list of timezones sorted by their offset from UTC.
-* @param {Array} timezones - An array of timezone objects.
-* @returns {Array} A list of the given timezones sorted by their offset from UTC.
+* @param {Object[]} timezones An array of timezone objects.
+* @returns {Object[]} A list of the given timezones sorted by their offset from UTC.
 *
 * Generated by Trelent
 */
@@ -63,6 +63,7 @@ export function timezoneList() {
     return result;
 }
 
+/** Set the locale of the HTML page */
 export function setPageLocale() {
     const html = document.documentElement;
     html.setAttribute("lang", currentLocale() );
@@ -70,7 +71,9 @@ export function setPageLocale() {
 }
 
 /**
+ * Get the base URL
  * Mainly used for dev, because the backend and the frontend are in different ports.
+ * @returns {string}
  */
 export function getResBaseURL() {
     const env = process.env.NODE_ENV;
diff --git a/src/util.js b/src/util.js
index ee6095e08..d5e3617fd 100644
--- a/src/util.js
+++ b/src/util.js
@@ -18,6 +18,7 @@ exports.PENDING = 2;
 exports.STATUS_PAGE_ALL_DOWN = 0;
 exports.STATUS_PAGE_ALL_UP = 1;
 exports.STATUS_PAGE_PARTIAL_DOWN = 2;
+/** Flip the status of s */
 function flipStatus(s) {
     if (s === exports.UP) {
         return exports.DOWN;
@@ -28,6 +29,10 @@ function flipStatus(s) {
     return s;
 }
 exports.flipStatus = flipStatus;
+/**
+ * Delays for specified number of seconds
+ * @param ms Number of milliseconds to sleep for
+ */
 function sleep(ms) {
     return new Promise(resolve => setTimeout(resolve, ms));
 }
@@ -83,6 +88,12 @@ class Logger {
             this.debug("server", this.hideLog);
         }
     }
+    /**
+     * Write a message to the log
+     * @param module The module the log comes from
+     * @param msg Message to write
+     * @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
+     */
     log(module, msg, level) {
         if (this.hideLog[level] && this.hideLog[level].includes(module)) {
             return;
@@ -109,18 +120,44 @@ class Logger {
             console.log(formattedMessage);
         }
     }
+    /**
+     * Log an INFO message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
     info(module, msg) {
         this.log(module, msg, "info");
     }
+    /**
+     * Log a WARN message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
     warn(module, msg) {
         this.log(module, msg, "warn");
     }
+    /**
+     * Log an ERROR message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
     error(module, msg) {
         this.log(module, msg, "error");
     }
+    /**
+     * Log a DEBUG message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
     debug(module, msg) {
         this.log(module, msg, "debug");
     }
+    /**
+     * Log an exeption as an ERROR
+     * @param module Module log comes from
+     * @param exception The exeption to include
+     * @param msg The message to write
+     */
     exception(module, exception, msg) {
         let finalMessage = exception;
         if (msg) {
@@ -130,13 +167,13 @@ class Logger {
     }
 }
 exports.log = new Logger();
+/**
+ * String.prototype.replaceAll() polyfill
+ * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
+ * @author Chris Ferdinandi
+ * @license MIT
+ */
 function polyfill() {
-    /**
-     * String.prototype.replaceAll() polyfill
-     * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
-     * @author Chris Ferdinandi
-     * @license MIT
-     */
     if (!String.prototype.replaceAll) {
         String.prototype.replaceAll = function (str, newStr) {
             // If a regex pattern
@@ -153,6 +190,10 @@ class TimeLogger {
     constructor() {
         this.startTime = dayjs().valueOf();
     }
+    /**
+     * Output time since start of monitor
+     * @param name Name of monitor
+     */
     print(name) {
         if (exports.isDev && process.env.TIMELOGGER === "1") {
             console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
@@ -201,6 +242,13 @@ let getRandomBytes = ((typeof window !== 'undefined' && window.crypto)
     : function () {
         return require("crypto").randomBytes;
     })();
+/**
+ * Get a random integer suitable for use in cryptography between upper
+ * and lower bounds.
+ * @param min Minimum value of integer
+ * @param max Maximum value of integer
+ * @returns Cryptographically suitable random integer
+ */
 function getCryptoRandomInt(min, max) {
     // synchronous version of: https://github.com/joepie91/node-random-number-csprng
     const range = max - min;
@@ -231,6 +279,11 @@ function getCryptoRandomInt(min, max) {
     }
 }
 exports.getCryptoRandomInt = getCryptoRandomInt;
+/**
+ * Generate a secret
+ * @param length Lenght of secret to generate
+ * @returns
+ */
 function genSecret(length = 64) {
     let secret = "";
     const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -241,6 +294,11 @@ function genSecret(length = 64) {
     return secret;
 }
 exports.genSecret = genSecret;
+/**
+ * Get the path of a monitor
+ * @param id ID of monitor
+ * @returns Formatted relative path
+ */
 function getMonitorRelativeURL(id) {
     return "/dashboard/" + id;
 }
diff --git a/src/util.ts b/src/util.ts
index 057090b72..b28ba3c15 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -19,7 +19,7 @@ export const STATUS_PAGE_ALL_DOWN = 0;
 export const STATUS_PAGE_ALL_UP = 1;
 export const STATUS_PAGE_PARTIAL_DOWN = 2;
 
-
+/** Flip the status of s */
 export function flipStatus(s: number) {
     if (s === UP) {
         return DOWN;
@@ -32,6 +32,10 @@ export function flipStatus(s: number) {
     return s;
 }
 
+/**
+ * Delays for specified number of seconds
+ * @param ms Number of milliseconds to sleep for
+ */
 export function sleep(ms: number) {
     return new Promise(resolve => setTimeout(resolve, ms));
 }
@@ -94,6 +98,12 @@ class Logger {
         }
     }
 
+    /**
+     * Write a message to the log
+     * @param module The module the log comes from
+     * @param msg Message to write
+     * @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
+     */
     log(module: string, msg: any, level: string) {
         if (this.hideLog[level] && this.hideLog[level].includes(module)) {
             return;
@@ -120,22 +130,48 @@ class Logger {
         }
     }
 
+    /**
+     * Log an INFO message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
     info(module: string, msg: any) {
         this.log(module, msg, "info");
     }
 
+    /**
+     * Log a WARN message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
     warn(module: string, msg: any) {
         this.log(module, msg, "warn");
     }
 
-   error(module: string, msg: any) {
+    /**
+     * Log an ERROR message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
+    error(module: string, msg: any) {
        this.log(module, msg, "error");
     }
 
-   debug(module: string, msg: any) {
+    /**
+     * Log a DEBUG message
+     * @param module Module log comes from
+     * @param msg Message to write
+     */
+    debug(module: string, msg: any) {
        this.log(module, msg, "debug");
     }
 
+    /**
+     * Log an exeption as an ERROR
+     * @param module Module log comes from
+     * @param exception The exeption to include
+     * @param msg The message to write
+     */
     exception(module: string, exception: any, msg: any) {
         let finalMessage = exception
 
@@ -151,13 +187,13 @@ export const log = new Logger();
 
 declare global { interface String { replaceAll(str: string, newStr: string): string; } }
 
+/**
+ * String.prototype.replaceAll() polyfill
+ * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
+ * @author Chris Ferdinandi
+ * @license MIT
+ */
 export function polyfill() {
-    /**
-     * String.prototype.replaceAll() polyfill
-     * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
-     * @author Chris Ferdinandi
-     * @license MIT
-     */
     if (!String.prototype.replaceAll) {
         String.prototype.replaceAll = function (str: string, newStr: string) {
             // If a regex pattern
@@ -177,7 +213,10 @@ export class TimeLogger {
     constructor() {
         this.startTime = dayjs().valueOf();
     }
-
+    /**
+     * Output time since start of monitor
+     * @param name Name of monitor
+     */
     print(name: string) {
         if (isDev && process.env.TIMELOGGER === "1") {
             console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
@@ -231,6 +270,13 @@ let getRandomBytes = (
         }
 )();
 
+/**
+ * Get a random integer suitable for use in cryptography between upper
+ * and lower bounds.
+ * @param min Minimum value of integer
+ * @param max Maximum value of integer
+ * @returns Cryptographically suitable random integer
+ */
 export function getCryptoRandomInt(min: number, max: number):number {
 
     // synchronous version of: https://github.com/joepie91/node-random-number-csprng
@@ -267,6 +313,11 @@ export function getCryptoRandomInt(min: number, max: number):number {
     }
 }
 
+/**
+ * Generate a secret
+ * @param length Lenght of secret to generate
+ * @returns 
+ */
 export function genSecret(length = 64) {
     let secret = "";
     const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -277,6 +328,11 @@ export function genSecret(length = 64) {
     return secret;
 }
 
+/**
+ * Get the path of a monitor
+ * @param id ID of monitor
+ * @returns Formatted relative path
+ */
 export function getMonitorRelativeURL(id: string) {
     return "/dashboard/" + id;
 }

From 0e28707307b77fbcec8d6aa7473322f47adfe304 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 15:15:21 +0100
Subject: [PATCH 8/9] Minor formatting for JSDoc comments

Added a number of minor formatting changes to JSDoc comments in /src
---
 src/components/Confirm.vue       | 2 +-
 src/components/Datetime.vue      | 2 +-
 src/components/MonitorList.vue   | 2 +-
 src/components/ToggleSection.vue | 2 +-
 src/components/Uptime.vue        | 2 +-
 src/mixins/mobile.js             | 2 +-
 src/pages/Settings.vue           | 2 +-
 7 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue
index 258724a39..1a1addc6e 100644
--- a/src/components/Confirm.vue
+++ b/src/components/Confirm.vue
@@ -58,7 +58,7 @@ export default {
             this.modal.show();
         },
         /**
-         * @emits "yes" - Notify the parent when Yes is pressed
+         * @emits string "yes" Notify the parent when Yes is pressed
          */
         yes() {
             this.$emit("yes");
diff --git a/src/components/Datetime.vue b/src/components/Datetime.vue
index 3c66f5d6e..b24ab0b3c 100644
--- a/src/components/Datetime.vue
+++ b/src/components/Datetime.vue
@@ -13,7 +13,7 @@ dayjs.extend(relativeTime);
 
 export default {
     props: {
-        /** Value of data time */
+        /** Value of date time */
         value: {
             type: String,
             default: null,
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
index 1d48af710..1ae12b2ff 100644
--- a/src/components/MonitorList.vue
+++ b/src/components/MonitorList.vue
@@ -125,7 +125,7 @@ export default {
         window.removeEventListener("scroll", this.onScroll);
     },
     methods: {
-        /** Called when the user scrolls */
+        /** Handle user scroll */
         onScroll() {
             if (window.top.scrollY <= 133) {
                 this.windowTop = window.top.scrollY;
diff --git a/src/components/ToggleSection.vue b/src/components/ToggleSection.vue
index 47dc21905..a2f7c9dae 100644
--- a/src/components/ToggleSection.vue
+++ b/src/components/ToggleSection.vue
@@ -29,7 +29,7 @@
 <script>
 export default {
     props: {
-        /** Heading of the Section */
+        /** Heading of the section */
         heading: {
             type: String,
             default: "",
diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue
index 8793b7a7b..8f7eba80c 100644
--- a/src/components/Uptime.vue
+++ b/src/components/Uptime.vue
@@ -15,7 +15,7 @@ export default {
             type: String,
             default: null,
         },
-        /** Is this a pill */
+        /** Is this a pill? */
         pill: {
             type: Boolean,
             default: false,
diff --git a/src/mixins/mobile.js b/src/mixins/mobile.js
index ffc50f50c..32ee209fc 100644
--- a/src/mixins/mobile.js
+++ b/src/mixins/mobile.js
@@ -12,7 +12,7 @@ export default {
     },
 
     methods: {
-        /** Called when the screen changes size */
+        /** Handle screen resize */
         onResize() {
             this.windowWidth = window.innerWidth;
             this.updateBody();
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
index c118226c9..f8effb4ad 100644
--- a/src/pages/Settings.vue
+++ b/src/pages/Settings.vue
@@ -120,7 +120,7 @@ export default {
 
         /**
          * Load the general settings page
-         * For desktop only, mobile do nothing
+         * For desktop only, on mobile do nothing
          */
         loadGeneralPage() {
             if (!this.currentPage && !this.$root.isMobile) {

From a927f5cd1589e1ee55ddb5ef01687dfa58a3cbc9 Mon Sep 17 00:00:00 2001
From: Matthew Nickson <mnickson@sidingsmedia.com>
Date: Thu, 2 Jun 2022 16:40:56 +0100
Subject: [PATCH 9/9] Fixed typos + improved clarity and detail of some JSDoc

Apply suggestions from code review

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
---
 src/components/ProxyDialog.vue | 2 +-
 src/components/TagsManager.vue | 6 +++---
 src/mixins/mobile.js           | 2 +-
 src/util.ts                    | 6 +++---
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/components/ProxyDialog.vue b/src/components/ProxyDialog.vue
index e52698f9f..434c571be 100644
--- a/src/components/ProxyDialog.vue
+++ b/src/components/ProxyDialog.vue
@@ -130,7 +130,7 @@ export default {
     },
 
     methods: {
-        /** Show dialog to confirm delection */
+        /** Show dialog to confirm deletion */
         deleteConfirm() {
             this.modal.hide();
             this.$refs.confirmDelete.show();
diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue
index fd94c93e2..2958babe8 100644
--- a/src/components/TagsManager.vue
+++ b/src/components/TagsManager.vue
@@ -271,9 +271,9 @@ export default {
         },
         /**
          * Get colour of text inside the tag
-         * @param {Object} option Object representing color choice. If
-         * option.color is set, the text color will be white, else it
-         * be chosen based upon application theme
+         * @param {Object} option The tag that needs to be displayed.
+         * Defaults to "white" unless the tag has no color, which will
+         * then return the body color (based on application theme)
          * @returns string
          */
         textColor(option) {
diff --git a/src/mixins/mobile.js b/src/mixins/mobile.js
index 32ee209fc..00ea88866 100644
--- a/src/mixins/mobile.js
+++ b/src/mixins/mobile.js
@@ -18,7 +18,7 @@ export default {
             this.updateBody();
         },
 
-        /** Update the document body */
+        /** Add css-class "mobile" to body if needed */
         updateBody() {
             if (this.isMobile) {
                 document.body.classList.add("mobile");
diff --git a/src/util.ts b/src/util.ts
index b28ba3c15..b69f31ace 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -314,9 +314,9 @@ export function getCryptoRandomInt(min: number, max: number):number {
 }
 
 /**
- * Generate a secret
- * @param length Lenght of secret to generate
- * @returns 
+ * Generate a random alphanumeric string of fixed length
+ * @param length Length of string to generate
+ * @returns string
  */
 export function genSecret(length = 64) {
     let secret = "";