From b2c337ab5692efa4d218d3bbcbe5aab167f26b21 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Sat, 29 Mar 2025 00:37:35 +0000
Subject: [PATCH 001/124] event_name is required (still need to add tests)

---
 frontend_microservice/static/script.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index b6dfe98..484e12c 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -179,6 +179,13 @@ function sendDates(){
     console.log("button pressed")
     console.log(possible_dates)
     let name = document.getElementById('event_name').value;
+
+    //check that event name is not empty
+    if (name == "") {
+        alert("Event name is required.");
+        return;
+    }
+
     $.ajax({
         url: `/create_event`, 
         type: 'POST',
-- 
GitLab


From 30043c2ccff4352ad152022b3f6818ee94a7018c Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Sat, 29 Mar 2025 02:42:51 +0000
Subject: [PATCH 002/124] added jest tests

---
 frontend_microservice/static/script.js |  372 +-
 package-lock.json                      | 4476 ++++++++++++++++++++++++
 package.json                           |   28 +
 tests/sendDates.test.js                |   44 +
 4 files changed, 4735 insertions(+), 185 deletions(-)
 create mode 100644 package-lock.json
 create mode 100644 package.json
 create mode 100644 tests/sendDates.test.js

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 484e12c..48752fc 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -1,226 +1,228 @@
-let date = new Date();
-let year = date.getFullYear();
-let month = date.getMonth();
-
-const day = document.querySelector(".calendar-dates");
-const currdate = document.querySelector(".calendar-current-date");
-const prenexIcons = document.querySelectorAll(".calendar-navigation span");
-
-// Array of month names
-const months = [
-    "January",
-    "February",
-    "March",
-    "April",
-    "May",
-    "June",
-    "July",
-    "August",
-    "September",
-    "October",
-    "November",
-    "December"
-];
-
 let possible_dates = [];
-//eventdates = [datetime.datetime(2025, 3, 27, 0, 0, 0), datetime.datetime(2025, 3, 28,0,0,0), datetime.datetime(2025,3,31,0,0,0)]
-const title = document.getElementById("title").textContent;
-console.log("Title: ",title);
-
-//Convert dates to string
-function convertDatesToString(dates){
-    //depending on data from database, can convert to date
-    dates_str = []
-    for (i=0; i<dates.length;i++){
-        let date_ = new Date(dates[i]);
-        let day_ = date_.getDate();
-        let month_ = date_.getMonth() +1;
-        let year_ = date_.getFullYear();
-        let fullDate_ = `${year_}-${month_}-${day_}`
-        dates_str.push(fullDate_)
+document.addEventListener('DOMContentLoaded', function() {
+
+    let date = new Date();
+    let year = date.getFullYear();
+    let month = date.getMonth();
+
+    const day = document.querySelector(".calendar-dates");
+    const currdate = document.querySelector(".calendar-current-date");
+    const prenexIcons = document.querySelectorAll(".calendar-navigation span");
+
+    // Array of month names
+    const months = [
+        "January",
+        "February",
+        "March",
+        "April",
+        "May",
+        "June",
+        "July",
+        "August",
+        "September",
+        "October",
+        "November",
+        "December"
+    ];
+
+    
+    //eventdates = [datetime.datetime(2025, 3, 27, 0, 0, 0), datetime.datetime(2025, 3, 28,0,0,0), datetime.datetime(2025,3,31,0,0,0)]
+    const title = document.getElementById("title").textContent;
+    console.log("Title: ",title);
+
+    //Convert dates to string
+    function convertDatesToString(dates){
+        //depending on data from database, can convert to date
+        dates_str = []
+        for (i=0; i<dates.length;i++){
+            let date_ = new Date(dates[i]);
+            let day_ = date_.getDate();
+            let month_ = date_.getMonth() +1;
+            let year_ = date_.getFullYear();
+            let fullDate_ = `${year_}-${month_}-${day_}`
+            dates_str.push(fullDate_)
+        }
+        console.log(dates_str)
+        return dates_str
     }
-    console.log(dates_str)
-    return dates_str
-}
 
-let event_dates_str = [];
-if (title=='Availability'){
-    event_dates_str = convertDatesToString(eventdates);
-}
+    let event_dates_str = [];
+    if (title=='Availability'){
+        event_dates_str = convertDatesToString(eventdates);
+    }
 
 
-// Function to generate the calendar
-const manipulate = () => {
-    //console.log(possible_dates)
-    // Get the first day of the month
-    let dayone = new Date(year, month, 1).getDay();
+    // Function to generate the calendar
+    const manipulate = () => {
+        //console.log(possible_dates)
+        // Get the first day of the month
+        let dayone = new Date(year, month, 1).getDay();
 
-    // Get the last date of the month
-    let lastdate = new Date(year, month + 1, 0).getDate();
+        // Get the last date of the month
+        let lastdate = new Date(year, month + 1, 0).getDate();
 
-    // Get the day of the last date of the month
-    let dayend = new Date(year, month, lastdate).getDay();
+        // Get the day of the last date of the month
+        let dayend = new Date(year, month, lastdate).getDay();
 
-    // Get the last date of the previous month
-    let monthlastdate = new Date(year, month, 0).getDate();
+        // Get the last date of the previous month
+        let monthlastdate = new Date(year, month, 0).getDate();
 
-    // Variable to store the generated calendar HTML
-    let lit = "";
+        // Variable to store the generated calendar HTML
+        let lit = "";
 
-    // Loop to add the last dates of the previous month
-    for (let i = dayone; i > 0; i--) {
-        lit +=
-            `<li class="not_this_month not_selectable">${monthlastdate - i + 1}</li>`;
-    }
+        // Loop to add the last dates of the previous month
+        for (let i = dayone; i > 0; i--) {
+            lit +=
+                `<li class="not_this_month not_selectable">${monthlastdate - i + 1}</li>`;
+        }
 
-    // Loop to add the dates of the current month
-    for (let i = 1; i <= lastdate; i++) {
+        // Loop to add the dates of the current month
+        for (let i = 1; i <= lastdate; i++) {
 
-        let fullDate = `${year}-${month+1}-${i}`;
+            let fullDate = `${year}-${month+1}-${i}`;
 
-        // Check if the current date is today
-        let isToday = i === date.getDate()
-            && month === new Date().getMonth()
-            && year === new Date().getFullYear()
-            ? "today"
-            : "";
+            // Check if the current date is today
+            let isToday = i === date.getDate()
+                && month === new Date().getMonth()
+                && year === new Date().getFullYear()
+                ? "today"
+                : "";
 
-        let isSelectable = ""
-        if (title=="Availability"){
-            isSelectable = event_dates_str.includes(fullDate) ? "" : "not_selectable";
+            let isSelectable = ""
+            if (title=="Availability"){
+                isSelectable = event_dates_str.includes(fullDate) ? "" : "not_selectable";
+            }
+            let isSelected = possible_dates.includes(fullDate) ? "selected" : "";  // Check if the date is selected
+            lit += `<li class="${isToday} ${isSelected} ${isSelectable}"data-date="${fullDate}">${i}</li>`;
         }
-        let isSelected = possible_dates.includes(fullDate) ? "selected" : "";  // Check if the date is selected
-        lit += `<li class="${isToday} ${isSelected} ${isSelectable}"data-date="${fullDate}">${i}</li>`;
-    }
 
-    // Loop to add the first dates of the next month
-    for (let i = dayend; i < 6; i++) {
-        lit += `<li class="not_this_month not_selectable">${i - dayend + 1}</li>`
-    }
+        // Loop to add the first dates of the next month
+        for (let i = dayend; i < 6; i++) {
+            lit += `<li class="not_this_month not_selectable">${i - dayend + 1}</li>`
+        }
 
-    // Update the text of the current date element 
-    // with the formatted current month and year
-    currdate.innerText = `${months[month]} ${year}`;
+        // Update the text of the current date element 
+        // with the formatted current month and year
+        currdate.innerText = `${months[month]} ${year}`;
 
-    // update the HTML of the dates element 
-    // with the generated calendar
-    day.innerHTML = lit;
+        // update the HTML of the dates element 
+        // with the generated calendar
+        day.innerHTML = lit;
 
-    // Add event listeners for the new dates
-    document.querySelectorAll('.calendar-dates li').forEach((dateItem) => {
-        if (!dateItem.classList.contains('not_selectable')) {
-            dateItem.addEventListener('click', () => toggleDateSelection(dateItem));
-        }
-    });
-}
-
-// Function to toggle date selection
-const toggleDateSelection = (dateItem) => {
-    const dateStr = dateItem.getAttribute("data-date");
-    const dateIndex = possible_dates.indexOf(dateStr);
-
-    if (dateIndex === -1) {
-        // If not already selected, add to the selected dates list
-        possible_dates.push(dateStr);
-        dateItem.classList.add("selected"); // Add selected class to change color
-    } else {
-        // If already selected, remove from the selected dates list
-        possible_dates.splice(dateIndex, 1);
-        dateItem.classList.remove("selected"); // Remove selected class to reset color
+        // Add event listeners for the new dates
+        document.querySelectorAll('.calendar-dates li').forEach((dateItem) => {
+            if (!dateItem.classList.contains('not_selectable')) {
+                dateItem.addEventListener('click', () => toggleDateSelection(dateItem));
+            }
+        });
     }
-};
-
-// Function to toggle the selection state of buttons
-function toggleDaySelection(button) {
-    button.classList.toggle('selected');
 
-}
+    // Function to toggle date selection
+    const toggleDateSelection = (dateItem) => {
+        const dateStr = dateItem.getAttribute("data-date");
+        const dateIndex = possible_dates.indexOf(dateStr);
+
+        if (dateIndex === -1) {
+            // If not already selected, add to the selected dates list
+            possible_dates.push(dateStr);
+            dateItem.classList.add("selected"); // Add selected class to change color
+        } else {
+            // If already selected, remove from the selected dates list
+            possible_dates.splice(dateIndex, 1);
+            dateItem.classList.remove("selected"); // Remove selected class to reset color
+        }
+    };
 
-manipulate();
+    // Function to toggle the selection state of buttons
+    function toggleDaySelection(button) {
+        button.classList.toggle('selected');
 
-// Attach a click event listener to each icon
-prenexIcons.forEach(icon => {
+    }
 
-    // When an icon is clicked
-    icon.addEventListener("click", () => {
+    manipulate();
 
-        // Check if the icon is "calendar-prev"
-        // or "calendar-next"
-        month = icon.id === "calendar-prev" ? month - 1 : month + 1;
+    // Attach a click event listener to each icon
+    prenexIcons.forEach(icon => {
 
-        // Check if the month is out of range
-        if (month < 0 || month > 11) {
+        // When an icon is clicked
+        icon.addEventListener("click", () => {
 
-            // Set the date to the first day of the 
-            // month with the new year
-            date = new Date(year, month, new Date().getDate());
+            // Check if the icon is "calendar-prev"
+            // or "calendar-next"
+            month = icon.id === "calendar-prev" ? month - 1 : month + 1;
 
-            // Set the year to the new year
-            year = date.getFullYear();
+            // Check if the month is out of range
+            if (month < 0 || month > 11) {
 
-            // Set the month to the new month
-            month = date.getMonth();
-        }
+                // Set the date to the first day of the 
+                // month with the new year
+                date = new Date(year, month, new Date().getDate());
 
-        else {
+                // Set the year to the new year
+                year = date.getFullYear();
 
-            // Set the date to the current date
-            date = new Date();
-        }
+                // Set the month to the new month
+                month = date.getMonth();
+            }
 
-        // Call the manipulate function to 
-        // update the calendar display
-        manipulate();
-    });
-});
+            else {
 
+                // Set the date to the current date
+                date = new Date();
+            }
 
-function sendDates(){
-    console.log("button pressed")
-    console.log(possible_dates)
-    let name = document.getElementById('event_name').value;
+            // Call the manipulate function to 
+            // update the calendar display
+            manipulate();
+        });
+    });
+})
 
-    //check that event name is not empty
-    if (name == "") {
-        alert("Event name is required.");
-        return;
-    }
+    function sendDates(){
+        console.log("button pressed")
+        console.log(possible_dates)
+        let name = document.getElementById('event_name').value;
 
-    $.ajax({
-        url: `/create_event`, 
-        type: 'POST',
-        contentType: 'application/json', 
-        data: JSON.stringify({ 'possible_dates': possible_dates, 
-                                'name':name }),
-        success: function(response) {
-            /*TODO: Create page to go to*/
-            window.location.href = "/index";
-        },
-        error: function(xhr, status, error) {
-            alert("Error: " + error);
+        //check that event name is not empty
+        if (name == "") {
+            alert("Event name cannot be empty.");
+            return;
         }
-    });
-}
-
-function sendAvailabilities(event_id, user_id){
-    console.log("button pressed")
-    $.ajax({
-        url: `/availability/${event_id}/${user_id}`, 
-        type: 'POST',
-        contentType: 'application/json', 
-        data: JSON.stringify({ 'availability': possible_dates, 'event_id':event_id, 'user_id':user_id}),
-        success: function(response) {
-            /*TODO: Create page to go to*/
-            window.location.href = "/index";
-        },
-        error: function(xhr, status, error) {
-            alert("Error: " + error);
-        }
-    });
-}
-
 
+        $.ajax({
+            url: `/create_event`, 
+            type: 'POST',
+            contentType: 'application/json', 
+            data: JSON.stringify({ 'possible_dates': possible_dates, 
+                                    'name':name }),
+            success: function(response) {
+                /*TODO: Create page to go to*/
+                window.location.href = "/index";
+            },
+            error: function(xhr, status, error) {
+                alert("Error: " + error);
+            }
+        });
+    }
+    
+
+    function sendAvailabilities(event_id, user_id){
+        console.log("button pressed")
+        $.ajax({
+            url: `/availability/${event_id}/${user_id}`, 
+            type: 'POST',
+            contentType: 'application/json', 
+            data: JSON.stringify({ 'availability': possible_dates, 'event_id':event_id, 'user_id':user_id}),
+            success: function(response) {
+                /*TODO: Create page to go to*/
+                window.location.href = "/index";
+            },
+            error: function(xhr, status, error) {
+                alert("Error: " + error);
+            }
+        });
+    }
 
+    module.exports = { sendDates, sendAvailabilities };
 
 
-  
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..598a1dc
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,4476 @@
+{
+  "name": "coursework",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "coursework",
+      "version": "1.0.0",
+      "license": "ISC",
+      "devDependencies": {
+        "jest": "^29.7.0",
+        "jest-environment-jsdom": "^29.7.0",
+        "jquery": "^3.7.1"
+      }
+    },
+    "node_modules/@ampproject/remapping": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.26.8",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
+      "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.26.10",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
+      "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ampproject/remapping": "^2.2.0",
+        "@babel/code-frame": "^7.26.2",
+        "@babel/generator": "^7.26.10",
+        "@babel/helper-compilation-targets": "^7.26.5",
+        "@babel/helper-module-transforms": "^7.26.0",
+        "@babel/helpers": "^7.26.10",
+        "@babel/parser": "^7.26.10",
+        "@babel/template": "^7.26.9",
+        "@babel/traverse": "^7.26.10",
+        "@babel/types": "^7.26.10",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
+      "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.27.0",
+        "@babel/types": "^7.27.0",
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
+      "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.26.8",
+        "@babel/helper-validator-option": "^7.25.9",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+      "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+      "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
+      "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+      "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+      "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.27.0",
+        "@babel/types": "^7.27.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+      "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-async-generators": {
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+      "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-bigint": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+      "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-class-properties": {
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+      "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.12.13"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-class-static-block": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+      "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-attributes": {
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
+      "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-meta": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-json-strings": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+      "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-jsx": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz",
+      "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+      "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+      "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-numeric-separator": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+      "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-object-rest-spread": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+      "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+      "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-optional-chaining": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+      "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-private-property-in-object": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+      "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-top-level-await": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+      "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-typescript": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
+      "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+      "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "@babel/parser": "^7.27.0",
+        "@babel/types": "^7.27.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
+      "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "@babel/generator": "^7.27.0",
+        "@babel/parser": "^7.27.0",
+        "@babel/template": "^7.27.0",
+        "@babel/types": "^7.27.0",
+        "debug": "^4.3.1",
+        "globals": "^11.1.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+      "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@bcoe/v8-coverage": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+      "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@istanbuljs/load-nyc-config": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+      "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.3.1",
+        "find-up": "^4.1.0",
+        "get-package-type": "^0.1.0",
+        "js-yaml": "^3.13.1",
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@istanbuljs/schema": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+      "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@jest/console": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz",
+      "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "jest-message-util": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/core": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
+      "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/console": "^29.7.0",
+        "@jest/reporters": "^29.7.0",
+        "@jest/test-result": "^29.7.0",
+        "@jest/transform": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^4.0.0",
+        "ci-info": "^3.2.0",
+        "exit": "^0.1.2",
+        "graceful-fs": "^4.2.9",
+        "jest-changed-files": "^29.7.0",
+        "jest-config": "^29.7.0",
+        "jest-haste-map": "^29.7.0",
+        "jest-message-util": "^29.7.0",
+        "jest-regex-util": "^29.6.3",
+        "jest-resolve": "^29.7.0",
+        "jest-resolve-dependencies": "^29.7.0",
+        "jest-runner": "^29.7.0",
+        "jest-runtime": "^29.7.0",
+        "jest-snapshot": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "jest-validate": "^29.7.0",
+        "jest-watcher": "^29.7.0",
+        "micromatch": "^4.0.4",
+        "pretty-format": "^29.7.0",
+        "slash": "^3.0.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "node-notifier": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@jest/environment": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
+      "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/fake-timers": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "jest-mock": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/expect": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
+      "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "expect": "^29.7.0",
+        "jest-snapshot": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/expect-utils": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+      "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "jest-get-type": "^29.6.3"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/fake-timers": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
+      "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "@sinonjs/fake-timers": "^10.0.2",
+        "@types/node": "*",
+        "jest-message-util": "^29.7.0",
+        "jest-mock": "^29.7.0",
+        "jest-util": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/globals": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
+      "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/environment": "^29.7.0",
+        "@jest/expect": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "jest-mock": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/reporters": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
+      "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@bcoe/v8-coverage": "^0.2.3",
+        "@jest/console": "^29.7.0",
+        "@jest/test-result": "^29.7.0",
+        "@jest/transform": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@jridgewell/trace-mapping": "^0.3.18",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "collect-v8-coverage": "^1.0.0",
+        "exit": "^0.1.2",
+        "glob": "^7.1.3",
+        "graceful-fs": "^4.2.9",
+        "istanbul-lib-coverage": "^3.0.0",
+        "istanbul-lib-instrument": "^6.0.0",
+        "istanbul-lib-report": "^3.0.0",
+        "istanbul-lib-source-maps": "^4.0.0",
+        "istanbul-reports": "^3.1.3",
+        "jest-message-util": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "jest-worker": "^29.7.0",
+        "slash": "^3.0.0",
+        "string-length": "^4.0.1",
+        "strip-ansi": "^6.0.0",
+        "v8-to-istanbul": "^9.0.1"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "node-notifier": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@jest/schemas": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+      "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sinclair/typebox": "^0.27.8"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/source-map": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
+      "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.18",
+        "callsites": "^3.0.0",
+        "graceful-fs": "^4.2.9"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/test-result": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz",
+      "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/console": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/istanbul-lib-coverage": "^2.0.0",
+        "collect-v8-coverage": "^1.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/test-sequencer": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz",
+      "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/test-result": "^29.7.0",
+        "graceful-fs": "^4.2.9",
+        "jest-haste-map": "^29.7.0",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/transform": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
+      "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.11.6",
+        "@jest/types": "^29.6.3",
+        "@jridgewell/trace-mapping": "^0.3.18",
+        "babel-plugin-istanbul": "^6.1.1",
+        "chalk": "^4.0.0",
+        "convert-source-map": "^2.0.0",
+        "fast-json-stable-stringify": "^2.1.0",
+        "graceful-fs": "^4.2.9",
+        "jest-haste-map": "^29.7.0",
+        "jest-regex-util": "^29.6.3",
+        "jest-util": "^29.7.0",
+        "micromatch": "^4.0.4",
+        "pirates": "^4.0.4",
+        "slash": "^3.0.0",
+        "write-file-atomic": "^4.0.2"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jest/types": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+      "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/schemas": "^29.6.3",
+        "@types/istanbul-lib-coverage": "^2.0.0",
+        "@types/istanbul-reports": "^3.0.0",
+        "@types/node": "*",
+        "@types/yargs": "^17.0.8",
+        "chalk": "^4.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@sinclair/typebox": {
+      "version": "0.27.8",
+      "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+      "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@sinonjs/commons": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+      "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "type-detect": "4.0.8"
+      }
+    },
+    "node_modules/@sinonjs/fake-timers": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+      "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@sinonjs/commons": "^3.0.0"
+      }
+    },
+    "node_modules/@tootallnate/once": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+      "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
+      "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+      "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.20.7"
+      }
+    },
+    "node_modules/@types/graceful-fs": {
+      "version": "4.1.9",
+      "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
+      "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/istanbul-lib-coverage": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+      "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/istanbul-lib-report": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+      "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/istanbul-lib-coverage": "*"
+      }
+    },
+    "node_modules/@types/istanbul-reports": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+      "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/istanbul-lib-report": "*"
+      }
+    },
+    "node_modules/@types/jsdom": {
+      "version": "20.0.1",
+      "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
+      "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/tough-cookie": "*",
+        "parse5": "^7.0.0"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "22.13.14",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
+      "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.20.0"
+      }
+    },
+    "node_modules/@types/stack-utils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+      "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/tough-cookie": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+      "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/yargs": {
+      "version": "17.0.33",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
+      "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/yargs-parser": "*"
+      }
+    },
+    "node_modules/@types/yargs-parser": {
+      "version": "21.0.3",
+      "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+      "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/abab": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+      "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+      "deprecated": "Use your platform's native atob() and btoa() methods instead",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/acorn": {
+      "version": "8.14.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+      "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-globals": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+      "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.1.0",
+        "acorn-walk": "^8.0.2"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+      "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.21.3"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/babel-jest": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
+      "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/transform": "^29.7.0",
+        "@types/babel__core": "^7.1.14",
+        "babel-plugin-istanbul": "^6.1.1",
+        "babel-preset-jest": "^29.6.3",
+        "chalk": "^4.0.0",
+        "graceful-fs": "^4.2.9",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.8.0"
+      }
+    },
+    "node_modules/babel-plugin-istanbul": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+      "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@istanbuljs/load-nyc-config": "^1.0.0",
+        "@istanbuljs/schema": "^0.1.2",
+        "istanbul-lib-instrument": "^5.0.4",
+        "test-exclude": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+      "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/core": "^7.12.3",
+        "@babel/parser": "^7.14.7",
+        "@istanbuljs/schema": "^0.1.2",
+        "istanbul-lib-coverage": "^3.2.0",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/babel-plugin-jest-hoist": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+      "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.3.3",
+        "@babel/types": "^7.3.3",
+        "@types/babel__core": "^7.1.14",
+        "@types/babel__traverse": "^7.0.6"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/babel-preset-current-node-syntax": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
+      "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/plugin-syntax-async-generators": "^7.8.4",
+        "@babel/plugin-syntax-bigint": "^7.8.3",
+        "@babel/plugin-syntax-class-properties": "^7.12.13",
+        "@babel/plugin-syntax-class-static-block": "^7.14.5",
+        "@babel/plugin-syntax-import-attributes": "^7.24.7",
+        "@babel/plugin-syntax-import-meta": "^7.10.4",
+        "@babel/plugin-syntax-json-strings": "^7.8.3",
+        "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+        "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+        "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+        "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+        "@babel/plugin-syntax-top-level-await": "^7.14.5"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/babel-preset-jest": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+      "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "babel-plugin-jest-hoist": "^29.6.3",
+        "babel-preset-current-node-syntax": "^1.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.24.4",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/bser": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+      "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "node-int64": "^0.4.0"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001707",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
+      "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/char-regex": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+      "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ci-info": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+      "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cjs-module-lexer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+      "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">= 1.0.0",
+        "node": ">= 0.12.0"
+      }
+    },
+    "node_modules/collect-v8-coverage": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
+      "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/create-jest": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
+      "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "chalk": "^4.0.0",
+        "exit": "^0.1.2",
+        "graceful-fs": "^4.2.9",
+        "jest-config": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "prompts": "^2.0.1"
+      },
+      "bin": {
+        "create-jest": "bin/create-jest.js"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssom": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+      "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cssstyle": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+      "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssom": "~0.3.6"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cssstyle/node_modules/cssom": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+      "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/data-urls": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+      "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "abab": "^2.0.6",
+        "whatwg-mimetype": "^3.0.0",
+        "whatwg-url": "^11.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
+      "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/dedent": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
+      "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "babel-plugin-macros": "^3.1.0"
+      },
+      "peerDependenciesMeta": {
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-newline": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+      "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/diff-sequences": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+      "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/domexception": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+      "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "webidl-conversions": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.128",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
+      "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emittery": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
+      "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+      "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/escodegen": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esprima": "^4.0.1",
+        "estraverse": "^5.2.0",
+        "esutils": "^2.0.2"
+      },
+      "bin": {
+        "escodegen": "bin/escodegen.js",
+        "esgenerate": "bin/esgenerate.js"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "optionalDependencies": {
+        "source-map": "~0.6.1"
+      }
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^6.0.0",
+        "human-signals": "^2.1.0",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.1",
+        "onetime": "^5.1.2",
+        "signal-exit": "^3.0.3",
+        "strip-final-newline": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/exit": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+      "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/expect": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+      "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/expect-utils": "^29.7.0",
+        "jest-get-type": "^29.6.3",
+        "jest-matcher-utils": "^29.7.0",
+        "jest-message-util": "^29.7.0",
+        "jest-util": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fb-watchman": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+      "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bser": "2.1.1"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+      "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-package-type": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+      "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/globals": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/html-encoding-sniffer": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+      "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-encoding": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/html-escaper": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tootallnate/once": "2",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.17.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/import-local": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+      "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pkg-dir": "^4.2.0",
+        "resolve-cwd": "^3.0.0"
+      },
+      "bin": {
+        "import-local-fixture": "fixtures/cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-generator-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+      "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/istanbul-lib-coverage": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-instrument": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+      "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/core": "^7.23.9",
+        "@babel/parser": "^7.23.9",
+        "@istanbuljs/schema": "^0.1.3",
+        "istanbul-lib-coverage": "^3.2.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-lib-instrument/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-lib-report": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "istanbul-lib-coverage": "^3.0.0",
+        "make-dir": "^4.0.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-lib-source-maps": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+      "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "istanbul-lib-coverage": "^3.0.0",
+        "source-map": "^0.6.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-reports": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "html-escaper": "^2.0.0",
+        "istanbul-lib-report": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/jest": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
+      "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/core": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "import-local": "^3.0.2",
+        "jest-cli": "^29.7.0"
+      },
+      "bin": {
+        "jest": "bin/jest.js"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "node-notifier": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jest-changed-files": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
+      "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "execa": "^5.0.0",
+        "jest-util": "^29.7.0",
+        "p-limit": "^3.1.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-circus": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz",
+      "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/environment": "^29.7.0",
+        "@jest/expect": "^29.7.0",
+        "@jest/test-result": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "co": "^4.6.0",
+        "dedent": "^1.0.0",
+        "is-generator-fn": "^2.0.0",
+        "jest-each": "^29.7.0",
+        "jest-matcher-utils": "^29.7.0",
+        "jest-message-util": "^29.7.0",
+        "jest-runtime": "^29.7.0",
+        "jest-snapshot": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "p-limit": "^3.1.0",
+        "pretty-format": "^29.7.0",
+        "pure-rand": "^6.0.0",
+        "slash": "^3.0.0",
+        "stack-utils": "^2.0.3"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-cli": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
+      "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/core": "^29.7.0",
+        "@jest/test-result": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "chalk": "^4.0.0",
+        "create-jest": "^29.7.0",
+        "exit": "^0.1.2",
+        "import-local": "^3.0.2",
+        "jest-config": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "jest-validate": "^29.7.0",
+        "yargs": "^17.3.1"
+      },
+      "bin": {
+        "jest": "bin/jest.js"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "node-notifier": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jest-config": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz",
+      "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.11.6",
+        "@jest/test-sequencer": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "babel-jest": "^29.7.0",
+        "chalk": "^4.0.0",
+        "ci-info": "^3.2.0",
+        "deepmerge": "^4.2.2",
+        "glob": "^7.1.3",
+        "graceful-fs": "^4.2.9",
+        "jest-circus": "^29.7.0",
+        "jest-environment-node": "^29.7.0",
+        "jest-get-type": "^29.6.3",
+        "jest-regex-util": "^29.6.3",
+        "jest-resolve": "^29.7.0",
+        "jest-runner": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "jest-validate": "^29.7.0",
+        "micromatch": "^4.0.4",
+        "parse-json": "^5.2.0",
+        "pretty-format": "^29.7.0",
+        "slash": "^3.0.0",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "@types/node": "*",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jest-diff": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+      "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.0.0",
+        "diff-sequences": "^29.6.3",
+        "jest-get-type": "^29.6.3",
+        "pretty-format": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-docblock": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
+      "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "detect-newline": "^3.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-each": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
+      "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "chalk": "^4.0.0",
+        "jest-get-type": "^29.6.3",
+        "jest-util": "^29.7.0",
+        "pretty-format": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-environment-jsdom": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
+      "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/environment": "^29.7.0",
+        "@jest/fake-timers": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/jsdom": "^20.0.0",
+        "@types/node": "*",
+        "jest-mock": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "jsdom": "^20.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "peerDependencies": {
+        "canvas": "^2.5.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jest-environment-node": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
+      "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/environment": "^29.7.0",
+        "@jest/fake-timers": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "jest-mock": "^29.7.0",
+        "jest-util": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-get-type": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
+      "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-haste-map": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
+      "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "@types/graceful-fs": "^4.1.3",
+        "@types/node": "*",
+        "anymatch": "^3.0.3",
+        "fb-watchman": "^2.0.0",
+        "graceful-fs": "^4.2.9",
+        "jest-regex-util": "^29.6.3",
+        "jest-util": "^29.7.0",
+        "jest-worker": "^29.7.0",
+        "micromatch": "^4.0.4",
+        "walker": "^1.0.8"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "^2.3.2"
+      }
+    },
+    "node_modules/jest-leak-detector": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
+      "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "jest-get-type": "^29.6.3",
+        "pretty-format": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-matcher-utils": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+      "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.0.0",
+        "jest-diff": "^29.7.0",
+        "jest-get-type": "^29.6.3",
+        "pretty-format": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-message-util": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
+      "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.12.13",
+        "@jest/types": "^29.6.3",
+        "@types/stack-utils": "^2.0.0",
+        "chalk": "^4.0.0",
+        "graceful-fs": "^4.2.9",
+        "micromatch": "^4.0.4",
+        "pretty-format": "^29.7.0",
+        "slash": "^3.0.0",
+        "stack-utils": "^2.0.3"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-mock": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
+      "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "jest-util": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-pnp-resolver": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+      "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "peerDependencies": {
+        "jest-resolve": "*"
+      },
+      "peerDependenciesMeta": {
+        "jest-resolve": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jest-regex-util": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
+      "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-resolve": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz",
+      "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.0.0",
+        "graceful-fs": "^4.2.9",
+        "jest-haste-map": "^29.7.0",
+        "jest-pnp-resolver": "^1.2.2",
+        "jest-util": "^29.7.0",
+        "jest-validate": "^29.7.0",
+        "resolve": "^1.20.0",
+        "resolve.exports": "^2.0.0",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-resolve-dependencies": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz",
+      "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "jest-regex-util": "^29.6.3",
+        "jest-snapshot": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-runner": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz",
+      "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/console": "^29.7.0",
+        "@jest/environment": "^29.7.0",
+        "@jest/test-result": "^29.7.0",
+        "@jest/transform": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "emittery": "^0.13.1",
+        "graceful-fs": "^4.2.9",
+        "jest-docblock": "^29.7.0",
+        "jest-environment-node": "^29.7.0",
+        "jest-haste-map": "^29.7.0",
+        "jest-leak-detector": "^29.7.0",
+        "jest-message-util": "^29.7.0",
+        "jest-resolve": "^29.7.0",
+        "jest-runtime": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "jest-watcher": "^29.7.0",
+        "jest-worker": "^29.7.0",
+        "p-limit": "^3.1.0",
+        "source-map-support": "0.5.13"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-runtime": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
+      "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/environment": "^29.7.0",
+        "@jest/fake-timers": "^29.7.0",
+        "@jest/globals": "^29.7.0",
+        "@jest/source-map": "^29.6.3",
+        "@jest/test-result": "^29.7.0",
+        "@jest/transform": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "cjs-module-lexer": "^1.0.0",
+        "collect-v8-coverage": "^1.0.0",
+        "glob": "^7.1.3",
+        "graceful-fs": "^4.2.9",
+        "jest-haste-map": "^29.7.0",
+        "jest-message-util": "^29.7.0",
+        "jest-mock": "^29.7.0",
+        "jest-regex-util": "^29.6.3",
+        "jest-resolve": "^29.7.0",
+        "jest-snapshot": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "slash": "^3.0.0",
+        "strip-bom": "^4.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-snapshot": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+      "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.11.6",
+        "@babel/generator": "^7.7.2",
+        "@babel/plugin-syntax-jsx": "^7.7.2",
+        "@babel/plugin-syntax-typescript": "^7.7.2",
+        "@babel/types": "^7.3.3",
+        "@jest/expect-utils": "^29.7.0",
+        "@jest/transform": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "babel-preset-current-node-syntax": "^1.0.0",
+        "chalk": "^4.0.0",
+        "expect": "^29.7.0",
+        "graceful-fs": "^4.2.9",
+        "jest-diff": "^29.7.0",
+        "jest-get-type": "^29.6.3",
+        "jest-matcher-utils": "^29.7.0",
+        "jest-message-util": "^29.7.0",
+        "jest-util": "^29.7.0",
+        "natural-compare": "^1.4.0",
+        "pretty-format": "^29.7.0",
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-snapshot/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/jest-util": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
+      "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "chalk": "^4.0.0",
+        "ci-info": "^3.2.0",
+        "graceful-fs": "^4.2.9",
+        "picomatch": "^2.2.3"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-validate": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
+      "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/types": "^29.6.3",
+        "camelcase": "^6.2.0",
+        "chalk": "^4.0.0",
+        "jest-get-type": "^29.6.3",
+        "leven": "^3.1.0",
+        "pretty-format": "^29.7.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-validate/node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/jest-watcher": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz",
+      "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/test-result": "^29.7.0",
+        "@jest/types": "^29.6.3",
+        "@types/node": "*",
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^4.0.0",
+        "emittery": "^0.13.1",
+        "jest-util": "^29.7.0",
+        "string-length": "^4.0.1"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
+      "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "jest-util": "^29.7.0",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/jest-worker/node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/jquery": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+      "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsdom": {
+      "version": "20.0.3",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
+      "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "abab": "^2.0.6",
+        "acorn": "^8.8.1",
+        "acorn-globals": "^7.0.0",
+        "cssom": "^0.5.0",
+        "cssstyle": "^2.3.0",
+        "data-urls": "^3.0.2",
+        "decimal.js": "^10.4.2",
+        "domexception": "^4.0.0",
+        "escodegen": "^2.0.0",
+        "form-data": "^4.0.0",
+        "html-encoding-sniffer": "^3.0.0",
+        "http-proxy-agent": "^5.0.0",
+        "https-proxy-agent": "^5.0.1",
+        "is-potential-custom-element-name": "^1.0.1",
+        "nwsapi": "^2.2.2",
+        "parse5": "^7.1.1",
+        "saxes": "^6.0.0",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^4.1.2",
+        "w3c-xmlserializer": "^4.0.0",
+        "webidl-conversions": "^7.0.0",
+        "whatwg-encoding": "^2.0.0",
+        "whatwg-mimetype": "^3.0.0",
+        "whatwg-url": "^11.0.0",
+        "ws": "^8.11.0",
+        "xml-name-validator": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "canvas": "^2.5.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/kleur": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+      "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/leven": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+      "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/make-dir": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/makeerror": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+      "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tmpl": "1.0.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-int64": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+      "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/nwsapi": {
+      "version": "2.2.20",
+      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
+      "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-locate/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
+      "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^4.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pretty-format": {
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+      "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jest/schemas": "^29.6.3",
+        "ansi-styles": "^5.0.0",
+        "react-is": "^18.0.0"
+      },
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/pretty-format/node_modules/ansi-styles": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+      "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/prompts": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+      "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^3.0.3",
+        "sisteransi": "^1.0.5"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/psl": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+      "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/lupomontero"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pure-rand": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+      "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/querystringify": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-cwd": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/resolve.exports": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
+      "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/saxes": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "xmlchars": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=v12.22.7"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/sisteransi": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+      "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.13",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+      "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/stack-utils": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+      "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/string-length": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+      "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "char-regex": "^1.0.2",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-bom": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+      "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/symbol-tree": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/test-exclude": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+      "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@istanbuljs/schema": "^0.1.2",
+        "glob": "^7.1.4",
+        "minimatch": "^3.0.4"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tmpl": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+      "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/tough-cookie": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+      "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "psl": "^1.1.33",
+        "punycode": "^2.1.1",
+        "universalify": "^0.2.0",
+        "url-parse": "^1.5.3"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+      "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.21.3",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/universalify": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+      "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/url-parse": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "querystringify": "^2.1.1",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "node_modules/v8-to-istanbul": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+      "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.12",
+        "@types/istanbul-lib-coverage": "^2.0.1",
+        "convert-source-map": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10.12.0"
+      }
+    },
+    "node_modules/w3c-xmlserializer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+      "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "xml-name-validator": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/walker": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+      "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "makeerror": "1.0.12"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/whatwg-encoding": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+      "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "iconv-lite": "0.6.3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+      "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+      "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "^3.0.0",
+        "webidl-conversions": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/write-file-atomic": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+      "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "imurmurhash": "^0.1.4",
+        "signal-exit": "^3.0.7"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9d654b1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "coursework",
+  "version": "1.0.0",
+  "description": "Change the name of the repo when we get a better group name.",
+  "main": "index.js",
+  "directories": {
+    "test": "tests"
+  },
+  "scripts": {
+    "test": "jest"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://gitlab.surrey.ac.uk/web-tech/coursework"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "devDependencies": {
+    "jest": "^29.7.0",
+    "jest-environment-jsdom": "^29.7.0",
+    "jquery": "^3.7.1"
+  },
+  "jest": {
+    "testEnvironment": "jsdom"
+  },
+  "type": "module"
+}
diff --git a/tests/sendDates.test.js b/tests/sendDates.test.js
new file mode 100644
index 0000000..bf7a548
--- /dev/null
+++ b/tests/sendDates.test.js
@@ -0,0 +1,44 @@
+//const $ = require('jquery');
+
+
+// Mock jQuery AJAX request
+//global.$ = jest.fn(() => ({
+  //  ajax: jest.fn().mockResolvedValue({}) 
+//}));
+
+// Mock alert function to prevent actual alerts in tests
+global.alert = jest.fn();
+
+// Mock jQuery AJAX request
+global.$ = {
+    ajax: jest.fn().mockResolvedValue({})
+};
+
+const { sendDates } = require('../frontend_microservice/static/script')
+
+
+describe("sendDates function", () => {
+    beforeEach(() => {
+        document.body.innerHTML = `
+            <input type="text" id="event_name">
+            <button id="create_event_button" onclick="sendDates()">Create Event</button>
+            <div id="title">Create Event</div>
+        `;
+
+    });
+
+    test('should alert if event name is empty', () => {
+        document.getElementById('event_name').value = '';
+        sendDates();
+
+        expect(alert).toHaveBeenCalledWith("Event name cannot be empty.");
+    });
+
+    test('should proceed with a valid event name', () => {
+        document.getElementById('event_name').value = 'Sample Event';
+        sendDates();
+
+        expect($.ajax).toHaveBeenCalled();
+    });
+});
+
-- 
GitLab


