Forráskód Böngészése

Merge branch 'main' of github.com:OliveTin/OliveTin

jamesread 2 éve
szülő
commit
3ea14f1353
60 módosított fájl, 2708 hozzáadás és 455 törlés
  1. 44 0
      .air.toml
  2. 1 1
      .github/ISSUE_TEMPLATE/support_request.md
  3. 4 4
      .github/PULL_REQUEST_TEMPLATE.md
  4. 1 1
      .github/workflows/build-snapshot.yml
  5. 3 2
      .github/workflows/codestyle.yml
  6. 1 0
      .gitignore
  7. 7 8
      .goreleaser.yml
  8. 10 0
      .pre-commit-config.yaml
  9. 11 11
      CONTRIBUTING.adoc
  10. 3 3
      Dockerfile
  11. 4 4
      Dockerfile.arm64
  12. 5 5
      Dockerfile.armv7
  13. 4 4
      Jenkinsfile
  14. 7 7
      Makefile
  15. 45 14
      OliveTin.proto
  16. 14 15
      README.md
  17. 2 2
      SECURITY.md
  18. 0 1
      buf.gen.yaml
  19. 1 1
      buf.yaml
  20. 2 0
      cmd/OliveTin/main.go
  21. 14 10
      config.yaml
  22. 50 43
      go.mod
  23. 103 90
      go.sum
  24. 1 1
      integration-tests/Makefile
  25. 1 1
      integration-tests/README.md
  26. 1 1
      integration-tests/Vagrantfile
  27. 5 6
      integration-tests/configs/config.general.yaml
  28. 3 3
      integration-tests/configs/config.hiddenFooter.yaml
  29. 4 4
      integration-tests/configs/config.hiddenNav.yaml
  30. 0 2
      integration-tests/cypress/e2e/general/defaultHomepageRender.cy.js
  31. 0 2
      integration-tests/cypress/e2e/hiddenFooter/hiddenFooter.cy.js
  32. 0 2
      integration-tests/cypress/e2e/hiddenNav/hiddenNav.cy.js
  33. 0 1
      integration-tests/cypress/support/commands.js
  34. 0 1
      integration-tests/cypressRun.sh
  35. 1373 0
      integration-tests/package-lock.json
  36. 5 2
      internal/acl/acl.go
  37. 4 1
      internal/config/config.go
  38. 4 0
      internal/config/sanitize.go
  39. 6 4
      internal/executor/arguments.go
  40. 140 48
      internal/executor/executor.go
  41. 80 11
      internal/grpcapi/grpcApi.go
  42. 5 4
      internal/grpcapi/grpcApiActions.go
  43. 3 3
      internal/grpcapi/grpcApi_test.go
  44. 6 2
      internal/httpservers/restapi.go
  45. 53 1
      internal/httpservers/restapi_auth_jwt.go
  46. 125 0
      internal/httpservers/restapi_test.go
  47. 8 2
      internal/httpservers/singleFrontend.go
  48. 3 0
      internal/httpservers/webuiServer_test.go
  49. 122 0
      internal/websocket/websocket.go
  50. 1 1
      var/initscript/OliveTin
  51. 1 1
      var/openrc/OliveTin
  52. 79 26
      webui/index.html
  53. 16 61
      webui/js/ActionButton.js
  54. 105 0
      webui/js/ExecutionButton.js
  55. 66 0
      webui/js/ExecutionDialog.js
  56. 1 1
      webui/js/marshaller.js
  57. 73 0
      webui/js/websocket.js
  58. 35 6
      webui/main.js
  59. 18 18
      webui/package-lock.json
  60. 25 13
      webui/style.css

+ 44 - 0
.air.toml

@@ -0,0 +1,44 @@
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+  args_bin = []
+  bin = "./OliveTin"
+  cmd = "go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin"
+  delay = 1
+  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
+  exclude_file = []
+  exclude_regex = ["_test.go"]
+  exclude_unchanged = false
+  follow_symlink = false
+  full_bin = ""
+  include_dir = []
+  include_ext = ["go", "tpl", "tmpl", "html"]
+  include_file = []
+  kill_delay = "0s"
+  log = "build-errors.log"
+  poll = false
+  poll_interval = 0
+  rerun = false
+  rerun_delay = 500
+  send_interrupt = false
+  stop_on_error = true
+
+[color]
+  app = ""
+  build = "yellow"
+  main = "magenta"
+  runner = "green"
+  watcher = "cyan"
+
+[log]
+  main_only = false
+  time = false
+
+[misc]
+  clean_on_exit = false
+
+[screen]
+  clear_on_rebuild = false
+  keep_scroll = true

+ 1 - 1
.github/ISSUE_TEMPLATE/support_request.md

@@ -39,7 +39,7 @@ If possible, please copy and paste your OliveTin logs from when the error happen
 **Screenshot of WebDeveloper console logs**
 
 If you know how, and if you think it's relevant, a screenshot of the
-WebDeveloper console from when you clicked a button is often really helpful. 
+WebDeveloper console from when you clicked a button is often really helpful.
 
 **Anything else?**
 

+ 4 - 4
.github/PULL_REQUEST_TEMPLATE.md

@@ -4,7 +4,7 @@ First of all, thank you for considering to raise a pull request!
 
 Don’t be afraid to ask for advice before working on a contribution. If you’re thinking about a bigger change, especially that might affect the core working or architecture, it’s almost essential to talk and ask about what you’re planning might affect things.  Some of the larger future plans may not be documented well so it’s difficult to understand how your change might affect the general direction and roadmap of this project without asking.
 
