]> git.immae.eu Git - github/bastienwirtz/homer.git/commitdiff
Octoprint service (#324) (#560) v22.11.2
authorBastien Wirtz <bastien.wirtz@gmail.com>
Sun, 27 Nov 2022 17:41:12 +0000 (18:41 +0100)
committerGitHub <noreply@github.com>
Sun, 27 Nov 2022 17:41:12 +0000 (09:41 -0800)
Add Octoprint service.

17 files changed:
docs/customservices.md
dummy-data/README.md
dummy-data/octoprint/api/job [new file with mode: 0644]
dummy-data/octoprint/api/status_printer_offline-error.json [new file with mode: 0644]
dummy-data/octoprint/api/status_printer_offline.json [new file with mode: 0644]
dummy-data/octoprint/api/status_printer_operational.json [new file with mode: 0644]
dummy-data/octoprint/api/status_printer_printing.json [new file with mode: 0644]
dummy-data/octoprint/api/status_printer_printing_1of2.json [new file with mode: 0644]
dummy-data/octoprint/api/status_printer_printing_2of2.json [new file with mode: 0644]
dummy-data/octoprint/api/status_printing_completion.json [new file with mode: 0644]
dummy-data/speedtesttracker/api/speedtest/latest [new file with mode: 0644]
package.json
public/assets/config-demo.yml.dist
src/assets/app.scss
src/assets/components/status.scss [new file with mode: 0644]
src/components/services/OctoPrint.vue [new file with mode: 0644]
yarn.lock

index 5bb08ff06b59cb8e4d58f212d20a4ce336cc04c9..88e4e2f363727fd8181fcece0be5002661ba86f6 100644 (file)
@@ -31,6 +31,7 @@ within Homer:
   - [Speedtest Tracker](#SpeedtestTracker)
   - [What's Up Docker](#whats-up-docker)
   - [SABnzbd](#sabnzbd)
+  - [OctoPrint](#sabnzbd)
 
 If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page.
 
@@ -84,7 +85,7 @@ Two lines are needed in the config.yml :
 
 ```yaml
   type: "Medusa"
-  apikey: "01234deb70424befb1f4ef6a23456789"
+  apikey: "<---insert-api-key-here--->"
 ```
 
 The url must be the root url of Medusa application.
@@ -97,7 +98,7 @@ Two lines are needed in the config.yml :
 
 ```yaml
   type: "Lidarr", "Prowlarr", "Radarr" or "Sonarr"
-  apikey: "01234deb70424befb1f4ef6a23456789"
+  apikey: "<---insert-api-key-here--->"
 ```
 
 The url must be the root url of Lidarr, Prowlarr, Radarr or Sonarr application.
@@ -108,7 +109,7 @@ If you are using an older version of Radarr or Sonarr which don't support the ne
 - name: "Radarr"
   type: "Radarr"
   url: "http://localhost:7878/"
-  apikey: "MY-SUPER-SECRET-API-KEY"
+  apikey: "<---insert-api-key-here--->"
   target: "_blank"
   legacyApi: true
 ```
@@ -119,7 +120,7 @@ This service displays total number of documents stored. Two lines are required:
 
 ```yaml
   type: "PaperlessNG"
-  apikey: "0123456789abcdef123456789abcdef"
+  apikey: "<---insert-api-key-here--->"
 ```
 
 API key can be generated in Settings > Administration > Auth Tokens
@@ -175,7 +176,7 @@ See https://docs.portainer.io/v/ce-2.11/user/account-settings#access-tokens
   logo: "assets/tools/sample.png"
   url: "http://192.168.0.151/"
   type: "Portainer"
-  apikey: "MY-SUPER-SECRET-API-KEY"
+  apikey: "<---insert-api-key-here--->"
   # environments:
   #   - "raspberry"
   #   - "local"
@@ -190,7 +191,7 @@ You need to set the type to Emby, provide an api key and choose which stats to s
   logo: "assets/tools/sample.png"
   url: "http://192.168.0.151/"
   type: "Emby"
-  apikey: "MY-SUPER-SECRET-API-KEY"
+  apikey: "<---insert-api-key-here--->"
   libraryType: "music" #Choose which stats to show. Can be one of: music, series or movies.
 ```
 
@@ -220,7 +221,7 @@ the "Web Interface" section of settings on the Tautulli web UI.
   logo: "assets/tools/sample.png"
   url: "http://192.168.0.151:8181"
   type: "Tautulli"
-  apikey: "MY-SUPER-SECRET-API-KEY"
+  apikey: "<---insert-api-key-here--->"
 ```
 
 Because the service type and link don't necessarily have to match, you could
@@ -233,7 +234,7 @@ endpoint pointing to Tautulli!
   url: "http://192.168.0.151:32400/web" # Plex
   endpoint: "http://192.168.0.151:8181" # Tautulli
   type: "Tautulli"
-  apikey: "MY-SUPER-SECRET-API-KEY"
+  apikey: "<---insert-api-key-here--->"
 ```
 
 ## Mealie
@@ -248,7 +249,7 @@ Two lines are needed in the config.yml :
 
 ```yaml
   type: "Healthchecks"
-  apikey: "01234deb70424befb1f4ef6a23456789"
+  apikey: "<---insert-api-key-here--->"
 ```
 
 The url must be the root url of the Healthchecks application.
@@ -377,3 +378,16 @@ the "Config" > "General" section of the SABnzbd config in the SABnzbd web UI.
   apikey: "MY-SUPER-SECRET-API-KEY"
   downloadInterval: 5000 # (Optional) Interval (in ms) for updating the download count
 ```
+
+## OctoPrint
+
+The OctoPrint service only needs an `apikey` & `url` and optionally a `display` option.
+
+```yaml
+- name: "Octoprint"
+  logo: "https://cdn-icons-png.flaticon.com/512/3112/3112529.png"
+  apikey: "xxxxxxxxxxxx" # insert your own API key here. Request one from https://openweathermap.org/api.
+  url: "http://192.168.0.151:8080"
+  display: "text" # 'text' or 'bar'. Default to `text`.
+  type: "OctoPrint"
+```
\ No newline at end of file
index 6db4712edcb2abecd7b353f02bf05ad10c5a08eb..78b2243608b8a6af0ba62cd72403541f2e80c361 100644 (file)
@@ -3,6 +3,12 @@
 This directory content makes possible to test custom services cards or create a demo without actually running the service.
 The principle is simple: save a sample output of the API used in the service in a static file in this directory. The path must be identical as the service endpoint to be used seamlessly.
 
+## Start the mock server to expose dummy data
+
+```sh
+yarn mock
+```
+
 ## How to add a new services sample
 
 - create a directory for your service, and any sub-folder existing in the service api path.
diff --git a/dummy-data/octoprint/api/job b/dummy-data/octoprint/api/job
new file mode 100644 (file)
index 0000000..c1ec6ed
--- /dev/null
@@ -0,0 +1,30 @@
+{
+       "job": {
+               "averagePrintTime": 669.3131185749999,
+               "estimatedPrintTime": 314.87566979223726,
+               "filament": {
+                       "tool0": {
+                               "length": 134.81171000000032,
+                               "volume": 0.0
+                       }
+               },
+               "file": {
+                       "date": 1665547748,
+                       "display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "origin": "local",
+                       "path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "size": 129581
+               },
+               "lastPrintTime": 669.3131185749999,
+               "user": "friendlyngeeks"
+       },
+       "progress": {
+               "completion": 27.456185706237797,
+               "filepos": 35578,
+               "printTime": 460,
+               "printTimeLeft": 4612,
+               "printTimeLeftOrigin": "linear"
+       },
+       "state": "Printing"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printer_offline-error.json b/dummy-data/octoprint/api/status_printer_offline-error.json
new file mode 100644 (file)
index 0000000..10fa163
--- /dev/null
@@ -0,0 +1,26 @@
+{
+       "error": "SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)",
+       "job": {
+               "averagePrintTime": null,
+               "estimatedPrintTime": null,
+               "filament": null,
+               "file": {
+                       "date": null,
+                       "display": null,
+                       "name": null,
+                       "origin": null,
+                       "path": null,
+                       "size": null
+               },
+               "lastPrintTime": null,
+               "user": null
+       },
+       "progress": {
+               "completion": null,
+               "filepos": null,
+               "printTime": null,
+               "printTimeLeft": null,
+               "printTimeLeftOrigin": null
+       },
+       "state": "Offline after error"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printer_offline.json b/dummy-data/octoprint/api/status_printer_offline.json
new file mode 100644 (file)
index 0000000..c733409
--- /dev/null
@@ -0,0 +1,26 @@
+{
+       "job": {
+               "estimatedPrintTime": null,
+               "filament": {
+                       "length": null,
+                       "volume": null
+               },
+               "file": {
+                       "date": null,
+                       "name": null,
+                       "origin": null,
+                       "path": null,
+                       "size": null
+               },
+               "lastPrintTime": null,
+               "user": null
+       },
+       "progress": {
+               "completion": null,
+               "filepos": null,
+               "printTime": null,
+               "printTimeLeft": null,
+               "printTimeOrigin": null
+       },
+       "state": "Offline"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printer_operational.json b/dummy-data/octoprint/api/status_printer_operational.json
new file mode 100644 (file)
index 0000000..993d798
--- /dev/null
@@ -0,0 +1,26 @@
+{
+       "job": {
+               "estimatedPrintTime": null,
+               "filament": {
+                       "length": null,
+                       "volume": null
+               },
+               "file": {
+                       "date": null,
+                       "name": null,
+                       "origin": null,
+                       "path": null,
+                       "size": null
+               },
+               "lastPrintTime": null,
+               "user": null
+       },
+       "progress": {
+               "completion": null,
+               "filepos": null,
+               "printTime": null,
+               "printTimeLeft": null,
+               "printTimeOrigin": null
+       },
+       "state": "Operational"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printer_printing.json b/dummy-data/octoprint/api/status_printer_printing.json
new file mode 100644 (file)
index 0000000..09cf3cf
--- /dev/null
@@ -0,0 +1,30 @@
+{
+       "job": {
+               "averagePrintTime": 669.3131185749999,
+               "estimatedPrintTime": 314.87566979223726,
+               "filament": {
+                       "tool0": {
+                               "length": 134.81171000000032,
+                               "volume": 0.0
+                       }
+               },
+               "file": {
+                       "date": 1665547748,
+                       "display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "origin": "local",
+                       "path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "size": 129581
+               },
+               "lastPrintTime": 669.3131185749999,
+               "user": "friendlyngeeks"
+       },
+       "progress": {
+               "completion": 0.1551153332664511,
+               "filepos": 201,
+               "printTime": 0,
+               "printTimeLeft": 668,
+               "printTimeLeftOrigin": "average"
+       },
+       "state": "Printing"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printer_printing_1of2.json b/dummy-data/octoprint/api/status_printer_printing_1of2.json
new file mode 100644 (file)
index 0000000..09cf3cf
--- /dev/null
@@ -0,0 +1,30 @@
+{
+       "job": {
+               "averagePrintTime": 669.3131185749999,
+               "estimatedPrintTime": 314.87566979223726,
+               "filament": {
+                       "tool0": {
+                               "length": 134.81171000000032,
+                               "volume": 0.0
+                       }
+               },
+               "file": {
+                       "date": 1665547748,
+                       "display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "origin": "local",
+                       "path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "size": 129581
+               },
+               "lastPrintTime": 669.3131185749999,
+               "user": "friendlyngeeks"
+       },
+       "progress": {
+               "completion": 0.1551153332664511,
+               "filepos": 201,
+               "printTime": 0,
+               "printTimeLeft": 668,
+               "printTimeLeftOrigin": "average"
+       },
+       "state": "Printing"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printer_printing_2of2.json b/dummy-data/octoprint/api/status_printer_printing_2of2.json
new file mode 100644 (file)
index 0000000..2acf861
--- /dev/null
@@ -0,0 +1,30 @@
+{
+       "job": {
+               "averagePrintTime": 669.3131185749999,
+               "estimatedPrintTime": 314.87566979223726,
+               "filament": {
+                       "tool0": {
+                               "length": 134.81171000000032,
+                               "volume": 0.0
+                       }
+               },
+               "file": {
+                       "date": 1665547748,
+                       "display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "origin": "local",
+                       "path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "size": 129581
+               },
+               "lastPrintTime": 669.3131185749999,
+               "user": "friendlyngeeks"
+       },
+       "progress": {
+               "completion": 27.456185706237797,
+               "filepos": 35578,
+               "printTime": 476,
+               "printTimeLeft": 1612,
+               "printTimeLeftOrigin": "linear"
+       },
+       "state": "Printing"
+}
\ No newline at end of file
diff --git a/dummy-data/octoprint/api/status_printing_completion.json b/dummy-data/octoprint/api/status_printing_completion.json
new file mode 100644 (file)
index 0000000..55728ba
--- /dev/null
@@ -0,0 +1,30 @@
+{
+       "job": {
+               "averagePrintTime": 698.814525153,
+               "estimatedPrintTime": 314.87566979223726,
+               "filament": {
+                       "tool0": {
+                               "length": 134.81171000000032,
+                               "volume": 0.0
+                       }
+               },
+               "file": {
+                       "date": 1665547748,
+                       "display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "origin": "local",
+                       "path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
+                       "size": 129581
+               },
+               "lastPrintTime": 728.315931731,
+               "user": "friendlyngeeks"
+       },
+       "progress": {
+               "completion": 100.0,
+               "filepos": 129581,
+               "printTime": 728,
+               "printTimeLeft": 0,
+               "printTimeLeftOrigin": null
+       },
+       "state": "Operational"
+}
\ No newline at end of file
diff --git a/dummy-data/speedtesttracker/api/speedtest/latest b/dummy-data/speedtesttracker/api/speedtest/latest
new file mode 100644 (file)
index 0000000..c8e3c1e
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "data": {
+        "download": 42.452234,
+        "upload": 34.3948,
+        "ping": 12.9873
+    }
+}
\ No newline at end of file
index 93953c17f05429bbd336ae2b11317e18c796e1a7..c97dc56e3c92c091bb04e05c789701f0347c35d7 100644 (file)
@@ -3,6 +3,7 @@
   "version": "22.07.2",
   "scripts": {
     "dev": "vite",
+    "mock": "http-server dummy-data/ --cors",
     "build": "vite build",
     "preview": "vite preview --port 5050",
     "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
@@ -10,9 +11,9 @@
   "dependencies": {
     "@fortawesome/fontawesome-free": "^6.2.0",
     "bulma": "^0.9.4",
-    "yaml": "^2.1.3",
     "lodash.merge": "^4.6.2",
-    "vue": "^3.2.41"
+    "vue": "^3.2.41",
+    "yaml": "^2.1.3"
   },
   "devDependencies": {
     "@rushstack/eslint-patch": "^1.2.0",
@@ -20,6 +21,7 @@
     "@vue/eslint-config-prettier": "^7.0.0",
     "eslint": "^8.26.0",
     "eslint-plugin-vue": "^9.6.0",
+    "http-server": "^14.1.1",
     "prettier": "^2.7.1",
     "sass": "^1.55.0",
     "vite": "^3.2.1",
index 4d088ccc9b831756593932cf0023415daf685c95..22dd3b0fe4b1143f1b9694aed0d3b549bea602b3 100644 (file)
@@ -93,6 +93,11 @@ services:
   - name: "Tools"
     icon: "fa-solid fa-screwdriver-wrench"
     items:
+      - name: "Octoprint"
+        logo: "https://cdn-icons-png.flaticon.com/512/3112/3112529.png"
+        apikey: "xxxxxxxxxxxx"
+        endpoint: "https://homer-demo-content.netlify.app/octoprint"
+        type: "OctoPrint"
       - name: "Example item"
         logo: "assets/tools/sample.png"
         subtitle: "This another example"
index d112482626220d32deb9b1042595c2cc08c86f16..4ce417a9064345ff3380949850e8a802683c75f3 100644 (file)
@@ -1,8 +1,8 @@
 @charset "utf-8";
 
 @import "./webfonts/webfonts.scss";
-
 @import "../../node_modules/bulma/bulma";
+@import "./components/status.scss";
 
 // Themes import
 @import "./themes/sui.scss";
diff --git a/src/assets/components/status.scss b/src/assets/components/status.scss
new file mode 100644 (file)
index 0000000..b5fd20b
--- /dev/null
@@ -0,0 +1,48 @@
+.status {
+    font-size: 0.8rem;
+    color: var(--text-title);
+
+    &.offline:before, &.error:before {
+      background-color: #d65c68;
+      box-shadow: 0 0 5px 1px #d65c68;
+      color: #d65c68;
+    }
+  
+    &.pending:before {
+      background-color: #e8bb7d;
+      box-shadow: 0 0 5px 1px #e8bb7d;
+    }
+  
+    &.online:before, &.ready:before {
+      background-color: #94e185;
+      box-shadow: 0 0 5px 1px #94e185;
+    }
+  
+    &.in-progress:before {
+      background-color: #8fe87d;
+      box-shadow: 0 0 5px 1px #8fe87d;
+      animation: pulse 1s alternate infinite;
+    }
+  
+    @keyframes pulse {
+      0% {
+        background: rgba(255, 255, 255, 0.2);
+        box-shadow: inset 0px 0px 10px 2px rgba(0, 255, 182, 0.3),
+          0px 0px 5px 2px rgba(0, 255, 135, 0.2);
+      }
+      100% {
+        background: rgba(255, 255, 255, 1);
+        box-shadow: inset 0px 0px 10px 2px rgba(0, 255, 182, 0.5),
+          0px 0px 15px 2px rgba(0, 255, 135, 1);
+      }
+    }
+  
+    &:before {
+      content: " ";
+      display: inline-block;
+      width: 8px;
+      height: 8px;
+      margin-right: 10px;
+      border-radius: 8px;
+    }
+  }
\ No newline at end of file
diff --git a/src/components/services/OctoPrint.vue b/src/components/services/OctoPrint.vue
new file mode 100644 (file)
index 0000000..c5da8d9
--- /dev/null
@@ -0,0 +1,108 @@
+<template>
+  <Generic :item="item" :title="state">
+    <template #content>
+      <p class="title is-4">{{ item.name }}</p>
+      <p class="subtitle is-6">
+        <template v-if="item.subtitle && !state">
+          {{ item.subtitle }}
+        </template>
+        <template v-if="!error && display == 'text'">
+          <i class="fa-solid fa-gear mr-1"></i>
+          <b v-if="completion">{{ completion.toFixed() }}%</b>
+          <span class="separator mx-1"> | </span>
+          <span v-if="printTime" :title="`${toTime(printTimeLeft)} left`">
+            <i class="fa-solid fa-stopwatch mr-1"></i>
+            {{ toTime(printTime) }}
+          </span>
+        </template>
+        <template v-if="!error && display == 'bar'">
+          <progress
+            v-if="completion"
+            class="progress is-primary"
+            :value="completion"
+            max="100"
+            :title="`${state} - ${completion.toFixed()}%, ${toTime(
+              printTimeLeft
+            )} left`"
+          >
+            {{ completion }}%
+          </progress>
+        </template>
+        <span v-if="error" :title="error">{{ error }}</span>
+      </p>
+    </template>
+    <template #indicator>
+      <i :class="['status', statusClass]" :title="state"></i>
+    </template>
+  </Generic>
+</template>
+
+<script>
+import service from "@/mixins/service.js";
+import Generic from "./Generic.vue";
+
+export default {
+  name: "OctoPrint",
+  mixins: [service],
+  props: {
+    item: Object,
+  },
+  components: {
+    Generic,
+  },
+  data: () => ({
+    printTime: null,
+    printTimeLeft: null,
+    completion: null,
+    state: null,
+    error: null,
+  }),
+  computed: {
+    statusClass: function () {
+      switch (this.state) {
+        case "Operational":
+          return "ready";
+        case "Offline":
+          return "offline";
+        case "Printing":
+          return "in-progress";
+        default:
+          return "pending";
+      }
+    },
+  },
+  created() {
+    this.display = this.item.display == "bar" ? this.item.display : "text";
+    this.fetchStatus();
+  },
+  methods: {
+    fetchStatus: async function () {
+      try {
+        const response = await this.fetch(`api/job?apikey=${this.item.apikey}`);
+        this.printTime = response.progress.printTime;
+        this.printTimeLeft = response.progress.printTimeLeft;
+        this.completion = response.progress.completion;
+        this.state = response.state;
+        this.error = response.error;
+      } catch (e) {
+        this.error = `Fail to fetch octoprint data (${e.message})`;
+        console.error(e);
+      }
+    },
+    toTime: function (timastamp) {
+      return new Date(timastamp * 1000).toTimeString().substring(0, 5);
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+.fa-triangle-exclamation::before {
+  color: #d65c68;
+}
+
+.progress {
+  height: 8px;
+  width: 90%;
+}
+</style>
index 1a2d7edfd03175a75ecc7d4082c7643f4ccbb862..ec9d0c08398523109ecd25e10e55d813e9bae256 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -1269,6 +1269,13 @@ argparse@^2.0.1:
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
+async@^2.6.4:
+  version "2.6.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
+  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
+  dependencies:
+    lodash "^4.17.14"
+
 async@^3.2.3:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
@@ -1315,6 +1322,13 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+basic-auth@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
+  integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
+  dependencies:
+    safe-buffer "5.1.2"
+
 binary-extensions@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@@ -1399,7 +1413,7 @@ chalk@^2.0.0:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^4.0.0, chalk@^4.0.2:
+chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
   integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -1476,6 +1490,11 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1:
     browserslist "^4.21.0"
     semver "7.0.0"
 
+corser@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
+  integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
+
 cross-spawn@^7.0.2:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1500,6 +1519,13 @@ csstype@^2.6.8:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
 
+debug@^3.2.7:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
 debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@@ -1872,6 +1898,11 @@ esutils@^2.0.2:
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+eventemitter3@^4.0.0:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -1952,6 +1983,11 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2"
   integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==
 
+follow-redirects@^1.0.0:
+  version "1.15.2"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
 fs-extra@^9.0.1:
   version "9.1.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@@ -2108,6 +2144,53 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
+he@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+html-encoding-sniffer@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
+  integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
+  dependencies:
+    whatwg-encoding "^2.0.0"
+
+http-proxy@^1.18.1:
+  version "1.18.1"
+  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
+  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
+  dependencies:
+    eventemitter3 "^4.0.0"
+    follow-redirects "^1.0.0"
+    requires-port "^1.0.0"
+
+http-server@^14.1.1:
+  version "14.1.1"
+  resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e"
+  integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==
+  dependencies:
+    basic-auth "^2.0.1"
+    chalk "^4.1.2"
+    corser "^2.0.1"
+    he "^1.2.0"
+    html-encoding-sniffer "^3.0.0"
+    http-proxy "^1.18.1"
+    mime "^1.6.0"
+    minimist "^1.2.6"
+    opener "^1.5.1"
+    portfinder "^1.0.28"
+    secure-compare "3.0.1"
+    union "~0.5.0"
+    url-join "^4.0.1"
+
+iconv-lite@0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
 idb@^7.0.1:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
@@ -2414,7 +2497,7 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
 
-lodash@^4.17.20, lodash@^4.17.21:
+lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -2451,6 +2534,11 @@ micromatch@^4.0.4:
     braces "^3.0.2"
     picomatch "^2.3.1"
 
+mime@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
 minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -2465,11 +2553,28 @@ minimatch@^5.0.1:
   dependencies:
     brace-expansion "^2.0.1"
 
+minimist@^1.2.6:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+  integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
+mkdirp@^0.5.6:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+  dependencies:
+    minimist "^1.2.6"
+
 ms@2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 nanoid@^3.3.4:
   version "3.3.4"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@@ -2524,6 +2629,11 @@ once@^1.3.0:
   dependencies:
     wrappy "1"
 
+opener@^1.5.1:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
+  integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
+
 optionator@^0.9.1:
   version "0.9.1"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@@ -2587,6 +2697,15 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 
+portfinder@^1.0.28:
+  version "1.0.32"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
+  integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
+  dependencies:
+    async "^2.6.4"
+    debug "^3.2.7"
+    mkdirp "^0.5.6"
+
 postcss-selector-parser@^6.0.9:
   version "6.0.10"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
@@ -2645,6 +2764,13 @@ punycode@^2.1.0:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
+qs@^6.4.0:
+  version "6.11.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
+  integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+  dependencies:
+    side-channel "^1.0.4"
+
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -2731,6 +2857,11 @@ require-from-string@^2.0.2:
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
   integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
 
+requires-port@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+  integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -2788,15 +2919,20 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
+safe-buffer@5.1.2, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
 safe-buffer@^5.1.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-safe-buffer@~5.1.1:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
-  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+"safer-buffer@>= 2.1.2 < 3.0.0":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
 sass@^1.55.0:
   version "1.55.0"
@@ -2807,6 +2943,11 @@ sass@^1.55.0:
     immutable "^4.0.0"
     source-map-js ">=0.6.2 <2.0.0"
 
+secure-compare@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
+  integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==
+
 semver@7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
@@ -3058,6 +3199,13 @@ unicode-property-aliases-ecmascript@^2.0.0:
   resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8"
   integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==
 
+union@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075"
+  integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==
+  dependencies:
+    qs "^6.4.0"
+
 unique-string@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
@@ -3090,6 +3238,11 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
+url-join@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
+  integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
+
 util-deprecate@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -3148,6 +3301,13 @@ webidl-conversions@^4.0.2:
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
   integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
 
+whatwg-encoding@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
+  integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
+  dependencies:
+    iconv-lite "0.6.3"
+
 whatwg-url@^7.0.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"