From 93c85316df0537fd7afd1aa7fd02ca8710bfe4b9 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 4 Apr 2025 19:53:11 +0100
Subject: [PATCH 003/124] add account events page

---
 frontend_microservice/static/script.js        | 13 ++++++++++
 .../templates/accountEvents.html              | 25 +++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 frontend_microservice/templates/accountEvents.html

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 41c9ec4..139ea4d 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -351,4 +351,17 @@ function new_list_element(availabilities, dates){
     };
 }
 
+function show_account_events(events, url){
+    event_block = document.getElementById('event_block')
+    for (const event in events){
+        var button = document.createElement("button");
+        button.classList.add('show_event')
+        button.textContent = event['name'];
+        //TODO: get url, probably able to get it from GET
+        button.onclick = "window.location.href = {{url}}/{{event['id']}}"
+        event_block.append(button)
+    }
+}
+
+
   
diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
new file mode 100644
index 0000000..87fdc55
--- /dev/null
+++ b/frontend_microservice/templates/accountEvents.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+        <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
+        <script>
+            const events = {{event | tojson}};
+            const url = {{summary_url | tojson}};
+        </script>
+    </head>
+    <body>
+        <div class="centered">
+            <div class="card">
+                <h1>These are your events</h1>
+                <h3>Click to go see the details</h3>
+                <div class="card internal">
+                    <div id="event_block">
+                    </div>
+                </div>
+            </div>
+        </div>
+    </body>
+    <script src="{{ url_for('static', filename='script.js') }}"></script>
+    <script>show_account_events(events,url)</script>
+</html>
\ No newline at end of file
-- 
GitLab


From b52ea8c177465a2f9a969041f4008427e0bda00a Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 4 Apr 2025 20:54:37 +0100
Subject: [PATCH 004/124] add backend and new table

---
 account_database/schema.sql                   |  9 +++++++-
 account_microservice/user.py                  | 18 ++++++++++++++++
 frontend_microservice/frontend.py             | 21 +++++++++++++++++++
 frontend_microservice/static/script.js        | 12 ++++++-----
 .../templates/accountEvents.html              |  3 +--
 5 files changed, 55 insertions(+), 8 deletions(-)

diff --git a/account_database/schema.sql b/account_database/schema.sql
index 44f6ef1..577b49b 100644
--- a/account_database/schema.sql
+++ b/account_database/schema.sql
@@ -3,4 +3,11 @@ CREATE TABLE IF NOT EXISTS users (
     email VARCHAR(255) UNIQUE NOT NULL,
     password_hash VARCHAR(255) NOT NULL,
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
\ No newline at end of file
+);
+
+CREATE TABLE IF NOT EXISTS account_events{
+    user_id INT NOT NULL,
+    event_id INT,
+    PRIMARY KEY (user_id, event_id),
+    FOREIGN KEY (user_id) REFERENCES users(user_id)
+};
\ No newline at end of file
diff --git a/account_microservice/user.py b/account_microservice/user.py
index 5b49c91..15c86d5 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -124,6 +124,24 @@ def login():
         logging.error(f"Login error: {str(e)}", exc_info=True)
         return jsonify({"error": "Login failed"}), 500
 
+@app.route('/get_events', methods=['GET'])
+def get_events():
+    data = request.get_json()
+
+    account_id = data['account_id']
+
+    conn = get_db_connection()
+    cursor = conn.cursor()
+    cursor.execute('SELECT event_id FROM account_events WHERE user_id = %s', (account_id,))
+    ids = cursor.fetchall()
+
+    event_ids = [event_id[0] for event_id in ids]
+
+    cursor.close()
+    conn.close()
+
+    return jsonify({'event_ids':event_ids}), 201
+
 @app.errorhandler(429)
 def ratelimit_handler(e):
     return jsonify({"error": "Too many requests"}), 429
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 55d2462..7ae2f69 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -135,6 +135,27 @@ def getDatesp():
         return data_output
     return render_template('eventSummary.html') 
 
+@app.route('/account/<int: account_id>', methods=['GET'])
+def get_events(account_id):
+    if request.method == 'GET':
+        data = {'account_id':account_id}
+        account_response = requests.get(f"{USER_API_URL}/get_events", json = data)
+        event_ids = account_response.json() if account_response.status_code == 200 else []
+        
+        events = []
+
+        for event_id in event_ids:
+            event ={}
+            data_input = {"id":event_id, "to_return": ['event_name']}
+            response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
+            data_output = response.json() if response.status_code == 200 else []
+            event['name'] = data_output['name']
+            event['url'] = url_for('availability', event_id=event_id)
+            events.append(event)
+                    
+        return render_template('accountEvents.html', event=events)  
+    return render_template('accountEvents.html')
+
 
 
 if __name__ == "__main__":
diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 139ea4d..036749c 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -351,15 +351,17 @@ function new_list_element(availabilities, dates){
     };
 }
 