-The preferred way to communicate is probably via Discord or GitHub issues. 
+The preferred way to communicate is probably via Discord or GitHub issues.
 
 Helpful information to understand the project can be found here: [CONTRIBUTING](https://github.com/OliveTin/OliveTin/blob/main/CONTRIBUTING.adoc)
 
@@ -13,10 +13,10 @@ Helpful information to understand the project can be found here: [CONTRIBUTING](
 # Checklist
 Please put a X in the boxes as evidence of reading through the checklist.
 
-- [ ] I have forked the project, and raised this PR on a feature branch. 
+- [ ] I have forked the project, and raised this PR on a feature branch.
 - [ ] `make githooks` has been run, and my git commit message was accepted by the git hook.
 - [ ] `make daemon-compile` runs without any issues.
 - [ ] `make daemon-codestyle` runs without any issues.
 - [ ] `make daemon-unittests` runs without any issues.
-- [ ] `make webui-codestyle` runs without any issues. 
-- [ ] I understand and accept the [AGPL-3.0 license](LICENSE) and [code of conduct](CODE_OF_CONDUCT.md), and my contributions fall under these. 
+- [ ] `make webui-codestyle` runs without any issues.
+- [ ] I understand and accept the [AGPL-3.0 license](LICENSE) and [code of conduct](CODE_OF_CONDUCT.md), and my contributions fall under these.

+ 1 - 1
.github/workflows/build-snapshot.yml

@@ -30,7 +30,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '>=1.18.0'
+          go-version: '^1.18.0'
           cache: true
 
       - name: grpc

+ 3 - 2
.github/workflows/codestyle.yml

@@ -19,9 +19,10 @@ jobs:
         uses: actions/checkout@v2
 
       - name: Setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
-          go-version: '^1.16.0'
+          go-version: '^1.18.0'
+          cache: true
 
       - name: deps
         run: make grpc

+ 1 - 0
.gitignore

@@ -9,3 +9,4 @@ reports
 releases/
 dist/
 installation-id.txt
+tmp/

+ 7 - 8
.goreleaser.yml

@@ -18,7 +18,7 @@ builds:
 
     goarm:
       - 5 # For old RPIs
-      - 6 
+      - 6
       - 7
 
     main: cmd/OliveTin/main.go
@@ -62,10 +62,10 @@ changelog:
       - '^refactor:'
 
 archives:
-  - 
+  -
     format: tar.gz
 
-    files: 
+    files:
       - config.yaml
       - LICENSE
       - README.md
@@ -225,15 +225,14 @@ release:
     ## Container images ([on Docker Hub](https://hub.docker.com/r/jamesread/olivetin/tags?page=1&ordering=last_updated))
 
     - `docker pull docker.io/jamesread/olivetin:{{ .Version }}`
-    
+
     ## Upgrade warnings, or breaking changes
-   
-    - No such issues between the last release and this version. 
+
+    - No such issues between the last release and this version.
 
     ## Useful links
 
     - [Which download do I need?](https://docs.olivetin.app/choose-package.html)
     - [Ask for help and chat with others users in the Discord community](https://discord.gg/jhYWWpNJ3v)
-    
+
     Thanks for your interest in OliveTin!
-  

+ 10 - 0
.pre-commit-config.yaml

@@ -0,0 +1,10 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.2.0
+    hooks:
+    -   id: trailing-whitespace
+    -   id: end-of-file-fixer
+    -   id: check-yaml
+    -   id: check-added-large-files

+ 11 - 11
CONTRIBUTING.adoc

@@ -7,9 +7,9 @@ of multiple projects, your time and interest in contributing is most welcome.
 If you're not sure where to get started, raise an issue in the project.
 
 Ideas may be discussed, purely on their merits and issues. Our Code of Conduct
-(CoC) is straightforward - it's important that contributors feel comfortable in 
-discussion throughout the whole process. This project respects the 
-link:https://www.kernel.org/doc/html/latest/process/code-of-conduct.html[Linux Kernel code of conduct]. 
+(CoC) is straightforward - it's important that contributors feel comfortable in
+discussion throughout the whole process. This project respects the
+link:https://www.kernel.org/doc/html/latest/process/code-of-conduct.html[Linux Kernel code of conduct].
 
 == If you're not sure, ask!
 
@@ -18,19 +18,19 @@ contribution. If you're thinking about a bigger change, especially that might
 affect the core working or architecture, it's almost essential to talk and ask
 about what you're planning might affect things. Some of the larger future plans may not be
 documented well so it's difficult to understand how your change might affect
-the general direction and roadmap of this project without asking. 
+the general direction and roadmap of this project without asking.
 
-The preferred way to communicate is probably via Discord or GitHub issues. 
+The preferred way to communicate is probably via Discord or GitHub issues.
 
-=== Dev environment setup and clean build - Fedora 
+=== Dev environment setup and clean build - Fedora
 
 ```
-dnf install git go protobuf-compiler make -y 
+dnf install git go protobuf-compiler make -y
 git clone https://github.com/OliveTin/OliveTin.git
 cd OliveTin
 
-# `make grpc` will also run `make go-tools`, which installs "buf". This binary 
-# will be put in your GOPATH/bin/, which should be on your path. buf is used to 
+# `make grpc` will also run `make go-tools`, which installs "buf". This binary
+# will be put in your GOPATH/bin/, which should be on your path. buf is used to
 # generate the protobuf / grpc stubs.
 make grpc
 make
@@ -39,10 +39,10 @@ make
 
 === Getting started to contribute;
 
-The project layout is reasonably straightforward; 
+The project layout is reasonably straightforward;
 
 * See the `Makefile` for common targets. This project was originally created on top of Fedora, but it should be usable on Debian/your faveourite distro with minor changes (if any).
-* The API is defined in protobuf+grpc - you will need to `make grpc`. 
+* The API is defined in protobuf+grpc - you will need to `make grpc`.
 * The Go daemon is built from the `cmd` and `internal` directories mostly.
 * The webui is just a single page application with a bit of Javascript in the `webui` directory. This can happily be hosted on another webserver.
 

+ 3 - 3
Dockerfile

@@ -4,16 +4,16 @@ LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
 LABEL org.opencontainers.image.title=OliveTin
 
 RUN mkdir -p /config /var/www/olivetin \
-    && microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \ 
+    && microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
 		iputils \
 		openssh-clients \
 		shadow-utils \
 		docker \
 	&& microdnf clean all
 
-RUN useradd --system --create-home olivetin -u 1000 
+RUN useradd --system --create-home olivetin -u 1000
 
-EXPOSE 1337/tcp 
+EXPOSE 1337/tcp
 
 VOLUME /config
 

+ 4 - 4
Dockerfile.arm64

@@ -5,14 +5,14 @@ LABEL org.opencontainers.image.title=OliveTin
 
 RUN mkdir -p /config /var/www/olivetin \
     && \
-    microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \ 
+    microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
 		iputils \
 		shadow-utils \
-		openssh-clients 
+		openssh-clients
 
-RUN useradd --system --create-home olivetin -u 1000 
+RUN useradd --system --create-home olivetin -u 1000
 
-EXPOSE 1337/tcp 
+EXPOSE 1337/tcp
 
 VOLUME /config
 

+ 5 - 5
Dockerfile.armv7

@@ -5,14 +5,14 @@ LABEL org.opencontainers.image.title=OliveTin
 
 RUN mkdir -p /config /var/www/olivetin \
     && \
-    microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \ 
+    microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
 		iputils \
-		shadow-utils \ 
-		openssh-clients 
+		shadow-utils \
+		openssh-clients
 
-RUN useradd --system --create-home olivetin -u 1000 
+RUN useradd --system --create-home olivetin -u 1000
 
-EXPOSE 1337/tcp 
+EXPOSE 1337/tcp
 
 VOLUME /config
 

+ 4 - 4
Jenkinsfile

@@ -14,7 +14,7 @@ pipeline {
 				sh 'make go-tools'
             }
         }
-        
+
         stage('Compile') {
             steps {
 				withEnv(["PATH+GO=/root/go/bin/"]) {
@@ -25,9 +25,9 @@ pipeline {
 				}
             }
         }
-        
+
         stage ('Post-Compile') {
-            parallel { 
+            parallel {
                 stage('Codestyle') {
                     steps {
 						withEnv(["PATH+GO=/root/go/bin/"]) {
@@ -45,6 +45,6 @@ pipeline {
                 }
             }
         }
-        
+
     }
 }

+ 7 - 7
Makefile

@@ -1,9 +1,9 @@
 compile: daemon-compile-x64-lin
 
-daemon-compile-armhf: 
+daemon-compile-armhf:
 	GOARCH=arm GOARM=6 go build -o OliveTin.armhf github.com/OliveTin/OliveTin/cmd/OliveTin
 
-daemon-compile-x64-lin: 
+daemon-compile-x64-lin:
 	GOOS=linux go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin
 
 daemon-compile-x64-win:
@@ -14,7 +14,7 @@ daemon-compile: daemon-compile-armhf daemon-compile-x64-lin daemon-compile-x64-w
 daemon-codestyle:
 	go fmt ./...
 	go vet ./...
-	gocyclo -over 4 cmd internal 
+	gocyclo -over 4 cmd internal
 	gocritic check ./...
 
 daemon-unittests:
@@ -24,7 +24,7 @@ daemon-unittests:
 
 githooks:
 	cp -v .githooks/* .git/hooks/
-	
+
 go-tools:
 	go install "github.com/bufbuild/buf/cmd/buf"
 	go install "github.com/fzipp/gocyclo/cmd/gocyclo"
@@ -37,7 +37,7 @@ go-tools:
 grpc: go-tools
 	buf generate
 
-dist: protoc 
+dist: protoc
 
 protoc:
 	protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. -I .:/usr/include/ OliveTin.proto
@@ -54,7 +54,7 @@ podman-container:
 integration-tests-docker-image:
 	docker rm -f olivetin && docker rmi -f olivetin
 	docker build -t olivetin:latest .
-	docker create --name olivetin -p 1337:1337 -v `pwd`/integration-tests/configs/:/config/ olivetin 
+	docker create --name olivetin -p 1337:1337 -v `pwd`/integration-tests/configs/:/config/ olivetin
 
 devrun: compile
 	killall OliveTin || true
@@ -70,4 +70,4 @@ webui-codestyle:
 clean:
 	rm -rf dist OliveTin OliveTin.armhf OliveTin.exe reports gen
 
-.PHONY: grpc 
+.PHONY: grpc

+ 45 - 14
OliveTin.proto

@@ -8,16 +8,16 @@ message Action {
 	string id = 1;
 	string title = 2;
 	string icon = 3;
-	bool canExec = 4;
-
+	bool can_exec = 4;
 	repeated ActionArgument arguments = 5;
+	bool popup_on_start = 6;
 }
 
 message ActionArgument {
 	string name = 1;
-	string title = 2; 
+	string title = 2;
 	string type = 3;
-	string defaultValue = 4;
+	string default_value = 4;
 
 	repeated ActionArgumentChoice choices = 5;
 
@@ -44,9 +44,11 @@ message GetDashboardComponentsResponse {
 message GetDashboardComponentsRequest {}
 
 message StartActionRequest {
-	string actionName = 1;
+	string action_name = 1;
 
 	repeated StartActionArgument arguments = 2;
+
+	string uuid = 3;
 }
 
 message StartActionArgument {
@@ -55,22 +57,28 @@ message StartActionArgument {
 }
 
 message StartActionResponse {
-	LogEntry logEntry = 1;
+	string execution_uuid = 2;
 }
 
 message GetLogsRequest{};
 
 message LogEntry {
-	string datetime = 1;
-	string actionTitle = 2;
+	string datetime_started = 1;
+	string action_title = 2;
 	string stdout = 3;
 	string stderr = 4;
-	bool timedOut = 5;
-	int32 exitCode = 6;
+	bool timed_out = 5;
+	int32 exit_code = 6;
 	string user = 7;
-	string userClass = 8;
-	string actionIcon = 9;
+	string user_class = 8;
+	string action_icon = 9;
 	repeated string tags = 10;
+	string execution_uuid = 11;
+	string datetime_finished = 12;
+	string uuid = 13;
+	bool execution_started = 14;
+	bool execution_finished = 15;
+	bool blocked = 16;
 }
 
 message GetLogsResponse {
@@ -87,10 +95,26 @@ message ValidateArgumentTypeResponse {
 	string description = 2;
 }
 
+message WatchExecutionRequest {
+	string execution_uuid = 1;
+}
+
+message WatchExecutionUpdate {
+	string update = 1;
+}
+
+message ExecutionStatusRequest {
+	string execution_uuid = 1;
+}
+
+message ExecutionStatusResponse {
+	LogEntry log_entry = 1;
+}
+
 message WhoAmIRequest {}
 
 message WhoAmIResponse {
-	string authenticatedUser = 1;
+	string authenticated_user = 1;
 }
 
 message SosReportRequest {}
@@ -99,7 +123,7 @@ message SosReportResponse {
 	string alert = 1;
 }
 
-service OliveTinApi {
+service OliveTinApiService {
 	rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
 		option (google.api.http) = {
 			get: "/api/GetDashboardComponents"
@@ -113,6 +137,13 @@ service OliveTinApi {
 		};
 	}
 
+	rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
+		option (google.api.http) = {
+			post: "/api/ExecutionStatus"
+			body: "*"
+		};
+	}
+
 	rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
 		option (google.api.http) = {
 			get: "/api/GetLogs"

+ 14 - 15
README.md

@@ -2,7 +2,7 @@
 
 <img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/webui/OliveTinLogo.png" align = "right" width = "160px" />
 
-OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface. 
+OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
 
 [![Discord](https://img.shields.io/discord/846737624960860180?label=Discord%20Server)](https://discord.gg/jhYWWpNJ3v)
 [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted#automation)
@@ -11,7 +11,7 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
 [![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
 [![Build Snapshot](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml/badge.svg)](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml)
 
-## Use cases 
+## Use cases
 
 **Safely** give access to commands, for less technical people;
 
@@ -34,10 +34,10 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
 ## Features
 
 * **Responsive, touch-friendly UI** - great for tablets and mobile
-* **Super simple config in YAML** - because if it's not YAML now-a-days, it's not "cloud native" :-) 
+* **Super simple config in YAML** - because if it's not YAML now-a-days, it's not "cloud native" :-)
 * **Dark mode** - for those of you that roll that way.
-* **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously.  
-* **Container** - available for quickly testing and getting it up and running, great for the selfhosted community. 
+* **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously.
+* **Container** - available for quickly testing and getting it up and running, great for the selfhosted community.
 * **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretially you could integrate with a bunch of stuff just by using curl, ping, etc. However, writing your own shell scripts is a great way to extend OliveTin.
 * **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/gRPC API.
 * **Good amount of unit tests and style checks** - helps potential contributors be consistent, and helps with maintainability.
@@ -48,17 +48,17 @@ Desktop web browser;
 
 ![Desktop screenshot](media/screenshotDesktop.png)
 
-Desktop web browser (dark mode); 
+Desktop web browser (dark mode);
 
 ![Desktop screenshot](media/screenshotDesktopDark.png)
 
-Mobile screen size (responsive layout); 
+Mobile screen size (responsive layout);
 
 ![Mobile screenshot](media/screenshotMobile.png)
 
 ## Documentation
 
-All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc. 
+All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc.
 
 ### Quickstart reference for `config.yaml`
 
@@ -75,19 +75,19 @@ Put this `config.yaml` in `/etc/OliveTin/` if you're running a standard service,
 
 ```yaml
 # Listen on all addresses available, port 1337
-listenAddressSingleHTTPFrontend: 0.0.0.0:1337 
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
 
 # Choose from INFO (default), WARN and DEBUG
 logLevel: "INFO"
 
 # Actions (buttons) to show up on the WebUI:
-actions: 
-  # Docs: https://docs.olivetin.app/action-container-control.html 
+actions:
+  # Docs: https://docs.olivetin.app/action-container-control.html
 - title: Restart Plex
   icon: restart
   shell: docker restart plex
-  
-  # This will send 1 ping 
+
+  # This will send 1 ping
   # Docs: https://docs.olivetin.app/action-ping.html
 - title: Ping host
   shell: ping {{ host }} -c {{ count }}
@@ -102,7 +102,7 @@ actions:
       title: Count
       type: int
       default: 1
-  
+
   # Restart http on host "webserver1"
   # Docs: https://docs.olivetin.app/action-ssh.html
 - title: restart httpd
@@ -111,4 +111,3 @@ actions:
 ```
 
 A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).
-

+ 2 - 2
SECURITY.md

@@ -2,7 +2,7 @@
 
 ## Supported Versions
 
-Currently, only the `main` branch is "supported". 
+Currently, only the `main` branch is "supported".
 
 | Version | Supported          |
 | ------- | ------------------ |
@@ -10,4 +10,4 @@ Currently, only the `main` branch is "supported".
 
 ## Reporting a Vulnerability
 
-Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit. 
+Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit.

+ 0 - 1
buf.gen.yaml

@@ -17,4 +17,3 @@ plugins:
 
 #  - name: openapiv2
 #    out: reports/openapiv2
-

+ 1 - 1
buf.yaml

@@ -1,5 +1,5 @@
 version: v1
-deps: 
+deps:
   - buf.build/googleapis/googleapis
 lint:
   use:

+ 2 - 0
cmd/OliveTin/main.go

@@ -11,6 +11,7 @@ import (
 	"github.com/OliveTin/OliveTin/internal/oncron"
 	"github.com/OliveTin/OliveTin/internal/onstartup"
 	updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
+	"github.com/OliveTin/OliveTin/internal/websocket"
 
 	"github.com/OliveTin/OliveTin/internal/httpservers"
 
@@ -110,6 +111,7 @@ func main() {
 	log.Debugf("Config: %+v", cfg)
 
 	executor := executor.DefaultExecutor()
+	executor.AddListener(websocket.ExecutionListener)
 
 	go onstartup.Execute(cfg, executor)
 	go oncron.Schedule(cfg, executor)

+ 14 - 10
config.yaml

@@ -1,25 +1,30 @@
-# There is a built-in micro proxy that will host the webui and REST API all on 
-# one port (this is called the "Single HTTP Frontend") and means you just need 
-# one open port in the container/firewalls/etc. 
+# There is a built-in micro proxy that will host the webui and REST API all on
+# one port (this is called the "Single HTTP Frontend") and means you just need
+# one open port in the container/firewalls/etc.
 #
 # Listen on all addresses available, port 1337
-listenAddressSingleHTTPFrontend: 0.0.0.0:1337 
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
 
 # Choose from INFO (default), WARN and DEBUG
 logLevel: "INFO"
 
 # Actions (buttons) to show up on the WebUI:
-actions:   
+actions:
   # This will run a simple script that you create.
 - title: Run backup script
   shell: /opt/backupScript.sh
+  maxConcurrent: 1
   icon: backup
 
+- title: date
+  shell: date
+
   # This will send 1 ping (-c 1)
   # Docs: https://docs.olivetin.app/action-ping.html
 - title: Ping host
   shell: ping {{ host }} -c {{ count }}
   icon: ping
+  timeout: 100
   arguments:
     - name: host
       title: host
@@ -32,15 +37,15 @@ actions:
       type: int
       default: 1
       description: How many times to do you want to ping?
-  
+
   # Restart lightdm on host "server1"
   # Docs: https://docs.olivetin.app/action-ping.html
 - title: restart httpd
   icon: restart
   shell: ssh root@server1 'service httpd restart'
 
-  # OliveTin can run long-running jobs like Ansible playbooks. 
-  # 
+  # OliveTin can run long-running jobs like Ansible playbooks.
+  #
   # For such jobs, you will need to install ansible-playbook on the host where
   # you are running OliveTin, or in the container.
   #
@@ -51,7 +56,7 @@ actions:
   timeout: 120
 
   # OliveTin can control containers - docker is just a command line app.
-  # 
+  #
   # However, if you are running in a container you will need to do some setup,
   # see the docs below.
   #
@@ -76,4 +81,3 @@ actions:
   shell: sleep 5
   timeout: 5
   icon: "&#x1F62A"
-

+ 50 - 43
go.mod

@@ -3,41 +3,45 @@ module github.com/OliveTin/OliveTin
 go 1.18
 
 require (
-	github.com/bufbuild/buf v1.15.1
+	github.com/bufbuild/buf v1.26.0
 	github.com/fsnotify/fsnotify v1.6.0
 	github.com/fzipp/gocyclo v0.6.0
-	github.com/go-critic/go-critic v0.7.0
+	github.com/go-critic/go-critic v0.8.2
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.3.0
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
+	github.com/gorilla/websocket v1.4.1
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2
 	github.com/robfig/cron/v3 v3.0.1
-	github.com/sirupsen/logrus v1.9.0
+	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/viper v1.15.0
-	github.com/stretchr/testify v1.8.2
-	golang.org/x/exp v0.0.0-20230307190834-24139beb5833
-	google.golang.org/grpc v1.53.0
+	github.com/stretchr/testify v1.8.4
+	golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
+	google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577
+	google.golang.org/grpc v1.57.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
-	google.golang.org/protobuf v1.30.0
+	google.golang.org/protobuf v1.31.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
 	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
-	github.com/Microsoft/go-winio v0.6.0 // indirect
-	github.com/bufbuild/connect-go v1.5.2 // indirect
-	github.com/bufbuild/protocompile v0.5.1 // indirect
+	github.com/Microsoft/go-winio v0.6.1 // indirect
+	github.com/bufbuild/connect-go v1.10.0 // indirect
+	github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect
+	github.com/bufbuild/protocompile v0.6.0 // indirect
+	github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/cristalhq/acmd v0.11.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/docker/cli v23.0.1+incompatible // indirect
-	github.com/docker/distribution v2.8.1+incompatible // indirect
-	github.com/docker/docker v23.0.3+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.7.0 // indirect
+	github.com/docker/cli v24.0.5+incompatible // indirect
+	github.com/docker/distribution v2.8.2+incompatible // indirect
+	github.com/docker/docker v24.0.5+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.8.0 // indirect
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/felixge/fgprof v0.9.3 // indirect
-	github.com/go-chi/chi/v5 v5.0.8 // indirect
-	github.com/go-logr/logr v1.2.3 // indirect
+	github.com/go-chi/chi/v5 v5.0.10 // indirect
+	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-toolsmith/astcast v1.1.0 // indirect
 	github.com/go-toolsmith/astcopy v1.1.0 // indirect
@@ -47,25 +51,24 @@ require (
 	github.com/go-toolsmith/pkgload v1.2.2 // indirect
 	github.com/go-toolsmith/strparse v1.1.0 // indirect
 	github.com/go-toolsmith/typep v1.1.0 // indirect
-	github.com/gofrs/flock v0.8.1 // indirect
 	github.com/gofrs/uuid/v5 v5.0.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.0.0 // indirect
+	github.com/golang/glog v1.1.2 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/google/go-containerregistry v0.13.0 // indirect
-	github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect
+	github.com/google/go-containerregistry v0.16.1 // indirect
+	github.com/google/pprof v0.0.0-20230808223545-4887780b67fb // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect
-	github.com/klauspost/compress v1.16.0 // indirect
-	github.com/klauspost/pgzip v1.2.5 // indirect
+	github.com/klauspost/compress v1.16.7 // indirect
+	github.com/klauspost/pgzip v1.2.6 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
-	github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
+	github.com/moby/term v0.5.0 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
+	github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
 	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
@@ -75,29 +78,33 @@ require (
 	github.com/quasilyte/gogrep v0.5.0 // indirect
 	github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
 	github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
-	github.com/rs/cors v1.8.3 // indirect
+	github.com/rs/cors v1.9.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/spf13/afero v1.9.3 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/cobra v1.6.1 // indirect
+	github.com/spf13/cobra v1.7.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.4.2 // indirect
-	go.opentelemetry.io/otel v1.14.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.14.0 // indirect
-	go.opentelemetry.io/otel/trace v1.14.0 // indirect
-	go.uber.org/atomic v1.10.0 // indirect
-	go.uber.org/multierr v1.10.0 // indirect
-	go.uber.org/zap v1.24.0 // indirect
-	golang.org/x/crypto v0.7.0 // indirect
-	golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb // indirect
-	golang.org/x/mod v0.9.0 // indirect
-	golang.org/x/net v0.8.0 // indirect
-	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.8.0 // indirect
-	golang.org/x/tools v0.7.0 // indirect
-	google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect
+	github.com/tetratelabs/wazero v1.4.0 // indirect
+	github.com/vbatts/tar-split v0.11.5 // indirect
+	go.opentelemetry.io/otel v1.16.0 // indirect
+	go.opentelemetry.io/otel/metric v1.16.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.16.0 // indirect
+	go.opentelemetry.io/otel/trace v1.16.0 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.25.0 // indirect
+	golang.org/x/crypto v0.12.0 // indirect
+	golang.org/x/exp/typeparams v0.0.0-20230809150735-7b3493d9a819 // indirect
+	golang.org/x/mod v0.12.0 // indirect
+	golang.org/x/net v0.14.0 // indirect
+	golang.org/x/sync v0.3.0 // indirect
+	golang.org/x/sys v0.11.0 // indirect
+	golang.org/x/term v0.11.0 // indirect
+	golang.org/x/text v0.12.0 // indirect
+	golang.org/x/tools v0.12.0 // indirect
+	google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 )

+ 103 - 90
go.sum

@@ -40,15 +40,17 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
-github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-github.com/bufbuild/buf v1.15.1 h1:v7sK2uMEsGX4Z2hvu+xiMheH3C3AKBGfxPBgdUZYDQ8=
-github.com/bufbuild/buf v1.15.1/go.mod h1:TQeGKam1QMfHy/xsSnnMpxN3JK5HBb6aNvZj4m52gkE=
-github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8=
-github.com/bufbuild/connect-go v1.5.2/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk=
-github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg=
-github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/bufbuild/buf v1.26.0 h1:rB8pErEkxdUNhVZ3+ClFEoJRaa+o2FjEecCWL2S3Qs4=
+github.com/bufbuild/buf v1.26.0/go.mod h1:UMPncXMWgrmIM+0QpwTEwjNr2SA0z2YIVZZsmNflvB4=
+github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
+github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
+github.com/bufbuild/connect-opentelemetry-go v0.4.0 h1:6JAn10SNqlQ/URhvRNGrIlczKw1wEXknBUUtmWqOiak=
+github.com/bufbuild/connect-opentelemetry-go v0.4.0/go.mod h1:nwPXYoDOoc2DGyKE/6pT1Q9MPSi2Et2e6BieMD0l6WU=
+github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY=
+github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@@ -57,7 +59,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -66,14 +69,14 @@ github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgB
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
-github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
-github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho=
-github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
+github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
+github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
+github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
+github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
 github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
 github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -91,16 +94,16 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
 github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
-github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
-github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-critic/go-critic v0.7.0 h1:tqbKzB8pqi0NsRZ+1pyU4aweAF7A7QN0Pi4Q02+rYnQ=
-github.com/go-critic/go-critic v0.7.0/go.mod h1:moYzd7GdVXE2C2hYTwd7h0CPcqlUeclsyBRwMa38v64=
+github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
+github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-critic/go-critic v0.8.2 h1:mekhZ9jw5NBEj3I8o/EywXw5zBfGAJuMo4VVVjtxF80=
+github.com/go-critic/go-critic v0.8.2/go.mod h1:nZPlrtVfOuLOe8GpvWTfcMzfkG0QVZWAziAeXpivfQo=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
-github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
@@ -122,7 +125,6 @@ github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJ
 github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
 github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
-github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
 github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -130,8 +132,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
+github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -173,8 +175,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k=
-github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo=
+github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
+github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -189,8 +191,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
-github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
-github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
+github.com/google/pprof v0.0.0-20230808223545-4887780b67fb h1:oqpb3Cwpc7EOml5PVGMYbSGmwNui2R7i8IW83gs4W0c=
+github.com/google/pprof v0.0.0-20230808223545-4887780b67fb/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -198,8 +200,10 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
+github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -207,7 +211,6 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
@@ -217,10 +220,10 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
-github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
-github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -233,15 +236,15 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
-github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
+github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
+github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
 github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
 github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@@ -266,18 +269,18 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
-github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
+github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
 github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
-github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -294,11 +297,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
+github.com/tetratelabs/wazero v1.4.0 h1:9/MirYvmkJ/zSUOygKY/ia3t+e+RqIZXKbylIby1WYk=
+github.com/tetratelabs/wazero v1.4.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
+github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
+github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -309,19 +315,22 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
-go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
-go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY=
-go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM=
-go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
-go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
-go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
-go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
-go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
-go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
-go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
+go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
+go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
+go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
+go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
+go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
+go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
+go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
+go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
+go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -329,8 +338,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
-golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -341,12 +350,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
-golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
+golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb h1:WGs/bGIWYyAY5PVgGGMXqGGCxSJz4fpoUExb/vgqNCU=
-golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/exp/typeparams v0.0.0-20230809150735-7b3493d9a819 h1:qdPQGUfaPzFgEbhQCulaXk+s0ETcox/vQd+f9YCIuxY=
+golang.org/x/exp/typeparams v0.0.0-20230809150735-7b3493d9a819/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -370,8 +379,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
-golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -403,8 +412,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -424,8 +433,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -465,19 +474,19 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -531,8 +540,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
-golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
+golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -599,8 +608,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds=
-google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
+google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 h1:Tyk/35yqszRCvaragTn5NnkY6IiKk/XvHzEWepo71N0=
+google.golang.org/genproto v0.0.0-20230807174057-1744710a1577/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577 h1:xv8KoglAClYGkprUSmDTKaILtzfD8XzG9NYVXMprjKo=
+google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -617,8 +630,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
+google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -633,8 +646,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 1 - 1
integration-tests/Makefile

@@ -1,6 +1,6 @@
 cypress:
 	npm install
 	./cypressRun.sh "general"
-	./cypressRun.sh "hiddenNav"		
+	./cypressRun.sh "hiddenNav"
 
 .PHONY: cypress container

+ 1 - 1
integration-tests/README.md

@@ -1 +1 @@
-# OliveTin-integration-tests
+# OliveTin-integration-tests

+ 1 - 1
integration-tests/Vagrantfile

@@ -2,7 +2,7 @@
 # (eg, snapshot builds on GitHub)
 
 
-Vagrant.configure("2") do |config| 
+Vagrant.configure("2") do |config|
   config.vm.box = "generic/centos8"
   config.vm.provision "shell", inline: "mkdir /etc/OliveTin && chmod o+w /etc/OliveTin/", privileged: true
   config.vm.provision "file", source: "configs/config.general.yaml/.", destination: "/etc/OliveTin/config.yaml"

+ 5 - 6
integration-tests/configs/config.general.yaml

@@ -1,17 +1,17 @@
 #
 # Integration Test Config: General
-# 
+#
 
-listenAddressSingleHTTPFrontend: 0.0.0.0:1337 
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
 
 logLevel: "DEBUG"
-checkForUpdates: false 
+checkForUpdates: false
 
-actions:   
+actions:
 - title: Ping Google.com
   shell: ping google.com -c 1
   icon: ping
-  
+
 - title: restart lightdm
   icon: poop
   shell: ssh root@overseer 'service lightdm restart'
@@ -32,4 +32,3 @@ actions:
 - title: Restart Plex
   icon: smile
   shell: docker restart plex
-

+ 3 - 3
integration-tests/configs/config.hiddenFooter.yaml

@@ -1,12 +1,12 @@
 #
 # Integration Test Config: General
-# 
+#
 
 showFooter: false
-checkForUpdates: false 
+checkForUpdates: false
 
 # Actions (buttons) to show up on the WebUI:
-actions:   
+actions:
 - title: Ping example.com
   shell: ping example.com -c 1
   icon: ping

+ 4 - 4
integration-tests/configs/config.hiddenNav.yaml

@@ -1,14 +1,14 @@
 #
 # Integration Test Config: General
-# 
+#
 
-listenAddressSingleHTTPFrontend: 0.0.0.0:1337 
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
 
 showNavigation: false
-checkForUpdates: false 
+checkForUpdates: false
 
 # Actions (buttons) to show up on the WebUI:
-actions:   
+actions:
 - title: Ping example.com
   shell: ping example.com -c 1
   icon: ping

+ 0 - 2
integration-tests/cypress/e2e/general/defaultHomepageRender.cy.js

@@ -17,5 +17,3 @@ describe('Homepage rendering', () => {
     })
   })
 });
-
-

+ 0 - 2
integration-tests/cypress/e2e/hiddenFooter/hiddenFooter.cy.js

@@ -10,5 +10,3 @@ describe('Hidden Footer', () => {
     })
   })
 });
-
-

+ 0 - 2
integration-tests/cypress/e2e/hiddenNav/hiddenNav.cy.js

@@ -14,5 +14,3 @@ describe('Hidden Nav', () => {
     })
   })
 });
-
-

+ 0 - 1
integration-tests/cypress/support/commands.js

@@ -23,4 +23,3 @@
 //
 // -- This will overwrite an existing command --
 // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-

+ 0 - 1
integration-tests/cypressRun.sh

@@ -8,4 +8,3 @@ cp -f ./configs/config.$1.yaml ./configs/config.yaml
 docker start olivetin
 NO_COLOR=1 ./node_modules/.bin/cypress run --headless -s cypress/integration/$1/*  || true
 docker kill olivetin
-

+ 1373 - 0
integration-tests/package-lock.json

@@ -26,7 +26,10 @@
       "version": "2.88.12",
       "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
       "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "dependencies": {
         "aws-sign2": "~0.7.0",
         "aws4": "^1.8.0",
@@ -345,7 +348,10 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
       "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "dependencies": {
         "function-bind": "^1.1.1",
         "get-intrinsic": "^1.0.2"
@@ -858,6 +864,25 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+      "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+      "dependencies": {
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/get-stream": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -944,6 +969,17 @@
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dependencies": {
+        "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -957,7 +993,10 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
       "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "engines": {
         "node": ">= 0.4"
       },
@@ -969,7 +1008,10 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "engines": {
         "node": ">= 0.4"
       },
@@ -1391,7 +1433,10 @@
       "version": "1.12.3",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
       "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -1510,8 +1555,12 @@
     "node_modules/psl": {
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+<<<<<<< HEAD
       "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
       "dev": true
+=======
+      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
     },
     "node_modules/pump": {
       "version": "3.0.0",
@@ -1527,7 +1576,10 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
       "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "engines": {
         "node": ">=6"
       }
@@ -1536,7 +1588,10 @@
       "version": "6.10.4",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
       "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "dependencies": {
         "side-channel": "^1.0.4"
       },
@@ -1547,11 +1602,32 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+<<<<<<< HEAD
     "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
+=======
+    "node_modules/querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
+      "engines": {
+        "node": ">=0.4.x"
+      }
+    },
+    "node_modules/querystringify": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+    },
+    "node_modules/ramda": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz",
+      "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA=="
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
     },
     "node_modules/request-progress": {
       "version": "3.0.0",
@@ -1565,8 +1641,12 @@
     "node_modules/requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+<<<<<<< HEAD
       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
       "dev": true
+=======
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
     },
     "node_modules/restore-cursor": {
       "version": "3.1.0",
@@ -1677,7 +1757,10 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
       "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "dependencies": {
         "call-bind": "^1.0.0",
         "get-intrinsic": "^1.0.2",
@@ -1810,7 +1893,10 @@
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
       "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "dependencies": {
         "psl": "^1.1.33",
         "punycode": "^2.1.1",
@@ -1825,7 +1911,10 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
       "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+<<<<<<< HEAD
       "dev": true,
+=======
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
       "engines": {
         "node": ">= 4.0.0"
       }
@@ -1894,6 +1983,23 @@
         "requires-port": "^1.0.0"
       }
     },
+<<<<<<< HEAD
+=======
+    "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==",
+      "dependencies": {
+        "querystringify": "^2.1.1",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "node_modules/url/node_modules/punycode": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+      "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+    },
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
     "node_modules/uuid": {
       "version": "8.3.2",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -1971,5 +2077,1272 @@
         "fd-slicer": "~1.1.0"
       }
     }
+<<<<<<< HEAD
+=======
+  },
+  "dependencies": {
+    "@cypress/request": {
+      "version": "2.88.12",
+      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
+      "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "http-signature": "~1.3.6",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "performance-now": "^2.1.0",
+        "qs": "~6.10.3",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "^4.1.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^8.3.2"
+      }
+    },
+    "@cypress/xvfb": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+      "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+      "requires": {
+        "debug": "^3.1.0",
+        "lodash.once": "^4.1.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.7",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "@types/node": {
+      "version": "14.18.12",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
+      "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A=="
+    },
+    "@types/sinonjs__fake-timers": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz",
+      "integrity": "sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A=="
+    },
+    "@types/sizzle": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+      "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ=="
+    },
+    "@types/yauzl": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
+      "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==",
+      "optional": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "aggregate-error": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+      "requires": {
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
+      }
+    },
+    "ansi-colors": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA=="
+    },
+    "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==",
+      "requires": {
+        "type-fest": "^0.21.3"
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+    },
+    "ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "requires": {
+        "color-convert": "^2.0.1"
+      }
+    },
+    "arch": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+      "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="
+    },
+    "asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+    },
+    "astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
+    },
+    "async": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
+      "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+    },
+    "at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
+    },
+    "aws4": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
+      "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "blob-util": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
+      "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ=="
+    },
+    "bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
+    },
+    "cachedir": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+      "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw=="
+    },
+    "call-bind": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "requires": {
+        "function-bind": "^1.1.1",
+        "get-intrinsic": "^1.0.2"
+      }
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+    },
+    "chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "requires": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "dependencies": {
+        "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==",
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "check-more-types": {
+      "version": "2.24.0",
+      "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+      "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA="
+    },
+    "ci-info": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
+      "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw=="
+    },
+    "clean-stack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
+    },
+    "cli-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "requires": {
+        "restore-cursor": "^3.1.0"
+      }
+    },
+    "cli-table3": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz",
+      "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==",
+      "requires": {
+        "colors": "1.4.0",
+        "string-width": "^4.2.0"
+      }
+    },
+    "cli-truncate": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+      "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+      "requires": {
+        "slice-ansi": "^3.0.0",
+        "string-width": "^4.2.0"
+      }
+    },
+    "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==",
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "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=="
+    },
+    "colorette": {
+      "version": "2.0.16",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz",
+      "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g=="
+    },
+    "colors": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+      "optional": true
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+      "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="
+    },
+    "common-tags": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+      "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
+    "cypress": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.7.0.tgz",
+      "integrity": "sha512-b1bMC3VQydC6sXzBMFnSqcvwc9dTZMgcaOzT0vpSD+Gq1yFc+72JDWi55sfUK5eIeNLAtWOGy1NNb6UlhMvB+Q==",
+      "requires": {
+        "@cypress/request": "^2.88.6",
+        "@cypress/xvfb": "^1.2.4",
+        "@types/node": "^14.14.31",
+        "@types/sinonjs__fake-timers": "^6.0.2",
+        "@types/sizzle": "^2.3.2",
+        "arch": "^2.2.0",
+        "blob-util": "^2.0.2",
+        "bluebird": "^3.7.2",
+        "cachedir": "^2.3.0",
+        "chalk": "^4.1.0",
+        "check-more-types": "^2.24.0",
+        "cli-cursor": "^3.1.0",
+        "cli-table3": "~0.6.0",
+        "commander": "^5.1.0",
+        "common-tags": "^1.8.0",
+        "dayjs": "^1.10.4",
+        "debug": "^4.3.2",
+        "enquirer": "^2.3.6",
+        "eventemitter2": "^6.4.3",
+        "execa": "4.1.0",
+        "executable": "^4.1.1",
+        "extract-zip": "2.0.1",
+        "figures": "^3.2.0",
+        "fs-extra": "^9.1.0",
+        "getos": "^3.2.1",
+        "is-ci": "^3.0.0",
+        "is-installed-globally": "~0.4.0",
+        "lazy-ass": "^1.6.0",
+        "listr2": "^3.8.3",
+        "lodash": "^4.17.21",
+        "log-symbols": "^4.0.0",
+        "minimist": "^1.2.5",
+        "ospath": "^1.2.2",
+        "pretty-bytes": "^5.6.0",
+        "proxy-from-env": "1.0.0",
+        "ramda": "~0.27.1",
+        "request-progress": "^3.0.0",
+        "supports-color": "^8.1.1",
+        "tmp": "~0.2.1",
+        "untildify": "^4.0.0",
+        "url": "^0.11.0",
+        "yauzl": "^2.10.0"
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "dayjs": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz",
+      "integrity": "sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug=="
+    },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "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=="
+    },
+    "end-of-stream": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "enquirer": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+      "requires": {
+        "ansi-colors": "^4.1.1"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "eventemitter2": {
+      "version": "6.4.5",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz",
+      "integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw=="
+    },
+    "execa": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+      "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+      "requires": {
+        "cross-spawn": "^7.0.0",
+        "get-stream": "^5.0.0",
+        "human-signals": "^1.1.1",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.0",
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2",
+        "strip-final-newline": "^2.0.0"
+      }
+    },
+    "executable": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+      "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+      "requires": {
+        "pify": "^2.2.0"
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+    },
+    "extract-zip": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+      "requires": {
+        "@types/yauzl": "^2.9.1",
+        "debug": "^4.1.1",
+        "get-stream": "^5.1.0",
+        "yauzl": "^2.10.0"
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+    },
+    "fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
+    "figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "requires": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+    },
+    "get-intrinsic": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+      "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+      "requires": {
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3"
+      }
+    },
+    "get-stream": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+      "requires": {
+        "pump": "^3.0.0"
+      }
+    },
+    "getos": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
+      "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
+      "requires": {
+        "async": "^3.2.0"
+      }
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "global-dirs": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+      "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+      "requires": {
+        "ini": "2.0.0"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.2.10",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+    },
+    "has-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
+    },
+    "has-symbols": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+    },
+    "http-signature": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
+      "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^2.0.2",
+        "sshpk": "^1.14.1"
+      }
+    },
+    "human-signals": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+      "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="
+    },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "ini": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+      "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
+    },
+    "is-ci": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+      "requires": {
+        "ci-info": "^3.2.0"
+      }
+    },
+    "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=="
+    },
+    "is-installed-globally": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+      "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+      "requires": {
+        "global-dirs": "^3.0.0",
+        "is-path-inside": "^3.0.2"
+      }
+    },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
+    },
+    "is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
+    },
+    "json-schema": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+      "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    },
+    "jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "requires": {
+        "graceful-fs": "^4.1.6",
+        "universalify": "^2.0.0"
+      }
+    },
+    "jsprim": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+      "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.4.0",
+        "verror": "1.10.0"
+      }
+    },
+    "lazy-ass": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+      "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM="
+    },
+    "listr2": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
+      "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
+      "requires": {
+        "cli-truncate": "^2.1.0",
+        "colorette": "^2.0.16",
+        "log-update": "^4.0.0",
+        "p-map": "^4.0.0",
+        "rfdc": "^1.3.0",
+        "rxjs": "^7.5.1",
+        "through": "^2.3.8",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+    },
+    "log-symbols": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "requires": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      }
+    },
+    "log-update": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+      "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+      "requires": {
+        "ansi-escapes": "^4.3.0",
+        "cli-cursor": "^3.1.0",
+        "slice-ansi": "^4.0.0",
+        "wrap-ansi": "^6.2.0"
+      },
+      "dependencies": {
+        "slice-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+          "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "astral-regex": "^2.0.0",
+            "is-fullwidth-code-point": "^3.0.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "string-width": "^4.1.0",
+            "strip-ansi": "^6.0.0"
+          }
+        }
+      }
+    },
+    "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=="
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "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==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
+    },
+    "minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "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==",
+      "requires": {
+        "path-key": "^3.0.0"
+      }
+    },
+    "object-inspect": {
+      "version": "1.12.3",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+      "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "ospath": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+      "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs="
+    },
+    "p-map": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+      "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+      "requires": {
+        "aggregate-error": "^3.0.0"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+    },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+    },
+    "pretty-bytes": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
+    },
+    "proxy-from-env": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+      "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
+    },
+    "psl": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
+    },
+    "pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "punycode": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="
+    },
+    "qs": {
+      "version": "6.10.4",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
+      "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
+      "requires": {
+        "side-channel": "^1.0.4"
+      }
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+    },
+    "querystringify": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+    },
+    "ramda": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz",
+      "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA=="
+    },
+    "request-progress": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+      "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
+      "requires": {
+        "throttleit": "^1.0.0"
+      }
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+    },
+    "restore-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "requires": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "rfdc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+      "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "rxjs": {
+      "version": "7.5.5",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
+      "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
+      "requires": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "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==",
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+    },
+    "side-channel": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "requires": {
+        "call-bind": "^1.0.0",
+        "get-intrinsic": "^1.0.2",
+        "object-inspect": "^1.9.0"
+      }
+    },
+    "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=="
+    },
+    "slice-ansi": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+      "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      }
+    },
+    "sshpk": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
+      "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "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==",
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      }
+    },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
+    "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=="
+    },
+    "supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "requires": {
+        "has-flag": "^4.0.0"
+      }
+    },
+    "throttleit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+      "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw="
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "tmp": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+      "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+      "requires": {
+        "rimraf": "^3.0.0"
+      }
+    },
+    "tough-cookie": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+      "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
+      "requires": {
+        "psl": "^1.1.33",
+        "punycode": "^2.1.1",
+        "universalify": "^0.2.0",
+        "url-parse": "^1.5.3"
+      },
+      "dependencies": {
+        "universalify": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+          "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="
+        }
+      }
+    },
+    "tslib": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+    },
+    "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=="
+    },
+    "universalify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
+    },
+    "untildify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+        }
+      }
+    },
+    "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==",
+      "requires": {
+        "querystringify": "^2.1.1",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "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==",
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+      "requires": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    }
+>>>>>>> e8cb6619389e869655ac3bd0ef699c7ba321be1f
   }
 }

+ 5 - 2
internal/acl/acl.go

@@ -75,7 +75,10 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
 func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
 	md, ok := metadata.FromIncomingContext(ctx)
 
-	ret := &AuthenticatedUser{}
+	ret := &AuthenticatedUser{
+		Username:  "guest",
+		Usergroup: "guest",
+	}
 
 	if ok {
 		ret.Username = getMetdataKeyOrEmpty(md, "username")
@@ -87,7 +90,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
 	log.WithFields(log.Fields{
 		"username":  ret.Username,
 		"usergroup": ret.Usergroup,
-	}).Infof("UserFromContext")
+	}).Debugf("UserFromContext")
 
 	return ret
 }

+ 4 - 1
internal/config/config.go

@@ -12,7 +12,9 @@ type Action struct {
 	Acls          []string
 	ExecOnStartup bool
 	ExecOnCron    []string
+	MaxConcurrent int
 	Arguments     []ActionArgument
+	PopupOnStart  bool
 }
 
 // ActionArgument objects appear on Actions.
@@ -73,9 +75,10 @@ type Config struct {
 	ShowNavigation                  bool
 	ShowNewVersions                 bool
 	AuthJwtCookieName               string
-	AuthJwtSecret                   string
+	AuthJwtSecret                   string // mutually exclusive with pub key config fields
 	AuthJwtClaimUsername            string
 	AuthJwtClaimUserGroup           string
+	AuthJwtPubKeyPath               string // will read pub key from file on disk
 	AuthHttpHeaderUsername          string
 	AuthHttpHeaderUserGroup         string
 	DefaultPermissions              PermissionsList

+ 4 - 0
internal/config/sanitize.go

@@ -30,6 +30,10 @@ func (action *Action) sanitize() {
 
 	action.Icon = lookupHTMLIcon(action.Icon)
 
+	if action.MaxConcurrent < 1 {
+		action.MaxConcurrent = 1
+	}
+
 	for idx := range action.Arguments {
 		action.Arguments[idx].sanitize()
 	}

+ 6 - 4
internal/executor/arguments.go

@@ -22,8 +22,9 @@ var (
 
 func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
 	log.WithFields(log.Fields{
-		"cmd": rawShellCommand,
-	}).Infof("Before Parse Args")
+		"actionTitle": action.Title,
+		"cmd":         rawShellCommand,
+	}).Infof("Action parse args - Before")
 
 	r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
 	matches := r.FindAllStringSubmatch(rawShellCommand, -1)
@@ -51,8 +52,9 @@ func parseActionArguments(rawShellCommand string, values map[string]string, acti
 	}
 
 	log.WithFields(log.Fields{
-		"cmd": rawShellCommand,
-	}).Infof("After Parse Args")
+		"actionTitle": action.Title,
+		"cmd":         rawShellCommand,
+	}).Infof("Action parse args - After")
 
 	return rawShellCommand, nil
 }

+ 140 - 48
internal/executor/executor.go

@@ -8,99 +8,167 @@ import (
 
 	"bytes"
 	"context"
+	"fmt"
+	"io"
 	"os/exec"
 	"runtime"
 	"time"
 )
 
+// Executor represents a helper class for executing commands. It's main method
+// is ExecRequest
+type Executor struct {
+	Logs map[string]*InternalLogEntry
+
+	listeners []listener
+
+	chainOfCommand []executorStepFunc
+}
+
 // ExecutionRequest is a request to execute an action. It's passed to an
 // Executor. They're created from the grpcapi.
 type ExecutionRequest struct {
 	ActionName         string
 	Arguments          map[string]string
+	UUID               string
 	Tags               []string
 	action             *config.Action
 	Cfg                *config.Config
 	AuthenticatedUser  *acl.AuthenticatedUser
 	logEntry           *InternalLogEntry
 	finalParsedCommand string
+	executor           *Executor
 }
 
 // InternalLogEntry objects are created by an Executor, and represent the final
 // state of execution (even if the command is not executed). It's designed to be
 // easily serializable.
 type InternalLogEntry struct {
-	Datetime string
-	Stdout   string
-	Stderr   string
-	TimedOut bool
-	ExitCode int32
-	Tags     []string
+	DatetimeStarted   string
+	DatetimeFinished  string
+	Stdout            string
+	Stderr            string
+	StdoutBuffer      io.ReadCloser
+	StderrBuffer      io.ReadCloser
+	TimedOut          bool
+	Blocked           bool
+	ExitCode          int32
+	Tags              []string
+	ExecutionStarted  bool
+	ExecutionFinished bool
 
 	/*
-		The following two properties are obviously on Action normally, but it's useful
+		The following 3 properties are obviously on Action normally, but it's useful
 		that logs are lightweight (so we don't need to have an action associated to
 		logs, etc. Therefore, we duplicate those values here.
 	*/
 	ActionTitle string
 	ActionIcon  string
+	UUID        string
 }
 
 type executorStepFunc func(*ExecutionRequest) bool
 
-// Executor represents a helper class for executing commands. It's main method
-// is ExecRequest
-type Executor struct {
-	Logs []InternalLogEntry
+// DefaultExecutor returns an Executor, with a sensible "chain of command" for
+// executing actions.
+func DefaultExecutor() *Executor {
+	e := Executor{}
+	e.Logs = make(map[string]*InternalLogEntry)
 
-	chainOfCommand []executorStepFunc
+	e.chainOfCommand = []executorStepFunc{
+		stepLogRequested,
+		stepFindAction,
+		stepConcurrencyCheck,
+		stepACLCheck,
+		stepParseArgs,
+		stepLogStart,
+		stepExec,
+		stepLogFinish,
+	}
+
+	return &e
+}
+
+type listener interface {
+	OnExecutionStarted(actionName string)
+	OnExecutionFinished(logEntry *InternalLogEntry)
+}
+
+func (e *Executor) AddListener(m listener) {
+	e.listeners = append(e.listeners, m)
 }
 
 // ExecRequest processes an ExecutionRequest
 func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
+	// req.UUID is now set by the client, so that they can track the request
+	// from start to finish. This means that a malicious client could send
+	// duplicate UUIDs (or just random strings), but this is the only way.
+	req.executor = e
 	req.logEntry = &InternalLogEntry{
-		Datetime:    time.Now().Format("2006-01-02 15:04:05"),
-		ActionTitle: req.ActionName,
-		Stdout:      "",
-		Stderr:      "",
-		ExitCode:    -1337, // If an Action is not actually executed, this is the default exit code.
+		DatetimeStarted:   time.Now().Format("2006-01-02 15:04:05"),
+		ActionTitle:       req.ActionName,
+		UUID:              req.UUID,
+		Stdout:            "",
+		Stderr:            "",
+		ExitCode:          -1337, // If an Action is not actually executed, this is the default exit code.
+		ExecutionStarted:  false,
+		ExecutionFinished: false,
+	}
+
+	e.Logs[req.UUID] = req.logEntry
+
+	for _, listener := range e.listeners {
+		listener.OnExecutionStarted(req.ActionName)
+	}
+
+	go e.execChain(req)
+
+	return &pb.StartActionResponse{
+		ExecutionUuid: req.UUID,
 	}
+}
 
+func (e *Executor) execChain(req *ExecutionRequest) {
 	for _, step := range e.chainOfCommand {
 		if !step(req) {
 			break
 		}
 	}
 
-	e.Logs = append(e.Logs, *req.logEntry)
+	req.logEntry.ExecutionFinished = true
 
-	return &pb.StartActionResponse{
-		LogEntry: &pb.LogEntry{
-			ActionTitle: req.logEntry.ActionTitle,
-			ActionIcon:  req.logEntry.ActionIcon,
-			Datetime:    req.logEntry.Datetime,
-			Stderr:      req.logEntry.Stderr,
-			Stdout:      req.logEntry.Stdout,
-			TimedOut:    req.logEntry.TimedOut,
-			ExitCode:    req.logEntry.ExitCode,
-		},
+	// This isn't a step, because we want to notify all listeners, irrespective
+	// of how many steps were actually executed.
+	notifyListeners(req)
+}
+
+func getConcurrentCount(req *ExecutionRequest) int {
+	concurrentCount := 0
+
+	for _, log := range req.executor.Logs {
+		if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
+			concurrentCount += 1
+		}
 	}
+
+	return concurrentCount
 }
 
-// DefaultExecutor returns an Executor, with a sensible "chain of command" for
-// executing actions.
-func DefaultExecutor() *Executor {
-	e := Executor{}
-	e.chainOfCommand = []executorStepFunc{
-		stepFindAction,
-		stepACLCheck,
-		stepParseArgs,
-		stepLogStart,
-		stepExec,
-		stepLogFinish,
+func stepConcurrencyCheck(req *ExecutionRequest) bool {
+	concurrentCount := getConcurrentCount(req)
+
+	// Note that the current execution is counted int the logs, so when checking we +1
+	if concurrentCount >= (req.action.MaxConcurrent + 1) {
+		msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.action.MaxConcurrent)
+
+		log.Warnf(msg)
+
+		req.logEntry.Stdout = msg
+		req.logEntry.Blocked = true
+		return false
 	}
 
-	return &e
+	return true
 }
 
 func stepFindAction(req *ExecutionRequest) bool {
@@ -142,10 +210,18 @@ func stepParseArgs(req *ExecutionRequest) bool {
 	return true
 }
 
+func stepLogRequested(req *ExecutionRequest) bool {
+	log.WithFields(log.Fields{
+		"actionTitle": req.ActionName,
+	}).Infof("Action requested")
+
+	return true
+}
+
 func stepLogStart(req *ExecutionRequest) bool {
 	log.WithFields(log.Fields{
-		"title":   req.action.Title,
-		"timeout": req.action.Timeout,
+		"actionTitle": req.action.Title,
+		"timeout":     req.action.Timeout,
 	}).Infof("Action starting")
 
 	return true
@@ -153,16 +229,22 @@ func stepLogStart(req *ExecutionRequest) bool {
 
 func stepLogFinish(req *ExecutionRequest) bool {
 	log.WithFields(log.Fields{
-		"title":    req.action.Title,
-		"stdout":   req.logEntry.Stdout,
-		"stderr":   req.logEntry.Stderr,
-		"timedOut": req.logEntry.TimedOut,
-		"exit":     req.logEntry.ExitCode,
+		"actionTitle": req.action.Title,
+		"stdout":      req.logEntry.Stdout,
+		"stderr":      req.logEntry.Stderr,
+		"timedOut":    req.logEntry.TimedOut,
+		"exit":        req.logEntry.ExitCode,
 	}).Infof("Action finished")
 
 	return true
 }
 
+func notifyListeners(req *ExecutionRequest) {
+	for _, listener := range req.executor.listeners {
+		listener.OnExecutionFinished(req.logEntry)
+	}
+}
+
 func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
 	if runtime.GOOS == "windows" {
 		return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
@@ -181,8 +263,17 @@ func stepExec(req *ExecutionRequest) bool {
 	cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
 	cmd.Stdout = &stdout
 	cmd.Stderr = &stderr
+	req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
+	req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
+
+	req.logEntry.ExecutionStarted = true
+
+	runerr := cmd.Start()
+
+	cmd.Wait()
 
-	runerr := cmd.Run()
+	// req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
+	// req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
 
 	req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
 	req.logEntry.Stdout = stdout.String()
@@ -197,6 +288,7 @@ func stepExec(req *ExecutionRequest) bool {
 	}
 
 	req.logEntry.Tags = req.Tags
+	req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
 
 	return true
 }

+ 80 - 11
internal/grpcapi/grpcApi.go

@@ -7,6 +7,7 @@ import (
 	"google.golang.org/grpc"
 
 	"net"
+	"sort"
 
 	acl "github.com/OliveTin/OliveTin/internal/acl"
 	config "github.com/OliveTin/OliveTin/internal/config"
@@ -19,7 +20,7 @@ var (
 )
 
 type oliveTinAPI struct {
-	pb.UnimplementedOliveTinApiServer
+	pb.UnimplementedOliveTinApiServiceServer
 
 	executor *executor.Executor
 }
@@ -35,6 +36,7 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
 
 	execReq := executor.ExecutionRequest{
 		ActionName:        req.ActionName,
+		UUID:              req.Uuid,
 		Arguments:         args,
 		AuthenticatedUser: acl.UserFromContext(ctx, cfg),
 		Cfg:               cfg,
@@ -43,6 +45,62 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
 	return api.executor.ExecRequest(&execReq), nil
 }
 
+func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
+	res := &pb.ExecutionStatusResponse{}
+
+	logEntry, ok := api.executor.Logs[req.ExecutionUuid]
+
+	if !ok {
+		return res, nil
+	}
+
+	res.LogEntry = &pb.LogEntry{
+		ActionTitle:       logEntry.ActionTitle,
+		ActionIcon:        logEntry.ActionIcon,
+		DatetimeStarted:   logEntry.DatetimeStarted,
+		DatetimeFinished:  logEntry.DatetimeFinished,
+		Stdout:            logEntry.Stdout,
+		Stderr:            logEntry.Stderr,
+		TimedOut:          logEntry.TimedOut,
+		Blocked:           logEntry.Blocked,
+		ExitCode:          logEntry.ExitCode,
+		Tags:              logEntry.Tags,
+		ExecutionUuid:     logEntry.UUID,
+		ExecutionStarted:  logEntry.ExecutionStarted,
+		ExecutionFinished: logEntry.ExecutionFinished,
+	}
+
+	return res, nil
+}
+
+/**
+func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
+	log.Infof("Watch")
+
+	if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
+		log.Errorf("Execution not found: %v", req.ExecutionUuid)
+
+		return nil
+	} else {
+		if logEntry.ExecutionStarted {
+			for !logEntry.ExecutionCompleted {
+				tmp := make([]byte, 256)
+
+				red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
+
+				log.Infof("%v %v", red, err)
+
+				srv.Send(&pb.WatchExecutionUpdate{
+					Update: string(tmp),
+				})
+			}
+		}
+
+		return nil
+	}
+}
+*/
+
 func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
 	user := acl.UserFromContext(ctx, cfg)
 
@@ -62,19 +120,30 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
 
 	// TODO Limit to 10 entries or something to prevent browser lag.
 
-	for _, logEntry := range api.executor.Logs {
+	for uuid, logEntry := range api.executor.Logs {
 		ret.Logs = append(ret.Logs, &pb.LogEntry{
-			ActionTitle: logEntry.ActionTitle,
-			ActionIcon:  logEntry.ActionIcon,
-			Datetime:    logEntry.Datetime,
-			Stdout:      logEntry.Stdout,
-			Stderr:      logEntry.Stderr,
-			TimedOut:    logEntry.TimedOut,
-			ExitCode:    logEntry.ExitCode,
-			Tags:        logEntry.Tags,
+			ActionTitle:       logEntry.ActionTitle,
+			ActionIcon:        logEntry.ActionIcon,
+			DatetimeStarted:   logEntry.DatetimeStarted,
+			DatetimeFinished:  logEntry.DatetimeFinished,
+			Stdout:            logEntry.Stdout,
+			Stderr:            logEntry.Stderr,
+			TimedOut:          logEntry.TimedOut,
+			Blocked:           logEntry.Blocked,
+			ExitCode:          logEntry.ExitCode,
+			Tags:              logEntry.Tags,
+			ExecutionUuid:     uuid,
+			ExecutionStarted:  logEntry.ExecutionStarted,
+			ExecutionFinished: logEntry.ExecutionFinished,
 		})
 	}
 
+	sorter := func(i, j int) bool {
+		return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
+	}
+
+	sort.Slice(ret.Logs, sorter)
+
 	return ret, nil
 }
 
@@ -130,7 +199,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
 	}
 
 	grpcServer := grpc.NewServer()
-	pb.RegisterOliveTinApiServer(grpcServer, newServer(ex))
+	pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
 
 	err = grpcServer.Serve(lis)
 

+ 5 - 4
internal/grpcapi/grpcApiActions.go

@@ -25,10 +25,11 @@ func actionsCfgToPb(cfgActions []config.Action, user *acl.AuthenticatedUser) *pb
 
 func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action {
 	btn := pb.Action{
-		Id:      fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
-		Title:   action.Title,
-		Icon:    action.Icon,
-		CanExec: acl.IsAllowedExec(cfg, user, &action),
+		Id:           fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
+		Title:        action.Title,
+		Icon:         action.Icon,
+		CanExec:      acl.IsAllowedExec(cfg, user, &action),
+		PopupOnStart: action.PopupOnStart,
 	}
 
 	for _, cfgArg := range action.Arguments {

+ 3 - 3
internal/grpcapi/grpcApi_test.go

@@ -26,7 +26,7 @@ func init() {
 
 	lis = bufconn.Listen(bufSize)
 	s := grpc.NewServer()
-	pb.RegisterOliveTinApiServer(s, newServer(ex))
+	pb.RegisterOliveTinApiServiceServer(s, newServer(ex))
 
 	go func() {
 		if err := s.Serve(lis); err != nil {
@@ -39,7 +39,7 @@ func bufDialer(context.Context, string) (net.Conn, error) {
 	return lis.Dial()
 }
 
-func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiClient) {
+func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiServiceClient) {
 	cfg = injectedConfig
 
 	ctx := context.Background()
@@ -50,7 +50,7 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr
 		t.Fatalf("Failed to dial bufnet: %v", err)
 	}
 
-	client := pb.NewOliveTinApiClient(conn)
+	client := pb.NewOliveTinApiServiceClient(conn)
 
 	return conn, client
 }

+ 6 - 2
internal/httpservers/restapi.go

@@ -59,6 +59,10 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
 	return md
 }
 
+func SetGlobalRestConfig(config *config.Config) {
+	cfg = config
+}
+
 func startRestAPIServer(globalConfig *config.Config) error {
 	cfg = globalConfig
 
@@ -76,7 +80,7 @@ func startRestAPIServer(globalConfig *config.Config) error {
 		runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
 			Marshaler: &runtime.JSONPb{
 				MarshalOptions: protojson.MarshalOptions{
-					UseProtoNames:   true,
+					UseProtoNames:   false, // eg: canExec for js instead of can_exec from protobuf
 					EmitUnpopulated: true,
 				},
 			},
@@ -84,7 +88,7 @@ func startRestAPIServer(globalConfig *config.Config) error {
 	)
 	opts := []grpc.DialOption{grpc.WithInsecure()}
 
-	err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
+	err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
 
 	if err != nil {
 		log.Errorf("Could not register REST API Handler %v", err)

+ 53 - 1
internal/httpservers/restapi_auth_jwt.go

@@ -1,14 +1,58 @@
 package httpservers
 
 import (
+	"crypto/rsa"
 	"errors"
 	"fmt"
 	"github.com/golang-jwt/jwt/v4"
 	log "github.com/sirupsen/logrus"
 	"net/http"
+	"os"
 )
 
-func parseJwtToken(cookieValue string) (*jwt.Token, error) {
+var (
+	pubKeyBytes []byte = nil
+	pubKey      *rsa.PublicKey
+)
+
+func readPublicKey() error {
+	if pubKeyBytes != nil {
+		return nil // Already read.
+	}
+
+	pubKeyBytes, err := os.ReadFile(cfg.AuthJwtPubKeyPath)
+	if err != nil {
+		return fmt.Errorf("couldn't read public key from file %s", cfg.AuthJwtPubKeyPath)
+	}
+
+	// Since the token is RSA (which we validated at the start of this function), the return type of this function actually has to be rsa.PublicKey!
+	pubKey, err = jwt.ParseRSAPublicKeyFromPEM(pubKeyBytes)
+	if err != nil {
+		return fmt.Errorf("error parsing public key object (from %s)", cfg.AuthJwtPubKeyPath)
+	}
+
+	return nil
+}
+
+func parseJwtTokenWithKey(cookieValue string) (*jwt.Token, error) {
+	err := readPublicKey()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
+		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
+			return nil, fmt.Errorf(
+				"expected token algorithm '%v' but got '%v'",
+				jwt.SigningMethodRS256.Name,
+				token.Header)
+		}
+		return pubKey, nil
+	})
+}
+
+func parseJwtTokenWithoutKey(cookieValue string) (*jwt.Token, error) {
 	return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
 		// Don't forget to validate the alg is what you expect:
 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
@@ -20,6 +64,14 @@ func parseJwtToken(cookieValue string) (*jwt.Token, error) {
 	})
 }
 
+func parseJwtToken(cookieValue string) (*jwt.Token, error) {
+	if cfg.AuthJwtPubKeyPath != "" { // activate this path only if pub key is specified
+		return parseJwtTokenWithKey(cookieValue)
+	} else {
+		return parseJwtTokenWithoutKey(cookieValue)
+	}
+}
+
 func getClaimsFromJwtToken(cookieValue string) (jwt.MapClaims, error) {
 	token, err := parseJwtToken(cookieValue)
 

+ 125 - 0
internal/httpservers/restapi_test.go

@@ -0,0 +1,125 @@
+package httpservers
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	config2 "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/cors"
+	"github.com/golang-jwt/jwt/v4"
+	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/protobuf/encoding/protojson"
+	"io"
+	"net"
+	"net/http"
+	"os"
+	"testing"
+	"time"
+)
+
+func createKeys() (*rsa.PrivateKey, string) {
+	tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
+	defer os.Remove(tmpFile.Name())
+
+	fmt.Println("Created File: " + tmpFile.Name())
+
+	privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
+	pubKey := &privateKey.PublicKey
+	// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
+	pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
+	pubPem := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "RSA PUBLIC KEY",
+			Bytes: pkixPubKey,
+		},
+	)
+
+	if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
+		fmt.Printf("error when dumping pubKey: %s \n", err)
+	}
+
+	return privateKey, tmpFile.Name()
+}
+
+func testBase(t *testing.T, expire int64, expectCode int) {
+	privateKey, publicKeyPath := createKeys()
+
+	// default config + overrides
+	config := config2.DefaultConfig()
+	config.AuthJwtPubKeyPath = publicKeyPath
+	config.AuthJwtClaimUsername = "sub"
+	config.AuthJwtClaimUserGroup = "olivetinGroup"
+	config.AuthJwtCookieName = "authorization_token"
+	SetGlobalRestConfig(config) // ugly, setting global var, we should pass configs as params to modules... :/
+
+	token := jwt.New(jwt.SigningMethodRS256)
+
+	claims := token.Claims.(jwt.MapClaims)
+	claims["nbf"] = time.Now().Unix() - 1000
+	claims["exp"] = time.Now().Unix() + expire
+	claims["sub"] = "test"
+	claims["olivetinGroup"] = "test"
+
+	tokenStr, _ := token.SignedString(privateKey)
+
+	// init mux endpoint like in restapi.go (but using dummy response handler)
+	mux := runtime.NewServeMux(
+		runtime.WithMetadata(parseRequestMetadata), // i am guessing this is critical middleware for authorizing request cookie
+		runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
+			Marshaler: &runtime.JSONPb{
+				MarshalOptions: protojson.MarshalOptions{
+					UseProtoNames:   true,
+					EmitUnpopulated: true,
+				},
+			},
+		}),
+	)
+	mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
+		username, usergroup := parseJwtCookie(r)
+		if username == "" {
+			w.WriteHeader(403)
+		}
+		w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
+	})
+
+	// make server and attach handler
+	srv := &http.Server{Handler: cors.AllowCors(mux)}
+	lis, _ := net.Listen("tcp", ":1337")
+
+	go func() {
+		if err := srv.Serve(lis); err != nil {
+			t.Errorf("couldn't start server: %v", err)
+		}
+	}()
+
+	// make http client and send request to myself
+	client := &http.Client{}
+	req, _ := http.NewRequest("GET", "http://localhost:1337/", nil)
+	cookie := &http.Cookie{
+		Name:   "authorization_token",
+		Value:  tokenStr,
+		MaxAge: 300,
+	}
+	req.AddCookie(cookie)
+	res, err := client.Do(req)
+
+	if err != nil {
+		assert.Equal(t, expectCode, -1)
+	} else {
+		defer res.Body.Close()
+		assert.Equal(t, expectCode, res.StatusCode)
+		body, _ := io.ReadAll(res.Body)
+		fmt.Println(string(body))
+	}
+}
+
+func TestJWTSignatureVerificationSucceeds(t *testing.T) {
+	testBase(t, 1000, 200)
+}
+
+func TestJWTSignatureVerificationFails(t *testing.T) {
+	testBase(t, -500, 403)
+}

+ 8 - 2
internal/httpservers/singleFrontend.go

@@ -10,10 +10,12 @@ away, and several other issues.
 
 import (
 	config "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/websocket"
 	log "github.com/sirupsen/logrus"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
+	"strings"
 )
 
 // StartSingleHTTPFrontend will create a reverse proxy that proxies the API
@@ -37,8 +39,12 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
 	})
 
 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		log.Debugf("ui req: %q", r.URL)
-		webuiProxy.ServeHTTP(w, r)
+		if strings.Contains(r.Header.Get("Connection"), "Upgrade") {
+			websocket.HandleWebsocket(w, r)
+		} else {
+			log.Debugf("ui req: %q", r.URL)
+			webuiProxy.ServeHTTP(w, r)
+		}
 	})
 
 	srv := &http.Server{

+ 3 - 0
internal/httpservers/webuiServer_test.go

@@ -1,6 +1,7 @@
 package httpservers
 
 import (
+	config "github.com/OliveTin/OliveTin/internal/config"
 	"github.com/stretchr/testify/assert"
 	"os"
 	"testing"
@@ -9,6 +10,8 @@ import (
 func TestGetWebuiDir(t *testing.T) {
 	os.Chdir("../../") // go test sets the cwd to "httpservers" by default
 
+	cfg = config.DefaultConfig()
+
 	dir := findWebuiDir()
 
 	assert.Equal(t, "./webui", dir, "Finding the webui dir")

+ 122 - 0
internal/websocket/websocket.go

@@ -0,0 +1,122 @@
+package websocket
+
+import (
+	pb "github.com/OliveTin/OliveTin/gen/grpc"
+	"github.com/OliveTin/OliveTin/internal/executor"
+	ws "github.com/gorilla/websocket"
+	log "github.com/sirupsen/logrus"
+	"google.golang.org/protobuf/encoding/protojson"
+	"net/http"
+)
+
+var upgrader = ws.Upgrader{}
+
+type WebsocketClient struct {
+	conn *ws.Conn
+}
+
+var clients []*WebsocketClient
+
+var marshalOptions = protojson.MarshalOptions{
+	UseProtoNames:   false, // eg: canExec for js instead of can_exec from protobuf
+	EmitUnpopulated: true,
+}
+
+var ExecutionListener WebsocketExecutionListener
+
+type WebsocketExecutionListener struct{}
+
+func (WebsocketExecutionListener) OnExecutionStarted(title string) {
+	/*
+		broadcast(ExecutionStarted{
+			Type: "ExecutionStarted",
+			Action: title,
+		});
+	*/
+}
+
+func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
+	le := &pb.LogEntry{
+		ActionTitle:       logEntry.ActionTitle,
+		ActionIcon:        logEntry.ActionIcon,
+		DatetimeStarted:   logEntry.DatetimeStarted,
+		DatetimeFinished:  logEntry.DatetimeFinished,
+		Stdout:            logEntry.Stdout,
+		Stderr:            logEntry.Stderr,
+		TimedOut:          logEntry.TimedOut,
+		Blocked:           logEntry.Blocked,
+		ExitCode:          logEntry.ExitCode,
+		Tags:              logEntry.Tags,
+		Uuid:              logEntry.UUID,
+		ExecutionStarted:  logEntry.ExecutionStarted,
+		ExecutionFinished: logEntry.ExecutionFinished,
+	}
+
+	broadcast("ExecutionFinished", le)
+}
+
+func broadcast(messageType string, pbmsg *pb.LogEntry) {
+	payload, err := marshalOptions.Marshal(pbmsg)
+
+	// <EVIL>
+	// So, the websocket wants to encode messages using the same protomarshaller
+	// as the REST API - this gives consistency instead of using encoding/json
+	// and allows us to set specific marshalOptions.
+	//
+	// However, the protomarshaller will marshal the type, but the JavaScript at
+	// the other end has no idea what type this object is - as we're just sending
+	// it as JSON over the websocket.
+	//
+	// Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
+	// literal and encode that string just with a byte array cast.
+	hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
+
+	hackyMessage := []byte{}
+	hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
+	hackyMessage = append(hackyMessage, payload...)
+	hackyMessage = append(hackyMessage, []byte("}")...)
+	// </EVIL>
+
+	if err != nil {
+		log.Errorf("websocket marshal error: %v", err)
+		return
+	}
+
+	for _, client := range clients {
+		client.conn.WriteMessage(1, hackyMessage)
+	}
+}
+
+func (c *WebsocketClient) messageLoop() {
+	for {
+		mt, message, err := c.conn.ReadMessage()
+
+		if err != nil {
+			log.Printf("err: %v", err)
+			break
+		}
+
+		log.Infof("websocket recv: %s %d", message, mt)
+	}
+}
+
+func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
+	c, err := upgrader.Upgrade(w, r, nil)
+
+	if err != nil {
+		log.Warnf("Websocket issue: %v", err)
+		return false
+	}
+
+	//	defer c.Close()
+
+	wsclient := &WebsocketClient{
+		conn: c,
+	}
+
+	clients = append(clients, wsclient)
+
+	go wsclient.messageLoop()
+
+	return true
+}

+ 1 - 1
var/initscript/OliveTin

@@ -48,7 +48,7 @@ stop()
 
 status() {
 	PID=$(pidof OliveTin)
-	RETVAL=$? 
+	RETVAL=$?
 
 	if [ $RETVAL -eq 1 ] ; then
 		echo "OliveTin is stopped"

+ 1 - 1
var/openrc/OliveTin

@@ -4,4 +4,4 @@ name=$RC_SVCNAME
 description="OliveTin"
 command="/usr/local/bin/OliveTin"
 pidfile="/run/${RC_SVCNAME}.pid"
-command_background=true
+command_background=true

+ 79 - 26
webui/index.html

@@ -62,47 +62,96 @@
 		</main>
 
 		<footer title = "footer">
-			<p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p>
-			<p>	
-				<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> | 
-				<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> | 
-				<span id = "currentVersion">Version: ?</p>  
+			<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
+			<p>
+				<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
+				<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
+				<span>Version: <span id = "currentVersion">?</span></span> |
+				<span>Server connection: <span id = "serverConnection">?</span></span>
+			</p>
+			<p>
 				<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
 			</p>
 		</footer>
 
-		<template id = "tplArgumentForm">
-			<form class = "action-arguments">
-				<div class = "wrapper">
-					<div>
-						<span class = "icon" role = "icon"></span>
-						<h2>Argument form</h2>
-					</div>
+		<dialog id = "executionResults">
+			<div class = "action-header">
+				<span class = "icon" role = "icon"></span>
+
+				<h2>Log:
+					<span class = "title">?</span>
+				</h2>
+			</div>
+			<p>
+				<strong>Started: </strong><span class = "datetimeStarted">unknown</span>.
+				<strong>Finished: </strong><span class = "datetimeFinished">unknown</span>
+			</p>
+			<p>
+				<strong>Exit Code: </strong><span class = "exitCode">unknown</span>
+			</p>
+			<p>
+				<strong>Status: </strong><span class = "status">unknown</span>
+			</p>
+
+
+			<details>
+				<summary>stdout</summary>
+				<pre class = "stdout">
+					?
+				</pre>
+			</details>
 
-					<div class = "arguments"></div>
+			<details>
+				<summary>stderr</summary>
+				<pre class = "stderr">
+					?
+				</pre>
+			</details>
 
-					<div class = "buttons">
-						<input name = "start" type = "submit" value = "Start">
-						<button name = "cancel">Cancel</button>
+			<form method = "dialog">
+				<button name = "cancel">Close</button>
+			</form>
+		</dialog>
+
+		<template id = "tplArgumentForm">
+			<dialog>
+				<form class = "action-arguments">
+					<div class = "wrapper">
+						<div class = "action-header">
+							<span class = "icon" role = "icon"></span>
+							<h2>Argument form</h2>
+						</div>
+
+						<div class = "arguments"></div>
+
+						<div class = "buttons">
+							<input name = "start" type = "submit" value = "Start">
+							<button name = "cancel">Cancel</button>
+						</div>
 					</div>
-				</div>
-			<form>
+				</form>
+			</button>
 		</template>
 
 		<template id = "tplActionButton">
-			<button>
-				<span role = "icon" title = "button icon" class = "icon">&#x1f4a9;</span>
-				<p role = "title" class = "title">Untitled Button</p>
-			</button>
+			<div>
+				<button>
+					<span role = "icon" title = "button icon" class = "icon">&#x1f4a9;</span>
+					<span role = "title" class = "title">Untitled Button</span>
+				</button>
+
+				<div class = "executions">
+				</div>
+			</div>
 		</template>
 
 		<template id = "tplLogRow">
 			<tr class = "log-row">
-				<td class = "timestamp">?</td> 
+				<td class = "timestamp">?</td>
 				<td>
 					<span class = "icon" role = "icon"></span>
 					<span class = "content">?</span>
-	
+
 					<details>
 						<summary>stdout</summary>
 						<pre class = "stdout">
@@ -124,12 +173,15 @@
 		</template>
 
 		<script type = "text/javascript">
-			/** 
+			/**
 			This is the bootstrap code, which relies on very simple, old javascript
 		  	to at least display a helpful error message if we can't use OliveTin.
 			*/
 			function showBigError (type, friendlyType, message) {
-			  clearInterval(window.buttonInterval)
+			  for (const oldError of document.querySelectorAll('div.error').values()) {
+				window.old = oldError;
+				oldError.remove();
+			  }
 
 			  console.error('Error ' + type + ': ', message)
 
@@ -146,5 +198,6 @@
 		</script>
 
 		<script type = "module" src = "main.js"></script>
+		<script type = "module" src = "js/websocket.js"></script>
 	</body>
 </html>

+ 16 - 61
webui/js/ActionButton.js

@@ -1,5 +1,5 @@
-import { marshalLogsJsonToHtml } from './marshaller.js'
 import './ArgumentForm.js'
+import './ExecutionButton.js'
 
 class ActionButton extends window.HTMLElement {
   constructFromJson (json) {
@@ -25,6 +25,7 @@ class ActionButton extends window.HTMLElement {
         })
 
         document.body.appendChild(frm)
+        frm.querySelector('dialog').showModal()
       } else {
         this.startAction()
       }
@@ -32,7 +33,8 @@ class ActionButton extends window.HTMLElement {
 
     this.updateFromJson(json)
 
-    this.updateDom()
+    this.domTitle.innerText = this.btn.title
+    this.domIcon.innerHTML = this.unicodeIcon
 
     this.setAttribute('id', 'actionButton_' + json.id)
   }
@@ -52,20 +54,27 @@ class ActionButton extends window.HTMLElement {
   }
 
   startAction (actionArgs) {
-    this.btn.disabled = true
-    this.isWaiting = true
-    this.updateDom()
+    //    this.btn.disabled = true
+    //    this.isWaiting = true
+    //    this.updateDom()
     this.btn.classList = [] // Removes old animation classes
 
     if (actionArgs === undefined) {
       actionArgs = []
     }
 
+    // UUIDs are create client side, so that we can setup a "execution-button"
+    // to track the execution before we send the request to the server.
     const startActionArgs = {
       actionName: this.btn.title,
-      arguments: actionArgs
+      arguments: actionArgs,
+      uuid: window.crypto.randomUUID()
     }
 
+    const btnExecution = document.createElement('execution-button')
+    btnExecution.constructFromJson(startActionArgs.uuid)
+    this.querySelector('div.executions').appendChild(btnExecution)
+
     window.fetch(this.actionCallUrl, {
       method: 'POST',
       headers: {
@@ -80,45 +89,12 @@ class ActionButton extends window.HTMLElement {
       }
     }
     ).then((json) => {
-      marshalLogsJsonToHtml({ logs: [json.logEntry] })
-
-      if (json.logEntry.timedOut) {
-        this.onActionResult('action-timeout', 'Timed out')
-      } else if (json.logEntry.exitCode === -1337) {
-        this.onActionError('Error')
-      } else if (json.logEntry.exitCode !== 0) {
-        this.onActionResult('action-nonzero-exit', 'Exit code ' + json.logEntry.exitCode)
-      } else {
-        this.onActionResult('action-success', 'Success!')
-      }
+      // The button used to wait for the action to finish, but now it is fire & forget
     }).catch(err => {
       this.onActionError(err)
     })
   }
 
-  onActionResult (cssClass, temporaryStatusMessage) {
-    this.btn.disabled = false
-    this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
-    this.updateDom()
-    this.btn.classList.add(cssClass)
-
-    setTimeout(() => {
-      this.btn.classList.remove(cssClass)
-    }, 1000)
-  }
-
-  onActionError (err) {
-    console.error('callback error', err)
-    this.btn.disabled = false
-    this.isWaiting = false
-    this.updateDom()
-    this.btn.classList.add('action-failed')
-
-    setTimeout(() => {
-      this.btn.classList.remove('action-failed')
-    }, 1000)
-  }
-
   constructDomFromTemplate () {
     const tpl = document.getElementById('tplActionButton')
     const content = tpl.content.cloneNode(true)
@@ -134,27 +110,6 @@ class ActionButton extends window.HTMLElement {
     this.domTitle = this.btn.querySelector('.title')
     this.domIcon = this.btn.querySelector('.icon')
   }
-
-  updateDom () {
-    if (this.temporaryStatusMessage != null) {
-      this.domTitle.innerText = this.temporaryStatusMessage
-      this.domTitle.classList.add('temporary-status-message')
-      this.isWaiting = false
-      this.disabled = false
-
-      setTimeout(() => {
-        this.temporaryStatusMessage = null
-        this.domTitle.classList.remove('temporary-status-message')
-        this.updateDom()
-      }, 2000)
-    } else if (this.isWaiting) {
-      this.domTitle.innerText = 'Waiting...'
-    } else {
-      this.domTitle.innerText = this.btn.title
-    }
-
-    this.domIcon.innerHTML = this.unicodeIcon
-  }
 }
 
 window.customElements.define('action-button', ActionButton)

+ 105 - 0
webui/js/ExecutionButton.js

@@ -0,0 +1,105 @@
+import { ExecutionDialog } from './ExecutionDialog.js'
+
+class ExecutionButton extends window.HTMLElement {
+  constructFromJson (json) {
+    this.executionUuid = json
+
+    this.appendChild(document.createElement('button'))
+    this.isWaiting = true
+
+    this.setAttribute('id', 'execution-' + json)
+
+    this.btn = this.querySelector('button')
+    this.btn.innerText = 'Executing...'
+    this.btn.onclick = () => {
+      this.show()
+    }
+  }
+
+  show () {
+    if (window.executionDialog === undefined) {
+      window.executionDialog = new ExecutionDialog()
+    }
+
+    const executionStatusArgs = {
+      executionUuid: this.executionUuid
+    }
+
+    window.executionDialog.constructFromJson(this.executionUuid)
+    window.executionDialog.show()
+
+    window.fetch(window.restBaseUrl + 'ExecutionStatus', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify(executionStatusArgs)
+    }).then((res) => {
+      if (res.ok) {
+        return res.json()
+      } else {
+        throw new Error(res.statusText)
+      }
+    }
+    ).then((json) => {
+      window.executionDialog.renderResult(json)
+    }).catch(err => {
+      window.executionDialog.renderError(err)
+    })
+  }
+
+  onFinished (LogEntry) {
+    if (LogEntry.timedOut) {
+      this.onActionResult('action-timeout', 'Timed out')
+    } else if (LogEntry.blocked) {
+      this.onActionResult('action-blocked', 'Blocked!')
+    } else if (LogEntry.exitCode !== 0) {
+      this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
+    } else {
+      this.onActionResult('action-success', 'Success!')
+    }
+  }
+
+  onActionResult (cssClass, temporaryStatusMessage) {
+    this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
+    this.updateDom()
+    this.btn.classList.add(cssClass)
+
+    setTimeout(() => {
+      this.btn.classList.remove(cssClass)
+    }, 1000)
+  }
+
+  onActionError (err) {
+    console.error('callback error', err)
+    this.btn.disabled = false
+    this.isWaiting = false
+    this.updateDom()
+    this.btn.classList.add('action-failed')
+
+    setTimeout(() => {
+      this.btn.classList.remove('action-failed')
+    }, 1000)
+  }
+
+  updateDom () {
+    if (this.temporaryStatusMessage != null) {
+      this.btn.innerText = this.temporaryStatusMessage
+      this.btn.classList.add('temporary-status-message')
+      this.isWaiting = false
+      this.disabled = false
+
+      setTimeout(() => {
+        this.temporaryStatusMessage = null
+        this.btn.classList.remove('temporary-status-message')
+        this.updateDom()
+      }, 2000)
+    } else if (this.isWaiting) {
+      this.btn.innerText = 'Waiting...'
+    } else {
+      this.btn.innerText = 'Finished'
+    }
+  }
+}
+
+window.customElements.define('execution-button', ExecutionButton)

+ 66 - 0
webui/js/ExecutionDialog.js

@@ -0,0 +1,66 @@
+// This ExecutionDialog is NOT a custom HTML element, but rather just picks up
+// the <dialog /> element out of index.html and just re-uses that - as only
+// one dialog can be shown at a time.
+export class ExecutionDialog {
+  constructFromJson (json) {
+    this.executionUuid = json
+
+    this.dlg = document.querySelector('dialog#executionResults')
+
+    this.domIcon = this.dlg.querySelector('.icon')
+    this.domTitle = this.dlg.querySelector('.title')
+    this.domStdout = this.dlg.querySelector('.stdout')
+    this.domStderr = this.dlg.querySelector('.stderr')
+    this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
+    this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
+    this.domExitCode = this.dlg.querySelector('.exitCode')
+    this.domStatus = this.dlg.querySelector('.status')
+  }
+
+  show () {
+    this.dlg.showModal()
+  }
+
+  renderResult (res) {
+    this.executionUuid = res.logEntry.executionUuid
+
+    if (res.logEntry.executionFinished) {
+      this.domStatus.innerText = 'Completed'
+      this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
+
+      if (res.logEntry.blocked) {
+        this.domStatus.innerText = 'Blocked'
+      }
+
+      if (res.logEntry.timedOut) {
+        this.domExitCode.innerText = 'Timed out'
+        this.domStatus.innerText = 'Timed out'
+      } else {
+        this.domExitCode.innerText = res.logEntry.exitCode
+      }
+    } else {
+      this.domDatetimeFinished.innerText = 'Still running...'
+      this.domExitCode.innerText = 'Still running...'
+      this.domStatus.innerText = 'Still running...'
+    }
+
+    this.domIcon.innerHTML = res.logEntry.actionIcon
+    this.domTitle.innerText = res.logEntry.actionTitle
+
+    this.domStdout.innerText = res.logEntry.stdout
+
+    if (res.logEntry.stderr === '') {
+      this.domStderr.parentElement.hidden = true
+      this.domStderr.innerText = res.logEntry.stderr
+    } else {
+      this.domStderr.parentElement.hidden = false
+      this.domStderr.innerText = res.logEntry.stderr
+    }
+
+    this.domDatetimeStarted.innerText = res.logEntry.datetimeStarted
+  }
+
+  renderError (err) {
+    this.dlg.querySelector('pre').innerText = JSON.stringify(err)
+  }
+}

+ 1 - 1
webui/js/marshaller.js

@@ -50,7 +50,7 @@ export function marshalLogsJsonToHtml (json) {
       logTableExitCode += ' (timed out)'
     }
 
-    row.querySelector('.timestamp').innerText = logEntry.datetime
+    row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
     row.querySelector('.content').innerText = logEntry.actionTitle
     row.querySelector('.icon').innerHTML = logEntry.actionIcon
     row.querySelector('pre.stdout').innerText = logEntry.stdout

+ 73 - 0
webui/js/websocket.js

@@ -0,0 +1,73 @@
+import { marshalLogsJsonToHtml } from './marshaller.js'
+
+window.ws = null
+
+export function checkWebsocketConnection () {
+  if (window.ws === null || window.ws.readyState === 3) {
+    reconnectWebsocket()
+  }
+}
+
+function reconnectWebsocket () {
+  window.websocketAvailable = false
+
+  let proto = 'ws:'
+
+  if (window.location.protocol === 'https:') {
+    proto = 'wss:'
+  }
+
+  const websocketConnectionUrl = proto + window.location.host + '/websocket'
+  const ws = window.ws = new WebSocket(websocketConnectionUrl)
+
+  ws.addEventListener('open', websocketOnOpen)
+  ws.addEventListener('message', websocketOnMessage)
+  ws.addEventListener('error', websocketOnError)
+  ws.addEventListener('close', websocketOnClose)
+}
+
+function websocketOnOpen (evt) {
+  window.websocketAvailable = true
+
+  window.ws.send('monitor')
+
+  window.refreshLoop()
+}
+
+function websocketOnMessage (msg) {
+  // FIXME check msg status is OK
+  const j = JSON.parse(msg.data)
+
+  switch (j.type) {
+    case 'ExecutionFinished':
+      updatePageAfterFinished(j.payload)
+      break
+    default:
+      window.showBigError('Unknown message type from server: ' + j.type)
+  }
+}
+
+function updatePageAfterFinished (logEntry) {
+  document.querySelector('execution-button#execution-' + logEntry.uuid).onFinished(logEntry)
+
+  marshalLogsJsonToHtml({
+    logs: [logEntry]
+  })
+
+  // If the current execution dialog is open, update that too
+  if (window.executionDialog != null && window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
+    window.executionDialog.renderResult({
+      logEntry: logEntry
+    })
+  }
+}
+
+function websocketOnError (err) {
+  window.websocketAvailable = false
+  window.refreshLoop()
+  console.error(err)
+}
+
+function websocketOnClose () {
+  window.websocketAvailable = false
+}

+ 35 - 6
webui/main.js

@@ -1,6 +1,7 @@
 'use strict'
 
 import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
+import { checkWebsocketConnection } from './js/websocket.js'
 
 function showSection (name) {
   for (const otherName of ['Actions', 'Logs']) {
@@ -19,15 +20,42 @@ function setupSections () {
   showSection('Actions')
 }
 
+function refreshLoop () {
+  if (window.websocketAvailable) {
+    document.querySelector('#serverConnection').classList.remove('error')
+    document.querySelector('#serverConnection').innerText = 'websocket'
+
+    // Websocket updates are streamed live, not updated on a loop.
+  } else if (window.restAvailable) {
+    // Fallback to rest, but try to reconnect the websocket anyway.
+
+    fetchGetDashboardComponents()
+    checkWebsocketConnection()
+
+    document.querySelector('#serverConnection').classList.remove('error')
+    document.querySelector('#serverConnection').innerText = 'rest'
+
+    fetchGetLogs()
+  } else {
+    document.querySelector('#serverConnection').innerText = 'disconnected, trying to reconnect...'
+    document.querySelector('#serverConnection').classList.add('error')
+
+    // Still try to fetch the dashboard, if successfull window.restAvailable = true
+    fetchGetDashboardComponents()
+  }
+}
+
 function fetchGetDashboardComponents () {
   window.fetch(window.restBaseUrl + 'GetDashboardComponents', {
     cors: 'cors'
   }).then(res => {
     return res.json()
   }).then(res => {
+    window.restAvailable = true
     marshalActionButtonsJsonToHtml(res)
-  }).catch(err => {
-    window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
+  }).catch(() => { // err is 1st arg
+    window.restAvailable = false
+    //    window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
   })
 }
 
@@ -55,7 +83,7 @@ function processWebuiSettingsJson (settings) {
     document.head.appendChild(themeCss)
   }
 
-  document.querySelector('#currentVersion').innerText = 'Version: ' + settings.CurrentVersion
+  document.querySelector('#currentVersion').innerText = settings.CurrentVersion
 
   if (settings.ShowNewVersions && settings.AvailableVersion !== 'none') {
     document.querySelector('#available-version').innerText = 'New Version Available: ' + settings.AvailableVersion
@@ -80,10 +108,11 @@ function main () {
   }).then(res => {
     processWebuiSettingsJson(res)
 
-    fetchGetDashboardComponents()
-    fetchGetLogs()
+    window.restAvailable = true
+    window.refreshLoop = refreshLoop
+    window.refreshLoop()
 
-    window.buttonInterval = setInterval(fetchGetDashboardComponents, 3000)
+    setInterval(refreshLoop, 3000)
   }).catch(err => {
     window.showBigError('fetch-webui-settings', 'getting webui settings', err)
   })

+ 18 - 18
webui/package-lock.json

@@ -1053,9 +1053,9 @@
       }
     },
     "node_modules/eslint-plugin-node/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
@@ -2639,9 +2639,9 @@
       }
     },
     "node_modules/read-pkg/node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "dev": true,
       "bin": {
         "semver": "bin/semver"
@@ -2796,9 +2796,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -4155,9 +4155,9 @@
           "dev": true
         },
         "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
           "dev": true
         }
       }
@@ -5281,9 +5281,9 @@
           }
         },
         "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "version": "5.7.2",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+          "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
           "dev": true
         },
         "type-fest": {
@@ -5399,9 +5399,9 @@
       }
     },
     "semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "requires": {
         "lru-cache": "^6.0.0"

+ 25 - 13
webui/style.css

@@ -7,6 +7,13 @@ body {
   margin: 0;
 }
 
+dialog {
+  min-width: 600px;
+  text-align: left;
+  padding: 1em;
+  box-shadow: 0 0 6px 0 #444;
+}
+
 fieldset {
   padding: 0;
 }
@@ -120,7 +127,7 @@ span[role="icon"] {
   vertical-align: middle;
 }
 
-form span[role="icon"],
+.action-header span[role="icon"],
 tr.log-row span[role="icon"] {
   display: inline-block;
   padding-right: 0.2em;
@@ -128,6 +135,9 @@ tr.log-row span[role="icon"] {
 
 .error {
   background-color: salmon;
+}
+
+div.error {
   padding: 1em;
 }
 
@@ -146,6 +156,7 @@ div.entity {
 }
 
 h2 {
+  margin-top: 0;
   font-size: 1em;
   display: inline-block;
 }
@@ -185,6 +196,11 @@ input[type="submit"]:disabled {
   cursor: not-allowed;
 }
 
+div.executions button {
+  margin-top: .2em;
+  margin-bottom: .2em;
+}
+
 fieldset#section-switcher {
   border: 0;
   text-align: right;
@@ -254,6 +270,14 @@ button.active-section {
   20% { background-color: cyan; }
 }
 
+.action-blocked {
+  animation: kf-action-blocked 1s;
+}
+
+@keyframes kf-action-blocked {
+  20% { background-color: purple; }
+}
+
 footer,
 footer a {
   color: black;
@@ -282,21 +306,9 @@ details[open] {
   display: block;
 }
 
-form.action-arguments {
-  position: absolute;
-  inset: 0;
-  padding: 1em;
-  box-shadow: 0 0 6px 0 #aaa;
-  background-color: #dee3e7;
-}
-
 form div.wrapper {
-  border-radius: 1em;
-  box-shadow: 0 0 10px 0 #444;
   background-color: white;
-  border: 1px solid #999;
   text-align: left;
-  padding: 1em;
 }
 
 label {