-function show_account_events(events, url){
+function show_account_events(events){
     event_block = document.getElementById('event_block')
+
     for (const event in events){
         var button = document.createElement("button");
-        button.classList.add('show_event')
+        
+        button.classList.add('show_event');
         button.textContent = event['name'];
-        //TODO: get url, probably able to get it from GET
-        button.onclick = "window.location.href = {{url}}/{{event['id']}}"
-        event_block.append(button)
+        button.onclick = "window.location.href = {{event[url]}}";
+
+        event_block.append(button);
     }
 }
 
diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
index 87fdc55..805b163 100644
--- a/frontend_microservice/templates/accountEvents.html
+++ b/frontend_microservice/templates/accountEvents.html
@@ -5,7 +5,6 @@
         <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
         <script>
             const events = {{event | tojson}};
-            const url = {{summary_url | tojson}};
         </script>
     </head>
     <body>
@@ -21,5 +20,5 @@
         </div>
     </body>
     <script src="{{ url_for('static', filename='script.js') }}"></script>
-    <script>show_account_events(events,url)</script>
+    <script>show_account_events(events)</script>
 </html>
\ No newline at end of file
-- 
GitLab


From 46c1282247c029b071c35ed345fc83a57bbf54a2 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 4 Apr 2025 22:29:10 +0100
Subject: [PATCH 005/124] fix frontend of account events

---
 account_database/schema.sql                       |  2 +-
 frontend_microservice/frontend.py                 | 10 +++++++---
 frontend_microservice/static/script.js            | 15 +++++++++++----
 .../templates/accountEvents.html                  |  2 +-
 4 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/account_database/schema.sql b/account_database/schema.sql
index 577b49b..c961c3f 100644
--- a/account_database/schema.sql
+++ b/account_database/schema.sql
@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS users (
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );
 
-CREATE TABLE IF NOT EXISTS account_events{
+CREATE TABLE IF NOT EXISTS account_events {
     user_id INT NOT NULL,
     event_id INT,
     PRIMARY KEY (user_id, event_id),
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 7ae2f69..ce0305f 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -135,7 +135,7 @@ def getDatesp():
         return data_output
     return render_template('eventSummary.html') 
 
-@app.route('/account/<int: account_id>', methods=['GET'])
+@app.route('/account/<int:account_id>', methods=['GET'])
 def get_events(account_id):
     if request.method == 'GET':
         data = {'account_id':account_id}
@@ -150,9 +150,13 @@ def get_events(account_id):
             response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
             data_output = response.json() if response.status_code == 200 else []
             event['name'] = data_output['name']
-            event['url'] = url_for('availability', event_id=event_id)
+            event['url'] = url_for('eventSummary', event_id=event_id)
             events.append(event)
-                    
+
+        #Test data
+        event1= {'name':'An event','url':url_for('eventSummary', event_id=1)}
+        event2= {'name':'An event 2','url':'a url'}       
+        events = [event1, event2]
         return render_template('accountEvents.html', event=events)  
     return render_template('accountEvents.html')
 
diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 036749c..290c82f 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -353,13 +353,20 @@ function new_list_element(availabilities, dates){
 
 function show_account_events(events){
     event_block = document.getElementById('event_block')
-
-    for (const event in events){
+    console.log(events)
+    for (i=0;i<events.length;i++){
+        event_i=events[i]
         var button = document.createElement("button");
         
         button.classList.add('show_event');
-        button.textContent = event['name'];
-        button.onclick = "window.location.href = {{event[url]}}";
+        button.textContent = event_i['name'];
+        console.log(event_i['url'])
+        button.onclick = button.onclick = function(event_i) {
+            return function() {
+                console.log('button pressed')
+                window.location.href = event_i['url'];
+            };
+        }(event_i);
 
         event_block.append(button);
     }
diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
index 805b163..6e1202f 100644
--- a/frontend_microservice/templates/accountEvents.html
+++ b/frontend_microservice/templates/accountEvents.html
@@ -11,7 +11,7 @@
         <div class="centered">
             <div class="card">
                 <h1>These are your events</h1>
-                <h3>Click to go see the details</h3>
+                <h3>Click to see the details</h3>
                 <div class="card internal">
                     <div id="event_block">
                     </div>
-- 
GitLab


From 7d884a0e0017d941d3a736af307bb7dd38be8310 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 4 Apr 2025 23:54:40 +0100
Subject: [PATCH 006/124] account events page works

---
 account_database/schema.sql       | 4 ++--
 frontend_microservice/frontend.py | 3 ++-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/account_database/schema.sql b/account_database/schema.sql
index c961c3f..2b71ebc 100644
--- a/account_database/schema.sql
+++ b/account_database/schema.sql
@@ -5,9 +5,9 @@ CREATE TABLE IF NOT EXISTS users (
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );
 
-CREATE TABLE IF NOT EXISTS account_events {
+CREATE TABLE IF NOT EXISTS account_events (
     user_id INT NOT NULL,
     event_id INT,
     PRIMARY KEY (user_id, event_id),
     FOREIGN KEY (user_id) REFERENCES users(user_id)
-};
\ No newline at end of file
+);
\ No newline at end of file
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index ce0305f..886aa30 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -152,11 +152,12 @@ def get_events(account_id):
             event['name'] = data_output['name']
             event['url'] = url_for('eventSummary', event_id=event_id)
             events.append(event)
-
+        '''
         #Test data
         event1= {'name':'An event','url':url_for('eventSummary', event_id=1)}
         event2= {'name':'An event 2','url':'a url'}       
         events = [event1, event2]
+        '''
         return render_template('accountEvents.html', event=events)  
     return render_template('accountEvents.html')
 
-- 
GitLab


From 5b3f1dce14de50ab0ac9ee2e3a1766477706896b Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Wed, 9 Apr 2025 23:34:01 +0100
Subject: [PATCH 007/124] changed password lenght to 8 char

---
 account_microservice/user.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 15c86d5..3d454cf 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -46,8 +46,8 @@ def get_db_connection():
     return conn
 
 def validate_password(password):
-    if len(password) < 12:
-        return "Password must be at least 12 characters long"
+    if len(password) < 8:
+        return "Password must be at least 8 characters long"
     if not re.search(r"[A-Z]", password):
         return "Password must contain at least one uppercase letter"
     if not re.search(r"[a-z]", password):
-- 
GitLab


From 40f673e4d9db8ba9489af6bfeb0089f6647cef34 Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Wed, 9 Apr 2025 23:34:52 +0100
Subject: [PATCH 008/124] added account page function

---
 frontend_microservice/frontend.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 886aa30..92352d1 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -161,7 +161,10 @@ def get_events(account_id):
         return render_template('accountEvents.html', event=events)  
     return render_template('accountEvents.html')
 
-
+@app.route('/account', methods=['GET'])
+def account():
+    """User account page where they can see their profile and logout"""
+    return render_template('account.html')
 
 if __name__ == "__main__":
     # checks for hostname to see if running in containers then sets host appropriately
-- 
GitLab


From 4cb8c2f3bbcd894b208807f45f04ba4ef31ff252 Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Wed, 9 Apr 2025 23:35:49 +0100
Subject: [PATCH 009/124] added login status nav

---
 frontend_microservice/static/auth.js | 30 ++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/frontend_microservice/static/auth.js b/frontend_microservice/static/auth.js
index df88016..4fe3816 100644
--- a/frontend_microservice/static/auth.js
+++ b/frontend_microservice/static/auth.js
@@ -1,4 +1,24 @@
 document.addEventListener('DOMContentLoaded', () => {
+    // Function to update navigation based on login status
+    function updateNavigation() {
+        const userId = localStorage.getItem('user_id');
+        const loginLink = document.getElementById('login-link');
+        const accountLink = document.getElementById('account-link');
+        
+        if (userId && loginLink && accountLink) {
+            // User is logged in
+            loginLink.style.display = 'none';
+            accountLink.style.display = 'inline';
+        } else if (loginLink && accountLink) {
+            // User is not logged in
+            loginLink.style.display = 'inline';
+            accountLink.style.display = 'none';
+        }
+    }
+
+    // Run navigation update on every page load
+    updateNavigation();
+
     // Registration form handler
     if (document.getElementById('registerForm')) {
         document.getElementById('registerForm').addEventListener('submit', async (e) => {
@@ -63,4 +83,14 @@ document.addEventListener('DOMContentLoaded', () => {
             }
         });
     }
+
+    // Logout button handler
+    const logoutButton = document.getElementById('logout-button');
+    if (logoutButton) {
+        logoutButton.addEventListener('click', () => {
+            localStorage.removeItem('user_id');
+            alert('Logged out successfully!');
+            window.location.href = '/';
+        });
+    }
 });
\ No newline at end of file
-- 
GitLab


From ce4688f068aca1898592b58f4abd876cfd1f2592 Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Wed, 9 Apr 2025 23:36:50 +0100
Subject: [PATCH 010/124] added user account page

---
 frontend_microservice/static/account.js       | 30 +++++++++++++++++++
 frontend_microservice/templates/account.html  | 30 +++++++++++++++++++
 frontend_microservice/templates/index.html    |  4 ++-
 frontend_microservice/templates/register.html |  2 +-
 4 files changed, 64 insertions(+), 2 deletions(-)
 create mode 100644 frontend_microservice/static/account.js
 create mode 100644 frontend_microservice/templates/account.html

diff --git a/frontend_microservice/static/account.js b/frontend_microservice/static/account.js
new file mode 100644
index 0000000..bb665fb
--- /dev/null
+++ b/frontend_microservice/static/account.js
@@ -0,0 +1,30 @@
+document.addEventListener('DOMContentLoaded', () => {
+    const userStatusDiv = document.getElementById('user-status');
+    const logoutButton = document.getElementById('logout-button');
+    const accountEventsLink = document.getElementById('account-events-link');
+    
+    // Check if user is logged in
+    const userId = localStorage.getItem('user_id');
+    
+    if (userId) {
+        userStatusDiv.textContent = `Logged in (User ID: ${userId})`;
+        
+        accountEventsLink.href = `/account/${userId}`;
+        
+        if (logoutButton) {
+            logoutButton.style.display = 'block';
+        }
+    } else {
+        userStatusDiv.textContent = 'You are not logged in';
+        
+        if (logoutButton) {
+            logoutButton.style.display = 'none';
+        }
+        
+        accountEventsLink.href = '/login';
+        accountEventsLink.textContent = 'Login to see your events';
+        
+        // Redirect to login page 
+        window.location.href = '/login';
+    }
+});
\ No newline at end of file
diff --git a/frontend_microservice/templates/account.html b/frontend_microservice/templates/account.html
new file mode 100644
index 0000000..8a8a10c
--- /dev/null
+++ b/frontend_microservice/templates/account.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+        <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
+        <script src="{{ url_for('static', filename='auth.js') }}"></script>
+    </head>
+    <body>
+        <nav class="top-nav">
+            <div class="auth-links">
+                <a href="/login" id="login-link">Login/Signup</a>
+                <a href="/account" id="account-link" style="display: none;">My Account</a>
+            </div>
+        </nav>
+        <div class="centered">
+            <div class="card">
+                <h1>My Account</h1>
+                <div id="user-status">
+                </div>
+                <div class="card internal">
+                    <a id="account-events-link" href="/accountEvents">My Events</a>
+                </div>
+                <div class="card internal">
+                    <button id="logout-button">Logout</button>
+                </div>
+            </div>
+        </div>
+        <script src="{{ url_for('static', filename='account.js') }}"></script>
+    </body>
+</html>
\ No newline at end of file
diff --git a/frontend_microservice/templates/index.html b/frontend_microservice/templates/index.html
index 67b7106..695e900 100644
--- a/frontend_microservice/templates/index.html
+++ b/frontend_microservice/templates/index.html
@@ -3,11 +3,13 @@
     <head>
         <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
         <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
+        <script src="{{ url_for('static', filename='auth.js') }}"></script>
     </head>
     <body>
         <nav class="top-nav">
             <div class="auth-links">
-                <a href="/login"> Login/Signup </a>
+                <a href="/login" id="login-link">Login/Signup</a>
+                <a href="/account" id="account-link" style="display: none;">My Account</a>
             </div>
         </nav>
         <div class="centered">
diff --git a/frontend_microservice/templates/register.html b/frontend_microservice/templates/register.html
index 06a532f..ef1d239 100644
--- a/frontend_microservice/templates/register.html
+++ b/frontend_microservice/templates/register.html
@@ -16,7 +16,7 @@
             <div class="form-group">
                 <label>Password:</label>
                 <input type="password" id="password" required>
-                <small>Must be at least 12 characters with uppercase, lowercase, and numbers</small>
+                <small>Must be at least 8 characters with uppercase, lowercase, and numbers</small>
             </div>
             <button type="submit">Register</button>
         </form>
-- 
GitLab


From 8560fc6f6a8f1a4e1b57e5c37507852076f9fa4c Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Thu, 10 Apr 2025 09:45:21 +0100
Subject: [PATCH 011/124] add get_best_dates to nginx

---
 nginx/nginx.conf | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 2d93af0..ced0533 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -32,4 +32,8 @@ server {
     location /login {
         proxy_pass http://account_service/login;
     }
+
+    location /get_best_dates {
+        proxy_pass http://event_service/get_best_dates;
+    }
 }
\ No newline at end of file
-- 
GitLab


From 32085265adfdc64a4f3cddf778106ab13d5ebd64 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Thu, 10 Apr 2025 09:46:00 +0100
Subject: [PATCH 012/124] add correct route between pages

---
 frontend_microservice/frontend.py                 | 8 ++++----
 frontend_microservice/templates/eventSummary.html | 2 ++
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 55d2462..42692b9 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -24,7 +24,7 @@ def create_event():
         data_output = response.json() if response.status_code == 201 else []
         event_id=data_output['event_id']
         if response.status_code == 201:  # Check if event was successfully createds
-            redirect_url = url_for('share_link', event_id=event_id)
+            redirect_url = url_for('eventSummary', event_id=event_id)
             #print(redirect_url)
             return jsonify({'url': redirect_url})
         else:
@@ -81,7 +81,7 @@ def availability(event_id):
         response = requests.post(f"{EVENT_API_URL}/add_availabilities", json=data)
 
         if response.status_code == 201:  # Check if was added successfully
-            return jsonify({'url': url_for('index')})
+            return jsonify({'url': url_for('eventSummary', event_id=event_id)})
         else:
             return f"Error: {response.text}", response.status_code
 
@@ -114,8 +114,8 @@ def share_link(event_id):
         response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
         data_output = response.json() if response.status_code == 200 else []
         event_name = data_output['name']
-        link = f"http://127.0.0.1:8000/availability/{event_id}"
-        redirect_url = url_for('availability', event_id=event_id)
+        link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
+        redirect_url = url_for('eventSummary', event_id=event_id)
         return render_template('share_link.html', event=event_name, link=link,url=redirect_url)
     return render_template('share_link.html')   
 
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 531cf5a..ea8a641 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -15,6 +15,7 @@
     </script>
 
     <h1>'{{ event }}' Summary</h1>
+    <a href="/share_link/{{ id }}"> Copy the event link </a>
     <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
 
     <div id="best_dates">
@@ -25,6 +26,7 @@
 
     <div id="summary_block" class="card">
         <h2>Availabilities</h2>
+        <a href="/availability/{{ id }}"> Enter your availabilities </a>
     </div>
 
 
-- 
GitLab


From e81aeada9c3a70e8e74c9aec04f5d4f83771a603 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Thu, 10 Apr 2025 10:33:59 +0100
Subject: [PATCH 013/124] fix repeating best dates

---
 frontend_microservice/static/script.js | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 41c9ec4..0cbe55a 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -244,13 +244,19 @@ function getDates(availabilities){
     console.log("button pressed");
     // Create card for best date section 
     var date_area = document.getElementById("best_dates");
-    date_area.classList.add("card")
+    date_area.classList.add("card");
+
+    var container = document.getElementById("date_container");
+    container.innerHTML = "";
+    all_old_div = container.querySelectorAll('div');
+    for (let i = 0; index < all_old_div.length; index++) {
+        const div_element = all_old_div[i];
+        div_element.remove();
+    };
 
     // Create a heading for that section
-    var date_heading = document.getElementById("date_heading")
-    var new_heading = document.createElement("h2");
-    new_heading.textContent = "CALCULATING BEST DATES"
-    date_heading.appendChild(new_heading);
+    var date_heading = document.getElementById("date_heading");
+    date_heading.textContent = "CALCULATING BEST DATES";
 
     // Start request for information
     $.ajax({
@@ -260,10 +266,9 @@ function getDates(availabilities){
         data: JSON.stringify({'availabilities': availabilities}),
         success: function(response) {
             // Format the section in which the dates will be listed
-            var container = document.getElementById("date_container");
             // https://www.w3schools.com/jsref/prop_element_classlist.asp
             container.classList.add("columns");
-            new_heading.textContent = "POSSIBLE DATES"
+            date_heading.textContent = "POSSIBLE DATES"
 
             // Loop through the response and add new div elements for each date
             for (const date in response){
-- 
GitLab


From b2edd5c2d10dac762e3ea5cad1680ac96ba8b075 Mon Sep 17 00:00:00 2001
From: "Trieu, Hoang Hiep (PG/T - Comp Sci & Elec Eng)" <ht00865@surrey.ac.uk>
Date: Fri, 11 Apr 2025 10:48:53 +0000
Subject: [PATCH 014/124] add load balancer on datefinder

---
 docker-compose.yml            | 17 +++++++++++++++--
 event_microservice/backend.py |  2 +-
 nginx/nginx.conf              | 10 ++++++++++
 3 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index ca47ec1..e1cb407 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -53,6 +53,7 @@ services:
       depends_on:
       - event_service
       - account_service
+      - datefinder
 
   frontend:
     build:
@@ -70,10 +71,22 @@ services:
     build:
       context: datefinder_microservice
       dockerfile: Dockerfile-datefinder
-    ports:
-      - "6000:6000"
+    #ports:
+    #  - "6000:6000"
     depends_on:
       - postgres
+    deploy:
+      replicas: 2
+      restart_policy:
+        condition: on-failure
+        delay: 1s
+        max_attempts: 3
+        window: 120s
+    networks:
+      - backend
+      - loadbalancing
+    environment:
+      - PORT=6000
 
   postgres:
     build:
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index 6c377f6..f744405 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -6,7 +6,7 @@ import os
 
 #TODO make sure the port is correct
 #Comment
-ALGORITHM_PORT = 6000
+ALGORITHM_PORT = 80
 EVENT_DATABASE_PORT = 5436
 PORT = 5002
 
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 2d93af0..3a5638c 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -6,6 +6,10 @@ upstream account_service {
     server account_service:5003 ;    
 }
 
+upstream datefinder {
+    server datefinder:6000 ;    
+}
+
 server {
     listen 80;
 
@@ -32,4 +36,10 @@ server {
     location /login {
         proxy_pass http://account_service/login;
     }
+    location /get_best_dates {
+        proxy_pass http://event_service/get_best_dates;
+    }
+    location /calculate {
+        proxy_pass http://datefinder/calculate;
+    }
 }
\ No newline at end of file
-- 
GitLab


From ed91587b5a4352ed8f6f3bd5ed1cae425147dd7d Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Fri, 11 Apr 2025 12:14:01 +0100
Subject: [PATCH 015/124] fix duplicating dates

---
 frontend_microservice/static/script.js | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 0cbe55a..01d25c4 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -247,11 +247,11 @@ function getDates(availabilities){
     date_area.classList.add("card");
 
     var container = document.getElementById("date_container");
-    container.innerHTML = "";
+    //container.innerHTML = "";
     all_old_div = container.querySelectorAll('div');
-    for (let i = 0; index < all_old_div.length; index++) {
-        const div_element = all_old_div[i];
-        div_element.remove();
+    for (let i = 0; i < all_old_div.length; i++) {
+       const div_element = all_old_div[i];
+       div_element.remove();
     };
 
     // Create a heading for that section
@@ -268,7 +268,7 @@ function getDates(availabilities){
             // Format the section in which the dates will be listed
             // https://www.w3schools.com/jsref/prop_element_classlist.asp
             container.classList.add("columns");
-            date_heading.textContent = "POSSIBLE DATES"
+            date_heading.textContent = "POSSIBLE DATES";
 
             // Loop through the response and add new div elements for each date
             for (const date in response){
-- 
GitLab


From 95d7c2d1a6113d9fccde9f5b444a362511f3ecec Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Fri, 11 Apr 2025 12:49:11 +0100
Subject: [PATCH 016/124] move copy link to event summary

---
 frontend_microservice/frontend.py             |  4 ++-
 .../templates/eventSummary.html               | 30 +++++++++++++++++++
 .../templates/share_link.html                 |  2 ++
 3 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 55d2462..07434d2 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -103,8 +103,10 @@ def eventSummary(event_id):
         dates = data_output['event_possible_dates']
         availabilities = data_output['availabilities']
 
+        link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
+
         # TODO: fix how this is being returned
-        return render_template('eventSummary.html', id=event_id, event=event_name, dates=dates, availabilities=availabilities)
+        return render_template('eventSummary.html', id=event_id, link=link, event=event_name, dates=dates, availabilities=availabilities)
     return render_template('eventSummary.html')    
 
 @app.route('/share_link/<int:event_id>', methods =['GET'])
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 531cf5a..668001b 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -15,6 +15,36 @@
     </script>
 
     <h1>'{{ event }}' Summary</h1>
+
+    <div class="centered">
+        <div class="card">
+            <h2>Here's the link to share for {{event}}</h2>
+            <div class="link-input-button">
+                <input type="text" class="link-input" id="link" value="{{link}}" readonly>
+                <button class="copy" onclick="copyToClipboard()">Copy</button>
+            </div>
+        </div>
+    </div>
+    
+
+    <script>
+        function copyToClipboard() {
+          var copyText = document.getElementById("link");
+    
+          copyText.select();
+          copyText.setSelectionRange(0, 99999); // For mobile devices
+    
+          // Use the Clipboard API
+          navigator.clipboard.writeText(copyText.value)
+            .then(function() {
+              alert("Text copied to clipboard!");
+            })
+            .catch(function(error) {
+              alert("Failed to copy text: " + error);
+            });
+        }
+    </script>
+
     <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
 
     <div id="best_dates">
diff --git a/frontend_microservice/templates/share_link.html b/frontend_microservice/templates/share_link.html
index ef7cf17..a340718 100644
--- a/frontend_microservice/templates/share_link.html
+++ b/frontend_microservice/templates/share_link.html
@@ -7,11 +7,13 @@
     </head>
     <body>
         <div class="centered">
+            <div>
             <h2>Here's the link to share for {{event}}</h2>
             <div class="link-input-button">
                 <input type="text" class="link-input" id="link" value="{{link}}" readonly>
                 <button class="copy" onclick="copyToClipboard()">Copy</button>
             </div>
+            </div>
             <a href="{{url}}">Go to link</a>
         </div>
         
-- 
GitLab


From d9756ac627c950ce50c7ad3c36dc613c1beadc3b Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Fri, 11 Apr 2025 12:49:18 +0100
Subject: [PATCH 017/124] style copy link

---
 frontend_microservice/static/styles.css | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/frontend_microservice/static/styles.css b/frontend_microservice/static/styles.css
index 6e88f0a..b651266 100644
--- a/frontend_microservice/static/styles.css
+++ b/frontend_microservice/static/styles.css
@@ -37,6 +37,7 @@ button:active{
 }
 
 .copy{
+    width: 20%;
     background-color: lightblue;
     color: black
 }
@@ -57,11 +58,13 @@ input{
 }
 
 .link-input {
-    width: 50%; 
-    max-width: 600px;
+    width: 100%; 
+    font-size: small;
 }
 
 .link-input-button {
+    display: flex;
+    flex-direction: row;
     width: 100%; 
     max-width: 600px;
 }
-- 
GitLab


From e6e22a2220be4b303508a0286ecd565d48d607fa Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 11 Apr 2025 16:22:33 +0100
Subject: [PATCH 018/124] see events on page

---
 account_microservice/user.py                  | 36 ++++++++--
 frontend_microservice/frontend.py             | 70 +++++++++++--------
 frontend_microservice/static/auth.js          |  2 +-
 frontend_microservice/static/script.js        | 23 +++++-
 .../templates/accountEvents.html              |  1 +
 .../templates/eventSummary.html               |  2 +
 6 files changed, 96 insertions(+), 38 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 3d454cf..40abd0e 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -124,23 +124,45 @@ def login():
         logging.error(f"Login error: {str(e)}", exc_info=True)
         return jsonify({"error": "Login failed"}), 500
 
-@app.route('/get_events', methods=['GET'])
+@app.route('/get_events', methods=['GET','POST'])
 def get_events():
+    with open("loging.txt","w") as file:
+        file.write('test')
+        data = request.get_json()
+        file.write(str(data['account_id']))
+        account_id = data['account_id']
+
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute('SELECT event_id FROM account_events WHERE user_id = %s', (account_id,))
+        ids = cursor.fetchall()
+
+
+        event_ids = [event_id[0] for event_id in ids]
+        
+        file.write('events')
+        file.write(str(event_ids[1]))
+
+        cursor.close()
+        conn.close()
+
+    return jsonify({'event_ids':event_ids}), 201
+
+@app.route('/add_events', methods=['POST'])
+def add_events():
     data = request.get_json()
 
-    account_id = data['account_id']
+    user_id = data['user_id']
+    event_id = data['event_id']
 
     conn = get_db_connection()
     cursor = conn.cursor()
-    cursor.execute('SELECT event_id FROM account_events WHERE user_id = %s', (account_id,))
-    ids = cursor.fetchall()
-
-    event_ids = [event_id[0] for event_id in ids]
+    cursor.execute('INSERT INTO account_events (user_id, event_id) VALUES (%s, %s)', (user_id,event_id,))
 
     cursor.close()
     conn.close()
 
-    return jsonify({'event_ids':event_ids}), 201
+    return jsonify({"message": "Event added"}), 201
 
 @app.errorhandler(429)
 def ratelimit_handler(e):
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index d7f4b60..c79f00f 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -88,14 +88,9 @@ def availability(event_id):
     return render_template('availability.html')
 
 
-@app.route('/eventSummary/<int:event_id>', methods =['GET'])
+@app.route('/eventSummary/<int:event_id>', methods =['GET', 'POST'])
 def eventSummary(event_id):
-    '''This is for '''
     if request.method == 'GET':
-        #event_name='help'
-
-        # sends list 'to_return' that contains all data requested more details in event_microservice docstring
-        # TODO: create correct 'to_return' for info I need
         data_input = {"id":event_id, "to_return": ['event_name','event_possible_dates','availabilities']}
         response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
         data_output = response.json() if response.status_code == 200 else []
@@ -103,8 +98,17 @@ def eventSummary(event_id):
         dates = data_output['event_possible_dates']
         availabilities = data_output['availabilities']
 
-        # TODO: fix how this is being returned
         return render_template('eventSummary.html', id=event_id, event=event_name, dates=dates, availabilities=availabilities)
+    
+    if request.method == 'POST':
+        data = request.get_json()
+        response = requests.post(f"{USER_API_URL}/add_events", json=data)
+
+        if response.status_code == 201:  # Check if was added successfully
+            return jsonify({'message': 'Event added'})
+        else:
+            return f"Error: {response.text}", response.status_code
+    
     return render_template('eventSummary.html')    
 
 @app.route('/share_link/<int:event_id>', methods =['GET'])
@@ -135,29 +139,39 @@ def getDatesp():
         return data_output
     return render_template('eventSummary.html') 
 
-@app.route('/account/<int:account_id>', methods=['GET'])
+@app.route('/account/<int:account_id>', methods=['GET','POST'])
 def get_events(account_id):
     if request.method == 'GET':
-        data = {'account_id':account_id}
-        account_response = requests.get(f"{USER_API_URL}/get_events", json = data)
-        event_ids = account_response.json() if account_response.status_code == 200 else []
-        
-        events = []
-
-        for event_id in event_ids:
-            event ={}
-            data_input = {"id":event_id, "to_return": ['event_name']}
-            response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
-            data_output = response.json() if response.status_code == 200 else []
-            event['name'] = data_output['name']
-            event['url'] = url_for('eventSummary', event_id=event_id)
-            events.append(event)
-        '''
-        #Test data
-        event1= {'name':'An event','url':url_for('eventSummary', event_id=1)}
-        event2= {'name':'An event 2','url':'a url'}       
-        events = [event1, event2]
-        '''
+        with open("loging.txt","w") as file:
+
+            data = {'account_id':account_id}
+            file.write('in front')
+            file.write(str(data['account_id']))
+            account_response = requests.get(f"{USER_API_URL}/get_events", json = data)
+            response = account_response.json() #if account_response.status_code == 201 else []
+            
+            event_ids = response['event_ids']
+            file.write(str(type(event_ids)))
+
+            events = []
+
+            for event_id in event_ids:
+                event ={}
+                file.write(str(event_id))
+                #event_id = 2
+                data_input = {"id":int(event_id), "to_return": ['event_name']}
+                response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
+                file.write(str(response.status_code))
+                data_output = response.json() if response.status_code == 200 else []
+                event['name'] = data_output['name']
+                event['url'] = url_for('eventSummary', event_id=event_id)
+                events.append(event)
+            '''
+            #Test data
+            event1= {'name':'An event','url':url_for('eventSummary', event_id=1)}
+            event2= {'name':'An event 2','url':'a url'}       
+            events = [event1, event2]
+            '''
         return render_template('accountEvents.html', event=events)  
     return render_template('accountEvents.html')
 
diff --git a/frontend_microservice/static/auth.js b/frontend_microservice/static/auth.js
index 4fe3816..e88616b 100644
--- a/frontend_microservice/static/auth.js
+++ b/frontend_microservice/static/auth.js
@@ -72,7 +72,7 @@ document.addEventListener('DOMContentLoaded', () => {
                     messageDiv.style.color = 'green';
                     messageDiv.textContent = 'Logged in! Redirecting...';
                     localStorage.setItem('user_id', data.user_id);
-                    setTimeout(() => window.location.href = '/', 1500);
+                    setTimeout(() => window.location.href = document.referrer || '/', 1500);
                 } else {
                     messageDiv.style.color = 'red';
                     messageDiv.textContent = data.error || 'Login failed';
diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index e1562f8..fff30c1 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -377,5 +377,24 @@ function show_account_events(events){
     }
 }
 
-
-  
+function addEventToAccount(event_id){
+    const userId = localStorage.getItem('user_id');
+    if (userId){
+        $.ajax({
+            url: `/eventSummary/${event_id}`, 
+            type: 'POST',
+            contentType: 'application/json', 
+            data: JSON.stringify({'event_id':event_id, 'user_id':userId}),
+            success: function(response) {
+                alert("Added to your Account")
+            },
+            error: function(xhr, status, error) {
+                alert("Error: " + error);
+            }
+        });
+    }
+    else{
+        alert("You're not logged in ... sending to login page")
+        window.location.href = '/login';
+    }
+}
\ No newline at end of file
diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
index 6e1202f..f516e1f 100644
--- a/frontend_microservice/templates/accountEvents.html
+++ b/frontend_microservice/templates/accountEvents.html
@@ -5,6 +5,7 @@
         <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
         <script>
             const events = {{event | tojson}};
+            console.log(events)
         </script>
     </head>
     <body>
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index ea8a641..03d160b 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -5,6 +5,7 @@
     <title>Document</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    
 </head>
 <body class="centered">
     <script>
@@ -17,6 +18,7 @@
     <h1>'{{ event }}' Summary</h1>
     <a href="/share_link/{{ id }}"> Copy the event link </a>
     <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
+    <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
 
     <div id="best_dates">
         <h2 id="date_heading"></h2>
-- 
GitLab


From abe42f4cdfe0ecbd9bada2d497c8b9417f5a4553 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 11 Apr 2025 16:23:20 +0100
Subject: [PATCH 019/124] committing nginx

---
 nginx/nginx.conf | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index ced0533..474cefc 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -33,6 +33,14 @@ server {
         proxy_pass http://account_service/login;
     }
 
+    location /add_events {
+        proxy_pass http://account_service/add_events;
+    }
+
+    location /get_events {
+        proxy_pass http://account_service/get_events;
+    }
+
     location /get_best_dates {
         proxy_pass http://event_service/get_best_dates;
     }
-- 
GitLab


From 88fc017b4658ff399a025463529f4b246db21216 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Sat, 12 Apr 2025 14:38:52 +0100
Subject: [PATCH 020/124] remove debugging comments

---
 account_microservice/user.py      |  8 ++++----
 frontend_microservice/frontend.py | 10 +++++-----
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 40abd0e..db4e1d6 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -127,9 +127,9 @@ def login():
 @app.route('/get_events', methods=['GET','POST'])
 def get_events():
     with open("loging.txt","w") as file:
-        file.write('test')
+        #file.write('test')
         data = request.get_json()
-        file.write(str(data['account_id']))
+        #file.write(str(data['account_id']))
         account_id = data['account_id']
 
         conn = get_db_connection()
@@ -140,8 +140,8 @@ def get_events():
 
         event_ids = [event_id[0] for event_id in ids]
         
-        file.write('events')
-        file.write(str(event_ids[1]))
+        #file.write('events')
+        #file.write(str(event_ids[1]))
 
         cursor.close()
         conn.close()
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index c79f00f..4b51dc8 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -145,23 +145,23 @@ def get_events(account_id):
         with open("loging.txt","w") as file:
 
             data = {'account_id':account_id}
-            file.write('in front')
-            file.write(str(data['account_id']))
+            #file.write('in front')
+            #file.write(str(data['account_id']))
             account_response = requests.get(f"{USER_API_URL}/get_events", json = data)
             response = account_response.json() #if account_response.status_code == 201 else []
             
             event_ids = response['event_ids']
-            file.write(str(type(event_ids)))
+            #file.write(str(type(event_ids)))
 
             events = []
 
             for event_id in event_ids:
                 event ={}
-                file.write(str(event_id))
+                #file.write(str(event_id))
                 #event_id = 2
                 data_input = {"id":int(event_id), "to_return": ['event_name']}
                 response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
-                file.write(str(response.status_code))
+                #file.write(str(response.status_code))
                 data_output = response.json() if response.status_code == 200 else []
                 event['name'] = data_output['name']
                 event['url'] = url_for('eventSummary', event_id=event_id)
-- 
GitLab


From c97510ec9674d6db39269f9932428079b8ced973 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sat, 12 Apr 2025 15:14:03 +0000
Subject: [PATCH 021/124] Limit deployment to only main branch

---
 .gitlab-ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f3a2109..005bbdc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -107,4 +107,3 @@ deploy:
     url: http://$SERVER_IP
   only:
     - main
-    - staging/deploy-test
-- 
GitLab


From 0293d5f845fb63b946a441334de12c573d838650 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sat, 12 Apr 2025 15:15:24 +0000
Subject: [PATCH 022/124] Uncomment python unit tests and add test tag

---
 .gitlab-ci.yml | 21 ++++++++++-----------
 1 file changed, 10 insertions(+), 11 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 005bbdc..961b044 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,23 +9,22 @@ variables:
   TAG_LATEST: $DOCKERHUB_REGISTRY:latest   # tag the docker image with "latest" (overriden by current commit every deployment)
   TAG_COMMIT: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA   # docker image tag for current commit
 
-#unit_tests:
-#  stage: test
-#  image: python:3.9
-#  #tags:
-#  #  - pages
-#  script:
-#    - echo "Running unittest for event_finder_test.py"
-#    - python -m unittest tests/event_finder_test.py
-#  #environment:
-#    #- testing
+unit_tests:
+  stage: test
+  image: python:3.9
+  tags:
+    - test
+  script:
+    - echo "Running unittest for event_finder_test.py"
+    - python -m unittest tests/event_finder_test.py
+  #environment:
+  #  - testing
 
 publish:  # Publish containers to gitlab container registry
   image: docker:stable
   stage: publish
   only:
     - main
-    - staging/deploy-test
   variables:
     # Tell docker CLI how to talk to Docker daemon; see
     # https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-executor
-- 
GitLab


From 9e2a0643931b15bbf454d32844af59a16df9ab20 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sat, 12 Apr 2025 15:22:06 +0000
Subject: [PATCH 023/124] Add environment and implement consistent code
 ordering for each stage

---
 .gitlab-ci.yml | 36 +++++++++++++++---------------------
 1 file changed, 15 insertions(+), 21 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 961b044..79d7e0a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,26 +5,31 @@ stages:
   - publish
   - deploy
 
+
 variables:
   TAG_LATEST: $DOCKERHUB_REGISTRY:latest   # tag the docker image with "latest" (overriden by current commit every deployment)
   TAG_COMMIT: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA   # docker image tag for current commit
 
+
 unit_tests:
-  stage: test
   image: python:3.9
+  stage: test
   tags:
     - test
+  environment:
+    name: testing
   script:
     - echo "Running unittest for event_finder_test.py"
     - python -m unittest tests/event_finder_test.py
-  #environment:
-  #  - testing
+
 
 publish:  # Publish containers to gitlab container registry
   image: docker:stable
   stage: publish
   only:
     - main
+  tags:
+    - deployment
   variables:
     # Tell docker CLI how to talk to Docker daemon; see
     # https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-executor
@@ -32,16 +37,9 @@ publish:  # Publish containers to gitlab container registry
     # Use the overlayfs driver for improved performance:
     DOCKER_DRIVER: overlay2
     DOCKER_TLS_CERTDIR: ""
-  tags:
-    - deployment # Temporarily pages tag, since Surrey's pre-existing runner only picks up jobs with this tag
   services:
     - docker:dind # Runs Docker Daemon in a separate container, which the job communicates with
-  #before_script:
-    #- docker rmi -f $(docker images -aq)  # remove all existing images
-    #- docker info # debugging
-    #- echo "$TAG_LATEST"
-    #- echo "$TAG_COMMIT"
-    #- ls -R
+  
   script:
     - echo "$DOCKERHUB_ACCESS_TOKEN" | docker login -u $DOCKERHUB_USER --password-stdin # Login to dockerhub
     
@@ -76,8 +74,14 @@ publish:  # Publish containers to gitlab container registry
 deploy:
   image: alpine:latest  # Lightweight Linux image
   stage: deploy
+  only:
+    - main
   tags:
     - deployment
+  environment:
+    name: production
+    url: http://$SERVER_IP
+  
   before_script:
     # Install SSH client and gettext (useful for templating functionality)
     - apk add gettext openssh-client
@@ -96,13 +100,3 @@ deploy:
 
     # Run docker-compose
     - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker-compose down && docker-compose up -d"  #"docker-compose down && docker-compose up -d"
-
-    #- ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker pull $TAG_COMMIT"  # Pull current commit's image
-    #- ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker container ls || true"
-    #- ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker container rm -f gregc-frontend || true"  # Delete "GregC" image if it exists.
-    #- ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker run -d -p 8080:80 --name gregc-frontend $TAG_COMMIT"
-  environment:
-    name: production
-    url: http://$SERVER_IP
-  only:
-    - main
-- 
GitLab


From e0b230eb594d79d603fb2b15cd6e24852e619fd3 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sat, 12 Apr 2025 15:54:20 +0000
Subject: [PATCH 024/124] Install requirements for unit tests

---
 .gitlab-ci.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 79d7e0a..7fb6c09 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,11 +18,14 @@ unit_tests:
     - test
   environment:
     name: testing
+  before_script:
+    - pip install -r datefinder_microservice/requirements.txt
   script:
     - echo "Running unittest for event_finder_test.py"
     - python -m unittest tests/event_finder_test.py
 
 
+
 publish:  # Publish containers to gitlab container registry
   image: docker:stable
   stage: publish
-- 
GitLab


From 044f9661b1b4c31a692f9480c3d4c9028998e1b9 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Tue, 15 Apr 2025 15:43:53 +0100
Subject: [PATCH 025/124] add which users are available to best dates

---
 datefinder_microservice/event_finder.py | 32 ++++++++++++-------------
 frontend_microservice/static/script.js  | 20 ++++++++++++++--
 2 files changed, 34 insertions(+), 18 deletions(-)

diff --git a/datefinder_microservice/event_finder.py b/datefinder_microservice/event_finder.py
index 6347df4..e28a95b 100644
--- a/datefinder_microservice/event_finder.py
+++ b/datefinder_microservice/event_finder.py
@@ -9,7 +9,7 @@ PORT = 6000
 import logging
 
 # Set up logging to a file
-logging.basicConfig(filename='app.log', level=logging.DEBUG)
+logging.basicConfig(filename='app.log', level=logging.INFO)
 
 
 
@@ -31,15 +31,12 @@ def best_date():
     Each dictionary entry has the username as a key and a list of datetime objects as a value"""
 
     users_dict = request.get_json()
-    print(users_dict)
-    logging.info(users_dict)
 
     date_dict = {}
     max_count = 0
-    best_dates = []
+    best_dates = {}
 
     users = users_dict["availabilities"]
-    logging.info(users)
 
     # Iterate through all event users
     for user in users:
@@ -56,22 +53,25 @@ def best_date():
             # If it is then wipe the list of best_date choices, add the new date and set the new max_count
             # If the count is equal to max count then also add date to list of best_dates
             if date_dict[slot] > max_count:
-                best_dates = []
+                best_dates = {}
                 max_count = date_dict[slot]
-                best_dates.append(slot)
+                best_dates[slot] = []
             elif date_dict[slot] == max_count:
-                best_dates.append(slot)
-
-            output_dates = {key: date for key, date in enumerate(best_dates)}
+                best_dates[slot] = []
+
+    
+    for best_date in best_dates:
+        logging.info("We made it into the loop")
+        for user in users:
+            logging.info("We made it into the loop")
+            if best_date in users[user]:
+                logging.info(best_date)
+                logging.info(user)
+                best_dates[best_date].append(str(user))
+    logging.info(best_dates)
 
     return best_dates, 200
 
-# This is for readablility and testing
-# Format the dates 
-#print(best_dates_list)
-#best_dates_formatted = [date.strftime('%Y-%m-%d') for date in best_dates_list]
-#print(f"The best event dates are: {best_dates_formatted}")
-
 
 if __name__ == "__main__":
     app.run(debug=True, host="0.0.0.0", port=PORT)
\ No newline at end of file
diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index ae2938f..aafd355 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -280,14 +280,29 @@ function getDates(availabilities){
             // https://www.w3schools.com/jsref/prop_element_classlist.asp
             container.classList.add("columns");
             date_heading.textContent = "POSSIBLE DATES";
+            console.log(response)
 
             // Loop through the response and add new div elements for each date
             for (const date in response){
                 var date_div = document.createElement("div");
-                best_date = response[date]
                 date_div.classList.add("card", "internal");
+
                 // https://www.w3schools.com/js/js_dates.asp
-                date_div.textContent = new Date(best_date).toDateString();
+                var date_div_heading = document.createElement("h3");
+                date_div_heading.textContent = new Date(date).toDateString();
+
+                date_div.appendChild(date_div_heading);
+
+                // create that dates list of available users
+                var available_users_list = document.createElement("ul");
+                for (const user in response[date]){
+                    var li = document.createElement("li");
+                    var available_user = response[date][user];
+                    li.textContent = available_user;
+                    available_users_list.appendChild(li);
+                };
+
+                date_div.appendChild(available_users_list)
                 container.appendChild(date_div);
             }
         },
@@ -315,6 +330,7 @@ function new_list_element(availabilities, dates){
     div_heading.textContent = heading;
     av_div.append(div_heading);
 
+    // Create list element of possible dates
     var dateslist = document.createElement("ul");
     for (const date in dates){
         var li = document.createElement("li");
-- 
GitLab


From cfbd1ae752fa5fcb24ee409ca947a203a7553346 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Wed, 16 Apr 2025 12:05:18 +0100
Subject: [PATCH 026/124] update unittests for new algorithm

---
 tests/event_finder_test.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/tests/event_finder_test.py b/tests/event_finder_test.py
index b67812f..5278187 100644
--- a/tests/event_finder_test.py
+++ b/tests/event_finder_test.py
@@ -66,8 +66,8 @@ class TestAlgorithmWorks(unittest.TestCase):
         response_data = response.get_json()
 
         # Assert that the response is a list and contains the correct dates
-        self.assertIsInstance(response_data, list)
-        self.assertEqual(response_data, ['2025-05-03T00:00:00'])
+        self.assertIsInstance(response_data, dict)
+        self.assertEqual(response_data, {'2025-05-03T00:00:00': ["user1", "user2", "user3", "user4"]})
 
 
     def test_gets_correct_dates_when_two_possible(self):
@@ -95,7 +95,7 @@ class TestAlgorithmWorks(unittest.TestCase):
         
 
         # Check that the correct dates are found (3rd and 6th)
-        self.assertEqual(response.get_json(), ['2025-05-03T00:00:00', '2025-05-06T00:00:00'])
+        self.assertEqual(response.get_json(), {'2025-05-03T00:00:00': ["user1", "user2", "user3", "user4"], '2025-05-06T00:00:00': ["user1", "user2", "user3", "user4"]})
 
     def test_user_with_no_availability(self):
         input_data = {
@@ -119,7 +119,7 @@ class TestAlgorithmWorks(unittest.TestCase):
         response = self.client.post('/calculate', json=input_data)
 
         # Check that the correct dates are found (3rd and 6th)
-        self.assertEqual(response.get_json(), ['2025-05-03T00:00:00', '2025-05-06T00:00:00'])
+        self.assertEqual(response.get_json(), {'2025-05-03T00:00:00':["user1", "user2", "user3"], '2025-05-06T00:00:00': ["user1", "user2", "user3"]})
        
 class TestWrongDataTypes(unittest.TestCase):
     @classmethod
-- 
GitLab


From cbee8c43a2dcff42f2046f0a17c0b1faf781450b Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Wed, 16 Apr 2025 12:06:10 +0100
Subject: [PATCH 027/124] improve styling

---
 frontend_microservice/static/styles.css           | 6 ++++++
 frontend_microservice/templates/eventSummary.html | 4 ++--
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/frontend_microservice/static/styles.css b/frontend_microservice/static/styles.css
index 6e88f0a..f966198 100644
--- a/frontend_microservice/static/styles.css
+++ b/frontend_microservice/static/styles.css
@@ -14,6 +14,12 @@ h1{
     font-size: 50px;
 }
 
+ul{
+    list-style-type: none;
+    margin: 0;
+    padding: 0;
+}
+
 button{
     color: #fff;
     display: inline-block;
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 03d160b..eb0bc4e 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -10,9 +10,9 @@
 <body class="centered">
     <script>
         var availabilities = {{ availabilities | tojson }};
-        console.log(availabilities)
+        console.log(availabilities);
         var dates = {{dates | tojson }};
-        var event_id = {{id | tojson}}
+        var event_id = {{id | tojson}};
     </script>
 
     <h1>'{{ event }}' Summary</h1>
-- 
GitLab


From fee9c809bc7fc84722b8080f80f071d898e0966c Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Wed, 16 Apr 2025 14:32:15 +0100
Subject: [PATCH 028/124] change url so doesn't go back to register page

---
 frontend_microservice/frontend.py    |  2 +-
 frontend_microservice/static/auth.js | 14 ++++++++++++--
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 4b51dc8..2af7f8e 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -49,7 +49,7 @@ def login():
         try:
             data = request.get_json()
             response = requests.post(f"{USER_API_URL}/login", json=data)
-            return jsonify(response.json()), response.status_code
+            return jsonify(response.json()),response.status_code
         except Exception as e:
             return jsonify({"error": str(e)}), 500
     return render_template('login.html')
diff --git a/frontend_microservice/static/auth.js b/frontend_microservice/static/auth.js
index e88616b..ed2f420 100644
--- a/frontend_microservice/static/auth.js
+++ b/frontend_microservice/static/auth.js
@@ -65,14 +65,24 @@ document.addEventListener('DOMContentLoaded', () => {
                     headers: { 'Content-Type': 'application/json' },
                     body: JSON.stringify({ email, password })
                 });
-
+                
+                
                 const data = await response.json();
                 
                 if (response.ok) {
                     messageDiv.style.color = 'green';
                     messageDiv.textContent = 'Logged in! Redirecting...';
+                    console.log(document.referrer)
                     localStorage.setItem('user_id', data.user_id);
-                    setTimeout(() => window.location.href = document.referrer || '/', 1500);
+                    setTimeout(() => {
+                        const registrationPage = 'http://127.0.0.1:8000/register';
+                        
+                        if (document.referrer === registrationPage || document.referrer === '') {
+                          window.location.href = '/';
+                        } else {
+                          window.location.href = document.referrer;
+                        }
+                      }, 1500);
                 } else {
                     messageDiv.style.color = 'red';
                     messageDiv.textContent = data.error || 'Login failed';
-- 
GitLab


From e8fe7fe70ad4a4bd722a7da481e4f21f6f9c1eec Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:44:22 +0100
Subject: [PATCH 029/124] updated schema for password changes

---
 account_database/schema.sql | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/account_database/schema.sql b/account_database/schema.sql
index 2b71ebc..87037bf 100644
--- a/account_database/schema.sql
+++ b/account_database/schema.sql
@@ -2,7 +2,9 @@ CREATE TABLE IF NOT EXISTS users (
     user_id SERIAL PRIMARY KEY,
     email VARCHAR(255) UNIQUE NOT NULL,
     password_hash VARCHAR(255) NOT NULL,
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    reset_token VARCHAR(100) UNIQUE,
+    reset_token_expiry TIMESTAMP     
 );
 
 CREATE TABLE IF NOT EXISTS account_events (
-- 
GitLab


From 8b4040bf779b4b8e6ad6a015f65864aa39a4e9da Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:44:51 +0100
Subject: [PATCH 030/124] updated req for password changes

---
 account_microservice/requirements.txt | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/account_microservice/requirements.txt b/account_microservice/requirements.txt
index b92756f..89a59d4 100644
--- a/account_microservice/requirements.txt
+++ b/account_microservice/requirements.txt
@@ -3,4 +3,6 @@ psycopg2-binary
 werkzeug>=3.0.0
 flask-limiter
 email-validator
-cryptography  
\ No newline at end of file
+cryptography  
+itsdangerous
+Flask-Mail 
\ No newline at end of file
-- 
GitLab


From a6665ffa4407f3ca9a77203486616e8bd110567c Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:45:50 +0100
Subject: [PATCH 031/124] added password forget and reset functions

---
 account_microservice/user.py | 138 ++++++++++++++++++++++++++++++++++-
 1 file changed, 137 insertions(+), 1 deletion(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index db4e1d6..216a81f 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -7,9 +7,24 @@ from flask_limiter import Limiter
 from flask_limiter.util import get_remote_address
 import re
 import os
+from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadTimeSignature
+from flask_mail import Mail, Message
+import datetime
+
 
 app = Flask(__name__)
 
+app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.example.com')
+app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587))
+app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() == 'true'
+app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
+app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
+app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'noreply@example.com')
+app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-very-secret-key-here') 
+
+mail = Mail(app)
+ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
+
 logging.basicConfig(
     level=logging.INFO,
     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -113,7 +128,7 @@ def login():
         conn.close()
 
         if not result or not check_password_hash(result[1], password):
-            return jsonify({"error": "Invalid credentials"}), 401
+            return jsonify({"error": "Invalid credentials. If you are not registered, please register first!"}), 401
 
         return jsonify({
             "message": "Login successful",
@@ -164,6 +179,127 @@ def add_events():
 
     return jsonify({"message": "Event added"}), 201
 
+@app.route('/request_password_reset', methods=['POST'])
+@limiter.limit("3 per 15 minute") 
+def request_password_reset():
+    """
+    Handles the initial request to reset a password.
+    Validates email, generates a token, stores it with expiry, and sends reset email.
+    """
+    data = request.get_json()
+    if not data or 'email' not in data:
+        return jsonify({"error": "Email is required"}), 400
+
+    email = data['email'].strip().lower()
+    try:
+        valid = validate_email(email)
+        email = valid.normalized
+    except EmailNotValidError:
+        logging.warning(f"Password reset attempt for invalid email format: {email}")
+        return jsonify({"message": "If your email is registered, you will receive a reset link."}), 200
+
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+
+        cursor.execute('SELECT user_id FROM users WHERE email = %s', (email,))
+        user = cursor.fetchone()
+
+        if user:
+            user_id = user[0]
+            token = ts.dumps(email, salt='password-reset-salt')
+            expiry_time = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
+
+            cursor.execute(
+                'UPDATE users SET reset_token = %s, reset_token_expiry = %s WHERE user_id = %s',
+                (token, expiry_time, user_id)
+            )
+            conn.commit()
+
+            reset_url = f"http://localhost:8000/reset_password/{token}"
+
+            subject = "Password Reset Request"
+            body = f"Click the following link to reset your password: {reset_url}\n\nThis link will expire in 1 hour.\n\nIf you did not request this, please ignore this email."
+            msg = Message(subject, recipients=[email], body=body)
+
+            try:
+                mail.send(msg)
+                logging.info(f"Password reset email successfully sent to {email} for user_id {user_id}")
+            except Exception as mail_error:
+                logging.error(f"Failed to send password reset email to {email}: {str(mail_error)}", exc_info=True)
+
+        else:
+            logging.warning(f"Password reset attempt for non-existent email: {email}")
+
+        return jsonify({"message": "If your email is registered, you will receive a reset link."}), 200
+
+    except Exception as e:
+        logging.error(f"Password reset request processing error for {email}: {str(e)}", exc_info=True)
+        return jsonify({"error": "An error occurred while processing your request."}), 500
+    finally:
+        if conn:
+            conn.close()
+
+@app.route('/reset_password', methods=['POST'])
+@limiter.limit("5 per minute") 
+def reset_password():
+    """
+    Handles the submission of the new password using the token.
+    Verifies the token, validates the new password, updates the hash, and invalidates the token.
+    """
+    data = request.get_json()
+    if not data or not all(k in data for k in ['token', 'password']):
+        return jsonify({"error": "Token and new password are required"}), 400
+
+    token = data['token']
+    new_password = data['password']
+
+    if pass_error := validate_password(new_password):
+        return jsonify({"error": pass_error}), 400
+
+    conn = None
+    try:
+        email = ts.loads(token, salt='password-reset-salt', max_age=3600) 
+
+        conn = get_db_connection()
+        cursor = conn.cursor()
+
+        cursor.execute(
+            'SELECT user_id FROM users WHERE email = %s AND reset_token = %s AND reset_token_expiry > NOW()',
+            (email, token)
+        )
+        user = cursor.fetchone()
+
+        if not user:
+            logging.warning(f"Attempt to reset password with invalid/used/expired token for email: {email}")
+            return jsonify({"error": "Invalid or expired password reset link."}), 400
+
+        user_id = user[0]
+        new_password_hash = generate_password_hash(new_password, method='scrypt', salt_length=16)
+
+        cursor.execute(
+            'UPDATE users SET password_hash = %s, reset_token = NULL, reset_token_expiry = NULL WHERE user_id = %s',
+            (new_password_hash, user_id)
+        )
+        conn.commit()
+
+        logging.info(f"Password successfully reset for user_id: {user_id} via token")
+        return jsonify({"message": "Password successfully updated"}), 200
+
+    except SignatureExpired:
+        logging.warning(f"Password reset attempt with expired token: {token[:10]}...") 
+        return jsonify({"error": "The password reset link has expired."}), 400
+    except BadTimeSignature:
+        logging.warning(f"Password reset attempt with invalid token signature: {token[:10]}...") 
+        return jsonify({"error": "Invalid password reset link."}), 400
+    except Exception as e:
+        logging.error(f"Password reset processing error: {str(e)}", exc_info=True)
+        return jsonify({"error": "An error occurred while resetting the password."}), 500
+    finally:
+        if conn:
+            conn.close()
+
 @app.errorhandler(429)
 def ratelimit_handler(e):
     return jsonify({"error": "Too many requests"}), 429
-- 
GitLab


From 41b4e5f3a24d1c78da39151bec216bc3590d15c0 Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:47:41 +0100
Subject: [PATCH 032/124] added password forget and reset functions

---
 frontend_microservice/frontend.py    |  41 ++++++++
 frontend_microservice/static/auth.js | 149 +++++++++++++++++++++------
 nginx/nginx.conf                     |   8 ++
 3 files changed, 164 insertions(+), 34 deletions(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 2af7f8e..5a254fa 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -3,6 +3,8 @@ import requests
 import json
 import datetime
 import os
+import logging
+
 app = Flask(__name__)
 
 PORT = 5001
@@ -180,6 +182,45 @@ def account():
     """User account page where they can see their profile and logout"""
     return render_template('account.html')
 
+@app.route('/forgot_password', methods=['GET', 'POST'])
+def forgot_password():
+    """
+    Handles the request to send a password reset link.
+    GET: Renders the forgot password page.
+    POST: Forwards the email to the account microservice.
+    """
+    if request.method == 'POST':
+        try:
+            data = request.get_json()
+            response = requests.post(f"{USER_API_URL}/request_password_reset", json=data)
+            return jsonify(response.json()), response.status_code
+        except Exception as e:
+            print(f"Frontend forgot password error: {str(e)}")
+            return jsonify({"error": "An internal error occurred while contacting the account service"}), 500
+    return render_template('forgot_password.html')
+
+@app.route('/reset_password/<token>', methods=['GET'])
+def reset_password_token(token):
+    """
+    Renders the page where the user can enter their new password.
+    The token from the URL is passed to the template.
+    """
+    return render_template('reset_password.html', token=token)
+
+@app.route('/reset_password', methods=['POST'])
+def reset_password_submit():
+    """
+    Handles the submission of the new password form.
+    POST: Forwards the token and new password to the account microservice.
+    """
+    try:
+        data = request.get_json()
+        response = requests.post(f"{USER_API_URL}/reset_password", json=data)
+        return jsonify(response.json()), response.status_code
+    except Exception as e:
+        print(f"Frontend reset password submission error: {str(e)}")
+        return jsonify({"error": "An internal error occurred while contacting the account service"}), 500
+
 if __name__ == "__main__":
     # checks for hostname to see if running in containers then sets host appropriately
     hostname = os.environ.get('HOSTNAME', '')
diff --git a/frontend_microservice/static/auth.js b/frontend_microservice/static/auth.js
index ed2f420..24e0fff 100644
--- a/frontend_microservice/static/auth.js
+++ b/frontend_microservice/static/auth.js
@@ -1,41 +1,38 @@
 document.addEventListener('DOMContentLoaded', () => {
-    // Function to update navigation based on login status
     function updateNavigation() {
         const userId = localStorage.getItem('user_id');
         const loginLink = document.getElementById('login-link');
         const accountLink = document.getElementById('account-link');
-        
+
         if (userId && loginLink && accountLink) {
-            // User is logged in
             loginLink.style.display = 'none';
             accountLink.style.display = 'inline';
         } else if (loginLink && accountLink) {
-            // User is not logged in
             loginLink.style.display = 'inline';
             accountLink.style.display = 'none';
         }
     }
 
-    // Run navigation update on every page load
     updateNavigation();
 
-    // Registration form handler
-    if (document.getElementById('registerForm')) {
-        document.getElementById('registerForm').addEventListener('submit', async (e) => {
+    const registerForm = document.getElementById('registerForm');
+    if (registerForm) {
+        registerForm.addEventListener('submit', async (e) => {
             e.preventDefault();
             const email = document.getElementById('email').value;
             const password = document.getElementById('password').value;
             const messageDiv = document.getElementById('message');
+            messageDiv.textContent = ''; 
 
             try {
-                const response = await fetch('/register', {
+                const response = await fetch('/register', { 
                     method: 'POST',
                     headers: { 'Content-Type': 'application/json' },
                     body: JSON.stringify({ email, password })
                 });
 
                 const data = await response.json();
-                
+
                 if (response.status === 201) {
                     messageDiv.style.color = 'green';
                     messageDiv.textContent = 'Registered successfully! Redirecting to login...';
@@ -51,56 +48,140 @@ document.addEventListener('DOMContentLoaded', () => {
         });
     }
 
-    // Login form handler
-    if (document.getElementById('loginForm')) {
-        document.getElementById('loginForm').addEventListener('submit', async (e) => {
+const loginForm = document.getElementById('loginForm');
+if (loginForm) {
+    loginForm.addEventListener('submit', async (e) => {
+        e.preventDefault();
+        const email = document.getElementById('email').value;
+        const password = document.getElementById('password').value;
+        const messageDiv = document.getElementById('message');
+        messageDiv.textContent = ''; 
+
+        try {
+            const response = await fetch('/login', { 
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ email, password })
+            });
+
+
+            const data = await response.json();
+
+            if (response.ok) {
+                messageDiv.style.color = 'green';
+                messageDiv.textContent = 'Logged in! Redirecting...';
+                console.log(document.referrer)
+                localStorage.setItem('user_id', data.user_id);
+                setTimeout(() => {
+                    const registrationPage = window.location.origin + '/register';
+                    const referrer = document.referrer;
+                    const shouldRedirectHome = referrer === registrationPage ||
+                                               referrer.includes('/reset_password') || 
+                                               referrer === '';
+
+                    if (shouldRedirectHome) {
+                      window.location.href = '/'; 
+                    } else {
+                      window.location.href = document.referrer; 
+                    }
+                  }, 1500);
+            } else {
+                messageDiv.style.color = 'red';
+                messageDiv.textContent = data.error || 'Login failed';
+            }
+        } catch (error) {
+            messageDiv.style.color = 'red';
+            messageDiv.textContent = 'Failed to connect to server';
+        }
+    });
+}
+    const forgotPasswordForm = document.getElementById('forgotPasswordForm');
+    if (forgotPasswordForm) {
+        forgotPasswordForm.addEventListener('submit', async (e) => {
             e.preventDefault();
             const email = document.getElementById('email').value;
+            const messageDiv = document.getElementById('message');
+            messageDiv.textContent = ''; 
+
+            try {
+                const response = await fetch('/forgot_password', { 
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ email })
+                });
+
+                const data = await response.json();
+
+                if (response.ok) {
+                    messageDiv.style.color = 'green';
+                   
+                    messageDiv.textContent = data.message || 'If your email is registered, you will receive a reset link.';
+                    forgotPasswordForm.reset();
+                } else {
+                    messageDiv.style.color = 'red';
+                    messageDiv.textContent = data.error || 'Failed to send reset link.';
+                }
+            } catch (error) {
+                messageDiv.style.color = 'red';
+                messageDiv.textContent = 'Network error. Please try again.';
+            }
+        });
+    }
+
+    const resetPasswordForm = document.getElementById('resetPasswordForm');
+    if (resetPasswordForm) {
+        resetPasswordForm.addEventListener('submit', async (e) => {
+            e.preventDefault();
+            const token = document.getElementById('token').value; 
             const password = document.getElementById('password').value;
+            const confirm_password = document.getElementById('confirm_password').value;
             const messageDiv = document.getElementById('message');
+            messageDiv.textContent = ''; 
+
+            if (password !== confirm_password) {
+                messageDiv.style.color = 'red';
+                messageDiv.textContent = 'Passwords do not match.';
+                return;
+            }
+
+            if (password.length < 8 || !/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password)) {
+                 messageDiv.style.color = 'red';
+                 messageDiv.textContent = 'Password must be at least 8 characters with uppercase, lowercase, and numbers.';
+                 return;
+            }
+
 
             try {
-                const response = await fetch('/login', {
+                const response = await fetch('/reset_password', { 
                     method: 'POST',
                     headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ email, password })
+                    body: JSON.stringify({ token, password })
                 });
-                
-                
+
                 const data = await response.json();
-                
+
                 if (response.ok) {
                     messageDiv.style.color = 'green';
-                    messageDiv.textContent = 'Logged in! Redirecting...';
-                    console.log(document.referrer)
-                    localStorage.setItem('user_id', data.user_id);
-                    setTimeout(() => {
-                        const registrationPage = 'http://127.0.0.1:8000/register';
-                        
-                        if (document.referrer === registrationPage || document.referrer === '') {
-                          window.location.href = '/';
-                        } else {
-                          window.location.href = document.referrer;
-                        }
-                      }, 1500);
+                    messageDiv.textContent = data.message || 'Password reset successful! Redirecting to login...';
+                    resetPasswordForm.reset(); 
+                    setTimeout(() => window.location.href = '/login', 2500);
                 } else {
                     messageDiv.style.color = 'red';
-                    messageDiv.textContent = data.error || 'Login failed';
+                    messageDiv.textContent = data.error || 'Failed to reset password. The link may be invalid or expired.';
                 }
             } catch (error) {
                 messageDiv.style.color = 'red';
-                messageDiv.textContent = 'Failed to connect to server';
+                messageDiv.textContent = 'Network error. Please try again.';
             }
         });
     }
 
-    // Logout button handler
     const logoutButton = document.getElementById('logout-button');
     if (logoutButton) {
         logoutButton.addEventListener('click', () => {
             localStorage.removeItem('user_id');
             alert('Logged out successfully!');
-            window.location.href = '/';
+            window.location.href = '/'; 
         });
     }
 });
\ No newline at end of file
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index d6fb835..00db418 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -52,4 +52,12 @@ server {
         proxy_pass http://datefinder/calculate;
     }
 
+    location /request_password_reset {
+        proxy_pass http://account_service; 
+    }
+
+    location /reset_password {
+        proxy_pass http://account_service; 
+    }
+
 }
\ No newline at end of file
-- 
GitLab


From 1a9ce7623afbb0db6300b7fe9092c432fdfdf4ec Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:48:33 +0100
Subject: [PATCH 033/124] added forgot password button

---
 frontend_microservice/templates/login.html | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/frontend_microservice/templates/login.html b/frontend_microservice/templates/login.html
index 8b2f8c2..e5e3124 100644
--- a/frontend_microservice/templates/login.html
+++ b/frontend_microservice/templates/login.html
@@ -5,7 +5,6 @@
     <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
     <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
-
 </head>
 <body class="centered">
     <div class="card">
@@ -19,6 +18,9 @@
                 <label>Password:</label>
                 <input type="password" id="password" required>
             </div>
+            <div style="text-align: right; margin-bottom: 10px;">
+                <a href="{{ url_for('forgot_password') }}" style="font-size: 0.9em; background: none; color: midnightblue; text-decoration: underline;">Forgot Password?</a>
+            </div>
             <button type="submit">Login</button>
         </form>
         <div class="auth-links">
-- 
GitLab


From 97d4d481370beaaf77045003fc90ec9c767beb72 Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:50:06 +0100
Subject: [PATCH 034/124]  added mailpit and related variables for dummy
 password reset requests

---
 docker-compose.yml | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index e1cb407..0a0583d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,14 @@
 services:
+  mailpit:
+    image: axllent/mailpit:latest 
+    container_name: mailpit
+    ports:
+    - "1025:1025" 
+    - "8025:8025" 
+    networks:
+    - backend 
+    restart: unless-stopped
+    
   event_service:
     build:
       context: event_microservice
@@ -39,7 +49,13 @@ services:
       - loadbalancing
     environment:
       - PORT=5003
-    
+      - MAIL_SERVER=mailpit # Point to the mailpit service name
+      - MAIL_PORT=1025      # Mailpit's default SMTP port
+      - MAIL_USE_TLS=false  # Mailpit doesn't use TLS by default
+      - MAIL_USERNAME=      # No username needed for Mailpit by default
+      - MAIL_PASSWORD=      # No password needed for Mailpit by default
+      - MAIL_DEFAULT_SENDER=noreply@gregc.com # Can be anything
+      - SECRET_KEY=your_very_long_random_and_unguessable_string
 
   nginx:
       image: nginx
-- 
GitLab


From af48ad06dba5c16003e6a89ffe5914e7d251c24a Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:50:46 +0100
Subject: [PATCH 035/124] added forget password html

---
 .../templates/forgot_password.html            | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)
 create mode 100644 frontend_microservice/templates/forgot_password.html

diff --git a/frontend_microservice/templates/forgot_password.html b/frontend_microservice/templates/forgot_password.html
new file mode 100644
index 0000000..fc25474
--- /dev/null
+++ b/frontend_microservice/templates/forgot_password.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Forgot Password</title>
+    <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
+    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+    <script src="{{ url_for('static', filename='auth.js') }}" defer></script>
+</head>
+<body class="centered">
+    <div class="card">
+        <h2>Reset Password</h2>
+        <p>Enter your email address and we'll send you a link to reset your password.</p>
+        <form id="forgotPasswordForm" class="card internal">
+            <div class="form-group">
+                <label>Email:</label>
+                <input type="email" id="email" required>
+            </div>
+            <button type="submit">Send Reset Link</button>
+        </form>
+        <div class="auth-links">
+            <p><a href="{{ url_for('login') }}">Back to Login</a></p>
+        </div>
+        <div id="message"></div>
+    </div>
+    </body>
+</html>
\ No newline at end of file
-- 
GitLab


From 52faa4e90d40193eaee79f1f48d4b7b1225c49df Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Thu, 17 Apr 2025 19:51:09 +0100
Subject: [PATCH 036/124] added reset password html

---
 .../templates/reset_password.html             | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 frontend_microservice/templates/reset_password.html

diff --git a/frontend_microservice/templates/reset_password.html b/frontend_microservice/templates/reset_password.html
new file mode 100644
index 0000000..f30a0ee
--- /dev/null
+++ b/frontend_microservice/templates/reset_password.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Reset Password</title>
+    <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
+    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+    <script src="{{ url_for('static', filename='auth.js') }}" defer></script>
+</head>
+<body class="centered">
+    <div class="card">
+        <h2>Enter New Password</h2>
+        <form id="resetPasswordForm" class="card internal">
+            <input type="hidden" id="token" value="{{ token }}">
+            <div class="form-group">
+                <label>New Password:</label>
+                <input type="password" id="password" required>
+                 <small>Must be at least 8 characters with uppercase, lowercase, and numbers</small>
+            </div>
+             <div class="form-group">
+                <label>Confirm New Password:</label>
+                <input type="password" id="confirm_password" required>
+            </div>
+            <button type="submit">Reset Password</button>
+        </form>
+        <div id="message"></div>
+    </div>
+     </body>
+</html>
\ No newline at end of file
-- 
GitLab


From 1f75ac6eb1638c8c5d48e0359da923cb7598a20d Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Mon, 21 Apr 2025 10:35:11 +0100
Subject: [PATCH 037/124] add references i had noted but forgotten to add to
 code

---
 frontend_microservice/frontend.py       |  4 ----
 frontend_microservice/static/script.js  |  5 ++++-
 frontend_microservice/static/styles.css | 17 -----------------
 3 files changed, 4 insertions(+), 22 deletions(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 1b65bae..4228b95 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -98,13 +98,10 @@ def eventSummary(event_id):
         dates = data_output['event_possible_dates']
         availabilities = data_output['availabilities']
 
-<<<<<<< HEAD
         link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
 
         # TODO: fix how this is being returned
         return render_template('eventSummary.html', id=event_id, link=link, event=event_name, dates=dates, availabilities=availabilities)
-=======
-        return render_template('eventSummary.html', id=event_id, event=event_name, dates=dates, availabilities=availabilities)
     
     if request.method == 'POST':
         data = request.get_json()
@@ -115,7 +112,6 @@ def eventSummary(event_id):
         else:
             return f"Error: {response.text}", response.status_code
     
->>>>>>> development
     return render_template('eventSummary.html')    
 
 @app.route('/share_link/<int:event_id>', methods =['GET'])
diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index aafd355..aed9c50 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -258,7 +258,6 @@ function getDates(availabilities){
     date_area.classList.add("card");
 
     var container = document.getElementById("date_container");
-    //container.innerHTML = "";
     all_old_div = container.querySelectorAll('div');
     for (let i = 0; i < all_old_div.length; i++) {
        const div_element = all_old_div[i];
@@ -279,6 +278,7 @@ function getDates(availabilities){
             // Format the section in which the dates will be listed
             // https://www.w3schools.com/jsref/prop_element_classlist.asp
             container.classList.add("columns");
+            // https://www.w3schools.com/jsref/prop_node_textcontent.asp
             date_heading.textContent = "POSSIBLE DATES";
             console.log(response)
 
@@ -318,7 +318,9 @@ function new_list_element(availabilities, dates){
     var summary_block = document.getElementById("summary_block");
     var sub_block = document.createElement("div");
     sub_block.classList.add("columns")
+    // https://www.w3schools.com/jsref/met_node_appendchild.asp
     summary_block.appendChild(sub_block);
+
     // Creates list of possible event dates
 
     var av_div = document.createElement("div");
@@ -374,6 +376,7 @@ function new_list_element(availabilities, dates){
         for (const av in user_dates){
             var li = document.createElement("li");
             var date = user_dates[av];
+            // https://www.w3schools.com/js/js_dates.asp
             var output_date = new Date(date).toDateString();
             li.textContent = output_date;
             availabilitieslist.appendChild(li);
diff --git a/frontend_microservice/static/styles.css b/frontend_microservice/static/styles.css
index 75d4c73..61295a8 100644
--- a/frontend_microservice/static/styles.css
+++ b/frontend_microservice/static/styles.css
@@ -172,23 +172,6 @@ a:active{
     margin: 20px;
 }
 
-/*
-.two_column {
-    display: flex;
-    justify-content: space-between;
-    gap: 15px;
-    text-align: center;
-}
-.two_column .column {
-    flex: 1;
-}
-.two_column li {
-    list-style-type: none;
-}
-.two_column ul{
-    padding-left: 0;
-}
-*/
 
 .columns {
     text-align: center;
-- 
GitLab


From b9be95eb1bd0f1974274e58897bb8975a2c12484 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Mon, 21 Apr 2025 10:36:02 +0100
Subject: [PATCH 038/124] add event link in event summary again

---
 frontend_microservice/templates/eventSummary.html | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index b9589bb..d1e6be6 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -16,7 +16,6 @@
     </script>
 
     <h1>'{{ event }}' Summary</h1>
-<<<<<<< HEAD
 
     <div class="centered">
         <div class="card">
@@ -47,9 +46,6 @@
         }
     </script>
 
-=======
-    <a href="/share_link/{{ id }}"> Copy the event link </a>
->>>>>>> development
     <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
     <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
 
-- 
GitLab


From 9da9eb5c1b34550d30e7c621c03d862f3e85fa17 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Mon, 21 Apr 2025 11:16:13 +0100
Subject: [PATCH 039/124] add home button (and nav bar to all pages)

---
 frontend_microservice/static/styles.css       |  9 +++++
 frontend_microservice/templates/account.html  |  3 +-
 .../templates/accountEvents.html              | 10 +++++-
 .../templates/availability.html               | 10 +++++-
 frontend_microservice/templates/create.html   | 15 ++++++--
 .../templates/eventSummary.html               | 34 ++++++++++++-------
 frontend_microservice/templates/index.html    |  3 +-
 frontend_microservice/templates/login.html    | 10 +++++-
 frontend_microservice/templates/register.html | 10 +++++-
 9 files changed, 83 insertions(+), 21 deletions(-)

diff --git a/frontend_microservice/static/styles.css b/frontend_microservice/static/styles.css
index f966198..46b3eb4 100644
--- a/frontend_microservice/static/styles.css
+++ b/frontend_microservice/static/styles.css
@@ -1,4 +1,8 @@
 /* Core Elements */
+
+/* https://www.w3schools.com/css/css3_flexbox_container.asp used to learn about flex containers */
+
+
 body{
     font-family: Verdana, Geneva, Tahoma, sans-serif;
     background-color: hsl(270, 4%, 90%); 
@@ -241,3 +245,8 @@ a:active{
     width: 200px;
 }
 
+.lowered {
+    position: absolute;
+    top: 130px;
+}
+
diff --git a/frontend_microservice/templates/account.html b/frontend_microservice/templates/account.html
index 8a8a10c..8b0f2b1 100644
--- a/frontend_microservice/templates/account.html
+++ b/frontend_microservice/templates/account.html
@@ -9,10 +9,11 @@
         <nav class="top-nav">
             <div class="auth-links">
                 <a href="/login" id="login-link">Login/Signup</a>
+                <a href="/index" id = "home-link" style="display: inline;">Home</a>
                 <a href="/account" id="account-link" style="display: none;">My Account</a>
             </div>
         </nav>
-        <div class="centered">
+        <div class="centered lowered">
             <div class="card">
                 <h1>My Account</h1>
                 <div id="user-status">
diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
index f516e1f..e1c6842 100644
--- a/frontend_microservice/templates/accountEvents.html
+++ b/frontend_microservice/templates/accountEvents.html
@@ -3,13 +3,21 @@
     <head>
         <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
         <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
+        <script src="{{ url_for('static', filename='auth.js') }}"></script>
         <script>
             const events = {{event | tojson}};
             console.log(events)
         </script>
     </head>
     <body>
-        <div class="centered">
+        <nav class="top-nav">
+            <div class="auth-links">
+                <a href="/login" id="login-link">Login/Signup</a>
+                <a href="/index" id = "home-link" style="display: inline;">Home</a>
+                <a href="/account" id="account-link" style="display: none;">My Account</a>
+            </div>
+        </nav>
+        <div class="centered lowered">
             <div class="card">
                 <h1>These are your events</h1>
                 <h3>Click to see the details</h3>
diff --git a/frontend_microservice/templates/availability.html b/frontend_microservice/templates/availability.html
index 0c238e6..060b586 100644
--- a/frontend_microservice/templates/availability.html
+++ b/frontend_microservice/templates/availability.html
@@ -8,6 +8,7 @@
     <link rel="stylesheet" href="{{ url_for('static', filename='calendar-styles.css') }}">
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
     <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
+    <script src="{{ url_for('static', filename='auth.js') }}"></script>
     
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
     <script type="text/javascript">
@@ -21,7 +22,14 @@
 
 
 <body class = "centered">
-    <div class ="card" style="width:50vw">
+    <nav class="top-nav">
+        <div class="auth-links">
+            <a href="/login" id="login-link">Login/Signup</a>
+            <a href="/index" id = "home-link" style="display: inline;">Home</a>
+            <a href="/account" id="account-link" style="display: none;">My Account</a>
+        </div>
+    </nav>
+    <div class ="card lowered" style="width:50vw">
         <header class="page-header">
             <div class="centered">
                 <h1>{{ event }}</h1>
diff --git a/frontend_microservice/templates/create.html b/frontend_microservice/templates/create.html
index c6e5867..7b8a0fb 100644
--- a/frontend_microservice/templates/create.html
+++ b/frontend_microservice/templates/create.html
@@ -9,13 +9,21 @@
         <link rel="stylesheet" href="{{ url_for('static', filename='calendar-styles.css') }}">
         <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
         <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
+        <script src="{{ url_for('static', filename='auth.js') }}"></script>
         
         <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
     </head>
 
 <body class = "centered">
+    <nav class="top-nav">
+        <div class="auth-links">
+            <a href="/login" id="login-link">Login/Signup</a>
+            <a href="/index" id = "home-link" style="display: inline;">Home</a>
+            <a href="/account" id="account-link" style="display: none;">My Account</a>
+        </div>
+    </nav>
     
-    <div class="card">
+    <div class="card lowered">
         <!-- TODO: Set up a grid container for this section -->
         <div class="event_title">
             <input type="text" id="event_name" placeholder="ENTER NAME OF EVENT HERE"></input>
@@ -51,9 +59,10 @@
                 </div>
             </div>
         </div>
-    </div>
+    
 
-    <button id="send-dates" onclick="sendDates()">Save Dates</button>
+        <button id="send-dates" onclick="sendDates()">Save Dates</button>
+    </div>
     <script src="{{ url_for('static', filename='script.js') }}"></script>
 </body>
 </html>
\ No newline at end of file
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index eb0bc4e..b29a1af 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -5,30 +5,40 @@
     <title>Document</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="{{ url_for('static', filename='auth.js') }}"></script>
     
 </head>
 <body class="centered">
+    <nav class="top-nav">
+        <div class="auth-links">
+            <a href="/login" id="login-link">Login/Signup</a>
+            <a href="/index" id = "home-link" style="display: inline;">Home</a>
+            <a href="/account" id="account-link" style="display: none;">My Account</a>
+        </div>
+    </nav>
+
     <script>
         var availabilities = {{ availabilities | tojson }};
         console.log(availabilities);
         var dates = {{dates | tojson }};
         var event_id = {{id | tojson}};
     </script>
+    <div class="lowered centered">
+        <h1>'{{ event }}' Summary</h1>
+        <a href="/share_link/{{ id }}"> Copy the event link </a>
+        <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
+        <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
 
-    <h1>'{{ event }}' Summary</h1>
-    <a href="/share_link/{{ id }}"> Copy the event link </a>
-    <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
-    <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
-
-    <div id="best_dates">
-        <h2 id="date_heading"></h2>
-        <div id="date_container"></div>
-    </div>
+        <div id="best_dates">
+            <h2 id="date_heading"></h2>
+            <div id="date_container"></div>
+        </div>
 
 
-    <div id="summary_block" class="card">
-        <h2>Availabilities</h2>
-        <a href="/availability/{{ id }}"> Enter your availabilities </a>
+        <div id="summary_block" class="card">
+            <h2>Availabilities</h2>
+            <a href="/availability/{{ id }}"> Enter your availabilities </a>
+        </div>
     </div>
 
 
diff --git a/frontend_microservice/templates/index.html b/frontend_microservice/templates/index.html
index 695e900..22c2515 100644
--- a/frontend_microservice/templates/index.html
+++ b/frontend_microservice/templates/index.html
@@ -9,10 +9,11 @@
         <nav class="top-nav">
             <div class="auth-links">
                 <a href="/login" id="login-link">Login/Signup</a>
+                <a href="/index" id = "home-link" style="display: inline;">Home</a>
                 <a href="/account" id="account-link" style="display: none;">My Account</a>
             </div>
         </nav>
-        <div class="centered">
+        <div class="centered lowered">
             <div class="card">
                 <h1>Welcome</h1>
                 <div class="card internal">
diff --git a/frontend_microservice/templates/login.html b/frontend_microservice/templates/login.html
index 8b2f8c2..80b8c9c 100644
--- a/frontend_microservice/templates/login.html
+++ b/frontend_microservice/templates/login.html
@@ -5,10 +5,18 @@
     <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
     <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+    <script src="{{ url_for('static', filename='auth.js') }}"></script>
 
 </head>
 <body class="centered">
-    <div class="card">
+    <nav class="top-nav">
+        <div class="auth-links">
+            <a href="/login" id="login-link">Login/Signup</a>
+            <a href="/index" id = "home-link" style="display: inline;">Home</a>
+            <a href="/account" id="account-link" style="display: none;">My Account</a>
+        </div>
+    </nav>
+    <div class="card lowered">
         <h2>Login</h2>
         <form id="loginForm" class="card internal">
             <div class="form-group">
diff --git a/frontend_microservice/templates/register.html b/frontend_microservice/templates/register.html
index ef1d239..68a4610 100644
--- a/frontend_microservice/templates/register.html
+++ b/frontend_microservice/templates/register.html
@@ -4,9 +4,17 @@
     <title>Register</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+    <script src="{{ url_for('static', filename='auth.js') }}"></script>
 </head>
 <body class="centered">
-    <div class="card">
+    <nav class="top-nav">
+        <div class="auth-links">
+            <a href="/login" id="login-link">Login/Signup</a>
+            <a href="/index" id = "home-link" style="display: inline;">Home</a>
+            <a href="/account" id="account-link" style="display: none;">My Account</a>
+        </div>
+    </nav>
+    <div class="card lowered">
         <h2>Register</h2>
         <form id="registerForm" class="card internal">
             <div class="form-group">
-- 
GitLab


From 7e6dabee0db8f69595aba0ba6f2234d9a6e52cc2 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Mon, 28 Apr 2025 10:01:21 +0100
Subject: [PATCH 040/124] Fix page titles

---
 frontend_microservice/templates/account.html       | 1 +
 frontend_microservice/templates/accountEvents.html | 1 +
 frontend_microservice/templates/eventSummary.html  | 2 +-
 3 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/frontend_microservice/templates/account.html b/frontend_microservice/templates/account.html
index 8b0f2b1..74c9442 100644
--- a/frontend_microservice/templates/account.html
+++ b/frontend_microservice/templates/account.html
@@ -4,6 +4,7 @@
         <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
         <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
         <script src="{{ url_for('static', filename='auth.js') }}"></script>
+        <title>Account</title>
     </head>
     <body>
         <nav class="top-nav">
diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
index e1c6842..d817d1c 100644
--- a/frontend_microservice/templates/accountEvents.html
+++ b/frontend_microservice/templates/accountEvents.html
@@ -8,6 +8,7 @@
             const events = {{event | tojson}};
             console.log(events)
         </script>
+        <title>Account Events</title>
     </head>
     <body>
         <nav class="top-nav">
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index b29a1af..5c090e2 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -2,7 +2,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Document</title>
+    <title>EventSummary</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
     <script src="{{ url_for('static', filename='auth.js') }}"></script>
-- 
GitLab


From 194b36ec3fc7e91984825f87962b1b2edafb0de6 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Mon, 28 Apr 2025 10:20:29 +0100
Subject: [PATCH 041/124] fix eventSummary page

---
 .../templates/eventSummary.html               | 75 +++++++++----------
 1 file changed, 36 insertions(+), 39 deletions(-)

diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 03c4bee..2d1a32f 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -23,55 +23,52 @@
         var dates = {{dates | tojson }};
         var event_id = {{id | tojson}};
     </script>
+
     <div class="lowered centered">
         <h1>'{{ event }}' Summary</h1>
-        <a href="/share_link/{{ id }}"> Copy the event link </a>
-        <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
-        <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
-
-    <h1>'{{ event }}' Summary</h1>
 
-    <div class="centered">
-        <div class="card">
-            <h2>Here's the link to share for {{event}}</h2>
-            <div class="link-input-button">
-                <input type="text" class="link-input" id="link" value="{{link}}" readonly>
-                <button class="copy" onclick="copyToClipboard()">Copy</button>
+        <div class="centered">
+            <div class="card">
+                <h2>Here's the link to share for {{event}}</h2>
+                <div class="link-input-button">
+                    <input type="text" class="link-input" id="link" value="{{link}}" readonly>
+                    <button class="copy" onclick="copyToClipboard()">Copy</button>
+                </div>
             </div>
         </div>
-    </div>
-    
+        
 
-    <script>
-        function copyToClipboard() {
-          var copyText = document.getElementById("link");
-    
-          copyText.select();
-          copyText.setSelectionRange(0, 99999); // For mobile devices
-    
-          // Use the Clipboard API
-          navigator.clipboard.writeText(copyText.value)
-            .then(function() {
-              alert("Text copied to clipboard!");
-            })
-            .catch(function(error) {
-              alert("Failed to copy text: " + error);
-            });
-        }
-    </script>
+        <script>
+            function copyToClipboard() {
+            var copyText = document.getElementById("link");
+        
+            copyText.select();
+            copyText.setSelectionRange(0, 99999); // For mobile devices
+        
+            // Use the Clipboard API
+            navigator.clipboard.writeText(copyText.value)
+                .then(function() {
+                alert("Text copied to clipboard!");
+                })
+                .catch(function(error) {
+                alert("Failed to copy text: " + error);
+                });
+            }
+        </script>
 
-    <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
-    <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
+        <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
+        <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
 
-    <div id="best_dates">
-        <h2 id="date_heading"></h2>
-        <div id="date_container"></div>
-    </div>
+        <div id="best_dates">
+            <h2 id="date_heading"></h2>
+            <div id="date_container"></div>
+        </div>
 
 
-        <div id="summary_block" class="card">
-            <h2>Availabilities</h2>
-            <a href="/availability/{{ id }}"> Enter your availabilities </a>
+            <div id="summary_block" class="card">
+                <h2>Availabilities</h2>
+                <a href="/availability/{{ id }}"> Enter your availabilities </a>
+            </div>
         </div>
     </div>
 
-- 
GitLab


From 22e5434756c40c2df8609b00d30348095c87e39a Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Mon, 28 Apr 2025 11:46:05 +0100
Subject: [PATCH 042/124] change email service;smtp

---
 docker-compose.yml | 26 ++++++++------------------
 1 file changed, 8 insertions(+), 18 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 0a0583d..21aceb3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,14 +1,5 @@
 services:
-  mailpit:
-    image: axllent/mailpit:latest 
-    container_name: mailpit
-    ports:
-    - "1025:1025" 
-    - "8025:8025" 
-    networks:
-    - backend 
-    restart: unless-stopped
-    
+
   event_service:
     build:
       context: event_microservice
@@ -48,14 +39,13 @@ services:
       - backend
       - loadbalancing
     environment:
-      - PORT=5003
-      - MAIL_SERVER=mailpit # Point to the mailpit service name
-      - MAIL_PORT=1025      # Mailpit's default SMTP port
-      - MAIL_USE_TLS=false  # Mailpit doesn't use TLS by default
-      - MAIL_USERNAME=      # No username needed for Mailpit by default
-      - MAIL_PASSWORD=      # No password needed for Mailpit by default
-      - MAIL_DEFAULT_SENDER=noreply@gregc.com # Can be anything
-      - SECRET_KEY=your_very_long_random_and_unguessable_string
+      - MAIL_SERVER=smtp.gmail.com
+      - MAIL_PORT=587
+      - MAIL_USE_TLS=true   
+      - MAIL_USERNAME=gregcmailer@gmail.com
+      - MAIL_PASSWORD=bkxu hkqq wiax fuke
+      - MAIL_DEFAULT_SENDER=gregcmailer@gmail.com
+      - SECRET_KEY=f361e60c60f0aaf062ba9b9fc727f08eb525ad93ac715e012b224c0506596025
 
   nginx:
       image: nginx
-- 
GitLab


From 1a01fb905fd1bc27a4162b500fc4cb81f713a29c Mon Sep 17 00:00:00 2001
From: "Singh, Siddhartha (PG/T - Comp Sci & Elec Eng)" <sk03140@surrey.ac.uk>
Date: Wed, 30 Apr 2025 21:33:56 +0000
Subject: [PATCH 043/124] add favicon

---
 frontend_microservice/templates/forgot_password.html | 1 +
 frontend_microservice/templates/register.html        | 1 +
 frontend_microservice/templates/reset_password.html  | 1 +
 3 files changed, 3 insertions(+)

diff --git a/frontend_microservice/templates/forgot_password.html b/frontend_microservice/templates/forgot_password.html
index fc25474..abc8be5 100644
--- a/frontend_microservice/templates/forgot_password.html
+++ b/frontend_microservice/templates/forgot_password.html
@@ -3,6 +3,7 @@
 <head>
     <title>Forgot Password</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
+    <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <script src="{{ url_for('static', filename='auth.js') }}" defer></script>
 </head>
diff --git a/frontend_microservice/templates/register.html b/frontend_microservice/templates/register.html
index 68a4610..3463696 100644
--- a/frontend_microservice/templates/register.html
+++ b/frontend_microservice/templates/register.html
@@ -3,6 +3,7 @@
 <head>
     <title>Register</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
+    <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <script src="{{ url_for('static', filename='auth.js') }}"></script>
 </head>
diff --git a/frontend_microservice/templates/reset_password.html b/frontend_microservice/templates/reset_password.html
index f30a0ee..0eee3a9 100644
--- a/frontend_microservice/templates/reset_password.html
+++ b/frontend_microservice/templates/reset_password.html
@@ -3,6 +3,7 @@
 <head>
     <title>Reset Password</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='auth.css') }}">
+    <link rel="icon" href="{{ url_for('static', filename='favicon.PNG') }}" type="image/png">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <script src="{{ url_for('static', filename='auth.js') }}" defer></script>
 </head>
-- 
GitLab


From 75bfafe98e6611de48f38b01bed299046541225a Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Fri, 2 May 2025 16:04:12 +0100
Subject: [PATCH 044/124] improve element sizing and rescaling

---
 frontend_microservice/static/styles.css       | 21 ++++++++++++++++---
 .../templates/eventSummary.html               | 12 ++++++-----
 2 files changed, 25 insertions(+), 8 deletions(-)

diff --git a/frontend_microservice/static/styles.css b/frontend_microservice/static/styles.css
index ae41260..2bee7e0 100644
--- a/frontend_microservice/static/styles.css
+++ b/frontend_microservice/static/styles.css
@@ -186,11 +186,26 @@ a:active{
     flex-wrap: wrap;
 }
 /* https://www.w3schools.com/cssref/atrule_media.php */
+/*
 @media only screen and (max-width: 1800px) {
-    #summary_block {
-        width: 100%;
+    #summary_block{
+        width: 90%;
     }
-  }
+}
+    */
+
+/* These blocks set the width of the event summary cards to 80% of the screen width
+This enables the flex containers to move elements around depending on screen size */
+#summary_block, #best_dates, #link_container, #EventSummary_buttons{
+    width: 80%;
+}
+
+#best_time_button, #addEvent{
+    width: 70%;
+    font-size: large;
+    font-weight: bold;
+}
+/* Some other elements inside EventSummary have had their widths set using inline CSS */
 
 .fixed_h {
     height: 200px;
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 2d1a32f..61907b6 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -24,11 +24,11 @@
         var event_id = {{id | tojson}};
     </script>
 
-    <div class="lowered centered">
+    <div class="lowered centered" style="width: 100%;">
         <h1>'{{ event }}' Summary</h1>
 
-        <div class="centered">
-            <div class="card">
+        <div class="centered" style="width: 100%;">
+            <div class="card" id="link_container">
                 <h2>Here's the link to share for {{event}}</h2>
                 <div class="link-input-button">
                     <input type="text" class="link-input" id="link" value="{{link}}" readonly>
@@ -56,8 +56,10 @@
             }
         </script>
 
-        <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
-        <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
+        <div class = "card" id="EventSummary_buttons">
+            <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
+            <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
+        </div>
 
         <div id="best_dates">
             <h2 id="date_heading"></h2>
-- 
GitLab


From f553d3cf8ed3f774287309208f304a734b622fab Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Fri, 2 May 2025 16:20:44 +0100
Subject: [PATCH 045/124] rewrite some element text

---
 frontend_microservice/templates/eventSummary.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 61907b6..3e82539 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -25,7 +25,7 @@
     </script>
 
     <div class="lowered centered" style="width: 100%;">
-        <h1>'{{ event }}' Summary</h1>
+        <h1>{{ event }} Summary</h1>
 
         <div class="centered" style="width: 100%;">
             <div class="card" id="link_container">
@@ -57,7 +57,7 @@
         </script>
 
         <div class = "card" id="EventSummary_buttons">
-            <button id="best_time_button" onclick="getDates(availabilities)">PRESS HERE TO FIND YOUR BEST DATE</button>
+            <button id="best_time_button" onclick="getDates(availabilities)">Press here to find the best date to meet!</button>
             <button id="addEvent" onclick="addEventToAccount(event_id)">Add {{event}} to your Account</button>
         </div>
 
-- 
GitLab


From 60d34d988a9b3af8e276ae79f712dafdbea827d3 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Fri, 2 May 2025 16:37:11 +0100
Subject: [PATCH 046/124] improve CSS on eventSummary

---
 frontend_microservice/static/auth.css             |  4 ----
 frontend_microservice/static/styles.css           | 14 +++++++++++---
 frontend_microservice/templates/eventSummary.html |  2 +-
 3 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/frontend_microservice/static/auth.css b/frontend_microservice/static/auth.css
index 3e5c97c..f3b3f9c 100644
--- a/frontend_microservice/static/auth.css
+++ b/frontend_microservice/static/auth.css
@@ -72,10 +72,6 @@ h2 {
     font-weight: bold;
 }
 
-.auth-links a:hover {
-    text-decoration: underline;
-}
-
 #message {
     text-align: center;
     margin-top: 1rem;
diff --git a/frontend_microservice/static/styles.css b/frontend_microservice/static/styles.css
index 2bee7e0..40a8294 100644
--- a/frontend_microservice/static/styles.css
+++ b/frontend_microservice/static/styles.css
@@ -26,6 +26,7 @@ ul{
 
 button{
     color: #fff;
+    font-weight: bold;
     display: inline-block;
     background-color: midnightblue;
     width: 300px;
@@ -39,7 +40,7 @@ button{
 }
 button:hover{
     /*background-color: rgb(38, 38, 172);*/
-    background-color: #3C5A99;
+    background-color: rgb(38, 38, 172);
 }
 button:active{
     background-color: hsl(270, 4%, 90%);
@@ -52,6 +53,11 @@ button:active{
     color: black
 }
 
+.copy:hover{
+    color: #fff;
+}
+
+
 hr{
     border-color: midnightblue;
     border-style: dotted;
@@ -105,6 +111,7 @@ form{
 
 a{
     color: white;
+    font-weight: bold;
     display: block;
     background-color: midnightblue;
     padding: 10px;
@@ -200,8 +207,9 @@ This enables the flex containers to move elements around depending on screen siz
     width: 80%;
 }
 
-#best_time_button, #addEvent{
+#best_time_button, #addEvent, #availabilities_a_tag{
     width: 70%;
+    height: max-content;
     font-size: large;
     font-weight: bold;
 }
@@ -236,7 +244,7 @@ This enables the flex containers to move elements around depending on screen siz
 }
 
 .auth-links a:hover {
-    background-color: midnightblue;
+    background-color: rgb(38, 38, 172);
 }
 
 
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 3e82539..4bed6a5 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -69,7 +69,7 @@
 
             <div id="summary_block" class="card">
                 <h2>Availabilities</h2>
-                <a href="/availability/{{ id }}"> Enter your availabilities </a>
+                <a href="/availability/{{ id }}" id="availabilities_a_tag"> Enter your availabilities </a>
             </div>
         </div>
     </div>
-- 
GitLab


From 96686482b88e13ba6cb32b8c6ad33812398aa883 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Mon, 5 May 2025 15:20:48 +0100
Subject: [PATCH 047/124] Add rescaling UI elements in accountEvents page

---
 frontend_microservice/templates/accountEvents.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/frontend_microservice/templates/accountEvents.html b/frontend_microservice/templates/accountEvents.html
index d817d1c..c4e2823 100644
--- a/frontend_microservice/templates/accountEvents.html
+++ b/frontend_microservice/templates/accountEvents.html
@@ -19,10 +19,10 @@
             </div>
         </nav>
         <div class="centered lowered">
-            <div class="card">
+            <div class="card" style="width: 80%;">
                 <h1>These are your events</h1>
                 <h3>Click to see the details</h3>
-                <div class="card internal">
+                <div class="card internal" style="width: 80%;">
                     <div id="event_block">
                     </div>
                 </div>
-- 
GitLab


From b5f82c2dbb29a1fd70e77a53a7ade87302642833 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 13 May 2025 16:07:56 +0100
Subject: [PATCH 048/124] add datefinder service to docker-compose.tmpl and
 change nginx port mapping back to 80:80

---
 .gitlab-ci.yml      |  5 +++++
 docker-compose.tmpl | 29 +++++++++++++++++++++++++++--
 2 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7fb6c09..84ad3e5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -61,6 +61,11 @@ publish:  # Publish containers to gitlab container registry
     - docker push $TAG_COMMIT-account-ms
     - docker push $TAG_LATEST-account-ms
 
+    # Build and push datefinder MS
+    - docker build -f ./datefinder_microservice/Dockerfile-datefinder -t $TAG_COMMIT-datefinder-ms -t $TAG_LATEST-datefinder-ms ./datefinder_microservice
+    - docker push $TAG_COMMIT-datefinder-ms
+    - docker push $TAG_LATEST-datefinder-ms
+
     # Build and push event DB
     - docker build -f ./event_database/Dockerfile -t $TAG_COMMIT-event-db -t $TAG_LATEST-event-db ./event_database
     - docker push $TAG_COMMIT-event-db
diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 8142f7f..a6736e6 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -34,7 +34,14 @@ services:
       - backend
       - loadbalancing
     environment:
-      - PORT=5003
+      - MAIL_SERVER=smtp.gmail.com
+      - MAIL_PORT=587
+      - MAIL_USE_TLS=true   
+      - MAIL_USERNAME=gregcmailer@gmail.com
+      - MAIL_PASSWORD=bkxu hkqq wiax fuke
+      - MAIL_DEFAULT_SENDER=gregcmailer@gmail.com
+      - SECRET_KEY=f361e60c60f0aaf062ba9b9fc727f08eb525ad93ac715e012b224c0506596025
+
 
   nginx:
     image: nginx
@@ -42,7 +49,7 @@ services:
       - ./nginx:/etc/nginx/conf.d
       #- ./frontend_microservice/templates:/usr/share/nginx/html
     ports:
-        - "5435:80"
+        - "80:80"
     networks:
     - loadbalancing
     depends_on:
@@ -59,6 +66,24 @@ services:
     networks:
       - backend
 
+  datefinder:
+    image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
+    depends_on:
+      - postgres
+    deploy:
+      replicas: 2
+      restart_policy:
+        condition: on-failure
+        delay: 1s
+        max_attempts: 3
+        window: 120s
+    networks:
+      - backend
+      - loadbalancing
+    environment:
+      - PORT=6000
+
+
   postgres:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-db
     container_name: event_database
-- 
GitLab


From d4a0630f3b8a819a43e15edd90f688a96d41b79e Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 13 May 2025 16:09:47 +0100
Subject: [PATCH 049/124] allow (temporary) publishing and deployment on
 feature/deployment_update branch

---
 .gitlab-ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 84ad3e5..aff712f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
+    - feature/deployment_update
   tags:
     - deployment
   variables:
@@ -84,6 +85,7 @@ deploy:
   stage: deploy
   only:
     - main
+    - feature/deployment_update
   tags:
     - deployment
   environment:
-- 
GitLab


From 02c1264b888948a5f7f63f28b8854ed8c4e6a377 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 13 May 2025 15:26:10 +0000
Subject: [PATCH 050/124] retry deployment having deactivated port 80

---
 docker-compose.tmpl | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index a6736e6..81b521c 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -1,6 +1,5 @@
 version: "3.8"
 
-
 services:
   event_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-ms
-- 
GitLab


From 3c1189467028b0653b6710509626ace1204b70ba Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 13 May 2025 15:52:17 +0000
Subject: [PATCH 051/124] restrict deployment to only main branch

---
 .gitlab-ci.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index aff712f..84ad3e5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,6 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - feature/deployment_update
   tags:
     - deployment
   variables:
@@ -85,7 +84,6 @@ deploy:
   stage: deploy
   only:
     - main
-    - feature/deployment_update
   tags:
     - deployment
   environment:
-- 
GitLab


From a2d128074718c3e20a8a2046f7a4a7c5f5da4892 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Fri, 16 May 2025 15:12:31 +0100
Subject: [PATCH 052/124] fix issue, now gives sensible error if you try to add
 the event twice

---
 account_microservice/user.py           | 12 ++++++++----
 frontend_microservice/frontend.py      |  4 ++--
 frontend_microservice/static/script.js |  2 +-
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 216a81f..78855e7 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -172,12 +172,16 @@ def add_events():
 
     conn = get_db_connection()
     cursor = conn.cursor()
-    cursor.execute('INSERT INTO account_events (user_id, event_id) VALUES (%s, %s)', (user_id,event_id,))
+    try:
+        cursor.execute('INSERT INTO account_events (user_id, event_id) VALUES (%s, %s)', (user_id,event_id,))
 
-    cursor.close()
-    conn.close()
+        cursor.close()
+        conn.close()
 
-    return jsonify({"message": "Event added"}), 201
+        return jsonify({"message": "Event added"}), 201
+    except Exception as e:
+        logging.error(f"Add event error error: {str(e)}", exc_info=True)
+        return jsonify({"error": "Event already added"}), 300
 
 @app.route('/request_password_reset', methods=['POST'])
 @limiter.limit("3 per 15 minute") 
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index b0f475f..22f973e 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -102,7 +102,7 @@ def eventSummary(event_id):
 
         link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
 
-        # TODO: fix how this is being returned
+
         return render_template('eventSummary.html', id=event_id, link=link, event=event_name, dates=dates, availabilities=availabilities)
     
     if request.method == 'POST':
@@ -112,7 +112,7 @@ def eventSummary(event_id):
         if response.status_code == 201:  # Check if was added successfully
             return jsonify({'message': 'Event added'})
         else:
-            return f"Error: {response.text}", response.status_code
+            return jsonify({'message':response.text})
     
     return render_template('eventSummary.html')    
 
diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index aed9c50..285b458 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -416,7 +416,7 @@ function addEventToAccount(event_id){
             contentType: 'application/json', 
             data: JSON.stringify({'event_id':event_id, 'user_id':userId}),
             success: function(response) {
-                alert("Added to your Account")
+                alert(response['message'])
             },
             error: function(xhr, status, error) {
                 alert("Error: " + error);
-- 
GitLab


From ee32022eb287064f6d3c57daadc9ce6a0ddadc12 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 16:49:34 +0100
Subject: [PATCH 053/124] make database persistent

---
 docker-compose.tmpl | 15 +++++++--------
 docker-compose.yml  | 15 +++++++--------
 2 files changed, 14 insertions(+), 16 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 81b521c..d1b50f2 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -90,9 +90,8 @@ services:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
       POSTGRES_DB: event
-    #volumes:
-      #- postgres-db-volume:/var/lib/postgresql/data
-      #- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
+    volumes:
+      - event-db-volume:/var/lib/postgresql/data
     ports:
       - 5436:5436
     networks:
@@ -112,9 +111,8 @@ services:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
       POSTGRES_DB: account
-    #volumes:
-      #- postgres-db-volume:/var/lib/postgresql/data
-      #- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
+    volumes:
+      - account-db-volume:/var/lib/postgresql/data 
     ports:
       - 5437:5437
     networks:
@@ -127,8 +125,9 @@ services:
     restart: always
     command: -p 5437
 
-#volumes:
-  #postgres-db-volume:
+volumes:
+  event-db-volume:
+  account-db-volume:
 networks:
   #frontend:
     #driver: bridge
diff --git a/docker-compose.yml b/docker-compose.yml
index 21aceb3..c429d51 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -103,9 +103,8 @@ services:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
       POSTGRES_DB: event
-    #volumes:
-      #- postgres-db-volume:/var/lib/postgresql/data
-      #- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
+    volumes:
+      - event-db-volume:/var/lib/postgresql/data
     ports:
       - 5436:5436
     networks:
@@ -127,9 +126,8 @@ services:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
       POSTGRES_DB: account
-    #volumes:
-      #- postgres-db-volume:/var/lib/postgresql/data
-      #- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
+    volumes:
+      - account-db-volume:/var/lib/postgresql/data  
     ports:
       - 5437:5437
     networks:
@@ -142,8 +140,9 @@ services:
     restart: always
     command: -p 5437
 
-#volumes:
-  #postgres-db-volume:
+volumes:
+  event-db-volume:
+  account-db-volume:
 networks:
   #frontend:
     #driver: bridge
-- 
GitLab


From b79f1db85aae0f83a6d5f63719b3b6b197a2a393 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 17:33:01 +0100
Subject: [PATCH 054/124] move nginx config to main config location (as opposed
 to secondary configs in conf.d)

---
 nginx/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/nginx/Dockerfile b/nginx/Dockerfile
index a63d974..8e9e432 100644
--- a/nginx/Dockerfile
+++ b/nginx/Dockerfile
@@ -1,6 +1,6 @@
 FROM nginx:stable-alpine
 
-COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY nginx.conf /etc/nginx/nginx.conf
 
 EXPOSE 80
 
-- 
GitLab


From 38a68f083d3475665f9f855dbca8c84a198708cc Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 17:34:47 +0100
Subject: [PATCH 055/124] use existing nginx image from Dockerfile instead of
 creating new one

---
 docker-compose.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docker-compose.yml b/docker-compose.yml
index 21aceb3..d8bb894 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -48,6 +48,9 @@ services:
       - SECRET_KEY=f361e60c60f0aaf062ba9b9fc727f08eb525ad93ac715e012b224c0506596025
 
   nginx:
+      build:
+        context: ./nginx
+        dockerfile: Dockerfile
       image: nginx
       volumes:
         - ./nginx:/etc/nginx/conf.d
-- 
GitLab


From 93c94a496acaca6c5f8cea252303b80532d5c9ef Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:26:06 +0100
Subject: [PATCH 056/124] revert back to using nginx.conf as secondary config

---
 nginx/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/nginx/Dockerfile b/nginx/Dockerfile
index 8e9e432..d146212 100644
--- a/nginx/Dockerfile
+++ b/nginx/Dockerfile
@@ -1,6 +1,6 @@
 FROM nginx:stable-alpine
 
-COPY nginx.conf /etc/nginx/nginx.conf
+COPY nginx.conf /etc/nginx/conf.d/nginx.conf
 
 EXPOSE 80
 
-- 
GitLab


From bdca32de1871f0f35a316a54766ac82aaa077b90 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:26:52 +0100
Subject: [PATCH 057/124] add frontend to loadbalancing network

---
 docker-compose.yml | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index d8bb894..93eef1f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -51,7 +51,6 @@ services:
       build:
         context: ./nginx
         dockerfile: Dockerfile
-      image: nginx
       volumes:
         - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
@@ -68,13 +67,12 @@ services:
     build:
       context: frontend_microservice
       dockerfile: Dockerfile-frontend
-    ports:
-      - "8000:5001"
     depends_on:
       - postgres
       - postgres_ac
     networks:
       - backend
+      - loadbalancing
 
   datefinder:
     build:
-- 
GitLab


From 74eaafd41d240581bdf388f4fffc293537448da2 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:27:13 +0100
Subject: [PATCH 058/124] attempt to add reverse proxy for frontend

---
 nginx/nginx.conf | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 00db418..ff68762 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -10,6 +10,10 @@ upstream datefinder {
     server datefinder:6000 ;    
 }
 
+upstream frontend {
+    server frontend:5001;
+}
+
 server {
     listen 80;
 
@@ -17,6 +21,15 @@ server {
     
     include /etc/nginx/mime.types;
 
+    location / {
+        proxy_pass http://frontend;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+    
     location /create_event {
         proxy_pass http://event_service/create_event;
     }
-- 
GitLab


From 2c3982b14002adb3688c0b0d8ef6a658b3fee0a7 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:28:12 +0100
Subject: [PATCH 059/124] temporarily use only one replica for debugging

---
 docker-compose.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 93eef1f..33f6564 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,7 +7,7 @@ services:
     #ports:
       #- "5002:5002"
     deploy:
-      replicas: 2
+      replicas: 1
       restart_policy:
         condition: on-failure
         delay: 1s
@@ -25,7 +25,7 @@ services:
       context: account_microservice
       dockerfile: Dockerfile
     deploy:
-      replicas: 2
+      replicas: 1
       restart_policy:
         condition: on-failure
         delay: 1s
@@ -83,7 +83,7 @@ services:
     depends_on:
       - postgres
     deploy:
-      replicas: 2
+      replicas: 1
       restart_policy:
         condition: on-failure
         delay: 1s
-- 
GitLab


From 18440bbbe9ba7383be8e32bed7121ac8dd90c076 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:45:54 +0100
Subject: [PATCH 060/124] update docker-compose template to match

---
 docker-compose.tmpl | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 81b521c..785e565 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -57,20 +57,19 @@ services:
 
   frontend:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-frontend-ms
-    ports:
-      - "8000:5001"
     depends_on:
       - postgres
       - postgres_ac
     networks:
       - backend
+      - loadbalancing
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
     depends_on:
       - postgres
     deploy:
-      replicas: 2
+      replicas: 1
       restart_policy:
         condition: on-failure
         delay: 1s
-- 
GitLab


From f8497f80199f13e3c348df32dba98d4093678b9b Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:49:17 +0100
Subject: [PATCH 061/124] test deployment

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 84ad3e5..cd2420f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -84,6 +84,7 @@ deploy:
   stage: deploy
   only:
     - main
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From dccd2f08933b5e8cc85a9ec749154f6662179874 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Sun, 18 May 2025 23:51:37 +0100
Subject: [PATCH 062/124] actually test deployment (allow publish and deploy
 stages in .gitlab-ci.yml)

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cd2420f..3fed445 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
-- 
GitLab


From 841d7308a4fd627c3d9abfbd934fcfef21c1d08e Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 00:02:00 +0100
Subject: [PATCH 063/124] add container names for readability

---
 docker-compose.tmpl | 5 +++++
 docker-compose.yml  | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 785e565..e627844 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -3,6 +3,7 @@ version: "3.8"
 services:
   event_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-ms
+    container_name: event_ms
     deploy:
       replicas: 1
       restart_policy:
@@ -20,6 +21,7 @@ services:
 
   account_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-ms
+    container_name: acount_ms
     deploy:
       replicas: 1
       restart_policy:
@@ -44,6 +46,7 @@ services:
 
   nginx:
     image: nginx
+    container_name: nginx_container
     volumes:
       - ./nginx:/etc/nginx/conf.d
       #- ./frontend_microservice/templates:/usr/share/nginx/html
@@ -57,6 +60,7 @@ services:
 
   frontend:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-frontend-ms
+    container_name: frontend_ms
     depends_on:
       - postgres
       - postgres_ac
@@ -66,6 +70,7 @@ services:
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
+    container_name: datefinder_ms
     depends_on:
       - postgres
     deploy:
diff --git a/docker-compose.yml b/docker-compose.yml
index 33f6564..24099b9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,6 +4,7 @@ services:
     build:
       context: event_microservice
       dockerfile: Dockerfile-backend
+    container_name: event_ms
     #ports:
       #- "5002:5002"
     deploy:
@@ -24,6 +25,7 @@ services:
     build:
       context: account_microservice
       dockerfile: Dockerfile
+    container_name: acount_ms
     deploy:
       replicas: 1
       restart_policy:
@@ -51,6 +53,7 @@ services:
       build:
         context: ./nginx
         dockerfile: Dockerfile
+      container_name: nginx_container
       volumes:
         - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
@@ -67,6 +70,7 @@ services:
     build:
       context: frontend_microservice
       dockerfile: Dockerfile-frontend
+    container_name: frontend_ms
     depends_on:
       - postgres
       - postgres_ac
@@ -78,6 +82,7 @@ services:
     build:
       context: datefinder_microservice
       dockerfile: Dockerfile-datefinder
+    container_name: datefinder_ms
     #ports:
     #  - "6000:6000"
     depends_on:
-- 
GitLab


From 7404051236132075f6532419f01fe060c57ebd4f Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 00:43:46 +0100
Subject: [PATCH 064/124] fix typo

---
 docker-compose.tmpl | 2 +-
 docker-compose.yml  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index e627844..9ac16af 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -21,7 +21,7 @@ services:
 
   account_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-ms
-    container_name: acount_ms
+    container_name: account_ms
     deploy:
       replicas: 1
       restart_policy:
diff --git a/docker-compose.yml b/docker-compose.yml
index 24099b9..5cd384d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -25,7 +25,7 @@ services:
     build:
       context: account_microservice
       dockerfile: Dockerfile
-    container_name: acount_ms
+    container_name: account_ms
     deploy:
       replicas: 1
       restart_policy:
-- 
GitLab


From bd3775fd01da5b4f2eeede01d82ebcac88f7367c Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 00:53:58 +0100
Subject: [PATCH 065/124] use prebuilt nginx image

---
 docker-compose.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 9ac16af..1370c62 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -45,7 +45,7 @@ services:
 
 
   nginx:
-    image: nginx
+    image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-nginx
     container_name: nginx_container
     volumes:
       - ./nginx:/etc/nginx/conf.d
-- 
GitLab


From 2a554ace1b5b3e12449ac939065a6d290f5df9aa Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 00:59:40 +0100
Subject: [PATCH 066/124] build and publish nginx image

---
 .gitlab-ci.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3fed445..3cc7e0f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -76,6 +76,11 @@ publish:  # Publish containers to gitlab container registry
     - docker build -f ./account_database/Dockerfile -t $TAG_COMMIT-account-db -t $TAG_LATEST-account-db ./account_database
     - docker push $TAG_COMMIT-account-db
     - docker push $TAG_LATEST-account-db
+
+    # Build and push nginx container
+    - docker build -f ./nginx/Dockerfile -t $TAG_COMMIT-nginx -t $TAG_LATEST-nginx ./nginx
+    - docker push $TAG_COMMIT-nginx
+    - docker push $TAG_LATEST-nginx
     
 
 
-- 
GitLab


From 52f1ffbc5c58dbebb9d8b21a253fc1f48e6108d4 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 01:00:27 +0100
Subject: [PATCH 067/124] change back to default.conf because that just works

---
 nginx/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/nginx/Dockerfile b/nginx/Dockerfile
index d146212..a63d974 100644
--- a/nginx/Dockerfile
+++ b/nginx/Dockerfile
@@ -1,6 +1,6 @@
 FROM nginx:stable-alpine
 
-COPY nginx.conf /etc/nginx/conf.d/nginx.conf
+COPY nginx.conf /etc/nginx/conf.d/default.conf
 
 EXPOSE 80
 
-- 
GitLab


From 3c47081b3367e2a94ba2220c462ddbe2cedcfe86 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 01:01:45 +0100
Subject: [PATCH 068/124] remove all images not listed in the
 docker-compose.yml (--remove-orphans)

---
 .gitlab-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3cc7e0f..c40868b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -114,4 +114,4 @@ deploy:
     - echo "$DOCKERHUB_ACCESS_TOKEN" | ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker login -u $DOCKERHUB_USER --password-stdin"
 
     # Run docker-compose
-    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker-compose down && docker-compose up -d"  #"docker-compose down && docker-compose up -d"
+    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker-compose down --remove-orphans && docker-compose up -d"  #"docker-compose down && docker-compose up -d"
-- 
GitLab


From e5c615ecd46bfff53629d92317e9e8ee02db3f2d Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 15:37:09 +0100
Subject: [PATCH 069/124] create JS test in gitlab ci yml

---
 .gitlab-ci.yml | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 84ad3e5..fd64783 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,7 +11,7 @@ variables:
   TAG_COMMIT: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA   # docker image tag for current commit
 
 
-unit_tests:
+python_tests:
   image: python:3.9
   stage: test
   tags:
@@ -24,6 +24,13 @@ unit_tests:
     - echo "Running unittest for event_finder_test.py"
     - python -m unittest tests/event_finder_test.py
 
+javascript_tests:
+  stage: test
+  image: node:18
+  before_script:
+    - npm ci  # Like npm install but witha few changes for automated env
+  script:
+    - npm test
 
 
 publish:  # Publish containers to gitlab container registry
-- 
GitLab


From d65042bf1e12a4d164e85dbec87cb99dd483f6ab Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 15:37:56 +0100
Subject: [PATCH 070/124] make sendDates function accessible to Jest

---
 frontend_microservice/static/script.js | 4 +++-
 tests/sendDates.test.js                | 2 ++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 285b458..d25fc99 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -427,4 +427,6 @@ function addEventToAccount(event_id){
         alert("You're not logged in ... sending to login page")
         window.location.href = '/login';
     }
-}
\ No newline at end of file
+}
+
+module.exports = { sendDates };
\ No newline at end of file
diff --git a/tests/sendDates.test.js b/tests/sendDates.test.js
index bf7a548..a8159e5 100644
--- a/tests/sendDates.test.js
+++ b/tests/sendDates.test.js
@@ -15,6 +15,8 @@ global.$ = {
 };
 
 const { sendDates } = require('../frontend_microservice/static/script')
+// import { sendDates } from '../frontend_microservice/static/script.js';
+
 
 
 describe("sendDates function", () => {
-- 
GitLab


From 6401b21d4c8a8f127e7150d4f064b47ee18cf015 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 15:40:16 +0100
Subject: [PATCH 071/124] ignore changes to node_modules folder

---
 .gitignore | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index a26b269..9a64afd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 __pycache__/
 *.pyc
 *.pyo
-.idea/*
\ No newline at end of file
+.idea/*
+node_modules/
\ No newline at end of file
-- 
GitLab


From 990e0f7dac439dea84e6a6e2b6004fb146604348 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 14:42:21 +0000
Subject: [PATCH 072/124] add test tag to JS test job

---
 .gitlab-ci.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fd64783..b8b035e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -27,9 +27,12 @@ python_tests:
 javascript_tests:
   stage: test
   image: node:18
+  tags:
+    - test
   before_script:
     - npm ci  # Like npm install but witha few changes for automated env
   script:
+    - echo "Running JS unit tests"
     - npm test
 
 
-- 
GitLab


From 052730ecd8cb300863ea710b04f92d32f0ae7f69 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 17:08:07 +0100
Subject: [PATCH 073/124] prune dangling images on server

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c40868b..4eb8b12 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -113,5 +113,5 @@ deploy:
     # Login to DockerHub registry
     - echo "$DOCKERHUB_ACCESS_TOKEN" | ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker login -u $DOCKERHUB_USER --password-stdin"
 
-    # Run docker-compose
-    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker-compose down --remove-orphans && docker-compose up -d"  #"docker-compose down && docker-compose up -d"
+    # Run docker-compose (remove dangling images)
+    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "docker-compose down --remove-orphans && docker image prune -f && docker-compose up -d"
-- 
GitLab


From f9c022e6ad06a7fc4a609d448164e90c295aae0a Mon Sep 17 00:00:00 2001
From: sid <sk03140@surrey.ac.uk>
Date: Mon, 19 May 2025 18:12:45 +0100
Subject: [PATCH 074/124] add password input validation across login, register,
 and reset password forms

---
 frontend_microservice/templates/login.html    | 10 ++++++---
 frontend_microservice/templates/register.html | 10 ++++++---
 .../templates/reset_password.html             | 21 +++++++++++++------
 3 files changed, 29 insertions(+), 12 deletions(-)

diff --git a/frontend_microservice/templates/login.html b/frontend_microservice/templates/login.html
index d70bd90..f7a52c4 100644
--- a/frontend_microservice/templates/login.html
+++ b/frontend_microservice/templates/login.html
@@ -22,10 +22,14 @@
             <div class="form-group">
                 <label>Email:</label>
                 <input type="email" id="email" required>
-            </div>
-            <div class="form-group">
+            </div>            <div class="form-group">
                 <label>Password:</label>
-                <input type="password" id="password" required>
+                <input type="password" 
+                       id="password" 
+                       required 
+                       pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
+                       oninvalid="this.setCustomValidity('Password must be at least 8 characters long and include uppercase, lowercase letters, and a number.')"
+                       oninput="this.setCustomValidity('')">
             </div>
             <div style="text-align: right; margin-bottom: 10px;">
                 <a href="{{ url_for('forgot_password') }}" style="font-size: 0.9em; background: none; color: midnightblue; text-decoration: underline;">Forgot Password?</a>
diff --git a/frontend_microservice/templates/register.html b/frontend_microservice/templates/register.html
index 3463696..d8ff092 100644
--- a/frontend_microservice/templates/register.html
+++ b/frontend_microservice/templates/register.html
@@ -21,10 +21,14 @@
             <div class="form-group">
                 <label>Email:</label>
                 <input type="email" id="email" required>
-            </div>
-            <div class="form-group">
+            </div>            <div class="form-group">
                 <label>Password:</label>
-                <input type="password" id="password" required>
+                <input type="password" 
+                       id="password" 
+                       required 
+                       pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
+                       oninvalid="this.setCustomValidity('Password must be at least 8 characters long and include uppercase, lowercase letters, and a number.')"
+                       oninput="this.setCustomValidity('')">
                 <small>Must be at least 8 characters with uppercase, lowercase, and numbers</small>
             </div>
             <button type="submit">Register</button>
diff --git a/frontend_microservice/templates/reset_password.html b/frontend_microservice/templates/reset_password.html
index 0eee3a9..67d5cbb 100644
--- a/frontend_microservice/templates/reset_password.html
+++ b/frontend_microservice/templates/reset_password.html
@@ -11,15 +11,24 @@
     <div class="card">
         <h2>Enter New Password</h2>
         <form id="resetPasswordForm" class="card internal">
-            <input type="hidden" id="token" value="{{ token }}">
-            <div class="form-group">
+            <input type="hidden" id="token" value="{{ token }}">            <div class="form-group">
                 <label>New Password:</label>
-                <input type="password" id="password" required>
-                 <small>Must be at least 8 characters with uppercase, lowercase, and numbers</small>
+                <input type="password" 
+                       id="password" 
+                       required 
+                       pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
+                       oninvalid="this.setCustomValidity('Password must be at least 8 characters long and include uppercase, lowercase letters, and a number.')"
+                       oninput="this.setCustomValidity('')">
+                <small>Must be at least 8 characters with uppercase, lowercase, and numbers</small>
             </div>
-             <div class="form-group">
+            <div class="form-group">
                 <label>Confirm New Password:</label>
-                <input type="password" id="confirm_password" required>
+                <input type="password" 
+                       id="confirm_password" 
+                       required 
+                       pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
+                       oninvalid="this.setCustomValidity('Password must be at least 8 characters long and include uppercase, lowercase letters, and a number.')"
+                       oninput="this.setCustomValidity('')">
             </div>
             <button type="submit">Reset Password</button>
         </form>
-- 
GitLab


From d5fd68b65ef39f6956ed8da170bcf9d1ba2400d1 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 21:48:59 +0100
Subject: [PATCH 075/124] attempt to make frontend load balance work

---
 nginx/nginx.conf | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index ff68762..0c7fcfd 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -31,45 +31,56 @@ server {
     }
     
     location /create_event {
+        limit_except POST { deny all; }
         proxy_pass http://event_service/create_event;
     }
 
     location /add_availabilities {
+        limit_except POST { deny all; }
         proxy_pass http://event_service/add_availabilities;
     }
 
     location /get_event_info {
+        limit_except GET POST { deny all; }
         proxy_pass http://event_service/get_event_info;
     }
 
    location /register {
+       limit_except GET POST { deny all; }
        proxy_pass http://account_service/register;
    }
 
     location /login {
+        limit_except GET POST { deny all; }
         proxy_pass http://account_service/login;
-    }
+        }
 
     location /add_events {
+        limit_except POST { deny all; }
         proxy_pass http://account_service/add_events;
     }
 
     location /get_events {
+        limit_except GET POST { deny all; }
         proxy_pass http://account_service/get_events;
     }
 
     location /get_best_dates {
+        limit_except GET POST { deny all; }
         proxy_pass http://event_service/get_best_dates;
     }
     location /calculate {
+        limit_except GET POST { deny all; }
         proxy_pass http://datefinder/calculate;
     }
 
     location /request_password_reset {
+        limit_except POST { deny all; }
         proxy_pass http://account_service; 
     }
 
     location /reset_password {
+        limit_except POST { deny all; }
         proxy_pass http://account_service; 
     }
 
-- 
GitLab


From d2b03de7c15ae4727dcc185a677a7803e9d16504 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:04:41 +0100
Subject: [PATCH 076/124] remove frontend from load balance

---
 docker-compose.tmpl |  2 ++
 docker-compose.yml  |  3 ++-
 nginx/nginx.conf    | 23 -----------------------
 3 files changed, 4 insertions(+), 24 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 1370c62..216ee7d 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -61,6 +61,8 @@ services:
   frontend:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-frontend-ms
     container_name: frontend_ms
+    ports:
+      - "8000:5001"
     depends_on:
       - postgres
       - postgres_ac
diff --git a/docker-compose.yml b/docker-compose.yml
index 5cd384d..38c7f67 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -71,12 +71,13 @@ services:
       context: frontend_microservice
       dockerfile: Dockerfile-frontend
     container_name: frontend_ms
+    ports:
+      - "8000:5001"
     depends_on:
       - postgres
       - postgres_ac
     networks:
       - backend
-      - loadbalancing
 
   datefinder:
     build:
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 0c7fcfd..32784b5 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -10,10 +10,6 @@ upstream datefinder {
     server datefinder:6000 ;    
 }
 
-upstream frontend {
-    server frontend:5001;
-}
-
 server {
     listen 80;
 
@@ -21,66 +17,47 @@ server {
     
     include /etc/nginx/mime.types;
 
-    location / {
-        proxy_pass http://frontend;
-        proxy_http_version 1.1;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto $scheme;
-    }
     
     location /create_event {
-        limit_except POST { deny all; }
         proxy_pass http://event_service/create_event;
     }
 
     location /add_availabilities {
-        limit_except POST { deny all; }
         proxy_pass http://event_service/add_availabilities;
     }
 
     location /get_event_info {
-        limit_except GET POST { deny all; }
         proxy_pass http://event_service/get_event_info;
     }
 
    location /register {
-       limit_except GET POST { deny all; }
        proxy_pass http://account_service/register;
    }
 
     location /login {
-        limit_except GET POST { deny all; }
         proxy_pass http://account_service/login;
         }
 
     location /add_events {
-        limit_except POST { deny all; }
         proxy_pass http://account_service/add_events;
     }
 
     location /get_events {
-        limit_except GET POST { deny all; }
         proxy_pass http://account_service/get_events;
     }
 
     location /get_best_dates {
-        limit_except GET POST { deny all; }
         proxy_pass http://event_service/get_best_dates;
     }
     location /calculate {
-        limit_except GET POST { deny all; }
         proxy_pass http://datefinder/calculate;
     }
 
     location /request_password_reset {
-        limit_except POST { deny all; }
         proxy_pass http://account_service; 
     }
 
     location /reset_password {
-        limit_except POST { deny all; }
         proxy_pass http://account_service; 
     }
 
-- 
GitLab


From 9d621c8e070806e0946c2626b81a7361a6e48393 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:07:43 +0100
Subject: [PATCH 077/124] Set nginx to listen to 8080 instead of 80

---
 docker-compose.tmpl | 2 +-
 docker-compose.yml  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 216ee7d..98d6caa 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -51,7 +51,7 @@ services:
       - ./nginx:/etc/nginx/conf.d
       #- ./frontend_microservice/templates:/usr/share/nginx/html
     ports:
-        - "80:80"
+        - "8080:80"
     networks:
     - loadbalancing
     depends_on:
diff --git a/docker-compose.yml b/docker-compose.yml
index 38c7f67..6b65f13 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -58,7 +58,7 @@ services:
         - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
       ports:
-         - "80:80"
+         - "8080:80"
       networks:
       - loadbalancing
       depends_on:
-- 
GitLab


From a27781197199b64ee8b43358a9198239c231067a Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:08:30 +0100
Subject: [PATCH 078/124] temporarily disable publish and deploy jobs on this
 branch

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4eb8b12..4bd998b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - feature/frontend-reverse-proxy
+    # - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
-    - feature/frontend-reverse-proxy
+    # - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From a37c24e6ead45bd18c6d07fa71db4ba3321f74a5 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:18:43 +0100
Subject: [PATCH 079/124] test continuous deployment (now with reverse proxy
 using caddy on server)

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4bd998b..4eb8b12 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    # - feature/frontend-reverse-proxy
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
-    # - feature/frontend-reverse-proxy
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From 7775f1606c12d6acaa245e667ef29156d514ee33 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 21:25:25 +0000
Subject: [PATCH 080/124] Reset to only publish and deploy on main branch
 (continuous deployment test success)

---
 .gitlab-ci.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4eb8b12..3c8f2d0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,6 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +89,6 @@ deploy:
   stage: deploy
   only:
     - main
-    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From f50a2c4da606419ce10d29c3cad62c96e3b8dbc8 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:31:06 +0100
Subject: [PATCH 081/124] take frontend off load balancing netwrok in docker
 compose template

---
 docker-compose.tmpl | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 98d6caa..fb79ae3 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -68,7 +68,6 @@ services:
       - postgres_ac
     networks:
       - backend
-      - loadbalancing
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
-- 
GitLab


From a3c93e781c804d2a4d1a4a78e307bcac06eb13a2 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:32:22 +0100
Subject: [PATCH 082/124] add replicas so loadbalancing actually occurs

---
 docker-compose.tmpl | 6 +++---
 docker-compose.yml  | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index fb79ae3..ac64460 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -5,7 +5,7 @@ services:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-ms
     container_name: event_ms
     deploy:
-      replicas: 1
+      replicas: 2
       restart_policy:
         condition: on-failure
         delay: 1s
@@ -23,7 +23,7 @@ services:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-ms
     container_name: account_ms
     deploy:
-      replicas: 1
+      replicas: 2
       restart_policy:
         condition: on-failure
         delay: 1s
@@ -75,7 +75,7 @@ services:
     depends_on:
       - postgres
     deploy:
-      replicas: 1
+      replicas: 2
       restart_policy:
         condition: on-failure
         delay: 1s
diff --git a/docker-compose.yml b/docker-compose.yml
index 6b65f13..f07fc51 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,7 +8,7 @@ services:
     #ports:
       #- "5002:5002"
     deploy:
-      replicas: 1
+      replicas: 2
       restart_policy:
         condition: on-failure
         delay: 1s
@@ -27,7 +27,7 @@ services:
       dockerfile: Dockerfile
     container_name: account_ms
     deploy:
-      replicas: 1
+      replicas: 2
       restart_policy:
         condition: on-failure
         delay: 1s
@@ -89,7 +89,7 @@ services:
     depends_on:
       - postgres
     deploy:
-      replicas: 1
+      replicas: 2
       restart_policy:
         condition: on-failure
         delay: 1s
-- 
GitLab


From b6d65723047691e7b4bf6a6524352c432bf4f4cc Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:33:08 +0100
Subject: [PATCH 083/124] test again with replicas and network change

---
 .gitlab-ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3c8f2d0..4eb8b12 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -89,6 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From 5472a263719aa07fed867016658844143a3396a4 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 22:40:44 +0100
Subject: [PATCH 084/124] remove container names because it can't make two
 containers with same name

---
 docker-compose.tmpl | 14 +++++++-------
 docker-compose.yml  | 14 +++++++-------
 2 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index ac64460..ecb06e9 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -3,7 +3,7 @@ version: "3.8"
 services:
   event_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-ms
-    container_name: event_ms
+    #container_name: event_ms
     deploy:
       replicas: 2
       restart_policy:
@@ -21,7 +21,7 @@ services:
 
   account_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-ms
-    container_name: account_ms
+    #container_name: account_ms
     deploy:
       replicas: 2
       restart_policy:
@@ -46,7 +46,7 @@ services:
 
   nginx:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-nginx
-    container_name: nginx_container
+    #container_name: nginx_container
     volumes:
       - ./nginx:/etc/nginx/conf.d
       #- ./frontend_microservice/templates:/usr/share/nginx/html
@@ -60,7 +60,7 @@ services:
 
   frontend:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-frontend-ms
-    container_name: frontend_ms
+    #container_name: frontend_ms
     ports:
       - "8000:5001"
     depends_on:
@@ -71,7 +71,7 @@ services:
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
-    container_name: datefinder_ms
+    #container_name: datefinder_ms
     depends_on:
       - postgres
     deploy:
@@ -90,7 +90,7 @@ services:
 
   postgres:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-db
-    container_name: event_database
+    #container_name: event_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
@@ -112,7 +112,7 @@ services:
 
   postgres_ac:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-db
-    container_name: account_database
+    #container_name: account_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
diff --git a/docker-compose.yml b/docker-compose.yml
index f07fc51..ab1dfae 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,7 @@ services:
     build:
       context: event_microservice
       dockerfile: Dockerfile-backend
-    container_name: event_ms
+    # container_name: event_ms
     #ports:
       #- "5002:5002"
     deploy:
@@ -25,7 +25,7 @@ services:
     build:
       context: account_microservice
       dockerfile: Dockerfile
-    container_name: account_ms
+    # container_name: account_ms
     deploy:
       replicas: 2
       restart_policy:
@@ -53,7 +53,7 @@ services:
       build:
         context: ./nginx
         dockerfile: Dockerfile
-      container_name: nginx_container
+      # container_name: nginx_container
       volumes:
         - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
@@ -70,7 +70,7 @@ services:
     build:
       context: frontend_microservice
       dockerfile: Dockerfile-frontend
-    container_name: frontend_ms
+    # container_name: frontend_ms
     ports:
       - "8000:5001"
     depends_on:
@@ -83,7 +83,7 @@ services:
     build:
       context: datefinder_microservice
       dockerfile: Dockerfile-datefinder
-    container_name: datefinder_ms
+    # container_name: datefinder_ms
     #ports:
     #  - "6000:6000"
     depends_on:
@@ -105,7 +105,7 @@ services:
     build:
       context: event_database
       dockerfile: Dockerfile
-    container_name: event_database
+    # container_name: event_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
@@ -129,7 +129,7 @@ services:
     build:
       context: account_database
       dockerfile: Dockerfile
-    container_name: account_database
+    # container_name: account_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
-- 
GitLab


From 44fb593081453165b9ba6a1b0f4f54dfb2206f47 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 21:52:22 +0000
Subject: [PATCH 085/124] Only allow publish and deploy on main (second test
 success)

---
 .gitlab-ci.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4eb8b12..3c8f2d0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,6 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +89,6 @@ deploy:
   stage: deploy
   only:
     - main
-    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From e53f12abdc652322a9dc2a8e70581ec6da6f1537 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 23:07:36 +0100
Subject: [PATCH 086/124] remove nginx volume because redundant

---
 docker-compose.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index ab1dfae..7bf8f59 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -54,8 +54,8 @@ services:
         context: ./nginx
         dockerfile: Dockerfile
       # container_name: nginx_container
-      volumes:
-        - ./nginx:/etc/nginx/conf.d
+      # volumes:
+        # - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
       ports:
          - "8080:80"
-- 
GitLab


From bd2b05f6a8108109756b7b9c5fcd8ff73f11142a Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 23:11:14 +0100
Subject: [PATCH 087/124] troubleshoot: reset nginx port to 80

---
 docker-compose.yml | 19 ++++++-------------
 1 file changed, 6 insertions(+), 13 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 7bf8f59..21aceb3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,6 @@ services:
     build:
       context: event_microservice
       dockerfile: Dockerfile-backend
-    # container_name: event_ms
     #ports:
       #- "5002:5002"
     deploy:
@@ -25,7 +24,6 @@ services:
     build:
       context: account_microservice
       dockerfile: Dockerfile
-    # container_name: account_ms
     deploy:
       replicas: 2
       restart_policy:
@@ -50,15 +48,12 @@ services:
       - SECRET_KEY=f361e60c60f0aaf062ba9b9fc727f08eb525ad93ac715e012b224c0506596025
 
   nginx:
-      build:
-        context: ./nginx
-        dockerfile: Dockerfile
-      # container_name: nginx_container
-      # volumes:
-        # - ./nginx:/etc/nginx/conf.d
+      image: nginx
+      volumes:
+        - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
       ports:
-         - "8080:80"
+         - "80:80"
       networks:
       - loadbalancing
       depends_on:
@@ -70,7 +65,6 @@ services:
     build:
       context: frontend_microservice
       dockerfile: Dockerfile-frontend
-    # container_name: frontend_ms
     ports:
       - "8000:5001"
     depends_on:
@@ -83,7 +77,6 @@ services:
     build:
       context: datefinder_microservice
       dockerfile: Dockerfile-datefinder
-    # container_name: datefinder_ms
     #ports:
     #  - "6000:6000"
     depends_on:
@@ -105,7 +98,7 @@ services:
     build:
       context: event_database
       dockerfile: Dockerfile
-    # container_name: event_database
+    container_name: event_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
@@ -129,7 +122,7 @@ services:
     build:
       context: account_database
       dockerfile: Dockerfile
-    # container_name: account_database
+    container_name: account_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
-- 
GitLab


From 6362c95632186d3ef1a36cb25cc1a6d39c8ba705 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 23:19:36 +0100
Subject: [PATCH 088/124] change all nginx ports to 8080

---
 docker-compose.yml                | 2 +-
 event_microservice/backend.py     | 2 +-
 frontend_microservice/frontend.py | 4 ++--
 nginx/Dockerfile                  | 2 +-
 nginx/nginx.conf                  | 2 +-
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 21aceb3..19b5748 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -53,7 +53,7 @@ services:
         - ./nginx:/etc/nginx/conf.d
         #- ./frontend_microservice/templates:/usr/share/nginx/html
       ports:
-         - "80:80"
+         - "8080:8080"
       networks:
       - loadbalancing
       depends_on:
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index f744405..a1465cd 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -6,7 +6,7 @@ import os
 
 #TODO make sure the port is correct
 #Comment
-ALGORITHM_PORT = 80
+ALGORITHM_PORT = 8080
 EVENT_DATABASE_PORT = 5436
 PORT = 5002
 
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 22f973e..9a14e89 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -8,8 +8,8 @@ import logging
 app = Flask(__name__)
 
 PORT = 5001
-EVENT_PORT = 80
-USER_PORT = 80
+EVENT_PORT = 8080
+USER_PORT = 8080
 
 @app.route('/')
 @app.route('/index')
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
index a63d974..87f4a57 100644
--- a/nginx/Dockerfile
+++ b/nginx/Dockerfile
@@ -2,6 +2,6 @@ FROM nginx:stable-alpine
 
 COPY nginx.conf /etc/nginx/conf.d/default.conf
 
-EXPOSE 80
+EXPOSE 8080
 
 CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 32784b5..a1108fd 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -11,7 +11,7 @@ upstream datefinder {
 }
 
 server {
-    listen 80;
+    listen 8080;
 
     resolver 127.0.0.11 valid=5s;
     
-- 
GitLab


From 8f070fe29448b8f3fa7f4540a64e6c8ec8bbf616 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 23:29:03 +0100
Subject: [PATCH 089/124] update docker compose template

---
 docker-compose.tmpl | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index ecb06e9..53a3f6c 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -3,7 +3,6 @@ version: "3.8"
 services:
   event_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-ms
-    #container_name: event_ms
     deploy:
       replicas: 2
       restart_policy:
@@ -21,7 +20,6 @@ services:
 
   account_service:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-ms
-    #container_name: account_ms
     deploy:
       replicas: 2
       restart_policy:
@@ -46,21 +44,20 @@ services:
 
   nginx:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-nginx
-    #container_name: nginx_container
     volumes:
       - ./nginx:/etc/nginx/conf.d
       #- ./frontend_microservice/templates:/usr/share/nginx/html
     ports:
-        - "8080:80"
+        - "8080:8080"
     networks:
     - loadbalancing
     depends_on:
     - event_service
     - account_service
+    - datefinder
 
   frontend:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-frontend-ms
-    #container_name: frontend_ms
     ports:
       - "8000:5001"
     depends_on:
-- 
GitLab


From 79c96f5c57b5c8f3bdd6872886b632657831be02 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 23:30:46 +0100
Subject: [PATCH 090/124] test again on deployment runner

---
 .gitlab-ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3c8f2d0..4eb8b12 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -89,6 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From a49a49b6e22a3b9df003f26996573c1f29ad21dc Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Mon, 19 May 2025 23:44:47 +0100
Subject: [PATCH 091/124] allow frontend to see external host during build

---
 docker-compose.tmpl | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 53a3f6c..293ca60 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -65,6 +65,8 @@ services:
       - postgres_ac
     networks:
       - backend
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
-- 
GitLab


From 9784c3b4bb8ba2ecfdc5d5c75530c1d39dc2a21f Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Tue, 20 May 2025 15:10:06 +0100
Subject: [PATCH 092/124] add documentation to functions in script.js

---
 frontend_microservice/static/script.js | 241 ++++++++++++++++++-------
 1 file changed, 179 insertions(+), 62 deletions(-)

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index d25fc99..9a9e48b 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -25,14 +25,20 @@ document.addEventListener('DOMContentLoaded', function() {
         "December"
     ];
 
-    
-    //eventdates = [datetime.datetime(2025, 3, 27, 0, 0, 0), datetime.datetime(2025, 3, 28,0,0,0), datetime.datetime(2025,3,31,0,0,0)]
     const title = document.getElementById("title").textContent;
     console.log("Title: ",title);
 
-    //Convert dates to string
     function convertDatesToString(dates){
-        //depending on data from database, can convert to date
+        /**
+         * Converts an array of date values into formatted date strings.
+         * @param {Array} dates - An array of date values (e.g., ISO strings, timestamps).
+         * @returns {Array} dates-str - An array of strings in 'YYYY-MM-DD' format.
+         *
+         * Example:
+         *    Input: ["2025-05-20T12:00:00Z", "2024-12-31"]
+         *    Output: ["2025-5-20", "2024-12-31"]
+         */
+
         dates_str = []
         for (i=0; i<dates.length;i++){
             let date_ = new Date(dates[i]);
@@ -51,10 +57,9 @@ document.addEventListener('DOMContentLoaded', function() {
         event_dates_str = convertDatesToString(eventdates);
     }
 
-
-    // Function to generate the calendar
+    // Function to generate the calendar, called at every load of the availability and create event pages
     const manipulate = () => {
-        //console.log(possible_dates)
+
         // Get the first day of the month
         let dayone = new Date(year, month, 1).getDay();
 
@@ -88,11 +93,15 @@ document.addEventListener('DOMContentLoaded', function() {
                 ? "today"
                 : "";
 
+            // If on availability page, any dates not associated with the event are not selectable 
             let isSelectable = ""
             if (title=="Availability"){
                 isSelectable = event_dates_str.includes(fullDate) ? "" : "not_selectable";
             }
-            let isSelected = possible_dates.includes(fullDate) ? "selected" : "";  // Check if the date is selected
+            // Check if the date is selected
+            let isSelected = possible_dates.includes(fullDate) ? "selected" : "";  
+
+            // Add css classes to the date to give is the correct apperance
             lit += `<li class="${isToday} ${isSelected} ${isSelectable}"data-date="${fullDate}">${i}</li>`;
         }
 
@@ -101,12 +110,10 @@ document.addEventListener('DOMContentLoaded', function() {
             lit += `<li class="not_this_month not_selectable">${i - dayend + 1}</li>`
         }
 
-        // Update the text of the current date element 
-        // with the formatted current month and year
+        // Update the text of the current date element with the formatted current month and year
         currdate.innerText = `${months[month]} ${year}`;
 
-        // update the HTML of the dates element 
-        // with the generated calendar
+        // update the HTML of the dates element with the generated calendar
         day.innerHTML = lit;
 
         // Add event listeners for the new dates
@@ -136,19 +143,18 @@ document.addEventListener('DOMContentLoaded', function() {
     // Function to toggle the selection state of buttons
     function toggleDaySelection(button) {
         button.classList.toggle('selected');
-
     }
 
+    // call the load of the calendar
     manipulate();
 
-    // Attach a click event listener to each icon
+    // Attach a click event listener to each next/prev icons
     prenexIcons.forEach(icon => {
 
         // When an icon is clicked
         icon.addEventListener("click", () => {
 
-            // Check if the icon is "calendar-prev"
-            // or "calendar-next"
+            // Check if the icon is "calendar-prev" or "calendar-next"
             month = icon.id === "calendar-prev" ? month - 1 : month + 1;
 
             // Check if the month is out of range
@@ -171,54 +177,85 @@ document.addEventListener('DOMContentLoaded', function() {
                 date = new Date();
             }
 
-            // Call the manipulate function to 
-            // update the calendar display
+            // Call the manipulate function to  update the calendar display
             manipulate();
         });
     });
 })
 
-    function sendDates(){
-        console.log("button pressed")
-        console.log(possible_dates)
-        let name = document.getElementById('event_name').value;
+//for create event page
+function sendDates(){
+    /**
+     * Sends selected event dates and event name to the server to create a new event.
+     * Inputs:
+     *   - Reads 'event_name' from an input element with ID 'event_name'
+     *   - Uses a global variable 'possible_dates'
+     *
+     * Output:
+     *   - Sends a POST request to the `/create_event` endpoint with JSON payload
+     *   - On success: alerts the user and redirects to a URL provided in the response (event summary page)
+     *   - On error: displays an error message.
+     */
+
+    console.log("button pressed")
+    console.log(possible_dates)
+
+
+    let name = document.getElementById('event_name').value;
+
+    //check that event name is not empty
+    if (name == "") {
+        alert("Event name cannot be empty.");
+        return;
+    }
 
-        //check that event name is not empty
-        if (name == "") {
-            alert("Event name cannot be empty.");
-            return;
+    $.ajax({
+        url: `/create_event`, 
+        type: 'POST',
+        contentType: 'application/json', 
+        data: JSON.stringify({ 'possible_dates': possible_dates, 
+                                'name':name }),
+        success: function(response) {
+            alert("You've created the event!")
+            window.location.href = response.url;
+        },
+        error: function(xhr, status, error) {
+            alert("Error: " + error);
         }
-
-        $.ajax({
-            url: `/create_event`, 
-            type: 'POST',
-            contentType: 'application/json', 
-            data: JSON.stringify({ 'possible_dates': possible_dates, 
-                                    'name':name }),
-            success: function(response) {
-                alert("You've created the event!")
-                window.location.href = response.url;
-            },
-            error: function(xhr, status, error) {
-                alert("Error: " + error);
-            }
-        });
-    }
+    });
+}
     
-
-    function nameEntered(){
+//For availabilities page
+function nameEntered(){
+/**
+ * Handles logic when a user enters their name into the availability form.
+ *
+ * Inputs:
+ *   - Reads the user name from an input field with ID 'user_name'.
+ *   - Uses global variable 'previous_availabilities', loaded from database when html page is loaded
+ *   - Uses global variable 'possible_dates', loaded from database when html page is loaded
+ *
+ * Outputs:
+ *   - Updates the 'possible_dates' array
+ *   - Displays a message if the name already exists in the system.
+ *   - Updates calendar UI
+ */
     const message = document.getElementById('message');
     let name = document.getElementById('user_name').value;
     let names = Object.keys(previous_availabilities)
     let nameInList = false
     possible_dates=[]
     message.textContent = ""
+
+    //Finds if the name has already been entered before
     for(i=0; i<names.length;i++){
         if (names[i]==name){
             nameInList=true
             break;
         }
     }
+
+    //If name does exist, show availabilites already added
     if (nameInList){
         let already_selected = convertDatesToString(previous_availabilities[name])
         console.log(already_selected)
@@ -232,26 +269,58 @@ document.addEventListener('DOMContentLoaded', function() {
     manipulate()
 }
 
+//For Availabilities page
 function sendAvailabilities(event_id){
-        console.log("button pressed")
-        let user_name = document.getElementById('user_name').value;
+/**
+ * Submits a user's selected availabilities for a specific event to the server.
+ *
+ * Inputs:
+ *   - event_id (parameter): The unique ID of the event being responded to.
+ *   - Reads user name from an input field with ID 'user_name'.
+ *   - Uses global variable 'possible_dates'
+ *
+ * Output:
+ *   - Sends a POST request to `/availability/{event_id}` with JSON payload.
+ *   - On success: redirects the user to a URL provided in the server response (event summary page)
+ *   - Error: displays error message
+ * */
+    console.log("button pressed")
+    let user_name = document.getElementById('user_name').value;
+
     $.ajax({
-            url: `/availability/${event_id}`, 
-            type: 'POST',
-            contentType: 'application/json', 
-            data: JSON.stringify({ 'availability': possible_dates, 'event_id':event_id, 'user_name':user_name}),
-            success: function(response) {
-                /*TODO: Create page to go to*/
-                window.location.href = response.url;
-            },
-            error: function(xhr, status, error) {
-                alert("Error: " + error);
-            }
-        });
-    }
+        url: `/availability/${event_id}`, 
+        type: 'POST',
+        contentType: 'application/json', 
+        data: JSON.stringify({ 'availability': possible_dates, 'event_id':event_id, 'user_name':user_name}),
+        success: function(response) {
+            window.location.href = response.url;
+        },
+        error: function(xhr, status, error) {
+            alert("Error: " + error);
+        }
+    });
+}
 
 
+// For event summary page
 function getDates(availabilities){
+/**
+ * Sends user availabilities to the server and displays the best possible dates for an event,
+ * including which users are available on each date.
+ *
+ * Input:
+ *   - availabilities (Object): A mapping of user names to arrays of date strings.
+ *     Example: { "Alice": ["2025-05-20", "2025-05-21"], "Bob": ["2025-05-21"]... }
+ *
+ * Output:
+ *   - Sends a POST request to `/getDates` with the availabilities data.
+ *   - On success:
+ *       - Clears any previous result cards.
+ *       - Populates the UI with new cards for each suggested date.
+ *       - Displays a list of users available on each date.
+ *   - On error:
+ *       - Displays an error message and updates the heading.
+ **/
     console.log("button pressed");
     // Create card for best date section 
     var date_area = document.getElementById("best_dates");
@@ -315,14 +384,30 @@ function getDates(availabilities){
 
 // This runs when the eventSummary page loads
 function new_list_element(availabilities, dates){
+/**
+ * Dynamically creates and displays a summary of all possible event dates 
+ * and each user's selected availabilities on the page.
+ *
+ * Inputs:
+ *   - availabilities (Object): A mapping of usernames to arrays of date strings.
+ *     Example: { "Alice": ["2025-05-20", "2025-05-21"], "Bob": ... }
+ *   - dates (Array): An array of possible event date strings.
+ *     Example: ["2025-10-20", "2025-10-21", ...]
+ *
+ * Output:
+ *   - Modifies the DOM by appending cards to the element with ID 'summary_block'.
+ *     These cards show:
+ *       1. The list of dates to choose from.
+ *       2. Each user's availability in grouped blocks.
+ * */
+
     var summary_block = document.getElementById("summary_block");
     var sub_block = document.createElement("div");
     sub_block.classList.add("columns")
     // https://www.w3schools.com/jsref/met_node_appendchild.asp
     summary_block.appendChild(sub_block);
 
-    // Creates list of possible event dates
-
+    // Create list of possible event dates
     var av_div = document.createElement("div");
     av_div.classList.add("card", "fixed_h", "internal");
     sub_block.append(av_div);
@@ -346,6 +431,7 @@ function new_list_element(availabilities, dates){
 
     // This section creates a list for each user of their availabilities
     // summary_block is the area in which these appear
+    // Max 4 on each row
     for (const user in availabilities) {
         // get the dates for the user
         var user_dates = availabilities[user];
@@ -386,16 +472,31 @@ function new_list_element(availabilities, dates){
     };
 }
 
+//Runs on loading of account events page
 function show_account_events(events){
+/**
+ * Dynamically displays a list of buttons for events associated with a user account.
+ *
+ * Input:
+ *   - events (Array): An array of event objects, each containing:
+ *       - name (String): The name of the event.
+ *       - url (String): A URL to navigate to when the event button is clicked.
+ *      Example:
+ *      { name: "Team Meeting", url: "/event/5" }
+ *
+ * Output:
+ *   - Appends a button for each event to the DOM element with ID 'event_block'.
+ *     Each button navigates to the associated event URL when clicked.
+ */
     event_block = document.getElementById('event_block')
     console.log(events)
+
     for (i=0;i<events.length;i++){
         event_i=events[i]
         var button = document.createElement("button");
         
         button.classList.add('show_event');
         button.textContent = event_i['name'];
-        console.log(event_i['url'])
         button.onclick = button.onclick = function(event_i) {
             return function() {
                 console.log('button pressed')
@@ -407,7 +508,22 @@ function show_account_events(events){
     }
 }
 
+//On event Summary page
 function addEventToAccount(event_id){
+/**
+ * Adds the current event with the logged-in user's account.
+ *
+ * Input:
+ *   - event_id (String): The ID of the event to add to the user's account.
+ *   - Retrieves the user ID from localStorage under the key 'user_id'.
+ *
+ * Output:
+ *   - If user logged in:
+ *      - Sends a POST request to the server at `/eventSummary/{event_id}` with the user ID.
+ *      - On success: shows a confirmation message from the server response.
+ *      - On error: alerts the user with the error message.
+ *   - If no user is logged in: redirects the user to the login page.
+ **/
     const userId = localStorage.getItem('user_id');
     if (userId){
         $.ajax({
@@ -429,4 +545,5 @@ function addEventToAccount(event_id){
     }
 }
 
+// For automated testing
 module.exports = { sendDates };
\ No newline at end of file
-- 
GitLab


From 3c662b05d1cff4aeb9326fc089b7fdc0322b04ae Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Tue, 20 May 2025 15:12:17 +0100
Subject: [PATCH 093/124] add consitency

---
 frontend_microservice/static/script.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/frontend_microservice/static/script.js b/frontend_microservice/static/script.js
index 9a9e48b..58a7f16 100644
--- a/frontend_microservice/static/script.js
+++ b/frontend_microservice/static/script.js
@@ -275,7 +275,7 @@ function sendAvailabilities(event_id){
  * Submits a user's selected availabilities for a specific event to the server.
  *
  * Inputs:
- *   - event_id (parameter): The unique ID of the event being responded to.
+ *   - @param event_id : The unique ID of the event being responded to.
  *   - Reads user name from an input field with ID 'user_name'.
  *   - Uses global variable 'possible_dates'
  *
@@ -309,7 +309,7 @@ function getDates(availabilities){
  * including which users are available on each date.
  *
  * Input:
- *   - availabilities (Object): A mapping of user names to arrays of date strings.
+ *   - @param availabilities (Object): A mapping of user names to arrays of date strings.
  *     Example: { "Alice": ["2025-05-20", "2025-05-21"], "Bob": ["2025-05-21"]... }
  *
  * Output:
@@ -389,9 +389,9 @@ function new_list_element(availabilities, dates){
  * and each user's selected availabilities on the page.
  *
  * Inputs:
- *   - availabilities (Object): A mapping of usernames to arrays of date strings.
+ *   - @param availabilities (Object): A mapping of usernames to arrays of date strings.
  *     Example: { "Alice": ["2025-05-20", "2025-05-21"], "Bob": ... }
- *   - dates (Array): An array of possible event date strings.
+ *   - @param dates (Array): An array of possible event date strings.
  *     Example: ["2025-10-20", "2025-10-21", ...]
  *
  * Output:
@@ -478,7 +478,7 @@ function show_account_events(events){
  * Dynamically displays a list of buttons for events associated with a user account.
  *
  * Input:
- *   - events (Array): An array of event objects, each containing:
+ *   - @param events (Array): An array of event objects, each containing:
  *       - name (String): The name of the event.
  *       - url (String): A URL to navigate to when the event button is clicked.
  *      Example:
@@ -514,7 +514,7 @@ function addEventToAccount(event_id){
  * Adds the current event with the logged-in user's account.
  *
  * Input:
- *   - event_id (String): The ID of the event to add to the user's account.
+ *   - @param event_id (String): The ID of the event to add to the user's account.
  *   - Retrieves the user ID from localStorage under the key 'user_id'.
  *
  * Output:
-- 
GitLab


From 18d907fd636d96c44db90a886dc889b735c7f3b2 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 15:41:18 +0100
Subject: [PATCH 094/124] remove extra hosts from frontend in docker-compose
 template

---
 docker-compose.tmpl | 2 --
 1 file changed, 2 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 293ca60..53a3f6c 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -65,8 +65,6 @@ services:
       - postgres_ac
     networks:
       - backend
-    extra_hosts:
-      - "host.docker.internal:host-gateway"
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
-- 
GitLab


From 44c0ceaad8c0f8dd42210d5ada942874ba18aa24 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 18:04:03 +0000
Subject: [PATCH 095/124] Edit README.md

---
 README.md | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 70ebc29..2238104 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,17 @@
-# Coursework
-Change the name of the repo when we get a better group name.
+# GregC
+- [COM3014](https://catalogue.surrey.ac.uk/2024-5/module/COM3014/SEMR2/2) Coursework Submission
+- **Authors**: William Young, Hazel Wilks, Hoang Hiep Trieu, Loic Peron, Siddhartha Singh, Thomas Smith
 
 
-## Installation
-Here we can put installation instructions once we have the beginnings of an app.
+**GregC** is a platform that helps people pla events by synchronizing their availablities.
+
 
 
-## Test and Deploy
-I temporarily left this in from the original README template so when we want to learn CI/CD in the near future, this is easily accessible.
+## Tech Stack
 
-Use the built-in continuous integration in GitLab.
 
-- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
-- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
-- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
-- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
+## Getting Started
+Here we can put installation instructions once we have the beginnings of an app.
+(or access on srver IP in uni network)
+
+## Demo
\ No newline at end of file
-- 
GitLab


From 34103b3efdcb3fb5e5ceee95beba1b6779f8bd94 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 19:49:17 +0000
Subject: [PATCH 096/124] Edit README.md

---
 README.md | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 2238104..236e1f0 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,37 @@
 
 
 ## Tech Stack
+* [![Flask][Flask]][Flask-url]
+* [![CSS][CSS]][CSS-url]
+* [![PostgreSQL][PostgreSQL]][PostgreSQL-url]
+* [![Docker][Docker]][Docker-url]
 
 
+* Javascript
+* NGINX
+
+* [![TypeScript][TypeScript]][TypeScript-url]
+* [![Vite][Vite]][Vite-url]
+* [![Tailwind][Tailwind]][Tailwind-url]
+* [![SolanaWeb3][SolanaWeb3]][SolanaWeb3-url]
+* [![QuickNode][QuickNode]][QuickNode-url]
+
 ## Getting Started
 Here we can put installation instructions once we have the beginnings of an app.
 (or access on srver IP in uni network)
 
-## Demo
\ No newline at end of file
+## Demo
+
+<!-- LINKS -->
+[Flask]: https://img.shields.io/badge/Flask-000000?logo=flask&logoColor=white
+[Flask-url]: https://flask.palletsprojects.com/en/stable/
+
+[CSS]: https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white
+[CSS-url]: https://www.w3.org/Style/CSS/Overview.en.html
+
+[PostgreSQL]: https://img.shields.io/badge/PostgreSQL-316192?logo=postgresql&logoColor=white
+[PostgreSQL-url]: https://www.postgresql.org/
+
+[Docker]:
+[Docker-url]: https://www.docker.com/
+
-- 
GitLab


From 67ce79894a4dcd993393f3f5ce257d3692addd07 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 21:35:54 +0000
Subject: [PATCH 097/124] Edit README.md

---
 README.md | 58 ++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 42 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index 236e1f0..096fe69 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,16 @@
 # GregC
-- [COM3014](https://catalogue.surrey.ac.uk/2024-5/module/COM3014/SEMR2/2) Coursework Submission
-- **Authors**: William Young, Hazel Wilks, Hoang Hiep Trieu, Loic Peron, Siddhartha Singh, Thomas Smith
+**[COM3014](https://catalogue.surrey.ac.uk/2024-5/module/COM3014/SEMR2/2) Coursework Submission**
 
+**Authors**:
+- Hazel Wilks (6898203)
+- Hoang Hiep Trieu (6860616)
+- Loïc Péron (6899778)
+- Siddhartha Singh (6893314)
+- Thomas Smith (6521482)
+- William Young (6481863)
 
-**GregC** is a platform that helps people pla events by synchronizing their availablities.
 
+**GregC** is a multi-service application that helps you plan events with friends by synchronizing your availablities.
 
 
 ## Tech Stack
@@ -12,33 +18,53 @@
 * [![CSS][CSS]][CSS-url]
 * [![PostgreSQL][PostgreSQL]][PostgreSQL-url]
 * [![Docker][Docker]][Docker-url]
+* [![JavaScript][JavaScript]][JavaScript-url]
+* [![NginX][NginX]][NginX-url]
+* [![GitLab-CI][GitLab-CI]][GitLab-CI-url]
 
 
-* Javascript
-* NGINX
+## Getting Started
 
-* [![TypeScript][TypeScript]][TypeScript-url]
-* [![Vite][Vite]][Vite-url]
-* [![Tailwind][Tailwind]][Tailwind-url]
-* [![SolanaWeb3][SolanaWeb3]][SolanaWeb3-url]
-* [![QuickNode][QuickNode]][QuickNode-url]
+For the purposes of this coursework, GregC is deployed on the university-provided instance.
 
-## Getting Started
-Here we can put installation instructions once we have the beginnings of an app.
-(or access on srver IP in uni network)
+To run it locally intead, make sure that Docker is installed, then follow these steps:
 
-## Demo
+1. Clone the repo
+   ```sh
+   git clone https://gitlab.surrey.ac.uk/web-tech/coursework.git
+   ```
+2. Navigate to the project folder
+    ```sh
+    cd coursework
+    ```
+3. Spin up docker containers
+    ```sh
+    docker-compose up --build
+    ```
+4. Navigate to the webpage by typing the following in your browser search bar:
+    ```sh
+    localhost:8000
+    ```
 
 <!-- LINKS -->
 [Flask]: https://img.shields.io/badge/Flask-000000?logo=flask&logoColor=white
 [Flask-url]: https://flask.palletsprojects.com/en/stable/
 
-[CSS]: https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white
+[CSS]: https://img.shields.io/badge/CSS-1572B6?logo=css3&logoColor=fff
 [CSS-url]: https://www.w3.org/Style/CSS/Overview.en.html
 
 [PostgreSQL]: https://img.shields.io/badge/PostgreSQL-316192?logo=postgresql&logoColor=white
 [PostgreSQL-url]: https://www.postgresql.org/
 
-[Docker]:
+[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff
 [Docker-url]: https://www.docker.com/
 
+[GitLab-CI]: https://img.shields.io/badge/GitLab%20CI-FC6D26?logo=gitlab&logoColor=fff
+[GitLab-CI-url]: https://docs.gitlab.com/ci/
+
+[NginX]: https://img.shields.io/badge/Nginx-222?&logo=nginx&logoColor=009639
+[NginX-url]: https://nginx.org/
+
+[JavaScript]: https://img.shields.io/badge/JavaScript-F7DF1E?logo=javascript&logoColor=000
+[JavaScript-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript
+
-- 
GitLab


From 7d94374214d7a5e07656a13a5a89876e955ea6a3 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 21:40:52 +0000
Subject: [PATCH 098/124] Edit README.md (typo)

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 096fe69..380949d 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
 
 For the purposes of this coursework, GregC is deployed on the university-provided instance.
 
-To run it locally intead, make sure that Docker is installed, then follow these steps:
+To run it locally instead, make sure that Docker is installed, then follow these steps:
 
 1. Clone the repo
    ```sh
-- 
GitLab


From 4d803ec4f59f12dd2ec2a105392dce1ec33a3db9 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 23:13:42 +0100
Subject: [PATCH 099/124] debugging step for host name

---
 .gitlab-ci.yml                | 4 ++--
 event_microservice/backend.py | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4eb8b12..4bd998b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - feature/frontend-reverse-proxy
+    # - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
-    - feature/frontend-reverse-proxy
+    # - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index a1465cd..095e844 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -138,6 +138,7 @@ def get_best_dates():
 if __name__ == "__main__":
     # checks for hostname to see if running in containers then sets host appropriately
     hostname = os.environ.get('HOSTNAME', '')
+    print(f"DEBUGGING: {hostname}")
     if hostname:
         HOST = 'host.docker.internal'
         print(f"running on docker, container hostname: {hostname}")
-- 
GitLab


From 5eb07caff725e228c43319436e070a69d213d75c Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 23:22:42 +0100
Subject: [PATCH 100/124] try simple fix

---
 event_microservice/backend.py     | 2 +-
 frontend_microservice/frontend.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index 095e844..a585f04 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -143,7 +143,7 @@ if __name__ == "__main__":
         HOST = 'host.docker.internal'
         print(f"running on docker, container hostname: {hostname}")
     else:
-        HOST = 'localhost'
+        HOST = hostname
         print("running locally")
 
         #initialising local database
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 9a14e89..3b4ede7 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -228,7 +228,7 @@ if __name__ == "__main__":
     # checks for hostname to see if running in containers then sets host appropriately
     hostname = os.environ.get('HOSTNAME', '')
     if hostname:
-        HOST = 'host.docker.internal'
+        HOST = hostname
         print(f"running on docker, container hostname: {hostname}")
     else:
         HOST = 'localhost'
-- 
GitLab


From 4a9be106f39d4f6f881840415ae116317a84cc6f Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 23:59:35 +0100
Subject: [PATCH 101/124] fix porting issues

---
 docker-compose.yml                |  1 +
 event_microservice/backend.py     | 63 ++++++++++++++++---------------
 frontend_microservice/frontend.py | 29 ++++++++------
 3 files changed, 50 insertions(+), 43 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 19b5748..21a41eb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,7 @@ services:
     depends_on:
       - postgres
     networks:
+      - backend
       - loadbalancing
     environment:
       - PORT=5002
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index a585f04..8a2d5d5 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -6,14 +6,19 @@ import os
 
 #TODO make sure the port is correct
 #Comment
-ALGORITHM_PORT = 8080
+ALGORITHM_PORT = 6000
+ALGORITHM_HOST = "datefinder"
+ALGORITHM_API_URL = f"http://{ALGORITHM_HOST}:{ALGORITHM_PORT}"
+
 EVENT_DATABASE_PORT = 5436
+EVENT_DB_HOST = "postgres"
+
 PORT = 5002
 
 app = Flask(__name__)
 def get_db_connection():
     # Connect to database
-    conn = psycopg2.connect(database = "event", user = "postgres", password = "postgres", host = HOST, port = EVENT_DATABASE_PORT)
+    conn = psycopg2.connect(database = "event", user = "postgres", password = "postgres", host = EVENT_DB_HOST, port = EVENT_DATABASE_PORT)
     conn.autocommit = True
     return conn
 
@@ -136,45 +141,41 @@ def get_best_dates():
 
 
 if __name__ == "__main__":
-    # checks for hostname to see if running in containers then sets host appropriately
-    hostname = os.environ.get('HOSTNAME', '')
-    print(f"DEBUGGING: {hostname}")
-    if hostname:
-        HOST = 'host.docker.internal'
-        print(f"running on docker, container hostname: {hostname}")
-    else:
-        HOST = hostname
-        print("running locally")
+
+    # if hostname:
+    #     HOST = 'host.docker.internal'
+    #     print(f"running on docker, container hostname: {hostname}")
+    # else:
+    #     HOST = hostname
+    #     print("running locally")
 
         #initialising local database
         #connects to base postgres
-        conn = psycopg2.connect(database="postgres", user='postgres', password='postgres', host=HOST, port=EVENT_DATABASE_PORT)
-        conn.autocommit = True
-        cursor = conn.cursor()
-
-        #checks if already exists
-        cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s;", ('event',))
-        exists = cursor.fetchone()
-
-        if not exists:
-            cursor.execute('CREATE DATABASE event')
-            print(f"Database created successfully.")
+    conn = psycopg2.connect(database="postgres", user='postgres', password='postgres', host=EVENT_DB_HOST, port=EVENT_DATABASE_PORT)
+    conn.autocommit = True
+    cursor = conn.cursor()
 
-            cursor.close()
-            conn.close()
+    #checks if already exists
+    cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s;", ('event',))
+    exists = cursor.fetchone()
 
-            #inserts db schema
-            conn = get_db_connection()
-            cursor = conn.cursor()
-            with open('database/schema.sql', 'r') as schema:
-                cursor.execute(schema.read())
+    if not exists:
+        cursor.execute('CREATE DATABASE event')
+        print(f"Database created successfully.")
 
-        conn.commit()
         cursor.close()
         conn.close()
 
+        #inserts db schema
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        with open('database/schema.sql', 'r') as schema:
+            cursor.execute(schema.read())
+
+    conn.commit()
+    cursor.close()
+    conn.close()
 
-    ALGORITHM_API_URL = f"http://{HOST}:{ALGORITHM_PORT}"
 
     app.debug = True
     app.run(host="0.0.0.0", port=PORT)
\ No newline at end of file
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 3b4ede7..5a84f4d 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -8,8 +8,14 @@ import logging
 app = Flask(__name__)
 
 PORT = 5001
-EVENT_PORT = 8080
-USER_PORT = 8080
+EVENT_HOST = "event_service"
+EVENT_PORT = 5002
+
+USER_HOST = "account_service"
+USER_PORT = 5003
+
+EVENT_API_URL = f"http://{EVENT_HOST}:{EVENT_PORT}"
+USER_API_URL = f"http://{USER_HOST}:{USER_PORT}" 
 
 @app.route('/')
 @app.route('/index')
@@ -225,14 +231,13 @@ def reset_password_submit():
         return jsonify({"error": "An internal error occurred while contacting the account service"}), 500
 
 if __name__ == "__main__":
-    # checks for hostname to see if running in containers then sets host appropriately
-    hostname = os.environ.get('HOSTNAME', '')
-    if hostname:
-        HOST = hostname
-        print(f"running on docker, container hostname: {hostname}")
-    else:
-        HOST = 'localhost'
-        print("running locally")
-    EVENT_API_URL = f"http://{HOST}:{EVENT_PORT}"
-    USER_API_URL = f"http://{HOST}:{USER_PORT}" 
+    # # checks for hostname to see if running in containers then sets host appropriately
+    # hostname = os.environ.get('HOSTNAME', '')
+    # if hostname:
+    #     HOST = hostname
+    #     print(f"running on docker, container hostname: {hostname}")
+    # else:
+    #     HOST = 'localhost'
+    #     print("running locally")
+
     app.run(debug=True, host="0.0.0.0", port=PORT)
\ No newline at end of file
-- 
GitLab


From 3ca16082129e303aded36087b32d1370bbfa880a Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:06:12 +0100
Subject: [PATCH 102/124] Test deployment with fixed porting

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4bd998b..4eb8b12 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    # - feature/frontend-reverse-proxy
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
-    # - feature/frontend-reverse-proxy
+    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From e4f1e3f02334e7604b4f49102505aa04ec001cb7 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:16:21 +0100
Subject: [PATCH 103/124] update docker-compose template to match

---
 docker-compose.tmpl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 53a3f6c..a2b6283 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -13,6 +13,7 @@ services:
     depends_on:
       - postgres
     networks:
+      -backend
       - loadbalancing
     environment:
       - PORT=5002
-- 
GitLab


From 413d7c9c088028e04fe0b535b490159b9fb66405 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:26:11 +0100
Subject: [PATCH 104/124] cleanup

---
 docker-compose.yml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 21a41eb..fce5580 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,8 +4,6 @@ services:
     build:
       context: event_microservice
       dockerfile: Dockerfile-backend
-    #ports:
-      #- "5002:5002"
     deploy:
       replicas: 2
       restart_policy:
@@ -32,8 +30,6 @@ services:
         delay: 1s
         max_attempts: 3
         window: 120s
-    #ports:
-    #  - "5003:5003"
     depends_on:
       - postgres_ac
     networks:
@@ -78,8 +74,6 @@ services:
     build:
       context: datefinder_microservice
       dockerfile: Dockerfile-datefinder
-    #ports:
-    #  - "6000:6000"
     depends_on:
       - postgres
     deploy:
-- 
GitLab


From 0e332a1164f78b8343c40f589287c36273d7c7ca Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:30:42 +0100
Subject: [PATCH 105/124] typo

---
 docker-compose.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index a2b6283..0df8e00 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -13,7 +13,7 @@ services:
     depends_on:
       - postgres
     networks:
-      -backend
+      - backend
       - loadbalancing
     environment:
       - PORT=5002
-- 
GitLab


From d38f7ef9c3e1959818c2ddf4a763f2528cc92fc8 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Tue, 20 May 2025 23:37:56 +0000
Subject: [PATCH 106/124] Edit README.md (add IP to server)

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 380949d..5b48f6e 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@
 
 ## Getting Started
 
-For the purposes of this coursework, GregC is deployed on the university-provided instance.
+For the purposes of this coursework, GregC is deployed on the university-provided instance accessible at http://10.2.8.53/ from within the university network.
 
 To run it locally instead, make sure that Docker is installed, then follow these steps:
 
-- 
GitLab


From ceb1fdaa008de09f1a0c5aa395a46a22ff27a5ad Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:40:08 +0100
Subject: [PATCH 107/124] cleanup

---
 docker-compose.tmpl               |  5 -----
 docker-compose.yml                |  2 --
 event_microservice/backend.py     | 10 ----------
 frontend_microservice/frontend.py |  9 ---------
 4 files changed, 26 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 0df8e00..f11d78a 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -69,7 +69,6 @@ services:
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
-    #container_name: datefinder_ms
     depends_on:
       - postgres
     deploy:
@@ -88,7 +87,6 @@ services:
 
   postgres:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-event-db
-    #container_name: event_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
@@ -110,7 +108,6 @@ services:
 
   postgres_ac:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-account-db
-    #container_name: account_database
     environment:
       POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
@@ -133,8 +130,6 @@ services:
 #volumes:
   #postgres-db-volume:
 networks:
-  #frontend:
-    #driver: bridge
   backend:
     driver: bridge
   loadbalancing:
diff --git a/docker-compose.yml b/docker-compose.yml
index fce5580..82058bf 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -140,8 +140,6 @@ services:
 #volumes:
   #postgres-db-volume:
 networks:
-  #frontend:
-    #driver: bridge
   backend:
     driver: bridge
   loadbalancing:
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index 8a2d5d5..3694e25 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -141,16 +141,6 @@ def get_best_dates():
 
 
 if __name__ == "__main__":
-
-    # if hostname:
-    #     HOST = 'host.docker.internal'
-    #     print(f"running on docker, container hostname: {hostname}")
-    # else:
-    #     HOST = hostname
-    #     print("running locally")
-
-        #initialising local database
-        #connects to base postgres
     conn = psycopg2.connect(database="postgres", user='postgres', password='postgres', host=EVENT_DB_HOST, port=EVENT_DATABASE_PORT)
     conn.autocommit = True
     cursor = conn.cursor()
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 5a84f4d..8128dc0 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -231,13 +231,4 @@ def reset_password_submit():
         return jsonify({"error": "An internal error occurred while contacting the account service"}), 500
 
 if __name__ == "__main__":
-    # # checks for hostname to see if running in containers then sets host appropriately
-    # hostname = os.environ.get('HOSTNAME', '')
-    # if hostname:
-    #     HOST = hostname
-    #     print(f"running on docker, container hostname: {hostname}")
-    # else:
-    #     HOST = 'localhost'
-    #     print("running locally")
-
     app.run(debug=True, host="0.0.0.0", port=PORT)
\ No newline at end of file
-- 
GitLab


From bebe28d4b00641c89861cd1bb221d58415e9327a Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:40:44 +0100
Subject: [PATCH 108/124] restrict deployment to main

---
 .gitlab-ci.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4eb8b12..3c8f2d0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,6 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - feature/frontend-reverse-proxy
   tags:
     - deployment
   variables:
@@ -90,7 +89,6 @@ deploy:
   stage: deploy
   only:
     - main
-    - feature/frontend-reverse-proxy
   tags:
     - deployment
   environment:
-- 
GitLab


From 5450c6e37ea33d91f0507d9f7349b25a59545d06 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 00:45:27 +0100
Subject: [PATCH 109/124] disable debug mode

---
 account_microservice/user.py      | 4 +++-
 docker-compose.tmpl               | 4 ++++
 docker-compose.yml                | 4 ++++
 event_microservice/backend.py     | 4 ++--
 frontend_microservice/frontend.py | 4 +++-
 5 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 78855e7..3cb8da0 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -313,4 +313,6 @@ def internal_error(e):
     return jsonify({"error": "Internal server error"}), 500
 
 if __name__ == "__main__":
-    app.run(host="0.0.0.0", port=PORT, debug=False)
\ No newline at end of file
+    debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
+    app.debug = debug_mode
+    app.run(host="0.0.0.0", port=PORT)
\ No newline at end of file
diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index f11d78a..3516938 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -17,6 +17,7 @@ services:
       - loadbalancing
     environment:
       - PORT=5002
+      - FLASK_DEBUG=FALSE
 
 
   account_service:
@@ -41,6 +42,7 @@ services:
       - MAIL_PASSWORD=bkxu hkqq wiax fuke
       - MAIL_DEFAULT_SENDER=gregcmailer@gmail.com
       - SECRET_KEY=f361e60c60f0aaf062ba9b9fc727f08eb525ad93ac715e012b224c0506596025
+      - FLASK_DEBUG=FALSE
 
 
   nginx:
@@ -66,6 +68,8 @@ services:
       - postgres_ac
     networks:
       - backend
+    environment:
+      - FLASK_DEBUG=FALSE
 
   datefinder:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-datefinder-ms
diff --git a/docker-compose.yml b/docker-compose.yml
index 82058bf..64a0bc3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,7 @@ services:
       - loadbalancing
     environment:
       - PORT=5002
+      - FLASK_DEBUG=FALSE
     
   account_service:
     build:
@@ -43,6 +44,7 @@ services:
       - MAIL_PASSWORD=bkxu hkqq wiax fuke
       - MAIL_DEFAULT_SENDER=gregcmailer@gmail.com
       - SECRET_KEY=f361e60c60f0aaf062ba9b9fc727f08eb525ad93ac715e012b224c0506596025
+      - FLASK_DEBUG=FALSE
 
   nginx:
       image: nginx
@@ -69,6 +71,8 @@ services:
       - postgres_ac
     networks:
       - backend
+    environment:
+      - FLASK_DEBUG=FALSE=value
 
   datefinder:
     build:
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index 3694e25..5c2f310 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -166,6 +166,6 @@ if __name__ == "__main__":
     cursor.close()
     conn.close()
 
-
-    app.debug = True
+    debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
+    app.debug = debug_mode
     app.run(host="0.0.0.0", port=PORT)
\ No newline at end of file
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 8128dc0..a2ef82f 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -231,4 +231,6 @@ def reset_password_submit():
         return jsonify({"error": "An internal error occurred while contacting the account service"}), 500
 
 if __name__ == "__main__":
-    app.run(debug=True, host="0.0.0.0", port=PORT)
\ No newline at end of file
+    debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
+    app.debug = debug_mode
+    app.run(host="0.0.0.0", port=PORT)
\ No newline at end of file
-- 
GitLab


From 06f221ffc66d9b4f734479af6b3007e9fbedb426 Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Wed, 21 May 2025 10:17:50 +0100
Subject: [PATCH 110/124] delete share_link page

---
 frontend_microservice/frontend.py             | 17 +-------
 .../templates/share_link.html                 | 40 -------------------
 2 files changed, 2 insertions(+), 55 deletions(-)
 delete mode 100644 frontend_microservice/templates/share_link.html

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 22f973e..b540ede 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -94,15 +94,15 @@ def availability(event_id):
 def eventSummary(event_id):
     if request.method == 'GET':
         data_input = {"id":event_id, "to_return": ['event_name','event_possible_dates','availabilities']}
+
         response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
         data_output = response.json() if response.status_code == 200 else []
+
         event_name = data_output['name']
         dates = data_output['event_possible_dates']
         availabilities = data_output['availabilities']
-
         link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
 
-
         return render_template('eventSummary.html', id=event_id, link=link, event=event_name, dates=dates, availabilities=availabilities)
     
     if request.method == 'POST':
@@ -116,19 +116,6 @@ def eventSummary(event_id):
     
     return render_template('eventSummary.html')    
 
-@app.route('/share_link/<int:event_id>', methods =['GET'])
-def share_link(event_id):
-    if request.method == 'GET':
-        data_input = {"id":event_id, "to_return": ['event_name']}
-        response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
-        data_output = response.json() if response.status_code == 200 else []
-        event_name = data_output['name']
-        link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
-        redirect_url = url_for('eventSummary', event_id=event_id)
-        return render_template('share_link.html', event=event_name, link=link,url=redirect_url)
-    return render_template('share_link.html')   
-
-
 @app.route('/getDates', methods=['POST'])
 def getDatesp():
     if request.method == 'POST':
diff --git a/frontend_microservice/templates/share_link.html b/frontend_microservice/templates/share_link.html
deleted file mode 100644
index a340718..0000000
--- a/frontend_microservice/templates/share_link.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-    <head>
-        <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
-        <title>Share Link</title>
-        
-    </head>
-    <body>
-        <div class="centered">
-            <div>
-            <h2>Here's the link to share for {{event}}</h2>
-            <div class="link-input-button">
-                <input type="text" class="link-input" id="link" value="{{link}}" readonly>
-                <button class="copy" onclick="copyToClipboard()">Copy</button>
-            </div>
-            </div>
-            <a href="{{url}}">Go to link</a>
-        </div>
-        
-
-        <script>
-            function copyToClipboard() {
-              var copyText = document.getElementById("link");
-        
-              copyText.select();
-              copyText.setSelectionRange(0, 99999); // For mobile devices
-        
-              // Use the Clipboard API
-              navigator.clipboard.writeText(copyText.value)
-                .then(function() {
-                  alert("Text copied to clipboard!");
-                })
-                .catch(function(error) {
-                  alert("Failed to copy text: " + error);
-                });
-            }
-        </script>
-    </body>
-    
-</html>
\ No newline at end of file
-- 
GitLab


From bc573e7f4936e71356e9c953b3fe70ecdce6e013 Mon Sep 17 00:00:00 2001
From: "Trieu, Hoang Hiep (PG/T - Comp Sci & Elec Eng)" <ht00865@surrey.ac.uk>
Date: Wed, 21 May 2025 09:54:55 +0000
Subject: [PATCH 111/124] fix the nginx problem

---
 docker-compose.yml                | 1 +
 event_microservice/backend.py     | 4 ++--
 frontend_microservice/frontend.py | 8 ++++----
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 64a0bc3..f0c45eb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -71,6 +71,7 @@ services:
       - postgres_ac
     networks:
       - backend
+      - loadbalancing
     environment:
       - FLASK_DEBUG=FALSE=value
 
diff --git a/event_microservice/backend.py b/event_microservice/backend.py
index 5c2f310..0665d63 100644
--- a/event_microservice/backend.py
+++ b/event_microservice/backend.py
@@ -6,8 +6,8 @@ import os
 
 #TODO make sure the port is correct
 #Comment
-ALGORITHM_PORT = 6000
-ALGORITHM_HOST = "datefinder"
+ALGORITHM_PORT = 8080
+ALGORITHM_HOST = "nginx"
 ALGORITHM_API_URL = f"http://{ALGORITHM_HOST}:{ALGORITHM_PORT}"
 
 EVENT_DATABASE_PORT = 5436
diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index a2ef82f..75e352d 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -8,11 +8,11 @@ import logging
 app = Flask(__name__)
 
 PORT = 5001
-EVENT_HOST = "event_service"
-EVENT_PORT = 5002
+EVENT_HOST = "nginx"
+EVENT_PORT = 8080
 
-USER_HOST = "account_service"
-USER_PORT = 5003
+USER_HOST = "nginx"
+USER_PORT = 8080
 
 EVENT_API_URL = f"http://{EVENT_HOST}:{EVENT_PORT}"
 USER_API_URL = f"http://{USER_HOST}:{USER_PORT}" 
-- 
GitLab


From dea7510c02433b9818a76720d424acb537eb293c Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Wed, 21 May 2025 10:59:51 +0100
Subject: [PATCH 112/124] make link dynamic so it always displays right link

---
 frontend_microservice/frontend.py                 | 3 +--
 frontend_microservice/templates/eventSummary.html | 6 +++++-
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index b540ede..3f4ecd6 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -101,9 +101,8 @@ def eventSummary(event_id):
         event_name = data_output['name']
         dates = data_output['event_possible_dates']
         availabilities = data_output['availabilities']
-        link = f"http://127.0.0.1:8000/eventSummary/{event_id}"
 
-        return render_template('eventSummary.html', id=event_id, link=link, event=event_name, dates=dates, availabilities=availabilities)
+        return render_template('eventSummary.html', id=event_id, event=event_name, dates=dates, availabilities=availabilities)
     
     if request.method == 'POST':
         data = request.get_json()
diff --git a/frontend_microservice/templates/eventSummary.html b/frontend_microservice/templates/eventSummary.html
index 4bed6a5..9f7f199 100644
--- a/frontend_microservice/templates/eventSummary.html
+++ b/frontend_microservice/templates/eventSummary.html
@@ -22,7 +22,10 @@
         console.log(availabilities);
         var dates = {{dates | tojson }};
         var event_id = {{id | tojson}};
+        var link = window.location.href
+        console.log('This is the link:',link)       
     </script>
+    
 
     <div class="lowered centered" style="width: 100%;">
         <h1>{{ event }} Summary</h1>
@@ -31,7 +34,7 @@
             <div class="card" id="link_container">
                 <h2>Here's the link to share for {{event}}</h2>
                 <div class="link-input-button">
-                    <input type="text" class="link-input" id="link" value="{{link}}" readonly>
+                    <input type="text" class="link-input" id="link" readonly>
                     <button class="copy" onclick="copyToClipboard()">Copy</button>
                 </div>
             </div>
@@ -79,5 +82,6 @@
 
     <script src="{{ url_for('static', filename='script.js') }}"></script>
     <script>new_list_element(availabilities,dates)</script>
+    <script>document.getElementById('link').value = link</script>
 </body>
 </html>
-- 
GitLab


From d957fe5e0f1ea6c77458471467f740fea91135dd Mon Sep 17 00:00:00 2001
From: hwilks <hazel.wilks@gmail.com>
Date: Wed, 21 May 2025 11:02:54 +0100
Subject: [PATCH 113/124] remove commented out code from frontend.py

---
 frontend_microservice/frontend.py | 30 +++---------------------------
 1 file changed, 3 insertions(+), 27 deletions(-)

diff --git a/frontend_microservice/frontend.py b/frontend_microservice/frontend.py
index 3f4ecd6..0fd3c31 100644
--- a/frontend_microservice/frontend.py
+++ b/frontend_microservice/frontend.py
@@ -27,7 +27,6 @@ def create_event():
         event_id=data_output['event_id']
         if response.status_code == 201:  # Check if event was successfully createds
             redirect_url = url_for('eventSummary', event_id=event_id)
-            #print(redirect_url)
             return jsonify({'url': redirect_url})
         else:
             return f"Error: {response.text}", response.status_code
@@ -60,26 +59,20 @@ def login():
 @app.route("/availability/<int:event_id>", methods=['GET', 'POST'])
 def availability(event_id):
     if request.method == 'GET':
-        #sends list 'to_return' that contains all data requested more details in event_microservice docstring
+
         data_input = {"id":event_id, "to_return": ['event_name','event_possible_dates','availabilities']}
-        #json_data = json.dumps(data_input, indent=4)
         response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
         data_output = response.json() if response.status_code == 200 else []
         event_name = data_output['name']
         dates = data_output['event_possible_dates']
         availabilities = data_output['availabilities']
-        #availabilities now outputs a dict with format user_id:[dates]
-        #print(data_output['availabilities'])
 
         return render_template('availability.html', event=event_name, event_id=event_id, event_dates = dates, availabilities = availabilities)
     
     if request.method == 'POST':
 
         data = request.get_json() #data contains event_id, user_name and availability
-        #To test frontend
-        #data = {"event_id":1,"availability":data['availability'], "user_id":9}
 
-        #TODO: backend needs to be changed to cope with: changing availabilites, user_name instead of user_id
         response = requests.post(f"{EVENT_API_URL}/add_availabilities", json=data)
 
         if response.status_code == 201:  # Check if was added successfully
@@ -118,13 +111,7 @@ def eventSummary(event_id):
 @app.route('/getDates', methods=['POST'])
 def getDatesp():
     if request.method == 'POST':
-        print("Hello")
-        # sends list 'to_return' that contains all data requested more details in event_microservice docstring
-        #data_input = requests.get(f"{EVENT_API_URL}/get_event_info", json = {"id":event_id, "to_return": ['availabilities']})
         data_input = request.get_json()
-        #data_input = {'availabilities':data['availabilities']}
-        print(data_input)
-        # TODO: Just send event ID and sort in backend
         response = requests.get(f"{EVENT_API_URL}/get_best_dates", json = data_input)
         data_output = response.json() if response.status_code == 200 else []
         return data_output
@@ -136,33 +123,22 @@ def get_events(account_id):
         with open("loging.txt","w") as file:
 
             data = {'account_id':account_id}
-            #file.write('in front')
-            #file.write(str(data['account_id']))
             account_response = requests.get(f"{USER_API_URL}/get_events", json = data)
-            response = account_response.json() #if account_response.status_code == 201 else []
+            response = account_response.json()
             
             event_ids = response['event_ids']
-            #file.write(str(type(event_ids)))
 
             events = []
 
             for event_id in event_ids:
                 event ={}
-                #file.write(str(event_id))
-                #event_id = 2
                 data_input = {"id":int(event_id), "to_return": ['event_name']}
                 response = requests.get(f"{EVENT_API_URL}/get_event_info", json = data_input)
-                #file.write(str(response.status_code))
                 data_output = response.json() if response.status_code == 200 else []
                 event['name'] = data_output['name']
                 event['url'] = url_for('eventSummary', event_id=event_id)
                 events.append(event)
-            '''
-            #Test data
-            event1= {'name':'An event','url':url_for('eventSummary', event_id=1)}
-            event2= {'name':'An event 2','url':'a url'}       
-            events = [event1, event2]
-            '''
+
         return render_template('accountEvents.html', event=events)  
     return render_template('accountEvents.html')
 
-- 
GitLab


From 20848d5b5933fb8f742da6c8cde03f71f217e956 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 10:18:13 +0000
Subject: [PATCH 114/124] test deployment of development branch

---
 .gitlab-ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3c8f2d0..f057df1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
+    - development
   tags:
     - deployment
   variables:
@@ -89,6 +90,7 @@ deploy:
   stage: deploy
   only:
     - main
+    - development
   tags:
     - deployment
   environment:
-- 
GitLab


From 22c21f7c2e2f1e80c392a6384792b45ae43e101b Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 11:27:34 +0100
Subject: [PATCH 115/124] update docker-compose tmpl to match

---
 docker-compose.tmpl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index 3516938..ba28611 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -68,6 +68,7 @@ services:
       - postgres_ac
     networks:
       - backend
+      - loadbalancing
     environment:
       - FLASK_DEBUG=FALSE
 
-- 
GitLab


From 1589d204d6648b1f0a6055eb48d19084b39fbddb Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Wed, 21 May 2025 12:08:50 +0100
Subject: [PATCH 116/124] remove old logging statements

---
 datefinder_microservice/event_finder.py | 23 -----------------------
 1 file changed, 23 deletions(-)

diff --git a/datefinder_microservice/event_finder.py b/datefinder_microservice/event_finder.py
index e28a95b..9a1ed13 100644
--- a/datefinder_microservice/event_finder.py
+++ b/datefinder_microservice/event_finder.py
@@ -6,24 +6,6 @@ import json
 app = Flask(__name__)
 PORT = 6000
 
-import logging
-
-# Set up logging to a file
-logging.basicConfig(filename='app.log', level=logging.INFO)
-
-
-
-
-
-# Simulating event with 4 users
-
-users = {
-    "user1": [datetime(2025, 5, 1), datetime(2025, 5, 2), datetime(2025, 5, 3), datetime(2025, 5, 4), datetime(2025, 5, 5), datetime(2025, 5, 6)],
-    "user2": [datetime(2025, 5, 2), datetime(2025, 5, 3), datetime(2025, 5, 4), datetime(2025, 5, 6), datetime(2025, 5, 7)],
-    "user3": [datetime(2025, 5, 3), datetime(2025, 5, 9), datetime(2025, 5, 5), datetime(2025, 5, 6)],
-    "user4": [datetime(2025, 5, 3), datetime(2025, 5, 6)]
-    }
-#print("expected output is:\ndict_values([1, 2, 4, 2, 2, 4, 1, 1]) and [3rd, 6th]")
 
 @app.route("/calculate" , methods = ['GET', 'POST'])
 def best_date():
@@ -61,14 +43,9 @@ def best_date():
 
     
     for best_date in best_dates:
-        logging.info("We made it into the loop")
         for user in users:
-            logging.info("We made it into the loop")
             if best_date in users[user]:
-                logging.info(best_date)
-                logging.info(user)
                 best_dates[best_date].append(str(user))
-    logging.info(best_dates)
 
     return best_dates, 200
 
-- 
GitLab


From 27a9ad1afb22ae5ee9a1b342aae92fa8c900eec6 Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Wed, 21 May 2025 12:12:10 +0100
Subject: [PATCH 117/124] remove unnecessary imports

---
 datefinder_microservice/event_finder.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/datefinder_microservice/event_finder.py b/datefinder_microservice/event_finder.py
index 9a1ed13..36bdf65 100644
--- a/datefinder_microservice/event_finder.py
+++ b/datefinder_microservice/event_finder.py
@@ -1,7 +1,4 @@
-from datetime import datetime
-from flask import Flask, render_template, request, redirect, url_for, jsonify
-import requests
-import json
+from flask import Flask, request
 
 app = Flask(__name__)
 PORT = 6000
-- 
GitLab


From e840fd7ef725a9350c98b974f33b377f3d0391b2 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 12:12:11 +0100
Subject: [PATCH 118/124] remove nginx volume mount in deployment

---
 docker-compose.tmpl | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/docker-compose.tmpl b/docker-compose.tmpl
index baba906..415ea91 100644
--- a/docker-compose.tmpl
+++ b/docker-compose.tmpl
@@ -47,9 +47,6 @@ services:
 
   nginx:
     image: $DOCKERHUB_REGISTRY:$CI_COMMIT_SHORT_SHA-nginx
-    volumes:
-      - ./nginx:/etc/nginx/conf.d
-      #- ./frontend_microservice/templates:/usr/share/nginx/html
     ports:
         - "8080:8080"
     networks:
-- 
GitLab


From b0a3fb9faee028c8fbdd32627590054aeaa8852a Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Wed, 21 May 2025 12:13:56 +0100
Subject: [PATCH 119/124] remove unnecessary requirements

---
 datefinder_microservice/requirements.txt | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/datefinder_microservice/requirements.txt b/datefinder_microservice/requirements.txt
index a4fa4f3..8ab6294 100644
--- a/datefinder_microservice/requirements.txt
+++ b/datefinder_microservice/requirements.txt
@@ -1,4 +1 @@
-flask
-#sqlite3
-psycopg2-binary
-requests
\ No newline at end of file
+flask
\ No newline at end of file
-- 
GitLab


From f8eec1eb990e8f97dbe59db0c98efa19ec3fbdbb Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 12:14:27 +0100
Subject: [PATCH 120/124] test deployment on this branch

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bc93f18..ffbd633 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -41,7 +41,7 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - development
+    - issue/nginx-empty-config
   tags:
     - deployment
   variables:
@@ -100,7 +100,7 @@ deploy:
   stage: deploy
   only:
     - main
-    - development
+    - issue/nginx-empty-config
   tags:
     - deployment
   environment:
-- 
GitLab


From 6632a369e07cf39ea2cd294c08942bac857d45b1 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 12:28:26 +0100
Subject: [PATCH 121/124] correct DB_HOST in account service

---
 account_microservice/user.py | 12 ++----------
 1 file changed, 2 insertions(+), 10 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 3cb8da0..782989b 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -36,15 +36,7 @@ limiter = Limiter(
     key_func=get_remote_address,
     default_limits=["200 per day", "50 per hour"]
 )
-
-hostname = os.environ.get('HOSTNAME', '')
-if hostname:
-    HOST = 'account_database'  
-    print(f"Running on docker, container hostname: {hostname}")
-else:
-    HOST = 'localhost'
-    print("Running locally")
-
+DB_HOST = "postgres_ac"
 PORT = 5003
 DB_PORT = 5437
 
@@ -54,7 +46,7 @@ def get_db_connection():
         database="account",
         user="postgres",
         password="postgres",
-        host=HOST,
+        host=DB_HOST,
         port=DB_PORT
     )
     conn.autocommit = True
-- 
GitLab


From 245a2d8610a2f0520328e9b598e188068203430d Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 12:43:40 +0100
Subject: [PATCH 122/124] restrict deployment to main

---
 .gitlab-ci.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ffbd633..e52cfab 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -41,7 +41,6 @@ publish:  # Publish containers to gitlab container registry
   stage: publish
   only:
     - main
-    - issue/nginx-empty-config
   tags:
     - deployment
   variables:
@@ -100,7 +99,6 @@ deploy:
   stage: deploy
   only:
     - main
-    - issue/nginx-empty-config
   tags:
     - deployment
   environment:
-- 
GitLab


From 6a2bd5bd3239a8e3e686f6a0edfc735f66ba62df Mon Sep 17 00:00:00 2001
From: "Smith, Thomas E (PG/T - Comp Sci & Elec Eng)" <ts00738@surrey.ac.uk>
Date: Wed, 21 May 2025 13:20:10 +0100
Subject: [PATCH 123/124] fix url in reset email

---
 account_microservice/user.py         | 3 ++-
 frontend_microservice/static/auth.js | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index 3cb8da0..dfa6c3f 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -195,6 +195,7 @@ def request_password_reset():
         return jsonify({"error": "Email is required"}), 400
 
     email = data['email'].strip().lower()
+    url = data['url']
     try:
         valid = validate_email(email)
         email = valid.normalized
@@ -221,7 +222,7 @@ def request_password_reset():
             )
             conn.commit()
 
-            reset_url = f"http://localhost:8000/reset_password/{token}"
+            reset_url = f"{url}/reset_password/{token}"
 
             subject = "Password Reset Request"
             body = f"Click the following link to reset your password: {reset_url}\n\nThis link will expire in 1 hour.\n\nIf you did not request this, please ignore this email."
diff --git a/frontend_microservice/static/auth.js b/frontend_microservice/static/auth.js
index 24e0fff..ccb91f3 100644
--- a/frontend_microservice/static/auth.js
+++ b/frontend_microservice/static/auth.js
@@ -102,12 +102,13 @@ if (loginForm) {
             const email = document.getElementById('email').value;
             const messageDiv = document.getElementById('message');
             messageDiv.textContent = ''; 
+            const url = window.location.hostname
 
             try {
                 const response = await fetch('/forgot_password', { 
                     method: 'POST',
                     headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ email })
+                    body: JSON.stringify({url, email })
                 });
 
                 const data = await response.json();
-- 
GitLab


From 043a4ffafa9122106db39cdd451e54b88b679744 Mon Sep 17 00:00:00 2001
From: "Peron, Loic R (PG/T - Comp Sci & Elec Eng)" <lp01242@surrey.ac.uk>
Date: Wed, 21 May 2025 13:29:12 +0100
Subject: [PATCH 124/124] https to http

---
 account_microservice/user.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/account_microservice/user.py b/account_microservice/user.py
index dfa6c3f..d464c0b 100644
--- a/account_microservice/user.py
+++ b/account_microservice/user.py
@@ -224,6 +224,10 @@ def request_password_reset():
 
             reset_url = f"{url}/reset_password/{token}"
 
+            if reset_url.startswith("https://"):
+                reset_url = reset_url.replace("https://", "http://", 1)
+
+
             subject = "Password Reset Request"
             body = f"Click the following link to reset your password: {reset_url}\n\nThis link will expire in 1 hour.\n\nIf you did not request this, please ignore this email."
             msg = Message(subject, recipients=[email], body=body)
-- 
GitLab