Repository: pufferpanel/pufferpanel

Issue:
memory in the console
Pufferpanel is almost perfect.
It would be nice if you could call up the last commands you had entered in the console with the up and down arrow keys without having to re-enter the whole command.

Hints from issue comments:
@Aareon: I think a server console command history would be a good feature

Solution patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b1431b92..93471102 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,7 +17,7 @@ on:
 
 env:
   SWAG_VERSION: "1.16.4"
-  NODE_VERSION: "20"
+  NODE_VERSION: "22"
   REGISTRY: ghcr.io
   IMAGE_NAME: ${{ github.repository }}
 
@@ -112,142 +112,6 @@ jobs:
           if-no-files-found: error
           retention-days: 1
 
-  build-windows-amd64-binaries:
-    name: Build Binaries - Windows AMD64
-    runs-on: ubuntu-latest
-    needs: [ 'build-shared' ]
-
-    timeout-minutes: 10
-
-    steps:
-      - name: Get the version
-        run: echo "VERSION=0.0.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
-
-      - name: Get the version
-        if: startsWith(github.ref, 'refs/tags/v')
-        run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
-
-      - uses: actions/download-artifact@v4
-        with:
-          name: pufferpanel_build_artifact
-
-      - name: Set up Go
-        uses: actions/setup-go@v5
-        with:
-          go-version-file: 'go.mod'
-
-      - name: Download APT packages
-        run: |
-          sudo apt-get update
-          sudo apt-get install --download-only -y gcc-mingw-w64
-
-      - name: APT Install
-        run: |
-          sudo apt-get install -y gcc-mingw-w64
-
-      - name: Build Windows
-        run: |
-          go build -o build-artifacts/pufferpanel.exe -ldflags "-X 'github.com/pufferpanel/pufferpanel/v3.Hash=${{ github.sha }}' -X 'github.com/pufferpanel/pufferpanel/v3.Version=${{ env.VERSION }}'" -v github.com/pufferpanel/pufferpanel/v3/cmd
-        env:
-          GOOS: windows
-          GOARCH: amd64
-          CC: x86_64-w64-mingw32-gcc
-          CXX: x86_64-w64-mingw32-g++
-          CGO_ENABLED: 1
-
-      - name: Publish Artifacts
-        uses: actions/upload-artifact@v4
-        with:
-          name: pufferpanel_${{ env.VERSION }}_windows_amd64
-          path: build-artifacts/
-
-  build-windows-installer:
-    name: Build Windows Installer
-    runs-on: windows-latest
-    needs: [ 'build-windows-amd64-binaries' ]
-
-    env:
-      DOTNET_CHANNEL: '8.0'
-      WIX_VERSION: '4.0.4'
-
-    timeout-minutes: 10
-
-    steps:
-      - name: Get the version
-        run: |
-          $VERSION="0.0.${{ github.run_number }}"
-          if ("${{ github.ref }}".startsWith("refs/tags/v")) {
-            $VERSION="${{ github.ref }}".substring('refs/tags/v'.length)
-          }
-          echo VERSION=$VERSION | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append
-
-      #      - name: Restore DOTNET BIN
-      #        uses: actions/cache@v3
-      #        id: dotnet-cache
-      #        with:
-      #          path: |
-      #            ~\dotnet
-      #          key: ${{ runner.os }}-dotnet-${{ env.DOTNET_CHANNEL }}
-      #          restore-keys: |
-      #            ${{ runner.os }}-dotnet-
-      #
-      #      - name: Restore Wix cache
-      #        uses: actions/cache@v3
-      #        id: wix-cache
-      #        with:
-      #          path: |
-      #            ~\.wix
-      #            ~\.dotnet
-      #          key: ${{ runner.os }}-wix-${{ env.DOTNET_CHANNEL }}
-      #          restore-keys: |
-      #            ${{ runner.os }}-wix-
-
-      - name: Prepare .NET
-        #if: steps.dotnet-cache.outputs.cache-hit != 'true'
-        run: |
-          Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile dotnet-install.ps1
-          .\dotnet-install.ps1 -Channel ${{ env.DOTNET_CHANNEL }} -InstallDir dotnet
-          Remove-Item dotnet-install.ps1
-
-      - name: Set Path
-        run: |
-          Add-Content $env:GITHUB_PATH "$pwd\dotnet"
-          Add-Content $env:GITHUB_PATH "$pwd\.dotnet\tools"
-          Add-Content $env:GITHUB_ENV "DOTNET_ROOT=$pwd\dotnet"
-
-      - name: Install WIX
-        #if: steps.wix-cache.outputs.cache-hit != 'true'
-        run: |
-          dotnet tool install --tool-path .dotnet\tools wix --version ${{ env.WIX_VERSION }}
-          wix extension add WixToolset.Firewall.wixext/${{ env.WIX_VERSION }}
-          wix extension add WixToolset.UI.wixext/${{ env.WIX_VERSION }}
-          wix extension add WixToolset.Util.wixext/${{ env.WIX_VERSION }}
-
-      - uses: actions/download-artifact@v4
-        with:
-          name: pufferpanel_${{ env.VERSION }}_windows_amd64
-          path: binaries
-
-      - uses: actions/download-artifact@v4
-        with:
-          name: pufferpanel_build_artifact
-          path: artifacts
-
-      - name: Generate MSI
-        run: |
-          Copy-Item -Recurse -Path binaries\* -Destination .
-          Copy-Item -Path artifacts\client\frontend\public\favicon.ico -Destination .
-          Copy-Item -Path artifacts\LICENSE.rtf -Destination .
-          Copy-Item -Path artifacts\installer.wxs -Destination .
-          set password $((New-Guid).ToString())
-          wix build -o pufferpanel.msi -arch x64 -d ProductVersion="${{ env.VERSION }}" -d Password="$password" -ext WixToolset.Firewall.wixext -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext installer.wxs
-
-      - name: Publish Artifacts
-        uses: actions/upload-artifact@v4
-        with:
-          name: pufferpanel_${{ env.VERSION }}_installer.msi
-          path: pufferpanel.msi
-
   build-linux-amd64-binaries:
     name: Build Binaries - Linux AMD64
     runs-on: ubuntu-latest
@@ -293,11 +157,12 @@ jobs:
         run: |
           mkdir -p DEBBUILD/usr/sbin DEBBUILD/var/www/pufferpanel RPMBUILD/var/www/pufferpanel
           mkdir -p DEBBUILD/var/log/pufferpanel RPMBUILD/var/log/pufferpanel
-          mkdir -p DEBBUILD/etc/pufferpanel DEBBUILD/lib/systemd/system/
+          mkdir -p DEBBUILD/etc/pufferpanel DEBBUILD/lib/systemd/system DEBBUILD/etc/apparmor.d/
           mkdir -p DEBBUILD/var/lib/pufferpanel/binaries RPMBUILD/var/lib/pufferpanel
           cp config.linux.json DEBBUILD/etc/pufferpanel/config.json
           cp build-artifacts/pufferpanel DEBBUILD/usr/sbin/pufferpanel
           cp systemd/servicefiles/pufferpanel.service DEBBUILD/lib/systemd/system/pufferpanel.service
+          cp systemd/servicefiles/apparmor.conf DEBBUILD/etc/apparmor.d/pufferpanel
 
       - name: Build Package DEB AMD64
         id: debamd64
@@ -318,8 +183,7 @@ jobs:
           before-remove: systemd/scripts/preremove.sh
           after-remove: systemd/scripts/postremove.sh
           after-purge: systemd/scripts/purge.sh
-          user: pufferpanel
-          group: pufferpanel
+          extra-dpkg-args: '-Zxz'
           suggested-packages: |
             lib32gcc1
 
@@ -420,11 +284,12 @@ jobs:
         run: |
           mkdir -p DEBBUILD/usr/sbin DEBBUILD/var/www/pufferpanel RPMBUILD/var/www/pufferpanel
           mkdir -p DEBBUILD/var/log/pufferpanel RPMBUILD/var/log/pufferpanel
-          mkdir -p DEBBUILD/etc/pufferpanel DEBBUILD/lib/systemd/system/
+          mkdir -p DEBBUILD/etc/pufferpanel DEBBUILD/lib/systemd/system/ DEBBUILD/etc/apparmor.d
           mkdir -p DEBBUILD/var/lib/pufferpanel/binaries RPMBUILD/var/lib/pufferpanel
           cp config.linux.json DEBBUILD/etc/pufferpanel/config.json
           cp build-artifacts/pufferpanel DEBBUILD/usr/sbin/pufferpanel
           cp systemd/servicefiles/pufferpanel.service DEBBUILD/lib/systemd/system/pufferpanel.service
+          cp systemd/servicefiles/apparmor.conf DEBBUILD/etc/apparmor.d/pufferpanel
 
       - name: Build Package DEB ARM64
         id: debarm64
@@ -445,8 +310,7 @@ jobs:
           before-remove: systemd/scripts/preremove.sh
           after-remove: systemd/scripts/postremove.sh
           after-purge: systemd/scripts/purge.sh
-          user: pufferpanel
-          group: pufferpanel
+          extra-dpkg-args: '-Zxz'
 
       - name: Build Package RPM ARM64
         id: rpmarm64
@@ -500,7 +364,7 @@ jobs:
     runs-on: ubuntu-latest
     name: "Cleanup"
 
-    needs: [ build-shared, build-docker-image-base, build-windows-amd64-binaries, build-windows-installer, build-linux-amd64-binaries, build-linux-arm64-binaries, release ]
+    needs: [ build-shared, build-docker-image-base, build-linux-amd64-binaries, build-linux-arm64-binaries, release ]
 
     continue-on-error: true
 
@@ -596,8 +460,6 @@ jobs:
     runs-on: ubuntu-latest
     needs:
       - build-docker-image-base
-      - build-windows-amd64-binaries
-      - build-windows-installer
       - build-linux-amd64-binaries
       - build-linux-arm64-binaries
     permissions:
@@ -643,4 +505,3 @@ jobs:
             pufferpanel_*_windows_amd64
             */*.rpm
             */*.deb
-            */*.msi
diff --git a/.github/workflows/run-template-tester.yml b/.github/workflows/run-template-tester.yml
index f262752c..61a859a0 100644
--- a/.github/workflows/run-template-tester.yml
+++ b/.github/workflows/run-template-tester.yml
@@ -4,48 +4,61 @@ on:
     types:
       - template-tester
 
-env:
-  TAG: templatetester
-  REGISTRY: ghcr.io
-  IMAGE_NAME: ${{ github.repository }}
-
 jobs:
   run-tester:
     name: Run Tool
-    runs-on: template-tester
+    runs-on: [ "template-tester-${{ matrix.arch }}" ]
     permissions:
       packages: read
 
     strategy:
       matrix:
         template: ${{ github.event.client_payload.templates }}
+        arch: [ x64, arm64 ]
       fail-fast: false
 
     steps:
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
+      - name: Download assets
+        uses: actions/download-artifact@v4
         with:
-          registry: ghcr.io
-          username: ${{ github.repository_owner }}
-          password: ${{ secrets.GITHUB_TOKEN }}
+          name: tool-binaries
+          run-id: '${{ github.event.client_payload.run_id }}'
+          github-token: ${{ secrets.ACCESS_DOWNLOAD_PAT }}
 
-      - name: Prepare
+        # We will only install deps if this is a "host", we will not do it for docker templates
+      - name: Install global deps
+        if: ${{ ! endsWith(matrix.template, '-docker') }}
+        run: |
+          sudo bash -c "\
+            apt-get update && \
+            apt-get install -y dirmngr gnupg apt-transport-https ca-certificates software-properties-common curl && \
+            gpg --homedir /tmp --no-default-keyring --keyring /usr/share/keyrings/mono-official-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
+            echo 'deb [signed-by=/usr/share/keyrings/mono-official-archive-keyring.gpg] https://download.mono-project.com/repo/ubuntu stable-focal main' > /etc/apt/sources.list.d/mono-official-stable.list && \
+            mkdir -p /etc/apt/keyrings && \
+            curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
+            echo 'deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main' > /etc/apt/sources.list.d/nodesource.list && \
+            apt-get update && \
+            apt-get install -y wget build-essential zip unzip tar git xz-utils nodejs
+          "
+        # Deps only involved on AMD64 architecture
+      - name: Install x64 deps
+        if: matrix.arch == 'x64' && !endsWith(matrix.template, '-docker')
+        run: |
+          sudo bash -c "\
+            dpkg --add-architecture i386 && \
+            echo steam steam/question select \"I AGREE\" | debconf-set-selections && \
+            echo steam steam/license note '' | debconf-set-selections && \
+            apt update && \
+            apt install -y lib32gcc-s1 libstdc++6 libstdc++6:i386 libtinfo5:i386 libncurses5:i386 libcurl3-gnutls:i386
+          "
+        # Add our user to the pufferpanel group so we can kick off our binary
+      - name: Create user
         run: |
-          docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}
-          docker rm -f templatetester-${{ matrix.template }} || true
-          docker volume rm -f template-tester-${{ matrix.template }} || true
-          echo "::add-mask::${{ secrets.RUNNER_IP }}"
+          sudo groupadd pufferpanel
+          sudo usermod -a -G pufferpanel $USER
 
+        # Run the tester on the template provided
       - name: Run
         run: |
-          docker volume create template-tester-${{ matrix.template }}
-          mountPath=$(docker volume inspect -f {{.Mountpoint}} template-tester-${{ matrix.template }})
-          echo "Docker will mount from $mountPath"
-          docker run --rm \
-                 --network host \
-                 --name templatetester-${{ matrix.template }} \
-                 -v template-tester-${{ matrix.template }}:/tester \
-                 -v /var/run/docker.sock:/var/run/docker.sock \
-                 -e PUFFER_DOCKER_ROOT="$mountPath" \
-                 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} --workDir=/tester --files=${{ matrix.template }}
-          docker volume rm -f template-tester-${{ matrix.template }}
\ No newline at end of file
+          chmod +x pufferpanel_* templatetester_*
+          ./templatetester_${{ matrix.arch }} --skip=* --require=${{ matrix.template }} --binary=$(pwd)/pufferpanel_${{ matrix.arch }}
\ No newline at end of file
diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml
index 40c67138..0553ad03 100644
--- a/.github/workflows/tester.yml
+++ b/.github/workflows/tester.yml
@@ -2,95 +2,125 @@ name: Build Tool - Tester
 on:
   workflow_dispatch:
     inputs:
-      skip-build:
-        description: 'Skips building of the image'
+      no-run-tool:
+        description: 'Skips running of the tool'
         default: false
         required: true
         type: boolean
 
 env:
-  TAG: templatetester
-  REGISTRY: ghcr.io
-  IMAGE_NAME: ${{ github.repository }}
+  SWAG_VERSION: "1.16.4"
+  NODE_VERSION: "22"
 
 jobs:
   build-tool:
     name: Build Tool
     runs-on: ubuntu-latest
-    permissions:
-      contents: write
-      packages: write
-    strategy:
-      matrix:
-        platform: [ "linux/amd64" ]
-    if: ${{ ! inputs.skip-build }}
 
     steps:
-      - name: Checkout repository
-        uses: actions/checkout@v3
+      - name: Check out code
+        uses: actions/checkout@v4
 
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: 'go.mod'
 
-      - name: Log into registry ${{ env.REGISTRY }}
-        uses: docker/login-action@v3
+      - name: Set up Node
+        uses: actions/setup-node@v4
         with:
-          registry: ${{ env.REGISTRY }}
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
+          node-version: ${{ env.NODE_VERSION }}
+          cache: 'yarn'
+          cache-dependency-path: 'client/yarn.lock'
 
-      - name: Extract Docker metadata
-        id: meta
-        uses: docker/metadata-action@v5
+      - name: Restore Go BIN
+        id: restore-go-bin
+        uses: actions/cache@v4
         with:
-          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
-          tags: type=raw,${{ env.TAG }}
+          path: |
+            ~/go/bin
+          key: ${{ runner.os }}-swag-${{ env.SWAG_VERSION }}
+          restore-keys: |
+            ${{ runner.os }}-swag-
 
-      - name: Prep
+      - name: Download Go Binaries
+        if: steps.restore-go-bin.outputs.cache-hit != 'true'
+        run: |
+          wget https://github.com/swaggo/swag/releases/download/v${{ env.SWAG_VERSION }}/swag_${{ env.SWAG_VERSION }}_Linux_x86_64.tar.gz
+          mkdir -p ~/go/bin
+          tar -zxf swag*.tar.gz -C ~/go/bin
+          rm -rf swag*.tar.gz
+          
+      - name: Set CurseForge Key
         env:
-          secret_set: ${{ secrets.CURSEFORGE_CODE != '' }}
-        if: ${{ env.secret_set }}
+          secret_set: ${{ secrets.CURSEFORGE_CODE }}
+        if: ${{ env.secret_set != '' }}
         run: |
           echo '${{ secrets.CURSEFORGE_CODE }}' > config/curseforge.go
 
-      - name: Build and push Docker image
-        id: build-and-push
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          file: Dockerfile-${{ env.TAG }}
-          push: ${{ github.event_name != 'pull_request' }}
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
-          cache-from: type=gha
-          cache-to: type=gha,mode=max
-          build-args: |
-            sha: ${{ github.sha }}
-            version: ${{ env.VERSION }}
-            curseforgeKey: ${{ secrets.CURSEFORGE_KEY }}
-          platforms: ${{ matrix.platform }}
-
-  run-tool:
-    name: Run Tool
-    runs-on: ubuntu-latest
+      - name: Download Go modules
+        run: |
+          go mod download
 
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v3
+      - name: Generate Swagger Docs
+        run: |
+          ~/go/bin/swag init --md . -o web/swagger -g web/loader.go
+
+      - name: yarn install, build
+        run: |
+          npm install -g yarn
+          yarn install
+          env VITE_CHECK_ENV="true" yarn build
+        env:
+          CI: true
+        working-directory: client
+          
+      - name: Download APT packages
+        run: |
+          sudo apt-get update
+          sudo apt-get install --download-only -y gcc-aarch64-linux-gnu
+
+      - name: APT Install
+        run: |
+          sudo apt-get install -y gcc-aarch64-linux-gnu
+
+      - name: Prep
+        run: |
+          mkdir build-artifacts
+
+      - name: Build AMD64
+        run: |
+          go build -o build-artifacts/pufferpanel_x64 -v github.com/pufferpanel/pufferpanel/v3/cmd
+          go build -o build-artifacts/templatetester_x64 -v github.com/pufferpanel/pufferpanel/v3/tools/templatetester
+        env:
+          GOOS: linux
+          GOARCH: amd64
+
+      - name: Build ARM64
+        run: |
+          go build -o build-artifacts/pufferpanel_arm64 -v github.com/pufferpanel/pufferpanel/v3/cmd
+          go build -o build-artifacts/templatetester_arm64 -v github.com/pufferpanel/pufferpanel/v3/tools/templatetester
+        env:
+          GOOS: linux
+          GOARCH: arm64
+          CC: aarch64-linux-gnu-gcc
+          CGO_ENABLED: 1
+
+      - name: Generate job artifact
+        uses: actions/upload-artifact@v4
         with:
-          repository: "pufferpanel/templates"
-          path: templates
-          ref: v3
+          name: tool-binaries
+          path: build-artifacts/
+          if-no-files-found: error
 
       - name: Get templates
         run: |
-          cd templates
-          FILES=$(find * -name "*.json" ! -name "data.json" ! -name "spec.json" -printf '%P\n' | awk '{print "\""$0"\""}' | tr '\n' ',' | sed 's/,$/\n/')
-          echo "TEMPLATES=$FILES" >> $GITHUB_ENV
+          build-artifacts/templatetester_x64 --print
 
       - name: Trigger tests
         id: trigger
+        if: ${{ ! inputs.no-run-tool }}
         uses: peter-evans/repository-dispatch@v2
         with:
           event-type: template-tester
-          client-payload: '{ "templates": [ ${{ env.TEMPLATES }} ] }'
\ No newline at end of file
+          client-payload: '{ "templates": [ ${{ env.TEMPLATES }} ], "run_id": "${{ github.run_id }}" }'
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index bda00fa2..daf8dadd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 ###
 # Builder container
 ###
-FROM --platform=$BUILDPLATFORM node:20-alpine AS node
+FROM --platform=$BUILDPLATFORM node:22-alpine AS node
 
 WORKDIR /build
 COPY client .
@@ -14,7 +14,7 @@ RUN yarn install && \
 
 FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
 
-FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
+FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
 
 RUN apk add clang lld
 COPY --from=xx / /
diff --git a/Dockerfile-templatetester b/Dockerfile-templatetester
index 780ce7e6..adb7cdb6 100644
--- a/Dockerfile-templatetester
+++ b/Dockerfile-templatetester
@@ -1,13 +1,14 @@
-FROM ubuntu:20.04 AS go
+FROM ubuntu:22.04 AS go
 
 ARG aptproxy=""
+ARG GOVERSION="1.24.1"
 ENV DEBIAN_FRONTEND=noninteractive
 
 WORKDIR /home/pufferpanel/build
 
 RUN apt-get update && \
     apt-get install -y build-essential wget tar && \
-    wget https://go.dev/dl/go1.22.3.linux-amd64.tar.gz && \
+    wget https://go.dev/dl/go${GOVERSION}.linux-amd64.tar.gz && \
     rm -rf /usr/local/go && tar -C /usr/local -xzf go*.linux-amd64.tar.gz && \
     apt-get clean && \
     rm -rf /var/cache/apt/archives
@@ -23,7 +24,7 @@ COPY . .
 ARG GOPROXY=""
 RUN go build -v -buildvcs=false -o /home/pufferpanel/templatetester github.com/pufferpanel/pufferpanel/v3/tools/templatetester
 
-FROM ubuntu:20.04 AS final
+FROM ubuntu:22.04 AS final
 
 ENV DEBIAN_FRONTEND=noninteractive
 ENV NODE_MAJOR=20
diff --git a/LICENSE.rtf b/LICENSE.rtf
deleted file mode 100644
index 160195a1..00000000
--- a/LICENSE.rtf
+++ /dev/null
@@ -1,307 +0,0 @@
-{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31506\stshfloch31506\stshfhich31506\stshfbi31507\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}
-{\f37\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
-{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}
-{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
-{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}
-{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f43\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f44\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
-{\f46\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f47\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f48\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f49\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
-{\f50\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f51\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f413\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f414\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}
-{\f416\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f417\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f418\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\f419\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}
-{\f420\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f421\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
-{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
-{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
-{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
-{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
-{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
-{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}
-{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}
-{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
-{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
-{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
-{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
-{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
-{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
-{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
-{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
-{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}
-{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}
-{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}
-{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
-{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
-{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}
-{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;
-\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;}{\*\defchp \f31506\fs22 }{\*\defpap \ql \li0\ri0\sa160\sl259\slmult1
-\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 
-\ltrch\fcs0 \f31506\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\*
-\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1
-\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext11 \ssemihidden \sunhideused Normal Table;}{
-\s15\ql \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 
-\sbasedon0 \snext15 \ssemihidden \sunhideused \styrsid1143598 Normal (Web);}{\*\cs16 \additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b \sbasedon10 \sqformat \spriority22 \styrsid1143598 Strong;}{\*\cs17 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf2 
-\sbasedon10 \ssemihidden \sunhideused \styrsid1143598 Hyperlink;}}{\*\listtable{\list\listtemplateid2124825698{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}
-\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li720\jclisttab\tx720\lin720 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'01.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 
-\fi-360\li1440\jclisttab\tx1440\lin1440 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'02.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li2160
-\jclisttab\tx2160\lin2160 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'03.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li2880\jclisttab\tx2880\lin2880 }
-{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'04.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li3600\jclisttab\tx3600\lin3600 }{\listlevel\levelnfc0
-\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'05.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li4320\jclisttab\tx4320\lin4320 }{\listlevel\levelnfc0\levelnfcn0\leveljc0
-\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'06.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li5040\jclisttab\tx5040\lin5040 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0
-\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'02\'07.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li5760\jclisttab\tx5760\lin5760 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative
-\levelspace0\levelindent0{\leveltext\'02\'08.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li6480\jclisttab\tx6480\lin6480 }{\listname ;}\listid104161547}}{\*\listoverridetable{\listoverride\listid104161547\listoverridecount0\ls1}}{\*\pgptbl 
-{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp1\itap0\li10\ri10\sb720\sa720\brdrt\brdrs\brdrw15\brsp480\brdrcf1 \brdrl\brdrs\brdrw15\brsp480\brdrcf1 \brdrb\brdrs\brdrw15\brsp480\brdrcf1 \brdrr\brdrs\brdrw15\brsp480\brdrcf1 }{\pgp\ipgp2\itap0\li240\ri0\sb0
-\sa0}}{\*\rsidtbl \rsid1143598\rsid2098402}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author Joshua Taylor}{\operator Joshua Taylor}
-{\creatim\yr2021\mo11\dy18\hr14\min46}{\revtim\yr2021\mo11\dy18\hr14\min46}{\version1}{\edmins0}{\nofpages3}{\nofwords1368}{\nofchars7799}{\nofcharsws9149}{\vern21}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}
-\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect 
-\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors1\noxlattoyen
-\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin1440\dgvorigin1440\dghshow1\dgvshow1
-\jexpand\viewkind1\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct
-\asianbrkrule\rsidroot1143598\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0
-{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang 
-{\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang 
-{\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}
-\pard\plain \ltrpar\qc \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid1143598 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 
-\f31506\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Apache License\line Version 2.0, January 2004\line }{\field\fldedit{\*\fldinst {\rtlch\fcs1 \ab\af0\afs24 
-\ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598  HYPERLINK "http://www.apache.org/licenses/" }}{\fldrslt {\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\ul\cf2\insrsid1143598\charrsid1143598 http://www.apache.org/licenses/}}}\sectd \ltrsect
-\linex0\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598  
-\par }\pard \ltrpar\qj \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid1143598 {\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart definitions}1. Definitions}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend definitions}.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 License}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Licensor}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Legal Entity}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 
-\b\f0\fs24\insrsid1143598\charrsid1143598 control}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " means (i) the power, direct or indirect, to cause the direc
-tion or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 You}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " (or "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 
-Your}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 ") shall mean an individual or Legal Entity exercising permissions granted by this License.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Source}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Object}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " form shall mean any form resulting fr
-om mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Work}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " shall mean the work of authorship, whether in Source or Object form, made
- available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Derivative Works}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-" shall mean any work, whether in Source or Object form, that is based on (or deri
-ved from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separ
-able from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Contribution}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 
-" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivati
-ve Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "}{\rtlch\fcs1 
-\ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 submitted}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " means
- any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or 
-on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 
-\b\f0\fs24\insrsid1143598\charrsid1143598 Not a Contribution.}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 "
-\par "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 Contributor}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " shall mean Licensor an
-d any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart copyright}2. Grant of Copyright License}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend copyright}
-. Subject to the terms and conditions of this License, each Contributor hereby grant
-s to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source o
-r Object form.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart patent}3. Grant of Patent License}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend patent}
-. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent licen
-se to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination
- 
-of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within t
-he Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart redistribution}4. Redistribution}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend redistribution}
-. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
-\par {\listtext\pard\plain\ltrpar \rtlch\fcs1 \af0 \ltrch\fcs0 \dbch\af0\insrsid1143598\charrsid1143598 \hich\af0\dbch\af0\loch\f0 1.\tab}}\pard \ltrpar\qj \fi-360\li730\ri0\sb100\sa100\sbauto1\saauto1\widctlpar
-\jclisttab\tx720\wrapdefault\aspalpha\aspnum\faauto\ls1\adjustright\rin0\lin730\itap0\pararsid1143598 {\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 You must give any other recipients of the Work or Derivativ
-e Works a copy of this License; and
-\par {\listtext\pard\plain\ltrpar \rtlch\fcs1 \af0 \ltrch\fcs0 \dbch\af0\insrsid1143598\charrsid1143598 \hich\af0\dbch\af0\loch\f0 2.\tab}You must cause any modified files to carry prominent notices stating that You changed the files; and
-\par {\listtext\pard\plain\ltrpar \rtlch\fcs1 \af0 \ltrch\fcs0 \dbch\af0\insrsid1143598\charrsid1143598 \hich\af0\dbch\af0\loch\f0 3.\tab}You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and 
-attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
-\par {\listtext\pard\plain\ltrpar \rtlch\fcs1 \af0 \ltrch\fcs0 \dbch\af0\insrsid1143598\charrsid1143598 \hich\af0\dbch\af0\loch\f0 4.\tab}If the Work includes a "}{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 NOTICE}{
-\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 " text file as part of its distribution, then any Derivative Works that You distribute 
-must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as p
-a
-rt of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE 
-f
-ile are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attrib
-ution notices cannot be construed as modifying the License. \line \line 
-You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, o
-r for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 
-\par }\pard \ltrpar\qj \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid1143598 {\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart contributions}
-5. Submission of Contributions}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend contributions}. Unless You explicitly state otherwise, any Contributio
-n intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms o
-f any separate license agreement you may have executed with Licensor regarding such Contributions.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart trademarks}6. Trademarks}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend trademarks}
-. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required 
-for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart no-warranty}7. Disclaimer of Warranty}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend no-warranty}
-. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor pro
-vides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PUR
-POSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart no-liability}8. Limitation of Liability}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend no-liability}
-. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect,
- 
-special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, o
-r any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-\par }{\rtlch\fcs1 \ab\af0\afs24 \ltrch\fcs0 \b\f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkstart additional}9. Accepting Warranty or Additional Liability}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \f0\fs24\insrsid1143598\charrsid1143598 {\*\bkmkend additional}
-. While redistributing the Work or Derivative Works thereof, You may choose to offe
-r, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, no
-t
- on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional lia
-bility.
-\par END OF TERMS AND CONDITIONS
-\par }\pard \ltrpar\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 {\rtlch\fcs1 \af31507 \ltrch\fcs0 \insrsid2098402 
-\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a
-9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad
-5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6
-b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0
-0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6
-a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f
-c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512
-0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462
-a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865
-6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b
-4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b
-4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b6f4679893070000c9200000160000007468656d652f7468656d652f
-7468656d65312e786d6cec59cd8b1bc915bf07f23f347d97f5d5ad8fc1f2a24fcfda33b6b164873dd648a5eef2547789aad28cc56208de532e81c026e49085bd
-ed21842cecc22eb9e48f31d8249b3f22afaa5bdd5552c99e191c3061463074977eefd5afde7bf5de53d5ddcf5e26d4bbc05c1096f6fcfa9d9aefe174ce16248d
-7afeb3d9a4d2f13d2151ba4094a5b8e76fb0f03fbbf7eb5fdd454732c609f6403e1547a8e7c752ae8eaa5531876124eeb0154ee1bb25e30992f0caa3ea82a34b
-d09bd06aa3566b55134452df4b51026a1f2f97648ebd9952e9dfdb2a1f53784da5500373caa74a35b6243476715e5708b11143cabd0b447b3eccb3609733fc52
-fa1e4542c2173dbfa6fffceabdbb5574940b517940d6909be8bf5c2e17589c37f49c3c3a2b260d823068f50bfd1a40e53e6edc1eb7c6ad429f06a0f91c569a71
-b175b61bc320c71aa0ecd1a17bd41e35eb16ded0dfdce3dc0fd5c7c26b50a63fd8c34f2643b0a285d7a00c1feee1c3417730b2f56b50866fede1dbb5fe28685b
-fa3528a6243ddf43d7c25673b85d6d0159327aec8477c360d26ee4ca4b144443115d6a8a254be5a1584bd00bc6270050408a24493db959e1259a43140f112567
-9c7827248a21f056286502866b8ddaa4d684ffea13e827ed5174849121ad780113b137a4f87862cec94af6fc07a0d537206f7ffef9cdeb1fdfbcfee9cd575fbd
-79fdf77c6eadca923b466964cafdf2dd1ffef3cd6fbd7ffff0ed2f5fff319b7a172f4cfcbbbffdeedd3ffef93ef5b0e2d2146ffff4fdbb1fbf7ffbe7dfffebaf
-5f3bb4f7393a33e1339260e13dc297de5396c0021dfcf119bf9ec42c46c494e8a791402952b338f48f656ca11f6d10450edc00db767cce21d5b880f7d72f2cc2
-d398af2571687c182716f094313a60dc6985876a2ec3ccb3751ab927e76b13f714a10bd7dc43945a5e1eaf579063894be530c616cd2714a5124538c5d253dfb1
-738c1dabfb8210cbaea764ce99604be97d41bc01224e93ccc899154da5d03149c02f1b1741f0b7659bd3e7de8051d7aa47f8c246c2de40d4417e86a965c6fb68
-2d51e252394309350d7e8264ec2239ddf0b9891b0b099e8e3065de78818570c93ce6b05ec3e90f21cdb8dd7e4a37898de4929cbb749e20c64ce4889d0f6394ac
-5cd829496313fbb938871045de13265df05366ef10f50e7e40e941773f27d872f787b3c133c8b026a53240d4376beef0e57dccacf89d6ee8126157aae9f3c44a
-b17d4e9cd131584756689f604cd1255a60ec3dfbdcc160c05696cd4bd20f62c82ac7d815580f901dabea3dc5027a25d5dcece7c91322ac909de2881de073bad9
-493c1b9426881fd2fc08bc6eda7c0ca52e7105c0633a3f37818f08f480102f4ea33c16a0c308ee835a9fc4c82a60ea5db8e375c32dff5d658fc1be7c61d1b8c2
-be04197c6d1948eca6cc7b6d3343d49aa00c9819822ec3956e41c4727f29a28aab165b3be596f6a62ddd00dd91d5f42424fd6007b4d3fb84ffbbde073a8cb77f
-f9c6b10f3e4ebfe3566c25ab6b763a8792c9f14e7f7308b7dbd50c195f904fbfa919a175fa04431dd9cf58b73dcd6d4fe3ffdff73487f6f36d2773a8dfb8ed64
-7ce8306e3b99fc70e5e3743265f3027d8d3af0c80e7af4b14f72f0d46749289dca0dc527421ffc08f83db398c0a092d3279eb838055cc5f0a8ca1c4c60e1228e
-b48cc799fc0d91f134462b381daafb4a492472d591f0564cc0a1911e76ea5678ba4e4ed9223becacd7d5c16656590592e5782d2cc6e1a04a66e856bb3cc02bd4
-6bb6913e68dd1250b2d721614c6693683a48b4b783ca48fa58178ce620a157f65158741d2c3a4afdd6557b2c805ae115f8c1edc1cff49e1f06200242701e07cd
-f942f92973f5d6bbda991fd3d3878c69450034d8db08283ddd555c0f2e4fad2e0bb52b78da2261849b4d425b46377822869fc17974aad1abd0b8aeafbba54b2d
-7aca147a3e08ad9246bbf33e1637f535c8ede6069a9a9982a6de65cf6f35430899395af5fc251c1ac363b282d811ea3717a211dcbccc25cf36fc4d32cb8a0b39
-4222ce0cae934e960d122231f728497abe5a7ee1069aea1ca2b9d51b90103e59725d482b9f1a3970baed64bc5ce2b934dd6e8c284b67af90e1b35ce1fc568bdf
-1cac24d91adc3d8d1797de195df3a708422c6cd795011744c0dd413db3e682c0655891c8caf8db294c79da356fa3740c65e388ae62945714339967709dca0b3a
-faadb081f196af190c6a98242f8467912ab0a651ad6a5a548d8cc3c1aafb6121653923699635d3ca2aaa6abab39835c3b60cecd8f26645de60b53531e434b3c2
-67a97b37e576b7b96ea74f28aa0418bcb09fa3ea5ea12018d4cac92c6a8af17e1a56393b1fb56bc776811fa07695226164fdd656ed8edd8a1ae19c0e066f54f9
-416e376a6168b9ed2bb5a5f5adb979b1cdce5e40f2184197bba6526857c2c92e47d0104d754f92a50dd8222f65be35e0c95b73d2f3bfac85fd60d80887955a27
-1c57826650ab74c27eb3d20fc3667d1cd66ba341e31514161927f530bbb19fc00506dde4f7f67a7cefee3ed9ded1dc99b3a4caf4dd7c5513d777f7f5c6e1bb7b
-8f40d2f9b2d598749bdd41abd26df627956034e854bac3d6a0326a0ddba3c9681876ba9357be77a1c141bf390c5ae34ea5551f0e2b41aba6e877ba9576d068f4
-8376bf330efaaff23606569ea58fdc16605ecdebde7f010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d65
-2f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d36
-3f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e
-3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d985
-0528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c020000130000000000000000000000
-0000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000
-000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c0000000000000000000000000019020000
-7468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100b6f4679893070000c92000001600000000000000
-000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b01000027000000
-000000000000000000009d0a00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000980b00000000}
-{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d
-617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169
-6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363
-656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}
-{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1;
-\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;
-\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;
-\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;
-\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;
-\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;
-\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;
-\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;
-\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;
-\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;
-\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;
-\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;
-\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;
-\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;
-\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;
-\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;
-\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;
-\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;
-\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;
-\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;
-\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;
-\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;
-\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5;
-\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5;
-\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;
-\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6;
-\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;
-\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;
-\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis;
-\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography;
-\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4;
-\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4;
-\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1;
-\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1;
-\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2;
-\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2;
-\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3;
-\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4;
-\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4;
-\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5;
-\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5;
-\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6;
-\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6;
-\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark;
-\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1;
-\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1;
-\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2;
-\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3;
-\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3;
-\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4;
-\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4;
-\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5;
-\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
-\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
-\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
-\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000
-02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000
-d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e50000000000000000000000002009
-145ebddcd701feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000
-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000
-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000105000000000000}}
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
index a03a70a4..897d4c48 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,12 +4,13 @@
 
 | Version | Supported          |
 | ------- | ------------------ |
+| 3.x.y   | :white_check_mark: |
 | 2.x.y   | :white_check_mark: |
 | 1.x.y   | :x:                |
 | 0.x.y   | :x:                |
 
 ## Reporting a Vulnerability
 
-Reporting sensitive information may be done by emailing pr@pufferpanel.com.
+Reporting sensitive information may be done by submitting a request on https://github.com/pufferpanel/pufferpanel/security/advisories?state=published
 
 Reports which do not contain a valid subject or desciption will be ignored.
diff --git a/client/api/src/templates.js b/client/api/src/templates.js
index b26dfc9a..931bc0ad 100644
--- a/client/api/src/templates.js
+++ b/client/api/src/templates.js
@@ -19,7 +19,16 @@ export class TemplateApi {
     const res = []
     const repos = await this.listRepos()
     await Promise.all(repos.map(async repo => {
-      res.push({ name: repo.name, id: repo.id, templates: await this.listRepoTemplates(repo.id) })
+      let templates = undefined
+      let error = undefined
+      try {
+        templates = await this.listRepoTemplates(repo.id)
+      } catch (e) {
+        error = e
+        if (error.response) error = error.response
+        if (error.error) error = error.error
+      }
+      res.push({ ...repo, templates, error })
     }))
     return res
   }
@@ -54,8 +63,8 @@ export class TemplateApi {
     return res.data
   }
 
-  async saveRepo(repo, config) {
-    await this._api.put(`/api/templates/${repo}`, config)
+  async addRepo(repo) {
+    await this._api.post('/api/templates/', repo)
     return true
   }
 
diff --git a/client/frontend/src/components/server/Admin.vue b/client/frontend/src/components/server/Admin.vue
index 23d602a2..d18e803a 100644
--- a/client/frontend/src/components/server/Admin.vue
+++ b/client/frontend/src/components/server/Admin.vue
@@ -98,7 +98,6 @@ onMounted(async () => {
           <hooks v-model="edit" />
         </tab>
         <tab id="environment" :title="t('templates.Environment')" icon="environment" hotkey="t e">
-          <div class="warning" v-text="t('servers.EnvironmentEditHint')" />
           <server-environment v-model="edit" />
         </tab>
         <tab id="json" :title="t('templates.Json')" icon="json" hotkey="t j">
diff --git a/client/frontend/src/components/server/Console.vue b/client/frontend/src/components/server/Console.vue
index c1ed6c9c..68915a41 100644
--- a/client/frontend/src/components/server/Console.vue
+++ b/client/frontend/src/components/server/Console.vue
@@ -74,10 +74,58 @@ function clearConsole() {
   if (console.value) console.value.replaceChildren([])
 }
 
+const history = ref([])
+const historyIndex = ref(-1)
+const temporaryCommand = ref('')
+
 function sendCommand() {
+  if (historyIndex.value !== -1) {
+    history.value.splice(historyIndex.value, 1)
+  }
+
+  if (history.value.length === 0 || history.value[history.value.length - 1] !== command.value) {
+    history.value.push(command.value)
+  }
+
+  historyIndex.value = -1
+  temporaryCommand.value = ''
+
+  if (history.value.length > 100) {
+    history.value.splice(0, 1)
+  }
+
   props.server.sendCommand(command.value)
   command.value = ''
 }
+
+function previousCommand() {
+  if (historyIndex.value === -1 && history.value.length > 0) {
+    historyIndex.value = history.value.length - 1
+    temporaryCommand.value = command.value
+  } else if (historyIndex.value > 0) {
+    historyIndex.value--
+  } else {
+    return
+  }
+
+  command.value = history.value[historyIndex.value]
+}
+
+function nextCommand() {
+  if (historyIndex.value === -1) {
+    return
+  }
+
+  historyIndex.value++
+
+  if (historyIndex.value >= history.value.length) {
+    historyIndex.value = -1
+    command.value = temporaryCommand.value
+  } else {
+    command.value = history.value[historyIndex.value]
+  }
+}
+
 </script>
 
 <template>
@@ -88,7 +136,8 @@ function sendCommand() {
       <div ref="console" class="console" />
     </div>
     <div v-if="server.hasScope('server.console.send')" dir="ltr" class="command">
-      <text-field v-model="command" v-hotkey="'c c'" :label="t('servers.Command')" @keyup.enter="sendCommand()" />
+      <text-field v-model="command" v-hotkey="'c c'" :label="t('servers.Command')" @keyup.enter="sendCommand()"
+        @keydown.up.prevent="previousCommand()" @keydown.down.prevent="nextCommand()" />
       <icon name="send" @click="sendCommand()" />
     </div>
   </div>
diff --git a/client/frontend/src/components/server/Files.vue b/client/frontend/src/components/server/Files.vue
index bd7c4bcc..f7c33cd6 100644
--- a/client/frontend/src/components/server/Files.vue
+++ b/client/frontend/src/components/server/Files.vue
@@ -11,6 +11,7 @@ import TextField from '@/components/ui/TextField.vue'
 
 const { t } = useI18n()
 const events = inject('events')
+const toast = inject('toast')
 
 const props = defineProps({
   server: { type: Object, required: true }
@@ -96,10 +97,13 @@ async function openFile(f, overrideWarn = false) {
   }
 }
 
-async function saveFile() {
+async function saveFile({close}) {
   await props.server.uploadFile(`${getCurrentPath()}/${file.value.name}`, file.value.content)
-  editorOpen.value = false
-  file.value = null
+  toast.success(t('files.Saved'))
+  if (close) {
+    editorOpen.value = false
+    file.value = null
+  }
   refresh()
 }
 
@@ -313,7 +317,7 @@ function trackFileEl(index) {
       <loader />
     </overlay>
     <overlay v-model="editorOpen" class="editor">
-      <editor v-if="file" v-model="file" :read-only="!canEdit" @save="saveFile()" @close="editorOpen = false" />
+      <editor v-if="file" v-model="file" :read-only="!canEdit" @save="saveFile($event)" @close="editorOpen = false" />
     </overlay>
   </div>
 </template>
diff --git a/client/frontend/src/components/server/files/Editor.vue b/client/frontend/src/components/server/files/Editor.vue
index 5cd8e8ea..c7e59490 100644
--- a/client/frontend/src/components/server/files/Editor.vue
+++ b/client/frontend/src/components/server/files/Editor.vue
@@ -59,7 +59,7 @@ export default {
   <div>
     <div class="overlay-header">
       <h1 class="title" v-text="modelValue.name" />
-      <btn v-if="!readOnly && !((extensions[modelValue.extension] || {}).disableSave)" variant="text" @click="emit('save')"><icon name="save" /> {{ t('common.Save') }}</btn>
+      <btn v-if="!readOnly && !((extensions[modelValue.extension] || {}).disableSave)" variant="text" @click="emit('save', {close: true})"><icon name="save" /> {{ t('common.Save') }}</btn>
       <btn v-hotkey="'Escape'" variant="icon" @click="emit('close')"><icon name="close" /></btn>
     </div>
     <img v-if="getType(modelValue) === 'image'" class="file-viewer" :src="modelValue.url" />
@@ -71,6 +71,6 @@ export default {
       <source :src="modelValue.url" />
       <div class="warning unsupported" v-text="t('errors.AudioUnsupported')" />
     </audio>
-    <ace v-else id="file-editor" :read-only="readOnly" :model-value="modelValue.content" class="file-editor" :file="modelValue.name" theme="monokai" @update:modelValue="emitUpdate" />
+    <ace v-else id="file-editor" :read-only="readOnly" :model-value="modelValue.content" class="file-editor" :file="modelValue.name" theme="monokai" @update:modelValue="emitUpdate" @save="emit('save', {close: false})" />
   </div>
 </template>
diff --git a/client/frontend/src/components/ui/Ace.vue b/client/frontend/src/components/ui/Ace.vue
index 6f5785d6..6907f837 100644
--- a/client/frontend/src/components/ui/Ace.vue
+++ b/client/frontend/src/components/ui/Ace.vue
@@ -9,7 +9,7 @@ const props = defineProps({
   modelValue: { type: String, required: true }
 })
 
-const emit = defineEmits(['update:modelValue'])
+const emit = defineEmits(['update:modelValue', 'save'])
 
 const el = ref(null)
 
@@ -76,6 +76,13 @@ function init() {
         editor.blur()
       }
     })
+    editor.commands.addCommand({
+      name: 'save',
+      bindKey: { win: 'Ctrl-S', mac: 'Cmd-S' },
+      exec: () => {
+        emit('save')
+      }
+    })
   })
 }
 
diff --git a/client/frontend/src/components/ui/EnvironmentConfig.vue b/client/frontend/src/components/ui/EnvironmentConfig.vue
index c7e5ad8e..a85284be 100644
--- a/client/frontend/src/components/ui/EnvironmentConfig.vue
+++ b/client/frontend/src/components/ui/EnvironmentConfig.vue
@@ -1,6 +1,21 @@
 <script>
 const fields = {
-  host: [],
+  host: [
+    {
+      name: 'disableUnshare',
+      type: 'boolean',
+      label: 'env.host.DisableUnshare',
+      hint: 'env.host.DisableUnshareHint',
+      default: false
+    },
+    {
+      name: 'mounts',
+      type: 'list',
+      label: 'env.host.Mounts',
+      hint: 'env.host.MountsHint',
+      default: []
+    }
+  ],
   docker: [
     {
       name: 'image',
@@ -47,9 +62,11 @@ const fields = {
 import { onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import KeyValueInput from '@/components/ui/KeyValueInput.vue'
+import ListInput from '@/components/ui/ListInput.vue'
 import PortMappingInput from '@/components/ui/PortMappingInput.vue'
 import Suggestion from '@/components/ui/Suggestion.vue'
 import TextField from '@/components/ui/TextField.vue'
+import Toggle from '@/components/ui/Toggle.vue'
 
 const props = defineProps({
   noFieldsMessage: { type: String, default: () => undefined },
@@ -95,6 +112,8 @@ function getLabel(field) {
       <port-mapping-input v-else-if="field.type === 'portBindings'" :model-value="modelValue[field.name] || field.default" :label="getLabel(field)" :hint="field.hint ? t(field.hint) : undefined" @update:modelValue="onInput(field.name, $event)" />
       <suggestion v-else-if="field.type === 'text' && field.options" :model-value="modelValue[field.name] || field.default" :label="getLabel(field)" :options="field.options" :hint="field.hint ? t(field.hint) : undefined" @update:modelValue="onInput(field.name, $event)" />
       <text-field v-else-if="field.type === 'text'" :model-value="modelValue[field.name] || field.default" :label="getLabel(field)" :hint="field.hint ? t(field.hint) : undefined" @update:modelValue="onInput(field.name, $event)" />
+      <toggle v-else-if="field.type === 'boolean'" :model-value="modelValue[field.name]" :label="getLabel(field)" :hint="field.hint ? t(field.hint) : undefined" @update:modelValue="onInput(field.name, $event)" />
+      <list-input v-else-if="field.type === 'list'" :model-value="modelValue[field.name]" :label="getLabel(field)" :hint="field.hint ? t(field.hint) : undefined" @update:modelValue="onInput(field.name, $event)" />
       <span v-else v-text="`${field.type} not yet implemented`" />
     </div>
   </div>
diff --git a/client/frontend/src/components/ui/OtpInput.vue b/client/frontend/src/components/ui/OtpInput.vue
index 32330f7e..0481173e 100644
--- a/client/frontend/src/components/ui/OtpInput.vue
+++ b/client/frontend/src/components/ui/OtpInput.vue
@@ -10,10 +10,22 @@ const emit = defineEmits(['update:modelValue', 'complete'])
 const inputs = ref([])
 
 function onInput(n, e) {
-  if (e.data === null) {
+  if (!e.data && e.target.value.length === props.digits) {
+    // data isn't set but there's a full code in the input when a password manager directly sets field value
+    [...e.target.value].map((c, i) => {
+      if (inputs.value[i]) inputs.value[i].value = c
+    })
+    if (inputs.value[n]) inputs.value[n].blur()
+    emit('update:modelValue', inputs.value.map(e => e.value).join(''))
+    emit('complete', inputs.value.map(e => e.value).join(''))
+    return
+  } else if (e.data === null) {
     // data is null when deleting chars
     emit('update:modelValue', inputs.value.map(e => e.value).join(''))
     return
+  } else if (e.data === undefined && e.target.value.length === 1) {
+    // likely a password manager detecting multiple fields and direct settings each field to a single digit of the code
+    return
   }
 
   if (e.data.length === props.digits && /^\d+$/.test(e.data)) {
@@ -65,10 +77,40 @@ onMounted(() => {
 function pushInput(input) {
   if (inputs.value.indexOf(input) === -1) inputs.value.push(input)
 }
+
+function onBackspace(n, e) {
+  if (e.target.value === '' && inputs.value[n - 1]) {
+    inputs.value[n - 1].value = ''
+    inputs.value[n - 1].focus()
+  }
+}
+
+function clearAll() {
+  inputs.value.map(e => e.value = '')
+  if (inputs.value[0]) inputs.value[0].focus()
+}
 </script>
 
 <template>
   <div class="otp-input">
-    <input v-for="n in Array(props.digits).keys()" :key="n" :ref="pushInput" :disabled="props.disabled" type="text" @input="onInput(n, $event)" />
+    <input
+      v-for="n in Array(props.digits).keys()"
+      :key="n"
+      :ref="pushInput"
+      :name="n === 0 ? 'totp' : 'ingnore'"
+      :autofill="n === 0 ? 'one-time-code' : 'none'"
+      :disabled="props.disabled"
+      type="text"
+      @input="onInput(n, $event)"
+      @keydown.exact.backspace="onBackspace(n, $event)"
+      @keydown.ctrl.backspace="clearAll"
+    />
+    <!--
+      apparently proton pass tries to handle multi field totp inputs on its end, however it sadly seems to have
+      a heart attach mid-attempt and just refuses to fill in the last digit of the code, adding another input
+      seems to confuse it into just throwing the full code into the first input though, which we handle for
+      other password managers as well, that's why this invisible to humans input exists
+    -->
+    <input type="text" name="fix_proton_pass" autofill="none" style="position:absolute;left:-99999px;width:1px" />
   </div>
 </template>
diff --git a/client/frontend/src/components/ui/serverCreateSteps/SelectTemplate.vue b/client/frontend/src/components/ui/serverCreateSteps/SelectTemplate.vue
index cf1e4da1..25e4dd16 100644
--- a/client/frontend/src/components/ui/serverCreateSteps/SelectTemplate.vue
+++ b/client/frontend/src/components/ui/serverCreateSteps/SelectTemplate.vue
@@ -10,6 +10,7 @@ const { t } = useI18n()
 const api = inject('api')
 const emit = defineEmits(['selected', 'back'])
 const templatesByRepo = ref([])
+const incompatibleTemplates = ref([])
 const showing = ref(false)
 const currentTemplate = ref({})
 
@@ -40,7 +41,8 @@ function templateArchMatches(template) {
 
 async function load() {
   const repos = await api.template.listAllTemplates()
-  const res = []
+  const compatible = []
+  const incompatible = []
   Object.keys(repos).sort((a, b) => repos[a].id > repos[b].id).map(repo => {
     if (repos[repo].templates.length === 0) return
     const templates = repos[repo].templates.filter(template => {
@@ -48,9 +50,19 @@ async function load() {
         templateOsMatches(template) &&
         templateArchMatches(template)
     })
-    res.push({ ...repos[repo], templates })
+    if (templates.length !== 0) compatible.push({ ...repos[repo], templates })
+    if (templates.length !== repos[repo].templates.length) {
+      incompatible.push({
+        ...repos[repo],
+        templates: undefined,
+        arch: repos[repo].templates.filter(t => !templateArchMatches(t)),
+        os: repos[repo].templates.filter(t => !templateOsMatches(t)),
+        env: repos[repo].templates.filter(t => !templateEnvMatches(t))
+      })
+    }
   })
-  templatesByRepo.value = res
+  templatesByRepo.value = compatible
+  incompatibleTemplates.value = incompatible
 }
 
 onMounted(async () => {
@@ -82,6 +94,29 @@ function choice(confirm) {
         <span class="title" v-text="template.display" />
       </div>
     </div>
+    <h2 v-if="incompatibleTemplates.length > 0" class="incompatible-title" v-text="t('servers.IncompatibleTemplates')" />
+    <div v-if="incompatibleTemplates.length > 0" class="incompatible-desc" v-text="t('servers.IncompatibleTemplatesDescription')" />
+    <div v-for="repo in incompatibleTemplates" :key="repo.id" class="list incompatible-list">
+      <h3 class="list-header" v-text="repo.name" />
+      <div v-if="repo.arch.length > 0" class="list">
+        <h4 class="list-header" v-text="t('servers.IncompatibleArch', {arch})" />
+        <div v-for="template in repo.arch" :key="template.name" class="list-item template disabled">
+          <span class="title" v-text="template.display" />
+        </div>
+      </div>
+      <div v-if="repo.os.length > 0" class="list">
+        <h4 class="list-header" v-text="t('servers.IncompatibleOs', {os})" />
+        <div v-for="template in repo.os" :key="template.name" class="list-item template disabled">
+          <span class="title" v-text="template.display" />
+        </div>
+      </div>
+      <div v-if="repo.env.length > 0" class="list">
+        <h4 class="list-header" v-text="t('servers.IncompatibleEnv', {env})" />
+        <div v-for="template in repo.env" :key="template.name" class="list-item template disabled">
+          <span class="title" v-text="template.display" />
+        </div>
+      </div>
+    </div>
     <btn color="error" @click="emit('back')"><icon name="back" />{{ t('common.Back') }}</btn>
 
     <overlay v-model="showing" :title="currentTemplate.display" closable>
diff --git a/client/frontend/src/lang/en_US/env.json b/client/frontend/src/lang/en_US/env.json
index 25f8f3c7..78306ade 100644
--- a/client/frontend/src/lang/en_US/env.json
+++ b/client/frontend/src/lang/en_US/env.json
@@ -15,6 +15,10 @@
     "containerRoot": "Container Root"
   },
   "host": {
-    "name": "Host"
+    "name": "Host",
+    "DisableUnshare": "Disable Unshare",
+    "DisableUnshareHint": "Unshare attempts to isolate servers to prevent them from touching other servers, PufferPanels or the host systems files. Disabling this is a security risk unless all entities interacting with this server are fully trusted.",
+    "Mounts": "Additional Mounts",
+    "MountsHint": "Grant this server access to specific files and directories on the host system without disabling Unshare completely. The server directory and necessary programs and files are always included."
   }
 }
diff --git a/client/frontend/src/lang/en_US/files.json b/client/frontend/src/lang/en_US/files.json
index 6402c919..bdb6700e 100644
--- a/client/frontend/src/lang/en_US/files.json
+++ b/client/frontend/src/lang/en_US/files.json
@@ -19,5 +19,6 @@
   "Current": "Current",
   "Total": "Total",
   "OpenLargeFile": "This file may cause performance issues for your browser due to its size, do you want to open it anyways?",
-  "OpenAnyways": "Open anyways"
+  "OpenAnyways": "Open anyways",
+  "Saved": "File saved"
 }
diff --git a/client/frontend/src/lang/en_US/hotkeys.json b/client/frontend/src/lang/en_US/hotkeys.json
index 6ce11398..e785d875 100644
--- a/client/frontend/src/lang/en_US/hotkeys.json
+++ b/client/frontend/src/lang/en_US/hotkeys.json
@@ -7,7 +7,8 @@
     "g n": "Go to Nodes",
     "g s": "Go to Servers",
     "g t": "Go to Templates",
-    "g u": "Go to Users"
+    "g u": "Go to Users",
+    "/": "Global Search"
   },
   "Self": {
     "Title": "Account",
diff --git a/client/frontend/src/lang/en_US/servers.json b/client/frontend/src/lang/en_US/servers.json
index 234e73f5..c7ad31e8 100644
--- a/client/frontend/src/lang/en_US/servers.json
+++ b/client/frontend/src/lang/en_US/servers.json
@@ -50,5 +50,10 @@
     "autoStart": "Start the server when the node starts",
     "autoRestartOnGraceful": "Restart the server when it stops normally",
     "autoRestartOnCrash": "Restart the server when it crashes"
-  }
+  },
+  "IncompatibleTemplates": "Incompatible templates",
+  "IncompatibleTemplatesDescription": "These templates are incompatible with at least one of your previous selections",
+  "IncompatibleArch": "Incompatible with architecture '{arch}'",
+  "IncompatibleOs": "Incompatible with OS '{os}'",
+  "IncompatibleEnv": "Incompatible with environment '{env}'"
 }
diff --git a/client/frontend/src/lang/en_US/templates.json b/client/frontend/src/lang/en_US/templates.json
index b2967eca..0c7125da 100644
--- a/client/frontend/src/lang/en_US/templates.json
+++ b/client/frontend/src/lang/en_US/templates.json
@@ -38,6 +38,12 @@
   "CommandConditionHint": "Use this command only if this condition is met",
   "AddCommand": "Add Command",
   "AddEnvironment": "Add Environment",
+  "AddRepo": "Add Template Repository",
+  "RepoName": "Repository Name",
+  "RepoUrl": "Repository URL",
+  "RepoBranch": "Repository Branch",
+  "ConfirmDeleteRepo": "Are you sure you want to delete the repository '{name}'?",
+  "DeleteRepo": "Delete Repository",
   "signals": {
     "1": "SIGHUP",
     "2": "SIGINT (CTRL+C)",
diff --git a/client/frontend/src/themes/default/components/create-link.scss b/client/frontend/src/themes/default/components/create-link.scss
index 7c2b0345..f993baa8 100644
--- a/client/frontend/src/themes/default/components/create-link.scss
+++ b/client/frontend/src/themes/default/components/create-link.scss
@@ -12,6 +12,16 @@
   }
 }
 
+.templatelist .repo.createLink {
+  display: block;
+  padding: .75em;
+  margin-top: 3rem;
+  text-align: center;
+  .icon::before {
+    margin-right: .5em;
+  }
+}
+
 html[dir="rtl"] .createLink .icon {
   margin-left: 1.5em;
   margin-right: unset;
diff --git a/client/frontend/src/themes/default/components/otp-input.scss b/client/frontend/src/themes/default/components/otp-input.scss
index 9e91b32e..ad804056 100644
--- a/client/frontend/src/themes/default/components/otp-input.scss
+++ b/client/frontend/src/themes/default/components/otp-input.scss
@@ -6,6 +6,7 @@
   margin-left: auto;
   margin-right: auto;
   input {
+    text-align: center;
     background-color: transparent;
     box-sizing: border-box;
     padding: var(--text-field-base-size, 1rem) var(--text-field-base-size, 1rem);
diff --git a/client/frontend/src/themes/default/theme.scss b/client/frontend/src/themes/default/theme.scss
index f4ca7727..cd6e38d0 100644
--- a/client/frontend/src/themes/default/theme.scss
+++ b/client/frontend/src/themes/default/theme.scss
@@ -179,7 +179,8 @@ hr {
 }
 
 .hotkey {
-  margin-top: 1em;
+  margin-top: .5em;
+  display: flex;
   .key {
     border: 1px solid var(--text-disabled);
     border-radius: 4px;
@@ -192,13 +193,10 @@ hr {
     align-items: center;
     background-color: var(--backdrop);
     box-sizing: border-box;
-    &:last-child {
-      margin-right: 0;
-    }
   }
   .keys {
     display: inline-block;
-    width: 8em;
+    min-width: 5em;
   }
   .description {
     height: 1.5em;
@@ -337,18 +335,6 @@ hr {
       @include cols(12);
       @include cols(6, md);
     }
-    .switch-wrapper {
-      @include cols(11);
-      @include cols(5, md);
-      @include breakpoint(md) {
-        display: flex;
-        justify-self: end;
-        .label {
-          align-self: center;
-          padding-top: 8px;
-        }
-      }
-    }
     .button.icon {
       @include cols(1);
     }
@@ -1139,10 +1125,18 @@ html[dir="rtl"] .settings .panel .master-url .icon {
   .select-template {
     @include row;
 
-    h2, h3 {
+    h2, h3, .incompatible-desc {
       @include cols(12);
     }
 
+    .incompatible-title {
+      margin-bottom: 0;
+    }
+
+    .incompatible-title ~ .list .list-header {
+      margin-top: 0;
+    }
+
     .list {
       @include cols(12);
       width: 100%;
@@ -1152,9 +1146,16 @@ html[dir="rtl"] .settings .panel .master-url .icon {
       @include cols(12);
       @include cols(4, md);
       @include card;
+      margin-top: 0;
+    }
+
+    .template:not(.disabled) {
       @include clickable;
       @include link;
-      margin-top: 0;
+    }
+
+    .template.disabled {
+      background-color: var(--backdrop);
     }
 
     .overlay .actions {
@@ -1218,6 +1219,17 @@ html[dir="rtl"] .settings .panel .master-url .icon {
   @include card;
 }
 
+.templatelist .template-repo-header {
+  display: flex;
+  align-items: center;
+  .name {
+    flex-grow: 1;
+  }
+  .button.icon {
+    font-size: 1.25em;
+  }
+}
+
 .templatelist .template {
   display: flex;
   flex-flow: row wrap;
diff --git a/client/frontend/src/utils/consoleWorker.js b/client/frontend/src/utils/consoleWorker.js
index adc3673f..8cd12f85 100644
--- a/client/frontend/src/utils/consoleWorker.js
+++ b/client/frontend/src/utils/consoleWorker.js
@@ -68,25 +68,27 @@ onmessage = function (e) {
 
   const updates = []
 
-  let last = null
-  newLines.map(line => {
+  newLines.map((line, i) => {
     line = ansiup.ansi_to_html(line)
     if (lastIncompleteLine) line = lastIncompleteLine.line + line
 
     if (lastIncompleteLine) {
       updates.push({ op: 'update', content: handleLine(line, e.data.panelName) })
-      last = { line }
+      if ((newLines.length - 1) === i && !endOnNewline) {
+        // this is the last line and it's not complete
+        lastIncompleteLine = { line }
+      } else {
+        // not the last line, must be complete
+        lastIncompleteLine = null
+      }
     } else {
       updates.push({ op: 'append', content: handleLine(line, e.data.panelName) })
-      last = { line }
+      if ((newLines.length - 1) === i && !endOnNewline) {
+        // this is the last line and it's not complete
+        lastIncompleteLine = { line }
+      }
     }
   })
 
   postMessage(updates)
-
-  if (!endOnNewline) {
-    lastIncompleteLine = last
-  } else {
-    lastIncompleteLine = null
-  }
 }
diff --git a/client/frontend/src/views/Login.vue b/client/frontend/src/views/Login.vue
index 456dfd15..b3ce5bac 100644
--- a/client/frontend/src/views/Login.vue
+++ b/client/frontend/src/views/Login.vue
@@ -93,8 +93,8 @@ function passwordErrorMsg() {
   <div class="login">
     <h1 v-text="t('users.Login')" />
     <form @keydown.enter="login()">
-      <text-field v-model="email" type="email" name="email" :disabled="loading" :label="t('users.Email')" :error="emailErrorMsg()" icon="email" autofocus @blur="validateEmail" @change="validateEmail(true)" />
-      <text-field v-model="password" type="password" name="password" :disabled="loading" :label="t('users.Password')" :error="passwordErrorMsg()" icon="lock" @blur="validatePassword" @change="validatePassword(true)" />
+      <text-field v-model="email" type="email" name="email" :disabled="loading || otpNeeded" :label="t('users.Email')" :error="emailErrorMsg()" icon="email" autofocus @blur="validateEmail" @change="validateEmail(true)" />
+      <text-field v-model="password" type="password" name="password" :disabled="loading || otpNeeded" :label="t('users.Password')" :error="passwordErrorMsg()" icon="lock" @blur="validatePassword" @change="validatePassword(true)" />
       <loader v-if="loading" />
       <btn color="primary" :disabled="emailError || passwordError || loading" @click="login()" v-text="t('users.Login')" />
       <btn v-if="$config.registrationEnabled" variant="text" @click="$router.push({ name: 'Register' })" v-text="t('users.RegisterLink')" />
diff --git a/client/frontend/src/views/TemplateList.vue b/client/frontend/src/views/TemplateList.vue
index afc744f3..fe8226ff 100644
--- a/client/frontend/src/views/TemplateList.vue
+++ b/client/frontend/src/views/TemplateList.vue
@@ -2,14 +2,20 @@
 import { ref, inject, onMounted } from 'vue'
 import { RouterLink } from 'vue-router'
 import { useI18n } from 'vue-i18n'
+import Btn from '@/components/ui/Btn.vue'
 import Icon from '@/components/ui/Icon.vue'
 import Loader from '@/components/ui/Loader.vue'
+import Overlay from '@/components/ui/Overlay.vue'
+import TextField from '@/components/ui/TextField.vue'
 
 const { t } = useI18n()
 const api = inject('api')
+const events = inject('events')
 const templatesLoaded = ref(false)
 const templatesByRepo = ref([])
 const firstEntry = ref(null)
+const addingRepo = ref(false)
+const currentRepo = ref({name: '', url: '', branch: ''})
 
 onMounted(async () => {
   loadTemplates()
@@ -29,6 +35,42 @@ function setFirstEntry(ref) {
 function focusList() {
   firstEntry.value.$el.focus()
 }
+
+function removeRepo(repo) {
+  events.emit(
+    'confirm',
+    t('templates.ConfirmDeleteRepo', { name: repo.name }),
+    {
+      text: t('templates.DeleteRepo'),
+      icon: 'remove',
+      color: 'error',
+      action: async () => {
+        await api.template.deleteRepo(repo.id)
+        await loadTemplates()
+      }
+    },
+    {
+      color: 'primary'
+    }
+  )
+}
+
+async function addRepo() {
+  await api.template.addRepo({...currentRepo.value, isLocal: false, id: 2})
+  resetAddRepo()
+  await loadTemplates()
+}
+
+function resetAddRepo() {
+  currentRepo.value = {name: '', url: '', branch: ''}
+  addingRepo.value = false
+}
+
+function canAddRepo() {
+  if (!currentRepo.value.name || currentRepo.value.name === '') return false
+  if (!currentRepo.value.url || currentRepo.value.url === '') return false
+  return true
+}
 </script>
 
 <template>
@@ -36,7 +78,10 @@ function focusList() {
     <h1 v-text="t('templates.Templates')" />
     <div v-hotkey="'l'" @hotkey="focusList()">
       <div v-for="repo in templatesByRepo" :key="repo.id" class="list">
-        <h2 class="list-header" v-text="repo.name" />
+        <h2 class="list-header template-repo-header">
+          <span class="name">{{repo.name}}</span>
+          <btn v-if="!repo.isLocal && $api.auth.hasScope('templates.repo.delete')" class="remove" variant="icon" @click="removeRepo(repo)"><icon name="remove" /></btn>
+        </h2>
         <div v-for="template in repo.templates" :key="template.name" class="list-item">
           <router-link :ref="setFirstEntry" :to="{ name: 'TemplateView', params: { repo: repo.id, id: template.name } }">
             <div class="template">
@@ -45,15 +90,30 @@ function focusList() {
             </div>
           </router-link>
         </div>
-        <div v-if="repo.id === 0 && $api.auth.hasScope('templates.local.edit')" class="list-item">
+        <div v-if="repo.error" class="template-repo-error alert error">
+          {{(repo.error.code === 'ErrGeneric' && repo.error.msg) ? t(repo.error.msg) : t('errors.' + repo.error.code)}}
+        </div>
+        <div v-if="repo.isLocal && $api.auth.hasScope('templates.local.edit')" class="list-item">
           <router-link v-hotkey="'c'" :to="{ name: 'TemplateCreate' }">
             <div class="createLink"><icon name="plus" />{{ t('templates.New') }}</div>
           </router-link>
         </div>
       </div>
-      <div v-if="!templatesLoaded" class="list-item">
+      <div v-if="templatesLoaded">
+        <a v-if="$api.auth.hasScope('templates.repo.create')" class="repo createLink" @click="addingRepo = true"><icon name="plus" /> {{t('templates.AddRepo')}}</a>
+      </div>
+      <div v-else class="list-item">
         <loader small />
       </div>
     </div>
+    <overlay v-model="addingRepo" :title="t('templates.AddRepo')" closable class="server-name" @close="resetAddRepo()">
+      <div class="actions">
+        <text-field v-model="currentRepo.name" :label="t('templates.RepoName')" />
+        <text-field v-model="currentRepo.url" :label="t('templates.RepoUrl')" />
+        <text-field v-model="currentRepo.branch" :label="t('templates.RepoBranch')" />
+        <btn v-hotkey="'Escape'" color="error" @click="resetAddRepo()"><icon name="close" />{{ t('common.Cancel') }}</btn>
+        <btn :disabled="!canAddRepo()" color="primary" @click="addRepo()"><icon name="save" />{{ t('templates.AddRepo') }}</btn>
+      </div>
+    </overlay>
   </div>
 </template>
diff --git a/client/package.json b/client/package.json
index 1164d9e6..e3cd9313 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,8 +1,12 @@
 {
   "private": true,
-  "workspaces": ["api", "frontend"],
+  "workspaces": [
+    "api",
+    "frontend"
+  ],
   "scripts": {
     "build": "yarn workspaces run build",
     "dev": "yarn workspaces run dev"
-  }
+  },
+  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
 }
diff --git a/cmd/db.go b/cmd/db.go
new file mode 100644
index 00000000..c44ac15d
--- /dev/null
+++ b/cmd/db.go
@@ -0,0 +1,12 @@
+package main
+
+import "github.com/spf13/cobra"
+
+var dbCmd = &cobra.Command{
+	Use:   "db",
+	Short: "Runs DB operations",
+}
+
+func init() {
+	dbCmd.AddCommand(dbUpgradeCmd, dbMigrateCmd)
+}
diff --git a/cmd/dbmigrate.go b/cmd/dbmigrate.go
new file mode 100644
index 00000000..09d87a7d
--- /dev/null
+++ b/cmd/dbmigrate.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"github.com/spf13/cobra"
+)
+
+var dbMigrateCmd = &cobra.Command{
+	Use:   "migrate",
+	Short: "Migrates the database to a new backend",
+	//Run:   executeDbMigrateToNewSystem,
+}
+
+/*
+func executeDbMigrateToNewSystem(cmd *cobra.Command, args []string) {
+	survey.AskOne(&survey.OptionAnswer{
+		Message: ""
+	})
+}
+*/
diff --git a/cmd/migrate.go b/cmd/dbupgrade.go
similarity index 71%
rename from cmd/migrate.go
rename to cmd/dbupgrade.go
index 8728db1a..e48dd3b8 100644
--- a/cmd/migrate.go
+++ b/cmd/dbupgrade.go
@@ -3,9 +3,9 @@ package main
 import (
 	"fmt"
 	"github.com/mattn/go-sqlite3"
+	"github.com/pterm/pterm"
 	"github.com/pufferpanel/pufferpanel/v3/config"
 	"github.com/pufferpanel/pufferpanel/v3/database"
-	"github.com/pufferpanel/pufferpanel/v3/logging"
 	"github.com/pufferpanel/pufferpanel/v3/utils"
 	"github.com/spf13/cobra"
 	"io"
@@ -13,19 +13,18 @@ import (
 	"path/filepath"
 )
 
-var migrateCmd = &cobra.Command{
-	Use:    "dbmigrate",
-	Short:  "Runs the database migrations",
-	Run:    executeDbMigrations,
-	Hidden: true,
+var dbUpgradeCmd = &cobra.Command{
+	Use:   "upgrade",
+	Short: "Runs the database upgrades",
+	Run:   executeDbUpgrade,
 }
 
-func executeDbMigrations(cmd *cobra.Command, args []string) {
+func executeDbUpgrade(cmd *cobra.Command, args []string) {
 	var currentFile string
 	var backupFile string
 
 	if !config.PanelEnabled.Value() {
-		logging.Info.Printf("Panel not enabled, skipping migration")
+		pterm.Info.Printfln("Panel not enabled, skipping upgrade")
 		os.Exit(0)
 	}
 
@@ -34,7 +33,7 @@ func executeDbMigrations(cmd *cobra.Command, args []string) {
 		drv := sqlite3.SQLiteDriver{}
 		conn, err := drv.Open(database.GetConnectionString())
 		if err != nil {
-			logging.Error.Printf("error connecting to database: %s", err.Error())
+			pterm.Error.Printfln("error connecting to database: %s", err.Error())
 			os.Exit(1)
 			return
 		}
@@ -56,7 +55,7 @@ func executeDbMigrations(cmd *cobra.Command, args []string) {
 
 		err = copyDatabaseFile(currentFile, backupFile)
 		if err != nil {
-			logging.Error.Printf("error backing up database: %s", err.Error())
+			pterm.Error.Printfln("error backing up database: %s", err.Error())
 			os.Exit(1)
 			return
 		}
@@ -64,16 +63,16 @@ func executeDbMigrations(cmd *cobra.Command, args []string) {
 
 	db, err := database.GetConnection()
 	if err != nil {
-		logging.Error.Printf("error connecting to database: %s", err.Error())
+		pterm.Error.Printfln("error connecting to database: %s", err.Error())
 		rollback(backupFile, currentFile)
 		os.Exit(1)
 		return
 	}
 
-	logging.Info.Printf("Starting database migration")
-	err = database.Migrate(db)
+	pterm.Info.Printfln("Starting database upgrade")
+	err = database.Upgrade(db, true)
 	if err != nil {
-		logging.Error.Printf("error upgrading database: %s", err.Error())
+		pterm.Error.Printfln("Database upgrade failed: %s", err.Error())
 		rollback(backupFile, currentFile)
 		os.Exit(1)
 		return
@@ -86,7 +85,7 @@ func rollback(backup, overrideTo string) {
 	}
 	err := copyDatabaseFile(backup, overrideTo)
 	if err != nil {
-		logging.Error.Printf("error restoring database: %s", err.Error())
+		pterm.Error.Printfln("error restoring database: %s", err.Error())
 		return
 	}
 }
diff --git a/cmd/debug.go b/cmd/debug.go
deleted file mode 100644
index c545b422..00000000
--- a/cmd/debug.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build debug
-
-package main
-
-import (
-	"log"
-	"net/http"
-	"os"
-)
-import _ "net/http/pprof"
-
-func init() {
-	go func() {
-		path := os.Getenv("DEBUG_BIND")
-		if path != "IKNOWWHATIAMDOING" {
-			log.Printf("DEBUG IS ENABLED, THIS SHOULD NOT BE ON")
-			log.Printf("UPDATE TO A RELEASE BUILD AND REMOVE THE DEBUG_BIND ENV")
-			http.ListenAndServe(path, nil)
-		}
-	}()
-}
diff --git a/cmd/main.go b/cmd/main.go
index 6ab7672f..6c0af24d 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"github.com/pufferpanel/pufferpanel/v3"
 	"github.com/pufferpanel/pufferpanel/v3/config"
-	"github.com/pufferpanel/pufferpanel/v3/groups"
 	"github.com/pufferpanel/pufferpanel/v3/logging"
 	"github.com/spf13/cobra"
 	"os"
@@ -12,10 +11,10 @@ import (
 )
 
 func main() {
-	if !groups.IsUserIn(groups.PufferPanelGroup) {
+	/*if !groups.IsUserIn(groups.PufferPanelGroup) {
 		fmt.Println("You do not have permission to use this command")
 		return
-	}
+	}*/
 
 	defer logging.Close()
 
@@ -34,6 +33,9 @@ func main() {
 var rootCmd = &cobra.Command{
 	Use:   "pufferpanel",
 	Short: "Game Server Management Panel",
+	CompletionOptions: cobra.CompletionOptions{
+		DisableDefaultCmd: true,
+	},
 }
 
 var configFile string
@@ -50,7 +52,7 @@ func init() {
 		versionCmd,
 		userCmd,
 		runServiceCmd,
-		migrateCmd,
+		dbCmd,
 	)
 }
 
diff --git a/cmd/run.go b/cmd/run.go
index 40687c21..b070195c 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -94,7 +94,7 @@ func internalRun() (terminate chan bool, success bool) {
 			return
 		}
 
-		err = database.Migrate(db)
+		err = database.Upgrade(db, false)
 		if err != nil {
 			logging.Error.Printf("error upgrading database: %s", err.Error())
 			terminate <- true
@@ -236,6 +236,11 @@ func daemon() error {
 		logging.Error.Printf("Error creating binaries folder: %s", err.Error())
 	}
 
+	err = os.MkdirAll(config.CacheFolder.Value(), 0755)
+	if err != nil {
+		logging.Error.Printf("Error creating cache folder: %s", err.Error())
+	}
+
 	//update path to include our binary folder
 	newPath := os.Getenv("PATH")
 	fullPath, _ := filepath.Abs(config.BinariesFolder.Value())
diff --git a/cmd/runservice.go b/cmd/runservice.go
index 4ab647f9..c047c109 100644
--- a/cmd/runservice.go
+++ b/cmd/runservice.go
@@ -1,9 +1,58 @@
 package main
 
-import "github.com/spf13/cobra"
+import (
+	"github.com/pufferpanel/pufferpanel/v3/logging"
+	"github.com/spf13/cobra"
+	"net"
+	"os"
+)
 
 var runServiceCmd = &cobra.Command{
 	Use:   "runService",
 	Short: "Runs the panel as a service",
 	Run:   executeRunService,
 }
+
+const NotifyReady = "READY=1"
+const NotifyShutdown = "STOPPING=1"
+
+func executeRunService(cmd *cobra.Command, args []string) {
+	term, success := internalRun()
+
+	if !success || term == nil {
+		return
+	}
+
+	notifySystemd(NotifyReady)
+
+	<-term
+	notifySystemd(NotifyShutdown)
+	closePanel()
+}
+
+func notifySystemd(msg string) {
+	//tell systemd we started
+	socketAddr := &net.UnixAddr{
+		Name: os.Getenv("NOTIFY_SOCKET"),
+		Net:  "unixgram",
+	}
+
+	// NOTIFY_SOCKET not set
+	if socketAddr.Name == "" {
+		logging.Debug.Printf("NOTIFY_SOCKET not set, will not communicate to systemd")
+		return
+	}
+
+	conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
+	// Error connecting to NOTIFY_SOCKET
+	if err != nil {
+		logging.Error.Printf("Failed opening NOTIFY_SOCKET: %s", err.Error())
+		return
+	}
+	defer conn.Close()
+
+	if _, err = conn.Write([]byte(msg)); err != nil {
+		logging.Error.Printf("Failed sending message to NOTIFY_SOCKET: %s", err.Error())
+		return
+	}
+}
diff --git a/cmd/runservice_others.go b/cmd/runservice_others.go
deleted file mode 100644
index f3c50138..00000000
--- a/cmd/runservice_others.go
+++ /dev/null
@@ -1,54 +0,0 @@
-//go:build !windows
-
-package main
-
-import (
-	"github.com/pufferpanel/pufferpanel/v3/logging"
-	"github.com/spf13/cobra"
-	"net"
-	"os"
-)
-
-const NotifyReady = "READY=1"
-const NotifyShutdown = "STOPPING=1"
-
-func executeRunService(cmd *cobra.Command, args []string) {
-	term, success := internalRun()
-
-	if !success || term == nil {
-		return
-	}
-
-	notifySystemd(NotifyReady)
-
-	<-term
-	notifySystemd(NotifyShutdown)
-	closePanel()
-}
-
-func notifySystemd(msg string) {
-	//tell systemd we started
-	socketAddr := &net.UnixAddr{
-		Name: os.Getenv("NOTIFY_SOCKET"),
-		Net:  "unixgram",
-	}
-
-	// NOTIFY_SOCKET not set
-	if socketAddr.Name == "" {
-		logging.Debug.Printf("NOTIFY_SOCKET not set, will not communicate to systemd")
-		return
-	}
-
-	conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
-	// Error connecting to NOTIFY_SOCKET
-	if err != nil {
-		logging.Error.Printf("Failed opening NOTIFY_SOCKET: %s", err.Error())
-		return
-	}
-	defer conn.Close()
-
-	if _, err = conn.Write([]byte(msg)); err != nil {
-		logging.Error.Printf("Failed sending message to NOTIFY_SOCKET: %s", err.Error())
-		return
-	}
-}
diff --git a/cmd/runservice_windows.go b/cmd/runservice_windows.go
deleted file mode 100644
index 8f542a3d..00000000
--- a/cmd/runservice_windows.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package main
-
-import (
-	"github.com/pufferpanel/pufferpanel/v3/logging"
-	"github.com/spf13/cobra"
-	"golang.org/x/sys/windows/svc"
-)
-
-func executeRunService(cmd *cobra.Command, args []string) {
-	_ = svc.Run("PufferPanel", &service{})
-}
-
-type service struct{}
-
-func (m *service) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
-	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
-	changes <- svc.Status{State: svc.StartPending}
-	changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
-
-	term, success := internalRun()
-
-	if !success || term == nil {
-		return
-	}
-
-	go func() {
-		<-term
-		closePanel()
-	}()
-
-loop:
-	for c := range r {
-		switch c.Cmd {
-		case svc.Stop, svc.Shutdown:
-			logging.Info.Printf("Received stop command\n")
-			break loop
-		}
-	}
-	changes <- svc.Status{State: svc.StopPending}
-	term <- true
-	return
-}
diff --git a/cmd/user.go b/cmd/user.go
index 8a75df9e..f2deb0cc 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -3,15 +3,16 @@ package main
 import (
 	"errors"
 	"fmt"
-	"github.com/pufferpanel/pufferpanel/v3/groups"
-	"github.com/pufferpanel/pufferpanel/v3/scopes"
-
-	"github.com/AlecAivazis/survey/v2"
+	"github.com/pterm/pterm"
 	"github.com/pufferpanel/pufferpanel/v3/database"
+	"github.com/pufferpanel/pufferpanel/v3/groups"
 	"github.com/pufferpanel/pufferpanel/v3/models"
+	"github.com/pufferpanel/pufferpanel/v3/scopes"
 	"github.com/pufferpanel/pufferpanel/v3/services"
 	"github.com/spf13/cobra"
 	"gorm.io/gorm"
+	"os"
+	"strings"
 )
 
 var AddUserCmd = &cobra.Command{
@@ -55,60 +56,68 @@ func addUser(cmd *cobra.Command, args []string) {
 		Password: addPassword,
 	}
 
-	//should we ask if this user is an admin should only appear if no flags are used
-	promptAdmin := true
-	if answers.Admin || answers.Username != "" || answers.Email != "" || answers.Password != "" {
-		promptAdmin = false
+	useFlags := false
+	if answers.Username != "" || answers.Email != "" || answers.Password != "" {
+		useFlags = true
 	}
 
-	questions := make([]*survey.Question, 0)
-
-	if answers.Username == "" {
-		questions = append(questions, &survey.Question{
-			Name: "username",
-			Prompt: &survey.Input{
-				Message: "Username:",
-			},
-			Validate: validateUsername,
-		})
+	firstAnswer := answers.Username == ""
+	err := validateUsername(answers.Username)
+	for err != nil {
+		if !firstAnswer {
+			pterm.Error.Println("Username validation failed: " + err.Error())
+			if useFlags {
+				os.Exit(1)
+			}
+		}
+		firstAnswer = false
+		answers.Username, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("Username").Show()
+		err = validateUsername(answers.Username)
 	}
 
-	if answers.Email == "" {
-		questions = append(questions, &survey.Question{
-			Name: "email",
-			Prompt: &survey.Input{
-				Message: "Email:",
-			},
-			Validate: validateEmail,
-		})
+	firstAnswer = answers.Email == ""
+	err = validateEmail(answers.Email)
+	for err != nil {
+		if !firstAnswer {
+			pterm.Error.Println("Email validation failed: " + err.Error())
+			if useFlags {
+				os.Exit(1)
+			}
+		}
+		firstAnswer = false
+		answers.Email, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("Email").Show()
+		err = validateEmail(answers.Email)
 	}
 
-	if answers.Password == "" {
-		questions = append(questions, &survey.Question{
-			Name: "password",
-			Prompt: &survey.Password{
-				Message: "Password:",
-			},
-			Validate: validatePassword,
-		})
-	}
+	firstAnswer = answers.Password == ""
+	err = validatePassword(answers.Password)
+	for err != nil {
+		if !firstAnswer {
+			pterm.Error.Println("Password validation failed: " + err.Error())
+			if useFlags {
+				os.Exit(1)
+			}
+		}
+		firstAnswer = false
+		answers.Password, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("Password").WithMask("*").Show()
+		err = validatePassword(answers.Password)
+		if err != nil {
+			continue
+		}
 
-	if promptAdmin {
-		questions = append(questions, &survey.Question{
-			Name: "admin",
-			Prompt: &survey.Confirm{
-				Message: "Admin",
-			},
-		})
+		confirm, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("Confirm Password").WithMask("*").Show()
+		if answers.Password != confirm {
+			err = errors.New("passwords do not match")
+		}
 	}
 
-	if len(questions) > 0 {
-		_ = survey.Ask(questions, &answers)
+	if !useFlags {
+		answers.Admin, _ = pterm.DefaultInteractiveConfirm.WithDefaultText("Set as Admin").Show()
 	}
 
 	db, err := database.GetConnection()
 	if err != nil {
-		fmt.Printf("Failed to connect to database: %s\n", err.Error())
+		pterm.Error.Printf("Failed to connect to database: %s\n", err.Error())
 		return
 	}
 	defer database.Close()
@@ -121,21 +130,21 @@ func addUser(cmd *cobra.Command, args []string) {
 		}
 		err = user.SetPassword(answers.Password)
 		if err != nil {
-			fmt.Printf("Failed to set password: %s\n", err.Error())
+			pterm.Error.Printf("Failed to set password: %s\n", err.Error())
 			return err
 		}
 
 		us := &services.User{DB: tx}
 		err = us.Create(user)
 		if err != nil {
-			fmt.Printf("Failed to create user: %s\n", err.Error())
+			pterm.Error.Printf("Failed to create user: %s\n", err.Error())
 			return err
 		}
 
 		ps := &services.Permission{DB: tx}
 		perms, err := ps.GetForUserAndServer(user.ID, "")
 		if err != nil {
-			fmt.Printf("Failed to get permissions: %s\n", err.Error())
+			pterm.Error.Printf("Failed to get permissions: %s\n", err.Error())
 			return err
 		}
 
@@ -145,7 +154,7 @@ func addUser(cmd *cobra.Command, args []string) {
 
 		err = ps.UpdatePermissions(perms)
 		if err != nil {
-			fmt.Printf("Failed to apply permissions: %s\n", err.Error())
+			pterm.Error.Printf("Failed to apply permissions: %s\n", err.Error())
 			return err
 		}
 
@@ -154,7 +163,7 @@ func addUser(cmd *cobra.Command, args []string) {
 		return
 	}
 
-	fmt.Printf("User added\n")
+	pterm.Info.Printf("User added\n")
 }
 
 func validateEmail(val interface{}) error {
@@ -190,21 +199,7 @@ func validatePassword(val interface{}) error {
 	}
 
 	us := &services.User{}
-	if !us.IsSecurePassword(pw) {
-		return errors.New("password must be at least 8 characters")
-	}
-
-	var secondAttempt string
-	confirm := &survey.Password{
-		Message: "Confirm Password",
-	}
-	_ = survey.AskOne(confirm, &secondAttempt)
-
-	if secondAttempt != pw {
-		return errors.New("password do not match")
-	}
-
-	return nil
+	return us.IsSecurePassword(pw)
 }
 
 type userCreate struct {
@@ -228,109 +223,130 @@ func editUser(cmd *cobra.Command, args []string) {
 	defer database.Close()
 
 	var username string
-	_ = survey.AskOne(&survey.Input{
-		Message: "Username:",
-	}, &username, survey.WithValidator(survey.Required))
+
+	username, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("Enter Username").Show()
 
 	us := &services.User{DB: db}
 
 	user, err := us.Get(username)
-	if err != nil && err == gorm.ErrRecordNotFound {
-		fmt.Printf("No user with username '%s'\n", username)
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		pterm.Error.Printfln("No user with username '%s'\n", username)
 		return
 	} else if err != nil {
-		fmt.Printf("Error getting user: %s\n", err.Error())
+		pterm.Error.Printfln("Error getting user: %s\n", err.Error())
 		return
 	}
 
-	action := ""
-	_ = survey.AskOne(&survey.Select{
-		Message: "Select option to edit",
-		Options: []string{"Username", "Email", "Password", "Change Admin Status", "Remove 2FA"},
-	}, &action)
-
-	switch action {
-	case "Username":
-		{
-			prompt := ""
-			_ = survey.AskOne(&survey.Input{
-				Message: "New Username:",
-			}, &prompt, survey.WithValidator(survey.Required))
-			user.Username = prompt
-
-			err = us.Update(user)
-			if err != nil {
-				fmt.Printf("Error updating username: %s\n", err.Error())
-			}
-		}
-	case "Email":
-		{
-			prompt := ""
-			_ = survey.AskOne(&survey.Input{
-				Message: "New Email:",
-			}, &prompt, survey.WithValidator(survey.Required))
-			user.Email = prompt
-
-			err = us.Update(user)
-			if err != nil {
-				fmt.Printf("Error updating email: %s\n", err.Error())
-			}
-		}
-	case "Password":
-		{
-			prompt := ""
-			_ = survey.AskOne(&survey.Password{
-				Message: "New Password:",
-			}, &prompt, survey.WithValidator(validatePassword))
-
-			err = user.SetPassword(prompt)
-			if err != nil {
-				fmt.Printf("Error updating password: %s\n", err.Error())
+	var usernameAction = "Username"
+	var emailAction = "Email"
+	var passwordAction = "Password"
+	var adminAction = "Admin Status"
+	var remove2FAAction = "Remove 2FA"
+	var quitAction = "Quit"
+
+	var currentAction string
+
+	for currentAction != quitAction {
+		currentAction, _ = pterm.DefaultInteractiveSelect.WithOptions([]string{
+			usernameAction,
+			emailAction,
+			passwordAction,
+			adminAction,
+			remove2FAAction,
+			quitAction,
+		}).WithFilter(false).WithMaxHeight(20).Show()
+
+		switch currentAction {
+		case usernameAction:
+			{
+				oldValue := user.Username
+				user.Username, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("Change username to").WithDefaultValue(oldValue).Show()
+
+				err = us.Update(user)
+				if err != nil {
+					pterm.Error.Printfln("Error updating username: %s", err.Error())
+				} else {
+					pterm.Info.Printfln("Username updated from %s to %s", oldValue, user.Username)
+				}
 			}
+		case emailAction:
+			{
+				oldValue := user.Username
+				user.Email, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("Change email to").WithDefaultValue(oldValue).Show()
 
-			err = us.Update(user)
-			if err != nil {
-				fmt.Printf("Error updating password: %s\n", err.Error())
-			}
-		}
-	case "Change Admin Status":
-		{
-			prompt := false
-			_ = survey.AskOne(&survey.Confirm{
-				Message: "Set Admin Status: ",
-			}, &prompt)
-
-			ps := &services.Permission{DB: db}
-			perms, err := ps.GetForUserAndServer(user.ID, "")
-			if err != nil {
-				fmt.Printf("Error updating permissions: %s\n", err.Error())
-				return
+				err = us.Update(user)
+				if err != nil {
+					pterm.Error.Printfln("Error updating email: %s", err.Error())
+				} else {
+					pterm.Info.Printfln("Email updated from %s to %s", oldValue, user.Username)
+				}
 			}
+		case passwordAction:
+			{
+				password, _ := pterm.DefaultInteractiveTextInput.WithMask("*").WithDefaultText("Change password to").Show()
 
-			//perms.Admin = prompt
-			if prompt {
-				perms.Scopes = scopes.AddScope(perms.Scopes, scopes.ScopeAdmin)
-			} else {
-				perms.Scopes = scopes.RemoveScope(perms.Scopes, scopes.ScopeAdmin)
+				err = user.SetPassword(password)
+				if err != nil {
+					pterm.Error.Printfln("Error updating password: %s", err.Error())
+					break
+				}
+				err = us.Update(user)
+				if err != nil {
+					pterm.Error.Printfln("Error updating password: %s", err.Error())
+				} else {
+					pterm.Info.Printfln("Password updated")
+				}
 			}
+		case adminAction:
+			{
+				result, _ := pterm.DefaultInteractiveContinue.WithDefaultText("Set as admin").WithOptions([]string{"yes", "no", "cancel"}).WithDefaultValue("cancel").Show()
 
-			err = ps.UpdatePermissions(perms)
-			if err != nil {
-				fmt.Printf("Error updating password: %s\n", err.Error())
-			}
-		}
-	case "Remove 2FA":
-		{
-			prompt := false
-			_ = survey.AskOne(&survey.Confirm{Message: "Confirm removal of 2FA?"}, &prompt)
-			if prompt {
-				us := &services.User{DB: db}
-				err = us.DisableOtp(user.ID)
+				if result == "cancel" {
+					break
+				}
+
+				ps := &services.Permission{DB: db}
+				perms, err := ps.GetForUserAndServer(user.ID, "")
 				if err != nil {
-					fmt.Printf("Error removing 2FA: %s\n", err.Error())
+					pterm.Error.Printfln("Error updating permissions: %s", err.Error())
+					break
+				}
+
+				//perms.Admin = prompt
+				result = strings.ToLower(result)
+				if result == "yes" || result == "y" {
+					perms.Scopes = scopes.AddScope(perms.Scopes, scopes.ScopeAdmin)
+				} else if result == "no" || result == "n" {
+					perms.Scopes = scopes.RemoveScope(perms.Scopes, scopes.ScopeAdmin)
+				} else {
+					break
+				}
+
+				err = ps.UpdatePermissions(perms)
+				if err != nil {
+					pterm.Error.Printfln("Error updating password: %s", err.Error())
+					break
+				}
+
+				if scopes.ContainsScope(perms.Scopes, scopes.ScopeAdmin) {
+					pterm.Info.Printfln("Admin status added")
+				} else {
+					pterm.Info.Printfln("Admin status removed")
+				}
+			}
+		case remove2FAAction:
+			{
+				result, _ := pterm.DefaultInteractiveConfirm.WithDefaultText("Remove 2FA").Show()
+				if result {
+					us := &services.User{DB: db}
+					err = us.DisableOtp(user.ID)
+					if err != nil {
+						fmt.Printf("Error removing 2FA: %s", err.Error())
+					} else {
+						pterm.Info.Printfln("2FA removed")
+					}
 				}
 			}
 		}
 	}
-
 }
diff --git a/config/config.go b/config/config.go
index edf82701..c5613ee0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -5,7 +5,6 @@ import (
 	"github.com/spf13/viper"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 )
 
@@ -20,16 +19,9 @@ func LoadConfigFile(configFile string) error {
 		var exists bool
 		configFile, exists = os.LookupEnv("PUFFER_CONFIG")
 		if !exists || configFile == "" {
-			if runtime.GOOS == "windows" {
-				//well, check for the program files path....
-				if _, err := os.Lstat("C:\\Program Files\\PufferPanel\\config.json"); err == nil {
-					configFile = "C:\\Program Files\\PufferPanel\\config.json"
-				}
-			} else {
-				//well, check for the /etc path
-				if _, err := os.Lstat("/etc/pufferpanel/config.json"); err == nil {
-					configFile = "/etc/pufferpanel/config.json"
-				}
+			//well, check for the /etc path
+			if _, err := os.Lstat("/etc/pufferpanel/config.json"); err == nil {
+				configFile = "/etc/pufferpanel/config.json"
 			}
 			//we got nothing
 			if configFile == "" {
diff --git a/config/entries.go b/config/entries.go
index edc8a7dc..6347fcc3 100644
--- a/config/entries.go
+++ b/config/entries.go
@@ -64,6 +64,7 @@ var SecurityForceOpenat2 = asBool("security.forceOpenat2", false)
 var SecurityForceOpenat = asBool("security.forceOpenat", false)
 var SecurityTrustedProxies = asStringArray("security.trustedProxies", []string{})
 var SecurityTrustedProxyHeader = asString("security.trustedProxyHeader", "")
+var SecurityDisableUnshare = asBool("security.disableUnshare", false)
 
 var DockerRootPath = asString("docker.root", "")
 var DockerDisallowHost = asBool("docker.disallowHost", false)
diff --git a/database/migrate.go b/database/upgrade.go
similarity index 85%
rename from database/migrate.go
rename to database/upgrade.go
index 30dc0db6..6b649759 100644
--- a/database/migrate.go
+++ b/database/upgrade.go
@@ -2,6 +2,7 @@ package database
 
 import (
 	"github.com/go-gormigrate/gormigrate/v2"
+	"github.com/pterm/pterm"
 	"github.com/pufferpanel/pufferpanel/v3"
 	"github.com/pufferpanel/pufferpanel/v3/config"
 	"github.com/pufferpanel/pufferpanel/v3/logging"
@@ -11,37 +12,112 @@ import (
 	"gorm.io/gorm"
 	"os"
 	"path/filepath"
+	"time"
 )
 
-func Migrate(dbConn *gorm.DB) error {
-	dbObjects := []interface{}{
-		&models.Node{},
-		&models.Server{},
-		&models.User{},
-		&models.Template{},
-		&models.Permissions{},
-		&models.Client{},
-		&models.UserSetting{},
-		&models.Session{},
-		&models.TemplateRepo{},
-		&models.Backup{},
-	}
+var dbObjects = []interface{}{
+	&models.Node{},
+	&models.Server{},
+	&models.User{},
+	&models.Template{},
+	&models.Permissions{},
+	&models.Client{},
+	&models.UserSetting{},
+	&models.Session{},
+	&models.TemplateRepo{},
+	&models.Backup{},
+}
 
+func Upgrade(dbConn *gorm.DB, prettyPrint bool) error {
 	session := dbConn.Session(&gorm.Session{})
 	migrator := session.Migrator()
 
-	z := gormigrate.New(session, &gormigrate.Options{TableName: "migrations", IDColumnName: "id", IDColumnSize: 255, UseTransaction: true, ValidateUnknownMigrations: false}, []*gormigrate.Migration{
+	var printer *pterm.ProgressbarPrinter
+	if prettyPrint {
+		size := 1 //include 1 for models
+		for _, z := range migrations {
+			size += len(z)
+		}
+
+		printer, _ = pterm.DefaultProgressbar.WithTitle("Upgrading database").WithTotal(size).Start()
+
+		defer func() {
+			_, _ = printer.Stop()
+		}()
+	}
+
+	options := &gormigrate.Options{TableName: "migrations", IDColumnName: "id", IDColumnSize: 255, UseTransaction: true, ValidateUnknownMigrations: false}
+
+	for k, v := range migrations {
+		for _, z := range v {
+			if printer != nil {
+				printer.UpdateTitle("Upgrading " + z.ID)
+			}
+
+			time.Sleep(time.Millisecond * 100)
+
+			if err := gormigrate.New(session, options, []*gormigrate.Migration{z}).Migrate(); err != nil {
+				return err
+			}
+			if printer != nil {
+				pterm.Success.Printfln("Upgrading %s", z.ID)
+				if printer.Total == printer.Current+1 {
+					printer.UpdateTitle("Upgrade complete")
+				}
+				_ = printer.Increment()
+			}
+		}
+
+		//these are migrations we need done first before we can do models
+		//now we can do the models directly
+		if k == 0 {
+			if printer != nil {
+				printer.UpdateTitle("Upgrading models")
+			}
+			if err := migrator.AutoMigrate(dbObjects...); err != nil {
+				return err
+			}
+			if printer != nil {
+				pterm.Success.Println("Upgrading models")
+				_ = printer.Increment()
+			}
+		}
+	}
+
+	if printer != nil {
+		_, _ = printer.Stop()
+		pterm.Success.Println("Database upgrade complete")
+	}
+
+	time.Sleep(time.Millisecond * 100)
+	return nil
+}
+
+func saveToFile(filename string, data []byte) error {
+	//just dump it into working dir
+	err := os.MkdirAll("migrations", 0755)
+	if err != nil {
+		return err
+	}
+	file, err := os.OpenFile(filepath.Join("migrations", filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	if err != nil {
+		return err
+	}
+	defer utils.Close(file)
+	_, err = file.Write(data)
+	return err
+}
+
+var migrations = [][]*gormigrate.Migration{
+	{
 		{
 			ID: "1726675832-mysql",
 			Migrate: func(db *gorm.DB) error {
-				logging.Info.Printf("Migrate id:1726675832-mysql")
-
 				if config.DatabaseDialect.Value() != "mysql" {
 					return nil
 				}
 
 				//at this point for mysql, just manually do the queries...
-
 				type FKs struct {
 					Table string `gorm:"column:TABLE_NAME"`
 					Name  string `gorm:"column:CONSTRAINT_NAME"`
@@ -57,16 +133,12 @@ func Migrate(dbConn *gorm.DB) error {
 						return err
 					}
 				}
-
 				return nil
 			},
 		},
 		{
 			ID: "1726675832",
 			Migrate: func(db *gorm.DB) error {
-				//delete all indices, because we need to just recreate them
-				logging.Info.Printf("Migrate id:1726675832")
-
 				for _, v := range dbObjects {
 					m := db.Migrator()
 					indices, err := m.GetIndexes(v)
@@ -88,34 +160,22 @@ func Migrate(dbConn *gorm.DB) error {
 				return nil
 			},
 		},
-	})
-
-	if err := z.Migrate(); err != nil {
-		return err
-	}
-
-	if err := migrator.AutoMigrate(dbObjects...); err != nil {
-		return err
-	}
-
-	m := gormigrate.New(session, &gormigrate.Options{TableName: "migrations", IDColumnName: "id", IDColumnSize: 255, UseTransaction: true, ValidateUnknownMigrations: false}, []*gormigrate.Migration{
+	},
+	{
 		{
 			ID: "1658926619",
 			Migrate: func(db *gorm.DB) error {
-				logging.Info.Printf("Migrate id:1658926619")
-
-				return db.Create(&models.TemplateRepo{
+				err := db.Create(&models.TemplateRepo{
 					Name:   "community",
 					Url:    "https://github.com/pufferpanel/templates",
 					Branch: "v3",
 				}).Error
+				return err
 			},
 		},
 		{
 			ID: "1677250619",
 			Migrate: func(db *gorm.DB) error {
-				logging.Info.Printf("Migrate id:1677250619")
-
 				var templates []*models.Template
 				err := db.Find(&templates).Error
 				if err != nil {
@@ -154,8 +214,6 @@ func Migrate(dbConn *gorm.DB) error {
 			ID: "permissions-from-v2",
 			Migrate: func(db *gorm.DB) error {
 				//this is going to be a nightmare
-				logging.Info.Printf("Migrate id:permissions-from-v2")
-
 				//go ahead and migrate the table, so that the columns we need are there
 				err := db.AutoMigrate(&models.Permissions{})
 				if err != nil {
@@ -344,22 +402,5 @@ func Migrate(dbConn *gorm.DB) error {
 				return nil
 			},
 		},
-	})
-
-	return m.Migrate()
-}
-
-func saveToFile(filename string, data []byte) error {
-	//just dump it into working dir
-	err := os.MkdirAll("migrations", 0755)
-	if err != nil {
-		return err
-	}
-	file, err := os.OpenFile(filepath.Join("migrations", filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(file)
-	_, err = file.Write(data)
-	return err
+	},
 }
diff --git a/download.go b/download.go
index c7c07de3..03c24ed1 100644
--- a/download.go
+++ b/download.go
@@ -1,19 +1,18 @@
 package pufferpanel
 
 import (
-	"crypto/sha1"
+	"crypto"
 	"fmt"
 	"github.com/pufferpanel/pufferpanel/v3/config"
 	"github.com/pufferpanel/pufferpanel/v3/logging"
 	"github.com/pufferpanel/pufferpanel/v3/utils"
 	"io"
-	"log"
 	"os"
 	"path/filepath"
 	"strings"
 )
 
-func DownloadFile(url, fileName string, env Environment) error {
+func DownloadFile(url, fileName string, env *Environment) error {
 	target, err := os.Create(filepath.Join(env.GetRootDirectory(), fileName))
 	defer utils.Close(target)
 	if err != nil {
@@ -45,7 +44,7 @@ func DownloadFileToCache(url, fileName string) error {
 		return err
 	}
 
-	logging.Info.Printf("Downloading: " + url)
+	logging.Info.Printf("Downloading: %s\n", url)
 
 	response, err := HttpGet(url)
 	defer utils.CloseResponse(response)
@@ -57,69 +56,123 @@ func DownloadFileToCache(url, fileName string) error {
 	return err
 }
 
-func DownloadViaMaven(downloadUrl string, env Environment) (string, error) {
-	localPath := filepath.Join(config.CacheFolder.Value(), strings.TrimPrefix(strings.TrimPrefix(downloadUrl, "http://"), "https://"))
-
-	if os.PathSeparator != '/' {
-		localPath = strings.Replace(localPath, "/", string(os.PathSeparator), -1)
+func downloadFile(url string) (io.ReadCloser, error) {
+	logging.Info.Printf("Downloading: %s", url)
+	response, err := HttpGet(url)
+	if err != nil {
+		return nil, err
 	}
+	return response.Body, err
+}
 
-	sha1Url := downloadUrl + ".sha1"
+func cacheFile(downloadUrl, localPath string) (io.ReadCloser, error) {
+	dl, err := downloadFile(downloadUrl)
+	if err != nil {
+		utils.Close(dl)
+		return nil, err
+	}
+	parent := filepath.Dir(localPath)
+	err = os.MkdirAll(parent, 0755)
+	if err != nil && !os.IsExist(err) {
+		logging.Info.Printf("Failed directories in cache: %s", err)
+		return dl, nil
+	}
+	f, err := os.Create(localPath)
+	if err != nil {
+		utils.Close(f)
+		logging.Info.Printf("Failed creating file in cache: %s", err)
+		return dl, nil
+	}
+	_, err = io.Copy(f, dl)
+	utils.Close(dl)
+	if err != nil {
+		// failed actually writing to the successfully created file
+		utils.Close(f)
+		return nil, err
+	}
+	err = f.Sync()
+	if err != nil {
+		return nil, err
+	}
+	_, err = f.Seek(0, io.SeekStart)
+	if err != nil {
+		return nil, err
+	}
+	return f, nil
+}
 
+func Download(downloadUrl, hash string, algorithm crypto.Hash, cache bool, env *Environment) (io.ReadCloser, error) {
 	if env != nil {
 		env.DisplayToConsole(true, "Downloading: %s\n", downloadUrl)
 	}
 
-	useCache := true
-	f, err := os.Open(localPath)
-	defer utils.Close(f)
-	//cache was readable, so validate
-	if err == nil {
-		h := sha1.New()
-		if _, err := io.Copy(h, f); err != nil {
-			log.Fatal(err)
+	if !cache {
+		// don't interact with cache, directly return download response
+		return downloadFile(downloadUrl)
+	} else {
+		// caching allowed
+		localPath := filepath.Join(config.CacheFolder.Value(), strings.TrimPrefix(strings.TrimPrefix(downloadUrl, "http://"), "https://"))
+
+		if os.PathSeparator != '/' {
+			localPath = strings.Replace(localPath, "/", string(os.PathSeparator), -1)
 		}
-		utils.Close(f)
 
-		actualHash := fmt.Sprintf("%x", h.Sum(nil))
+		// try to open existing cached file
+		f, err := os.Open(localPath)
+		if os.IsNotExist(err) {
+			// cache miss, need to download
+			return cacheFile(downloadUrl, localPath)
+		} else if err != nil {
+			logging.Info.Printf("Failed opening cached file despite it existing: %s", err)
+			return downloadFile(downloadUrl)
+		}
 
-		logging.Info.Printf("Downloading hash from %s", sha1Url)
-		response, err := HttpGet(sha1Url)
-		defer utils.CloseResponse(response)
+		h := algorithm.New()
+		if _, err := io.Copy(h, f); err != nil {
+			utils.Close(f)
+			logging.Info.Printf("Cached file is not readable, will download (%s)", localPath)
+			return downloadFile(downloadUrl)
+		}
+		actualHash := fmt.Sprintf("%x", h.Sum(nil))
+		_, err = f.Seek(0, io.SeekStart)
 		if err != nil {
-			useCache = false
+			return nil, err
+		}
+		if hash == actualHash {
+			logging.Info.Printf("Using cached copy of file: %s\n", downloadUrl)
+			return f, nil
 		} else {
-			data := make([]byte, 40)
-			_, err := response.Body.Read(data)
-			expectedHash := string(data)
-
-			if err != nil {
-				useCache = false
-			} else if expectedHash != actualHash {
-				logging.Info.Printf("Cache expected %s but was actually %s", expectedHash, actualHash)
-				useCache = false
-			}
+			logging.Info.Printf("Cache expected %s but was actually %s, downloading new version and caching to %s", hash, actualHash, localPath)
+			utils.Close(f)
+			return cacheFile(downloadUrl, localPath)
 		}
+	}
+}
 
-		if useCache {
-			if env != nil {
-				logging.Info.Printf("Using cached copy of file: %s\n", downloadUrl)
-			}
-		}
-	} else if !os.IsNotExist(err) {
-		logging.Info.Printf("Cached file is not readable, will download (%s)", localPath)
+func DownloadHash(hashUrl string, algorithm crypto.Hash) (string, error) {
+	logging.Info.Printf("Downloading hash from %s", hashUrl)
+	response, err := HttpGet(hashUrl)
+	defer utils.CloseResponse(response)
+	if err != nil {
+		return "", err
 	} else {
-		useCache = false
-	}
+		data := make([]byte, algorithm.Size() * 2)
+		_, err := response.Body.Read(data)
+		if err != nil {
+			return "", err
+		}
 
-	//if we can't use cache, redownload it to the cache
-	if !useCache {
-		logging.Info.Printf("Downloading new version and caching to %s", localPath)
-		err = DownloadFileToCache(downloadUrl, localPath)
+		return string(data), nil
 	}
-	if err == nil {
-		return localPath, err
-	} else {
-		return "", err
+}
+
+func DownloadViaMaven(downloadUrl string, env *Environment) (io.ReadCloser, error) {
+	hashUrl := downloadUrl + ".sha1"
+	expectedHash, err := DownloadHash(hashUrl, crypto.SHA1)
+	if err != nil {
+		logging.Info.Printf("Failed downloading hash, not using cache")
+		return Download(downloadUrl, "", crypto.SHA1, false, env)
 	}
+
+	return Download(downloadUrl, expectedHash, crypto.SHA1, true, env)
 }
diff --git a/entrypoint.sh b/entrypoint.sh
index fc4c4ab5..3a396c88 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,3 +1,7 @@
 #!/usr/bin/env sh
 
+/pufferpanel/bin/pufferpanel db migrate
+exitCode=$?
+[ $exitCode -eq 0 ] || [ $exitCode -eq 9 ] || exit $exitCode
+
 /pufferpanel/bin/pufferpanel run
\ No newline at end of file
diff --git a/environment.go b/environment.go
index 40877741..0e3e90ff 100644
--- a/environment.go
+++ b/environment.go
@@ -13,105 +13,56 @@ import (
 	"time"
 )
 
-type Environment interface {
-	// Execute Executes a command within the environment.
-	Execute(steps ExecutionData) error
+type EnvironmentImpl interface {
+	ExecuteAsyncImpl(environment *Environment, steps ExecutionData) error
 
-	// ExecuteAsync Executes a command within the environment and immediately return
-	ExecuteAsync(steps ExecutionData) error
+	KillImpl(environment *Environment) error
 
-	// ExecuteInMainProcess Sends a string to the StdIn of the main program process
-	ExecuteInMainProcess(cmd string) error
+	GetStatsImpl(environment *Environment) (*ServerStats, error)
 
-	// Kill Kills the main process, but leaves the environment running.
-	Kill() error
+	SendCodeImpl(environment *Environment, code int) error
 
-	// Create Creates the environment setting needed to run programs.
-	Create() error
+	GetUidImpl(environment *Environment) int
 
-	// Delete Deletes the environment.
-	Delete() error
+	GetGidImpl(environment *Environment) int
 
-	Update() error
-
-	IsRunning() (isRunning bool, err error)
-
-	IsInstalling() bool
-
-	SetInstalling(bool)
-
-	WaitForMainProcess() error
-
-	WaitForMainProcessFor(timeout time.Duration) error
-
-	GetRootDirectory() string
-
-	GetConsole() (console []byte, epoch int64)
-
-	GetConsoleFrom(time int64) (console []byte, epoch int64)
-
-	AddConsoleListener(ws *Socket)
-
-	AddStatusListener(ws *Socket)
-
-	AddStatsListener(ws *Socket)
-
-	GetStats() (*ServerStats, error)
-
-	DisplayToConsole(prefix bool, msg string, data ...interface{})
-
-	SendCode(code int) error
-
-	GetBase() *BaseEnvironment
-
-	GetLastExitCode() int
-
-	GetWrapper() io.Writer
-
-	GetStatsTracker() *Tracker
-
-	GetServer() Server
-
-	GetUid() int
-
-	GetGid() int
+	IsRunningImpl(environment *Environment) (isRunning bool, err error)
 }
 
-type BaseEnvironment struct {
-	Environment
-	Type              string               `json:"type"`
-	RootDirectory     string               `json:"root,omitempty"`
-	BackupDirectory   string               `json:"-"`
-	ConsoleBuffer     *MemoryCache         `json:"-"`
-	Wait              *sync.WaitGroup      `json:"-"`
-	ExecutionFunction ExecutionFunction    `json:"-"`
-	ServerId          string               `json:"-"`
-	LastExitCode      int                  `json:"-"`
-	Wrapper           io.Writer            `json:"-"` //our proxy back to the main
-	ConsoleTracker    *Tracker             `json:"-"`
-	StatusTracker     *Tracker             `json:"-"`
-	StatsTracker      *Tracker             `json:"-"`
-	Installing        bool                 `json:"-"`
-	BackingUp         bool                 `json:"-"`
-	IsRunningFunc     func() (bool, error) `json:"-"`
-	KillFunc          func() error         `json:"-"`
-	Console           Console              `json:"-"`
-	Server            Server               `json:"-"`
+type Environment struct {
+	Type            string          `json:"type"`
+	RootDirectory   string          `json:"root,omitempty"`
+	BackupDirectory string          `json:"-"`
+	ConsoleBuffer   *MemoryCache    `json:"-"`
+	Wait            *sync.WaitGroup `json:"-"`
+	ServerId        string          `json:"-"`
+	LastExitCode    int             `json:"-"`
+	Wrapper         io.Writer       `json:"-"` //our proxy back to the main
+	ConsoleTracker  *Tracker        `json:"-"`
+	StatusTracker   *Tracker        `json:"-"`
+	StatsTracker    *Tracker        `json:"-"`
+	Installing      bool            `json:"-"`
+	BackingUp       bool            `json:"-"`
+	Console         Console         `json:"-"`
+	Server          Server          `json:"-"`
+	Implementation  EnvironmentImpl `json:"-"`
 }
 
 type ExecutionData struct {
 	Command          string
-	Arguments        []string
 	Environment      map[string]string
 	WorkingDirectory string
 	Variables        map[string]interface{}
 	Callback         func(exitCode int)
 	StdInConfig      StdinConsoleConfiguration
+	DisableStdin     bool
+	DisableQuery     bool
+	DisableStats     bool
 }
 
 type ExecutionFunction func(steps ExecutionData) (err error)
 
-func (e *BaseEnvironment) Execute(steps ExecutionData) error {
+func (e *Environment) Execute(steps ExecutionData) error {
 	err := e.ExecuteAsync(steps)
 	if err != nil {
 		return err
@@ -119,7 +70,7 @@ func (e *BaseEnvironment) Execute(steps ExecutionData) error {
 	return e.WaitForMainProcess()
 }
 
-func (e *BaseEnvironment) ExecuteAsync(steps ExecutionData) (err error) {
+func (e *Environment) ExecuteAsync(steps ExecutionData) (err error) {
 	running, err := e.IsRunning()
 	if err != nil {
 		return
@@ -132,10 +83,10 @@ func (e *BaseEnvironment) ExecuteAsync(steps ExecutionData) (err error) {
 	//update configs
 	steps.StdInConfig = steps.StdInConfig.Replace(steps.Variables)
 
-	return e.ExecutionFunction(steps)
+	return e.Implementation.ExecuteAsyncImpl(e, steps)
 }
 
-func (e *BaseEnvironment) CreateConsoleStdinProxy(config StdinConsoleConfiguration, base io.WriteCloser) {
+func (e *Environment) CreateConsoleStdinProxy(config StdinConsoleConfiguration, base io.WriteCloser) {
 	if config.Type == "telnet" {
 		e.Console = &connections.TelnetConnection{
 			IP:       config.IP,
@@ -160,37 +111,37 @@ func (e *BaseEnvironment) CreateConsoleStdinProxy(config StdinConsoleConfigurati
 	}
 }
 
-func (e *BaseEnvironment) GetRootDirectory() string {
+func (e *Environment) GetRootDirectory() string {
 	return e.RootDirectory
 }
 
-func (e *BaseEnvironment) GetConsole() (console []byte, epoch int64) {
+func (e *Environment) GetConsole() (console []byte, epoch int64) {
 	console, epoch = e.ConsoleBuffer.Read()
 	return
 }
 
-func (e *BaseEnvironment) GetConsoleFrom(time int64) (console []byte, epoch int64) {
+func (e *Environment) GetConsoleFrom(time int64) (console []byte, epoch int64) {
 	console, epoch = e.ConsoleBuffer.ReadFrom(time)
 	return
 }
 
-func (e *BaseEnvironment) AddConsoleListener(ws *Socket) {
+func (e *Environment) AddConsoleListener(ws *Socket) {
 	e.ConsoleTracker.Register(ws)
 }
 
-func (e *BaseEnvironment) AddStatsListener(ws *Socket) {
+func (e *Environment) AddStatsListener(ws *Socket) {
 	e.StatsTracker.Register(ws)
 }
 
-func (e *BaseEnvironment) AddStatusListener(ws *Socket) {
+func (e *Environment) AddStatusListener(ws *Socket) {
 	e.StatusTracker.Register(ws)
 }
 
-func (e *BaseEnvironment) GetStatsTracker() *Tracker {
+func (e *Environment) GetStatsTracker() *Tracker {
 	return e.StatsTracker
 }
 
-func (e *BaseEnvironment) DisplayToConsole(daemon bool, msg string, data ...interface{}) {
+func (e *Environment) DisplayToConsole(daemon bool, msg string, data ...interface{}) {
 	format := msg
 	if daemon {
 		if !strings.HasSuffix(format, "\n") {
@@ -207,16 +158,16 @@ func (e *BaseEnvironment) DisplayToConsole(daemon bool, msg string, data ...inte
 	}
 }
 
-func (e *BaseEnvironment) Update() error {
+func (e *Environment) Update() error {
 	return nil
 }
 
-func (e *BaseEnvironment) Delete() (err error) {
+func (e *Environment) Delete() (err error) {
 	err = os.RemoveAll(e.RootDirectory)
 	return
 }
 
-func (e *BaseEnvironment) Create() error {
+func (e *Environment) Create() error {
 	err := os.Mkdir(e.RootDirectory, 0755)
 	if os.IsExist(err) {
 		return nil
@@ -224,11 +175,11 @@ func (e *BaseEnvironment) Create() error {
 	return err
 }
 
-func (e *BaseEnvironment) WaitForMainProcess() error {
+func (e *Environment) WaitForMainProcess() error {
 	return e.WaitForMainProcessFor(0)
 }
 
-func (e *BaseEnvironment) WaitForMainProcessFor(timeout time.Duration) (err error) {
+func (e *Environment) WaitForMainProcessFor(timeout time.Duration) (err error) {
 	running, err := e.IsRunning()
 	if err != nil {
 		return
@@ -247,36 +198,33 @@ func (e *BaseEnvironment) WaitForMainProcessFor(timeout time.Duration) (err erro
 	return
 }
 
-func (e *BaseEnvironment) CreateWrapper() io.Writer {
+func (e *Environment) CreateWrapper() {
 	if config.ConsoleForward.Value() {
 		//return io.MultiWriter(newLogger(e.ServerId).Writer(), e.ConsoleBuffer, e.ConsoleTracker)
-		return io.MultiWriter(logging.OriginalStdOut, e.ConsoleBuffer, e.ConsoleTracker)
+		e.Wrapper = io.MultiWriter(logging.OriginalStdOut, e.ConsoleBuffer, e.ConsoleTracker)
+	} else {
+		e.Wrapper = io.MultiWriter(e.ConsoleBuffer, e.ConsoleTracker)
 	}
-	return io.MultiWriter(e.ConsoleBuffer, e.ConsoleTracker)
-}
-
-func (e *BaseEnvironment) GetBase() *BaseEnvironment {
-	return e
 }
 
-func (e *BaseEnvironment) GetLastExitCode() int {
+func (e *Environment) GetLastExitCode() int {
 	return e.LastExitCode
 }
 
-func (e *BaseEnvironment) GetWrapper() io.Writer {
+func (e *Environment) GetWrapper() io.Writer {
 	return e.Wrapper
 }
 
-func (e *BaseEnvironment) Log(l *log.Logger, format string, obj ...interface{}) {
+func (e *Environment) Log(l *log.Logger, format string, obj ...interface{}) {
 	msg := fmt.Sprintf("[%s] ", e.ServerId) + format
 	l.Printf(msg, obj...)
 }
 
-func (e *BaseEnvironment) IsInstalling() bool {
+func (e *Environment) IsInstalling() bool {
 	return e.Installing
 }
 
-func (e *BaseEnvironment) SetInstalling(flag bool) {
+func (e *Environment) SetInstalling(flag bool) {
 	e.Installing = flag
 	_ = e.StatusTracker.WriteMessage(Transmission{
 		Message: ServerRunning{
@@ -286,7 +234,7 @@ func (e *BaseEnvironment) SetInstalling(flag bool) {
 	})
 }
 
-func (e *BaseEnvironment) ExecuteInMainProcess(cmd string) (err error) {
+func (e *Environment) ExecuteInMainProcess(cmd string) (err error) {
 	running, err := e.IsRunning()
 	if err != nil {
 		return err
@@ -299,10 +247,26 @@ func (e *BaseEnvironment) ExecuteInMainProcess(cmd string) (err error) {
 	return
 }
 
-func (e *BaseEnvironment) IsRunning() (isRunning bool, err error) {
-	return e.IsRunningFunc()
+func (e *Environment) IsRunning() (isRunning bool, err error) {
+	return e.Implementation.IsRunningImpl(e)
+}
+
+func (e *Environment) Kill() error {
+	return e.Implementation.KillImpl(e)
+}
+
+func (e *Environment) GetStats() (*ServerStats, error) {
+	return e.Implementation.GetStatsImpl(e)
+}
+
+func (e *Environment) SendCode(code int) error {
+	return e.Implementation.SendCodeImpl(e, code)
+}
+
+func (e *Environment) GetUid() int {
+	return e.Implementation.GetUidImpl(e)
 }
 
-func (e *BaseEnvironment) Kill() error {
-	return e.KillFunc()
+func (e *Environment) GetGid() int {
+	return e.Implementation.GetGidImpl(e)
 }
diff --git a/environmentfactory.go b/environmentfactory.go
index ffbba270..192ef527 100644
--- a/environmentfactory.go
+++ b/environmentfactory.go
@@ -1,7 +1,7 @@
 package pufferpanel
 
 type EnvironmentFactory interface {
-	Create(id string) Environment
+	Create() EnvironmentImpl
 
 	Key() string
 }
diff --git a/files/compression.go b/files/compression.go
index 9e253289..e2be7055 100644
--- a/files/compression.go
+++ b/files/compression.go
@@ -129,9 +129,9 @@ func walker(fs FileServer, targetPath, filter string, skipRoot bool) archiver.Wa
 			}
 			var outFile *os.File
 			if fs != nil {
-				outFile, err = fs.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode())
+				outFile, err = fs.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode()|0600)
 			} else {
-				outFile, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode())
+				outFile, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode()|0600)
 			}
 
 			if err != nil {
diff --git a/files/files.go b/files/files.go
index cc1bbc32..670455eb 100644
--- a/files/files.go
+++ b/files/files.go
@@ -26,3 +26,13 @@ func CopyFile(src, dest string) error {
 	_, err = io.Copy(destination, source)
 	return err
 }
+
+func WriteFile(src io.Reader, dest string) error {
+	destination, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	defer utils.Close(destination)
+	_, err = io.Copy(destination, src)
+	return err
+}
diff --git a/files/filesystem.go b/files/filesystem.go
index f6296845..06629644 100644
--- a/files/filesystem.go
+++ b/files/filesystem.go
@@ -2,10 +2,13 @@ package files
 
 import (
 	"errors"
+	"github.com/pufferpanel/pufferpanel/v3/sys"
 	"github.com/pufferpanel/pufferpanel/v3/utils"
+	"golang.org/x/sys/unix"
 	"io/fs"
 	"os"
 	"path/filepath"
+	"strings"
 )
 
 type FileServer interface {
@@ -93,6 +96,226 @@ func (sfp *fileServer) Glob(pattern string) ([]string, error) {
 	return results, nil
 }
 
+func (sfp *fileServer) Close() error {
+	return sfp.root.Close()
+}
+
+func (sfp *fileServer) OpenFile(path string, flags int, mode os.FileMode) (*os.File, error) {
+	path = prepPath(path)
+
+	if path == "" {
+		return os.Open(sfp.dir)
+	}
+
+	//if this is not a create request, nuke mode
+	if flags&os.O_CREATE == 0 {
+		mode = 0
+	}
+
+	var fd int
+	var err error
+	if utils.UseOpenat2() {
+		//at this point, we are going to work on openat2
+		fd, err = unix.Openat2(getFd(sfp.root), path, &unix.OpenHow{
+			Flags:   uint64(flags),
+			Mode:    uint64(sys.SyscallMode(mode)),
+			Resolve: unix.RESOLVE_BENEATH,
+		})
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		//because openat is not permitted, we will have to play a game...
+		parts := strings.Split(path, string(filepath.Separator))
+
+		//follow the chain, this is just directories we're going through
+		var rootFd = getFd(sfp.root)
+		var previousFd = rootFd
+		for _, v := range parts[:len(parts)-1] {
+			fd, err = unix.Openat(previousFd, v, unix.O_NOFOLLOW|unix.O_PATH, sys.SyscallMode(0))
+			if previousFd != rootFd {
+				_ = unix.Close(previousFd)
+			}
+			if err != nil {
+				return nil, err
+			}
+			previousFd = fd
+		}
+		//now.... we can open the file
+		fd, err = unix.Openat(previousFd, parts[len(parts)-1], unix.O_NOFOLLOW|flags, sys.SyscallMode(mode))
+		if previousFd != rootFd {
+			_ = unix.Close(previousFd)
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	file := os.NewFile(uintptr(fd), filepath.Base(path))
+	if flags&os.O_CREATE == 1 && sfp.uid != -1 {
+		err = file.Chown(sfp.uid, sfp.gid)
+	}
+	return file, err
+}
+
+func (sfp *fileServer) MkdirAll(path string, mode os.FileMode) error {
+	//this is going to be recursive...
+	path = prepPath(path)
+
+	//now for each one, we just need to make each path, and hope this works
+	//in theory, the mkdir will be safe enough
+	parts := strings.Split(path, string(filepath.Separator))
+	//if it was just mkdir root... we don't do anything
+	if len(parts) == 0 {
+		return nil
+	}
+
+	var err error
+	for i := range parts {
+		err = sfp.Mkdir(filepath.Join(parts[:i+1]...), mode)
+		if err != nil && !errors.Is(err, os.ErrExist) {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (sfp *fileServer) Rename(source, target string) error {
+	source = prepPath(source)
+	target = prepPath(target)
+
+	sourceParent := filepath.Dir(source)
+	targetParent := filepath.Dir(target)
+
+	sourceName := filepath.Base(source)
+	targetName := filepath.Base(target)
+
+	sourceFolder, err := sfp.OpenFile(sourceParent, os.O_RDONLY, 0755)
+	if err != nil {
+		return err
+	}
+	defer utils.Close(sourceFolder)
+
+	targetFolder, err := sfp.OpenFile(targetParent, os.O_RDONLY, 0755)
+	if err != nil {
+		return err
+	}
+	defer utils.Close(targetFolder)
+
+	err = unix.Renameat2(getFd(sourceFolder), sourceName, getFd(targetFolder), targetName, 0)
+	return err
+}
+
+func (sfp *fileServer) Mkdir(path string, mode os.FileMode) error {
+	path = prepPath(path)
+	parent := filepath.Dir(path)
+	f := filepath.Base(path)
+
+	if parent == "" {
+		err := unix.Mkdirat(getFd(sfp.root), f, sys.SyscallMode(mode))
+		if err != nil {
+			return err
+		}
+		if sfp.uid != -1 {
+			err = unix.Fchown(getFd(sfp.root), sfp.uid, sfp.gid)
+		}
+
+		return err
+	} else {
+		folder, err := sfp.OpenFile(parent, os.O_RDONLY, mode)
+		if err != nil {
+			return err
+		}
+		defer utils.Close(folder)
+		err = unix.Mkdirat(getFd(folder), f, sys.SyscallMode(mode))
+		if err != nil {
+			return err
+		}
+		if sfp.uid != -1 {
+			err = unix.Fchown(getFd(folder), sfp.uid, sfp.gid)
+		}
+		return err
+	}
+}
+
+func (sfp *fileServer) Remove(path string) error {
+	path = prepPath(path)
+	parent := filepath.Dir(path)
+	f := filepath.Base(path)
+
+	folder, err := sfp.OpenFile(parent, os.O_RDONLY, 0755)
+	if err != nil {
+		return err
+	}
+	defer utils.Close(folder)
+
+	expected, err := sfp.OpenFile(path, os.O_RDONLY, 0644)
+	if err != nil {
+		return err
+	}
+	stat, err := expected.Stat()
+	utils.Close(expected)
+	if err != nil {
+		return err
+	}
+
+	if stat.IsDir() {
+		return unix.Unlinkat(getFd(folder), f, unix.AT_REMOVEDIR)
+	} else {
+		return unix.Unlinkat(getFd(folder), f, 0)
+	}
+}
+
+func (sfp *fileServer) RemoveAll(path string) error {
+	path = prepPath(path)
+
+	folder, err := sfp.OpenFile(path, os.O_RDONLY, 0755)
+	if err != nil {
+		return err
+	}
+	defer utils.Close(folder)
+
+	files, err := folder.ReadDir(0)
+	if err != nil {
+		return err
+	}
+
+	//go through all the files, and call our stuff to delete it
+	for _, v := range files {
+		if v.Type()&os.ModeSymlink == 0 && v.IsDir() {
+			//recursive call, so we need to go into this one and delete things
+			err = sfp.RemoveAll(filepath.Join(path, v.Name()))
+			if err != nil {
+				return err
+			}
+		} else {
+			err = unix.Unlinkat(getFd(folder), v.Name(), 0)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	err = sfp.Remove(path)
+	return err
+}
+
+func getFd(f *os.File) int {
+	return int(f.Fd())
+}
+
+func prepPath(path string) string {
+	path = filepath.Clean(path)
+	path = strings.TrimPrefix(path, "/")
+
+	if path == "." || path == "/" {
+		return ""
+	}
+
+	return path
+}
+
 // shorten maps name, which should start with f.dir, back to the suffix after f.dir.
 func (sfp *fileServer) shorten(name string) (rel string, ok bool) {
 	if name == sfp.dir {
@@ -118,7 +341,3 @@ func (sfp *fileServer) fixErr(err error) error {
 func (sfp *fileServer) resolveRootFd() (*os.File, error) {
 	return os.Open(sfp.dir)
 }
-
-func (sfp *fileServer) Close() error {
-	return sfp.root.Close()
-}
diff --git a/files/filesystem_linux.go b/files/filesystem_linux.go
deleted file mode 100644
index 98921cae..00000000
--- a/files/filesystem_linux.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package files
-
-import (
-	"errors"
-	"github.com/pufferpanel/pufferpanel/v3/sys"
-	"github.com/pufferpanel/pufferpanel/v3/utils"
-	"golang.org/x/sys/unix"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-func (sfp *fileServer) OpenFile(path string, flags int, mode os.FileMode) (*os.File, error) {
-	path = prepPath(path)
-
-	if path == "" {
-		return os.Open(sfp.dir)
-	}
-
-	//if this is not a create request, nuke mode
-	if flags&os.O_CREATE == 0 {
-		mode = 0
-	}
-
-	var fd int
-	var err error
-	if utils.UseOpenat2() {
-		//at this point, we are going to work on openat2
-		fd, err = unix.Openat2(getFd(sfp.root), path, &unix.OpenHow{
-			Flags:   uint64(flags),
-			Mode:    uint64(sys.SyscallMode(mode)),
-			Resolve: unix.RESOLVE_BENEATH,
-		})
-		if err != nil {
-			return nil, err
-		}
-	} else {
-		//because openat is not permitted, we will have to play a game...
-		parts := strings.Split(path, string(filepath.Separator))
-
-		//follow the chain, this is just directories we're going through
-		var rootFd = getFd(sfp.root)
-		var previousFd = rootFd
-		for _, v := range parts[:len(parts)-1] {
-			fd, err = unix.Openat(previousFd, v, unix.O_NOFOLLOW|unix.O_PATH, sys.SyscallMode(0))
-			if previousFd != rootFd {
-				_ = unix.Close(previousFd)
-			}
-			if err != nil {
-				return nil, err
-			}
-			previousFd = fd
-		}
-		//now.... we can open the file
-		fd, err = unix.Openat(previousFd, parts[len(parts)-1], unix.O_NOFOLLOW|flags, sys.SyscallMode(mode))
-		if previousFd != rootFd {
-			_ = unix.Close(previousFd)
-		}
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	file := os.NewFile(uintptr(fd), filepath.Base(path))
-	if flags&os.O_CREATE == 1 && sfp.uid != -1 {
-		err = file.Chown(sfp.uid, sfp.gid)
-	}
-	return file, err
-}
-
-func (sfp *fileServer) MkdirAll(path string, mode os.FileMode) error {
-	//this is going to be recursive...
-	path = prepPath(path)
-
-	//now for each one, we just need to make each path, and hope this works
-	//in theory, the mkdir will be safe enough
-	parts := strings.Split(path, string(filepath.Separator))
-	//if it was just mkdir root... we don't do anything
-	if len(parts) == 0 {
-		return nil
-	}
-
-	var err error
-	for i := range parts {
-		err = sfp.Mkdir(filepath.Join(parts[:i+1]...), mode)
-		if err != nil && !errors.Is(err, os.ErrExist) {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (sfp *fileServer) Rename(source, target string) error {
-	source = prepPath(source)
-	target = prepPath(target)
-
-	sourceParent := filepath.Dir(source)
-	targetParent := filepath.Dir(target)
-
-	sourceName := filepath.Base(source)
-	targetName := filepath.Base(target)
-
-	sourceFolder, err := sfp.OpenFile(sourceParent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(sourceFolder)
-
-	targetFolder, err := sfp.OpenFile(targetParent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(targetFolder)
-
-	err = unix.Renameat2(getFd(sourceFolder), sourceName, getFd(targetFolder), targetName, 0)
-	return err
-}
-
-func (sfp *fileServer) Mkdir(path string, mode os.FileMode) error {
-	path = prepPath(path)
-	parent := filepath.Dir(path)
-	f := filepath.Base(path)
-
-	if parent == "" {
-		err := unix.Mkdirat(getFd(sfp.root), f, sys.SyscallMode(mode))
-		if err != nil {
-			return err
-		}
-		if sfp.uid != -1 {
-			err = unix.Fchown(getFd(sfp.root), sfp.uid, sfp.gid)
-		}
-
-		return err
-	} else {
-		folder, err := sfp.OpenFile(parent, os.O_RDONLY, mode)
-		if err != nil {
-			return err
-		}
-		defer utils.Close(folder)
-		err = unix.Mkdirat(getFd(folder), f, sys.SyscallMode(mode))
-		if err != nil {
-			return err
-		}
-		if sfp.uid != -1 {
-			err = unix.Fchown(getFd(folder), sfp.uid, sfp.gid)
-		}
-		return err
-	}
-}
-
-func (sfp *fileServer) Remove(path string) error {
-	path = prepPath(path)
-	parent := filepath.Dir(path)
-	f := filepath.Base(path)
-
-	folder, err := sfp.OpenFile(parent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(folder)
-
-	expected, err := sfp.OpenFile(path, os.O_RDONLY, 0644)
-	if err != nil {
-		return err
-	}
-	stat, err := expected.Stat()
-	utils.Close(expected)
-	if err != nil {
-		return err
-	}
-
-	if stat.IsDir() {
-		return unix.Unlinkat(getFd(folder), f, unix.AT_REMOVEDIR)
-	} else {
-		return unix.Unlinkat(getFd(folder), f, 0)
-	}
-}
-
-func (sfp *fileServer) RemoveAll(path string) error {
-	path = prepPath(path)
-
-	folder, err := sfp.OpenFile(path, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(folder)
-
-	files, err := folder.ReadDir(0)
-	if err != nil {
-		return err
-	}
-
-	//go through all the files, and call our stuff to delete it
-	for _, v := range files {
-		if v.Type()&os.ModeSymlink == 0 && v.IsDir() {
-			//recursive call, so we need to go into this one and delete things
-			err = sfp.RemoveAll(filepath.Join(path, v.Name()))
-			if err != nil {
-				return err
-			}
-		} else {
-			err = unix.Unlinkat(getFd(folder), v.Name(), 0)
-			if err != nil {
-				return err
-			}
-		}
-	}
-
-	err = sfp.Remove(path)
-	return err
-}
-
-func getFd(f *os.File) int {
-	return int(f.Fd())
-}
-
-func prepPath(path string) string {
-	path = filepath.Clean(path)
-	path = strings.TrimPrefix(path, "/")
-
-	if path == "." || path == "/" {
-		return ""
-	}
-
-	return path
-}
diff --git a/files/filesystem_windows.go b/files/filesystem_windows.go
deleted file mode 100644
index 8ddc8021..00000000
--- a/files/filesystem_windows.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package files
-
-import (
-	"errors"
-	"github.com/pufferpanel/pufferpanel/v3/utils"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-func (sfp *fileServer) OpenFile(path string, flags int, mode os.FileMode) (*os.File, error) {
-	path = prepPath(path)
-
-	if path == "" {
-		return os.Open(sfp.dir)
-	}
-
-	//if this is not a create request, nuke mode
-	if flags&os.O_CREATE == 0 {
-		mode = 0
-	}
-
-	//at this point, we are going to work on openat2
-	fd, err := os.OpenFile(filepath.Join(sfp.dir, path), flags, mode)
-	if err != nil {
-		return nil, err
-	}
-	fi, err := fd.Stat()
-	if err != nil {
-		return nil, err
-	}
-	//for windows, hard deny symlinks
-	if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
-		return nil, errors.New("access denied")
-	}
-	return fd, nil
-}
-
-func (sfp *fileServer) MkdirAll(path string, mode os.FileMode) error {
-	//this is going to be recursive...
-	path = prepPath(path)
-
-	//now for each one, we just need to make each path, and hope this works
-	//in theory, the mkdir will be safe enough
-	parts := strings.Split(path, string(filepath.Separator))
-	//if it was just mkdir root... we don't do anything
-	if len(parts) == 0 {
-		return nil
-	}
-
-	var err error
-	for i := range parts {
-		err = sfp.Mkdir(filepath.Join(parts[:i+1]...), mode)
-		if err != nil && !errors.Is(err, os.ErrExist) {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (sfp *fileServer) Rename(source, target string) error {
-	source = prepPath(source)
-	target = prepPath(target)
-
-	sourceParent := filepath.Dir(source)
-	targetParent := filepath.Dir(target)
-
-	sourceName := filepath.Base(source)
-	targetName := filepath.Base(target)
-
-	sourceFolder, err := sfp.OpenFile(sourceParent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(sourceFolder)
-
-	targetFolder, err := sfp.OpenFile(targetParent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(targetFolder)
-
-	err = os.Rename(filepath.Join(sfp.dir, sourceParent, sourceName), filepath.Join(sfp.dir, targetParent, targetName))
-	return err
-}
-
-func (sfp *fileServer) Mkdir(path string, mode os.FileMode) error {
-	path = prepPath(path)
-	parent := filepath.Dir(path)
-
-	folder, err := sfp.OpenFile(parent, os.O_RDONLY, mode)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(folder)
-	return os.Mkdir(filepath.Join(sfp.dir, path), mode)
-}
-
-func (sfp *fileServer) Remove(path string) error {
-	path = prepPath(path)
-	parent := filepath.Dir(path)
-
-	folder, err := sfp.OpenFile(parent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(folder)
-
-	return os.Remove(filepath.Join(sfp.dir, path))
-}
-
-func (sfp *fileServer) RemoveAll(path string) error {
-	path = prepPath(path)
-	parent := filepath.Dir(path)
-
-	folder, err := sfp.OpenFile(parent, os.O_RDONLY, 0755)
-	if err != nil {
-		return err
-	}
-	defer utils.Close(folder)
-
-	return os.Remove(filepath.Join(sfp.dir, path))
-}
-
-func prepPath(path string) string {
-	path = strings.Replace(path, "/", "\\", -1)
-	path = filepath.Clean(path)
-	path = strings.TrimPrefix(path, "\\")
-	return path
-}
diff --git a/go.mod b/go.mod
index 75cb7e62..86188b8d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,75 +1,81 @@
 module github.com/pufferpanel/pufferpanel/v3
 
-go 1.23.0
+go 1.24
 
 require (
-	github.com/AlecAivazis/survey/v2 v2.3.7
-	github.com/MicahParks/jwkset v0.8.0
-	github.com/MicahParks/keyfunc/v3 v3.3.10
+	github.com/MicahParks/jwkset v0.9.6
+	github.com/MicahParks/keyfunc/v3 v3.4.0
 	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
 	github.com/braintree/manners v0.0.0-20160418043613-82a8879fc5fd
 	github.com/cavaliergopher/grab/v3 v3.0.1
 	github.com/creack/pty v1.1.24
-	github.com/docker/docker v27.5.1+incompatible
+	github.com/docker/docker v28.2.2+incompatible
 	github.com/docker/go-connections v0.5.0
 	github.com/dreamscached/minequery/v2 v2.5.0
-	github.com/gin-contrib/cors v1.7.3
-	github.com/gin-contrib/gzip v1.2.2
-	github.com/gin-contrib/sessions v1.0.2
-	github.com/gin-gonic/gin v1.10.0
-	github.com/go-co-op/gocron/v2 v2.15.0
-	github.com/go-git/go-git/v5 v5.13.2
-	github.com/go-gormigrate/gormigrate/v2 v2.1.3
-	github.com/gofrs/uuid/v5 v5.3.1
-	github.com/golang-jwt/jwt/v5 v5.2.1
-	github.com/google/cel-go v0.23.2
+	github.com/gin-contrib/cors v1.7.5
+	github.com/gin-contrib/gzip v1.2.3
+	github.com/gin-contrib/sessions v1.0.4
+	github.com/gin-gonic/gin v1.10.1
+	github.com/go-co-op/gocron/v2 v2.16.2
+	github.com/go-git/go-git/v5 v5.16.2
+	github.com/go-gormigrate/gormigrate/v2 v2.1.4
+	github.com/gofrs/uuid/v5 v5.3.2
+	github.com/golang-jwt/jwt/v5 v5.2.2
+	github.com/google/cel-go v0.25.0
 	github.com/gorcon/rcon v1.4.0
 	github.com/gorilla/securecookie v1.1.2
 	github.com/gorilla/websocket v1.5.3
 	github.com/hashicorp/go-version v1.7.0
-	github.com/klauspost/compress v1.17.11
-	github.com/mailgun/mailgun-go/v4 v4.22.1
+	github.com/klauspost/compress v1.18.0
+	github.com/mailgun/mailgun-go/v4 v4.23.0
 	github.com/mailjet/mailjet-apiv3-go/v4 v4.0.7
-	github.com/mattn/go-sqlite3 v1.14.24
+	github.com/mattn/go-sqlite3 v1.14.28
 	github.com/mholt/archiver/v3 v3.5.1
-	github.com/opencontainers/image-spec v1.1.0
-	github.com/pkg/sftp v1.13.7
-	github.com/pquerna/otp v1.4.0
-	github.com/sendgrid/sendgrid-go v3.16.0+incompatible
+	github.com/opencontainers/image-spec v1.1.1
+	github.com/pkg/sftp v1.13.9
+	github.com/pquerna/otp v1.5.0
+	github.com/pterm/pterm v0.12.81
+	github.com/sendgrid/sendgrid-go v3.16.1+incompatible
 	github.com/shirou/gopsutil v3.21.11+incompatible
-	github.com/spf13/cast v1.7.1
-	github.com/spf13/cobra v1.9.0
-	github.com/spf13/viper v1.19.0
+	github.com/spf13/cast v1.9.2
+	github.com/spf13/cobra v1.9.1
+	github.com/spf13/viper v1.20.1
 	github.com/stretchr/testify v1.10.0
 	github.com/swaggo/files v1.0.1
 	github.com/swaggo/gin-swagger v1.6.0
 	github.com/swaggo/swag v1.16.4
 	github.com/wneessen/go-mail v0.6.2
-	golang.org/x/crypto v0.33.0
-	golang.org/x/sys v0.30.0
+	golang.org/x/crypto v0.39.0
+	golang.org/x/sys v0.33.0
 	gopkg.in/go-playground/validator.v9 v9.31.0
-	gorm.io/driver/mysql v1.5.7
-	gorm.io/driver/postgres v1.5.11
-	gorm.io/driver/sqlite v1.5.7
-	gorm.io/driver/sqlserver v1.5.4
-	gorm.io/gorm v1.25.12
+	gorm.io/driver/mysql v1.6.0
+	gorm.io/driver/postgres v1.6.0
+	gorm.io/driver/sqlite v1.6.0
+	gorm.io/driver/sqlserver v1.6.0
+	gorm.io/gorm v1.30.0
 )
 
 require (
-	cel.dev/expr v0.20.0 // indirect
-	dario.cat/mergo v1.0.1 // indirect
+	atomicgo.dev/cursor v0.2.0 // indirect
+	atomicgo.dev/keyboard v0.2.9 // indirect
+	atomicgo.dev/schedule v0.1.0 // indirect
+	cel.dev/expr v0.23.1 // indirect
+	dario.cat/mergo v1.0.2 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
 	github.com/KyleBanks/depth v1.2.1 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
-	github.com/ProtonMail/go-crypto v1.1.5 // indirect
+	github.com/ProtonMail/go-crypto v1.3.0 // indirect
 	github.com/andybalholm/brotli v1.1.1 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
 	github.com/boombuler/barcode v1.0.2 // indirect
-	github.com/bytedance/sonic v1.12.8 // indirect
-	github.com/bytedance/sonic/loader v0.2.3 // indirect
-	github.com/cloudflare/circl v1.6.0 // indirect
+	github.com/bytedance/sonic v1.13.3 // indirect
+	github.com/bytedance/sonic/loader v0.2.4 // indirect
+	github.com/cloudflare/circl v1.6.1 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
+	github.com/containerd/console v1.0.5 // indirect
+	github.com/containerd/errdefs v1.0.0 // indirect
+	github.com/containerd/errdefs/pkg v0.3.0 // indirect
 	github.com/containerd/log v0.1.0 // indirect
 	github.com/cyphar/filepath-securejoin v0.4.1 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -78,59 +84,58 @@ require (
 	github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/fsnotify/fsnotify v1.8.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
-	github.com/gin-contrib/sse v1.0.0 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+	github.com/gin-contrib/sse v1.1.0 // indirect
 	github.com/go-chi/chi/v5 v5.2.1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-git/go-billy/v5 v5.6.2 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
-	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonpointer v0.21.1 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/spec v0.21.0 // indirect
-	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-openapi/swag v0.23.1 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.25.0 // indirect
-	github.com/go-sql-driver/mysql v1.8.1 // indirect
+	github.com/go-playground/validator/v10 v10.26.0 // indirect
+	github.com/go-sql-driver/mysql v1.9.2 // indirect
+	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
 	github.com/golang-sql/sqlexp v0.1.0 // indirect
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
-	github.com/golang/snappy v0.0.4 // indirect
+	github.com/golang/snappy v1.0.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/gookit/color v1.5.4 // indirect
 	github.com/gorilla/context v1.1.2 // indirect
 	github.com/gorilla/sessions v1.4.0 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
-	github.com/jackc/pgx/v5 v5.7.2 // indirect
+	github.com/jackc/pgx/v5 v5.7.5 // indirect
 	github.com/jackc/puddle/v2 v2.2.2 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
-	github.com/jonboulle/clockwork v0.4.0 // indirect
+	github.com/jonboulle/clockwork v0.5.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
-	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/klauspost/pgzip v1.2.6 // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
-	github.com/magiconair/properties v1.8.9 // indirect
+	github.com/lithammer/fuzzysearch v1.1.8 // indirect
 	github.com/mailgun/errors v0.4.0 // indirect
 	github.com/mailru/easyjson v0.9.0 // indirect
-	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
-	github.com/microsoft/go-mssqldb v1.8.0 // indirect
-	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/microsoft/go-mssqldb v1.8.2 // indirect
 	github.com/moby/docker-image-spec v1.3.1 // indirect
+	github.com/moby/sys/atomicwriter v0.1.0 // indirect
 	github.com/moby/term v0.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -138,51 +143,52 @@ require (
 	github.com/nwaples/rardecode v1.1.3 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
-	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
 	github.com/pjbgf/sha1cd v0.3.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
-	github.com/sagikazarmark/locafero v0.7.0 // indirect
-	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+	github.com/sagikazarmark/locafero v0.9.0 // indirect
 	github.com/sendgrid/rest v2.6.9+incompatible // indirect
-	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+	github.com/sergi/go-diff v1.4.0 // indirect
 	github.com/skeema/knownhosts v1.3.1 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
-	github.com/spf13/afero v1.12.0 // indirect
+	github.com/spf13/afero v1.14.0 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/stoewer/go-strcase v1.3.0 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/tklauser/go-sysconf v0.3.14 // indirect
 	github.com/tklauser/numcpus v0.9.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.12 // indirect
+	github.com/ugorji/go/codec v1.2.14 // indirect
 	github.com/ulikunitz/xz v0.5.12 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
 	go.opentelemetry.io/otel v1.34.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
 	go.opentelemetry.io/otel/metric v1.34.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.27.0 // indirect
 	go.opentelemetry.io/otel/trace v1.34.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/arch v0.14.0 // indirect
+	golang.org/x/arch v0.18.0 // indirect
 	golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
-	golang.org/x/net v0.35.0 // indirect
-	golang.org/x/sync v0.11.0 // indirect
-	golang.org/x/term v0.29.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	golang.org/x/time v0.10.0 // indirect
-	golang.org/x/tools v0.30.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
+	golang.org/x/sync v0.15.0 // indirect
+	golang.org/x/term v0.32.0 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	golang.org/x/time v0.12.0 // indirect
+	golang.org/x/tools v0.34.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
-	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	gotest.tools/v3 v3.5.1 // indirect
+	gotest.tools/v3 v3.5.2 // indirect
 )
diff --git a/go.sum b/go.sum
index 2833fd6d..778e3e4f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,22 +1,25 @@
-cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
-cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
-dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
-dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
+atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
+atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
+atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
+atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
+atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
+atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
+atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
+cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
+cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
-github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
@@ -25,23 +28,29 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occ
 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
 github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
-github.com/MicahParks/jwkset v0.8.0 h1:jHtclI38Gibmu17XMI6+6/UB59srp58pQVxePHRK5o8=
-github.com/MicahParks/jwkset v0.8.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
-github.com/MicahParks/keyfunc/v3 v3.3.10 h1:JtEGE8OcNeI297AMrR4gVXivV8fyAawFUMkbwNreJRk=
-github.com/MicahParks/keyfunc/v3 v3.3.10/go.mod h1:1TEt+Q3FO7Yz2zWeYO//fMxZMOiar808NqjWQQpBPtU=
+github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
+github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
+github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
+github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
+github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
+github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
+github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
+github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
+github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
+github.com/MicahParks/jwkset v0.9.6 h1:Tf8l2/MOby5Kh3IkrqzThPQKfLytMERoAsGZKlyYZxg=
+github.com/MicahParks/jwkset v0.9.6/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
+github.com/MicahParks/keyfunc/v3 v3.4.0 h1:g03TXq6NjhZyO/UkODl//abm4KiLLNRi0VhW7vGOHyg=
+github.com/MicahParks/keyfunc/v3 v3.4.0/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
-github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
-github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
-github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
@@ -53,29 +62,36 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
 github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
 github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/braintree/manners v0.0.0-20160418043613-82a8879fc5fd h1:ePesaBzdTmoMQjwqRCLP2jY+jjWMBpwws/LEQdt1fMM=
 github.com/braintree/manners v0.0.0-20160418043613-82a8879fc5fd/go.mod h1:TNehV1AhBwtT7Bd+rh8G6MoGDbBLNs/sKdk3nvr4Yzg=
-github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
-github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
+github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
+github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
-github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
-github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
 github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
-github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
-github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
+github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
 github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
 github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
 github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
+github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
-github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
 github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
@@ -88,8 +104,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
 github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
-github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
-github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
+github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -100,44 +116,44 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6
 github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
 github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
-github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
-github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
+github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
+github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
-github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
-github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
-github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
-github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
-github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
-github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
-github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
-github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
-github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
-github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
-github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
-github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
+github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
+github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
+github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
+github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
+github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
 github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
 github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
-github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
-github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
+github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
+github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
-github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
-github.com/go-gormigrate/gormigrate/v2 v2.1.3 h1:ei3Vq/rpPI/jCJY9mRHJAKg5vU+EhZyWhBAkaAomQuw=
-github.com/go-gormigrate/gormigrate/v2 v2.1.3/go.mod h1:VJ9FIOBAur+NmQ8c4tDVwOuiJcgupTG105FexPFrXzA=
+github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
+github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
+github.com/go-gormigrate/gormigrate/v2 v2.1.4 h1:KOPEt27qy1cNzHfMZbp9YTmEuzkY4F4wrdsJW9WFk1U=
+github.com/go-gormigrate/gormigrate/v2 v2.1.4/go.mod h1:y/6gPAH6QGAgP1UfHMiXcqGeJ88/GRQbfCReE1JJD5Y=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -146,35 +162,38 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
-github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
-github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
+github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
 github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
 github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
 github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
 github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
+github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
-github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
-github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
-github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
-github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
+github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
-github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
+github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
+github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -182,20 +201,25 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4=
-github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
+github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
+github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
+github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
+github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
 github.com/gorcon/rcon v1.4.0 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME=
 github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I=
 github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -211,55 +235,50 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
 github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
-github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
-github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
+github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
 github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
 github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
 github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
-github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
-github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
 github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
-github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
+github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
+github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
-github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
-github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
+github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -277,36 +296,34 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
-github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
+github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
 github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
 github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0=
-github.com/mailgun/mailgun-go/v4 v4.22.1 h1:yMvPeo9m5XPVVg3XF0aPiJiiGt/n/cayBa4eQBDYqtc=
-github.com/mailgun/mailgun-go/v4 v4.22.1/go.mod h1:JA2xbLTkEWrX2TO+RUiJQALZus6WLLoXym2i8a8F5sE=
+github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
+github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
 github.com/mailjet/mailjet-apiv3-go/v4 v4.0.7 h1:Na8QAWN7g6VgAxK2fYPnbxQ7Vws2tE0hrb08oOhNNyw=
 github.com/mailjet/mailjet-apiv3-go/v4 v4.0.7/go.mod h1:2SU3t6eh/uK6BSeBmdhpIUau99L4iPlIfbx4o4pAUQs=
 github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
-github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
-github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
+github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
 github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
-github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
-github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
-github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
-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/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
+github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
+github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
 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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -315,7 +332,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
-github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
@@ -325,44 +342,56 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
 github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
-github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
-github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
 github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
 github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
+github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
-github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
+github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
+github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
-github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
+github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
+github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
+github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
+github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
+github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
+github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
+github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
+github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
+github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
-github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
 github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
 github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
-github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
-github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
-github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
-github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs=
+github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
+github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
 github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
 github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -372,22 +401,21 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
 github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
-github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
-github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
-github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
-github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE=
-github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
+github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
+github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
 github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -396,7 +424,6 @@ 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -413,8 +440,8 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD
 github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
-github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
+github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
@@ -425,6 +452,9 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
+github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -444,8 +474,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
 go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
 go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
-go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
+go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
+go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
 go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
 go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
@@ -454,25 +484,22 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
-golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
+golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
 golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -482,30 +509,26 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
+golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -514,20 +537,27 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -535,50 +565,44 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
-golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
 golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -587,8 +611,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
 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=
@@ -597,10 +621,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
-google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
-google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -609,29 +633,29 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
 gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
-gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
-gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
-gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
-gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
-gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
-gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
-gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
-gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
-gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
+gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
+gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
+gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
+gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
+gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
+gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
+gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
 gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
 gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/groups/groups.go b/groups/groups.go
index 2253fa8f..b670ff49 100644
--- a/groups/groups.go
+++ b/groups/groups.go
@@ -1,5 +1,3 @@
-//go:build !windows
-
 package groups
 
 import (
diff --git a/groups/groups_windows.go b/groups/groups_windows.go
deleted file mode 100644
index 8e28d622..00000000
--- a/groups/groups_windows.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build !linux
-
-package groups
-
-func IsUserIn(groups ...string) bool {
-	return true
-}
diff --git a/installer.wxs b/installer.wxs
deleted file mode 100644
index 1f795bc7..00000000
--- a/installer.wxs
+++ /dev/null
@@ -1,70 +0,0 @@
-<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:firewall="http://wixtoolset.org/schemas/v4/wxs/firewall"
-     xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
-    <Package Version="$(var.ProductVersion)" UpgradeCode="b0ef7063-2eaa-48b0-948d-00167774557a" Language="1033"
-             Name="PufferPanel" Manufacturer="PufferPanel" InstallerVersion="300">
-        <Media Id="1" Cabinet="pufferpanel.cab" EmbedCab="yes"/>
-        <MajorUpgrade AllowSameVersionUpgrades="yes"
-                      DowngradeErrorMessage="A newer version of [ProductName] is already installed."/>
-
-        <!-- Step 1: Define the directory structure -->
-        <StandardDirectory Id="ProgramFiles64Folder">
-            <Directory Id="APPLICATIONROOTDIRECTORY" Name="PufferPanel">
-            </Directory>
-        </StandardDirectory>
-
-        <StandardDirectory Id="CommonAppDataFolder">
-            <Directory Id="ProgramDataVendorFolder" Name="PufferPanel">
-            </Directory>
-        </StandardDirectory>
-
-        <!-- Step 2: Add files to your installer package -->
-        <DirectoryRef Id="APPLICATIONROOTDIRECTORY">
-            <Component Id="pufferpanel" Guid="6a923f7e-b685-417e-8baf-612b0cade288">
-                <util:User Id="UpdateUserLogonAsService" UpdateIfExists="yes" CreateUser="yes" Name="PufferPanelService"
-                           PasswordNeverExpires="yes" Password="$(var.Password)"
-                           LogonAsService="yes"/>
-                <File Id="pufferpanel.exe" Source="pufferpanel.exe" KeyPath="yes" Checksum="yes">
-                    <firewall:FirewallException Id="PanelRule" Name="PufferPanel Web Interface" Scope="any"/>
-                    <firewall:FirewallException Id="SFTPRule" Name="PufferPanel SFTP Interface" Scope="any"/>
-                </File>
-                <ServiceInstall Id="ServiceInstaller" Type="ownProcess" Start="auto" Name="PufferPanel"
-                                Description="Game management server panel. This service can be either the daemon or the panel, or both."
-                                Account=".\PufferPanelService"
-                                Password="$(var.Password)"
-                                ErrorControl="normal"
-                                Arguments="runService --workDir=&quot;[ProgramDataVendorFolder]\&quot;" Vital="yes">
-                </ServiceInstall>
-                <ServiceControl Id="StartService" Stop="both" Remove="uninstall" Name="PufferPanel" Wait="yes"/>
-            </Component>
-            <Component Id="RegistryEntries" Guid="34c0bf3c-fa7f-485f-9704-805549e90f66">
-                <RegistryKey Root="HKLM" Key="SYSTEM\CurrentControlSet\Services\EventLog\Application\PufferPanel">
-                    <RegistryValue Type="expandable" Name="EventMessageFile"
-                                   Value="%SystemRoot%\System32\EventCreate.exe" KeyPath="yes"/>
-                    <RegistryValue Type="integer" Name="TypesSupported" Value="7"/>
-                </RegistryKey>
-            </Component>
-        </DirectoryRef>
-
-        <DirectoryRef Id="ProgramDataVendorFolder">
-            <Component Id="CreateServerDirectory" Guid="13ae94b7-9ef5-4181-bfa9-933844a13418"
-                       Permanent="yes">
-                <CreateFolder>
-                    <util:PermissionEx User="PufferPanelService" GenericAll="yes"/>
-                </CreateFolder>
-            </Component>
-        </DirectoryRef>
-
-        <!-- Step 3: Tell WiX to install the files -->
-        <Feature Id="MainApplication" Title="PufferPanel" Level="1">
-            <ComponentRef Id="pufferpanel"/>
-            <ComponentRef Id="RegistryEntries"/>
-            <ComponentRef Id="CreateServerDirectory"/>
-        </Feature>
-
-        <Icon Id="icon.ico" SourceFile="favicon.ico"/>
-        <WixVariable Id="WixUILicenseRtf" Value="license.rtf"/>
-        <Property Id="ARPPRODUCTICON" Value="icon.ico"/>
-        <Property Id="WIXUI_INSTALLDIR" Value="APPLICATIONROOTDIRECTORY"/>
-        <ui:WixUI Id="WixUI_InstallDir"/>
-    </Package>
-</Wix>
diff --git a/logging/logger.go b/logging/logger.go
index 752d3fa8..f416ea33 100644
--- a/logging/logger.go
+++ b/logging/logger.go
@@ -42,11 +42,11 @@ func Initialize(useFiles bool) {
 
 	//first, create STDERR
 
-	stderr := MultiWriter(rotation, os.Stderr, CreateServiceLogger("error"))
+	stderr := MultiWriter(rotation, os.Stderr)
 	Error = log.New(stderr, "[ERROR] ", flags)
 
 	//now, STDOUT
-	stdout := MultiWriter(rotation, os.Stdout, CreateServiceLogger("info"))
+	stdout := MultiWriter(rotation, os.Stdout)
 	Info = log.New(stdout, "[INFO] ", flags)
 
 	//and now, a DEBUG
diff --git a/logging/logger_noservice.go b/logging/logger_noservice.go
deleted file mode 100644
index 2099f48d..00000000
--- a/logging/logger_noservice.go
+++ /dev/null
@@ -1,11 +0,0 @@
-//go:build !windows
-
-package logging
-
-import (
-	"io"
-)
-
-func CreateServiceLogger(string) io.Writer {
-	return nil
-}
diff --git a/logging/logger_service.go b/logging/logger_service.go
deleted file mode 100644
index ac8f6b8e..00000000
--- a/logging/logger_service.go
+++ /dev/null
@@ -1,37 +0,0 @@
-//go:build windows
-
-package logging
-
-import (
-	"golang.org/x/sys/windows/svc/eventlog"
-	"io"
-)
-
-var elog *eventlog.Log
-
-func CreateServiceLogger(t string) io.Writer {
-	if elog == nil {
-		var err error
-		elog, err = eventlog.Open("PufferPanel")
-		if err != nil {
-			panic(err)
-		}
-	}
-
-	return &eventLogWriter{Type: t}
-}
-
-type eventLogWriter struct {
-	Type string
-}
-
-func (e *eventLogWriter) Write(p []byte) (n int, err error) {
-	switch e.Type {
-	case "error":
-		err = elog.Error(1, string(p))
-	case "info":
-		err = elog.Info(1, string(p))
-	}
-
-	return len(p), err
-}
diff --git a/logging/rotator.go b/logging/rotator.go
index 027929de..51716f3f 100644
--- a/logging/rotator.go
+++ b/logging/rotator.go
@@ -2,7 +2,11 @@ package logging
 
 import (
 	"io"
+	"os"
+	"os/signal"
+	"path"
 	"sync"
+	"syscall"
 )
 
 type Rotator struct {
@@ -34,3 +38,21 @@ func (r *Rotator) Rotate(newBackend io.WriteCloser) {
 	r.backer = newBackend
 	_ = oldBacker.Close()
 }
+
+func (r *Rotator) StartRotation(dir string) {
+	go func(directory string) {
+		sig := make(chan os.Signal, 1)
+		for {
+			signal.Notify(sig, syscall.SIGUSR1)
+
+			<-sig
+
+			newFile, err := os.OpenFile(path.Join(directory, "pufferpanel.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+			if err != nil {
+				panic(err)
+			}
+
+			r.Rotate(newFile)
+		}
+	}(dir)
+}
diff --git a/logging/rotator_notwindows.go b/logging/rotator_notwindows.go
deleted file mode 100644
index 1adab006..00000000
--- a/logging/rotator_notwindows.go
+++ /dev/null
@@ -1,28 +0,0 @@
-//go:build !windows
-
-package logging
-
-import (
-	"os"
-	"os/signal"
-	"path"
-	"syscall"
-)
-
-func (r *Rotator) StartRotation(dir string) {
-	go func(directory string) {
-		sig := make(chan os.Signal, 1)
-		for {
-			signal.Notify(sig, syscall.SIGUSR1)
-
-			<-sig
-
-			newFile, err := os.OpenFile(path.Join(directory, "pufferpanel.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
-			if err != nil {
-				panic(err)
-			}
-
-			r.Rotate(newFile)
-		}
-	}(dir)
-}
diff --git a/logging/rotator_windows.go b/logging/rotator_windows.go
deleted file mode 100644
index 731fb69d..00000000
--- a/logging/rotator_windows.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package logging
-
-func (r *Rotator) StartRotation(dir string) {
-}
diff --git a/operation.go b/operation.go
index eff94c19..6c5c7908 100644
--- a/operation.go
+++ b/operation.go
@@ -17,7 +17,7 @@ type CreateOperation struct {
 }
 
 type RunOperatorArgs struct {
-	Environment Environment
+	Environment *Environment
 	Server      DaemonServer
 }
 
diff --git a/operations/command/command.go b/operations/command/command.go
index f7e291c8..8a92704a 100644
--- a/operations/command/command.go
+++ b/operations/command/command.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"github.com/pufferpanel/pufferpanel/v3"
 	"github.com/pufferpanel/pufferpanel/v3/logging"
-	"github.com/pufferpanel/pufferpanel/v3/utils"
 )
 
 type Command struct {
@@ -21,11 +20,9 @@ func (c Command) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationResu
 	for _, cmd := range c.Commands {
 		logging.Info.Printf("Executing command: %s", cmd)
 		env.DisplayToConsole(true, fmt.Sprintf("Executing: %s\n", cmd))
-		cmdToExec, cmdArgs := utils.SplitArguments(cmd)
 		ch := make(chan error, 1)
 		err := env.Execute(pufferpanel.ExecutionData{
-			Command:     cmdToExec,
-			Arguments:   cmdArgs,
+			Command:     cmd,
 			Environment: c.Env,
 			Callback: func(exitCode int) {
 				if exitCode != 0 {
@@ -33,8 +30,9 @@ func (c Command) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationResu
 				}
 				ch <- nil
 			},
-			StdInConfig: c.StdIn,
-			Variables:   c.Variables,
+			StdInConfig:  c.StdIn,
+			Variables:    c.Variables,
+			DisableStats: true,
 		})
 		if err != nil {
 			return pufferpanel.OperationResult{Error: err}
diff --git a/operations/curseforge/const.go b/operations/curseforge/const.go
index 71e39569..5c931638 100644
--- a/operations/curseforge/const.go
+++ b/operations/curseforge/const.go
@@ -5,6 +5,6 @@ const ImprovedFabricInstallerUrl = "https://meta.fabricmc.net/v2/versions/loader
 const ForgeInstallerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/${mcVersion}-${version}/forge-${mcVersion}-${version}-installer.jar"
 const ForgeInstallerName = "forge-${mcVersion}-${version}-installer.jar"
 const NeoforgeInstallerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/${mcVersion}-${version}/forge-${mcVersion}-${version}-installer.jar"
-const NeoforgeInstallerName = "neoforge-${version}-installer.jar"
+const NeoforgeInstallerName = "neoforgedl-${version}-installer.jar"
 const NeoForgeServerStarterVersion = "0.1.25"
 const NeoForgeServerStarter = "https://github.com/neoforged/ServerStarterJar/releases/download/" + NeoForgeServerStarterVersion + "/server.jar"
diff --git a/operations/curseforge/curseforge.go b/operations/curseforge/curseforge.go
index 6d8a7da6..3484109c 100644
--- a/operations/curseforge/curseforge.go
+++ b/operations/curseforge/curseforge.go
@@ -2,6 +2,7 @@ package curseforge
 
 import (
 	"errors"
+	"fmt"
 	"github.com/pufferpanel/pufferpanel/v3"
 	"github.com/pufferpanel/pufferpanel/v3/config"
 	"github.com/pufferpanel/pufferpanel/v3/files"
@@ -39,7 +40,7 @@ type CurseForge struct {
 //-   download "old" installer to the cache
 //-   run installer in cache
 //-   copy directory to server
-//- neoforge
+//- neoforgedl
 //-   same as forge, but screw downloading
 //- quilt
 //-   will not deal with
@@ -142,8 +143,8 @@ func (c CurseForge) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationR
 	var vars map[string]string
 	var manifest Manifest
 	if jar, err = findInstallerJar(env); err == nil {
-		if strings.HasPrefix(jar, "neoforge") {
-			modLoader = "neoforge"
+		if strings.HasPrefix(jar, "neoforgedl") {
+			modLoader = "neoforgedl"
 		} else {
 			modLoader = "forge"
 		}
@@ -183,12 +184,13 @@ func (c CurseForge) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationR
 				forgeUrl := replaceTokens(ForgeInstallerUrl, data)
 				forgeInstaller := replaceTokens(ForgeInstallerName, data)
 
-				jarFile, err = pufferpanel.DownloadViaMaven(forgeUrl, env)
+				dl, err := pufferpanel.DownloadViaMaven(forgeUrl, env)
+				defer utils.Close(dl)
 				if err != nil {
 					return pufferpanel.OperationResult{Error: err}
 				}
 				//copy to server
-				err = files.CopyFile(jarFile, filepath.Join(env.GetRootDirectory(), forgeInstaller))
+				err = files.WriteFile(dl, filepath.Join(env.GetRootDirectory(), forgeInstaller))
 				if err != nil {
 					return pufferpanel.OperationResult{Error: err}
 				}
@@ -206,9 +208,9 @@ func (c CurseForge) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationR
 				return pufferpanel.OperationResult{Error: err}
 			}
 		}
-	case "neoforge":
+	case "neoforgedl":
 		{
-			//for neoforge, we need a jar
+			//for neoforgedl, we need a jar
 			jarFile := data["jar"]
 			if jarFile == "" {
 				env.DisplayToConsole(true, "Cannot locate Neoforge installer")
@@ -223,7 +225,7 @@ func (c CurseForge) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationR
 			env.DisplayToConsole(true, "Grabbing ServerStarter")
 			runJarFile := filepath.Join(env.GetRootDirectory(), "server.jar")
 			if _, err = os.Stat(runJarFile); os.IsNotExist(err) {
-				var cachePath = filepath.Join(config.CacheFolder.Value(), "github.com", "neoforge", "serverstarter", NeoForgeServerStarterVersion, "server.jar")
+				var cachePath = filepath.Join(config.CacheFolder.Value(), "github.com", "neoforgedl", "serverstarter", NeoForgeServerStarterVersion, "server.jar")
 				if _, err = os.Stat(cachePath); os.IsNotExist(err) {
 					env.DisplayToConsole(true, "Downloading "+NeoForgeServerStarter)
 					err = pufferpanel.DownloadFileToCache(NeoForgeServerStarter, cachePath)
@@ -249,7 +251,7 @@ func (c CurseForge) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationR
 	return pufferpanel.OperationResult{Error: nil}
 }
 
-func findInstallerJar(env pufferpanel.Environment) (string, error) {
+func findInstallerJar(env *pufferpanel.Environment) (string, error) {
 	entries, err := os.ReadDir(env.GetRootDirectory())
 	if err != nil {
 		return "", err
@@ -266,12 +268,11 @@ func findInstallerJar(env pufferpanel.Environment) (string, error) {
 	return "", os.ErrNotExist
 }
 
-func installViaJar(server pufferpanel.DaemonServer, env pufferpanel.Environment, jarFile string, javaBinary string) error {
+func installViaJar(server pufferpanel.DaemonServer, env *pufferpanel.Environment, jarFile string, javaBinary string) error {
 	//installer found, we will run this one
 	result := make(chan int, 1)
 	err := env.Execute(pufferpanel.ExecutionData{
-		Command:   javaBinary,
-		Arguments: []string{"-jar", jarFile, "--installServer"},
+		Command: fmt.Sprintf("%s -jar %s --installServer", javaBinary, jarFile),
 		Callback: func(exitCode int) {
 			result <- exitCode
 			env.DisplayToConsole(true, "Installer exit code: %d", exitCode)
@@ -319,7 +320,7 @@ func installViaJar(server pufferpanel.DaemonServer, env pufferpanel.Environment,
 	return nil
 }
 
-func installFabric(env pufferpanel.Environment, data map[string]string, javaBinary string) error {
+func installFabric(env *pufferpanel.Environment, data map[string]string, javaBinary string) error {
 	//this is a mess
 	//there's 2 options that exist for fabric
 	//there is an "improved" launcher, which is just a jar that we need
@@ -348,8 +349,7 @@ func installFabric(env pufferpanel.Environment, data map[string]string, javaBina
 		//forge installer found, we will run this one
 		result := make(chan int, 1)
 		err = env.Execute(pufferpanel.ExecutionData{
-			Command:   javaBinary,
-			Arguments: []string{"-jar", "fabric-installer", "server", "-mcversion", data["mcVersion"], "-loader", data["version"], "-downloadMinecraft"},
+			Command: fmt.Sprintf("%s -jar fabric-installer server -mcversion %s -loader %s -downloadMinecraft", javaBinary, data["mcVersion"], data["version"]),
 			Callback: func(exitCode int) {
 				result <- exitCode
 				env.DisplayToConsole(true, "Installer exit code: %d", exitCode)
diff --git a/operations/dockerpull/dockerpull.go b/operations/dockerpull/dockerpull.go
index 469dbe67..4b2df2ea 100644
--- a/operations/dockerpull/dockerpull.go
+++ b/operations/dockerpull/dockerpull.go
@@ -12,12 +12,12 @@ type DockerPull struct {
 
 func (d DockerPull) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationResult {
 	env := args.Environment
-	dockerEnv, ok := env.(*docker.Docker)
+	dockerEnv, ok := env.Implementation.(*docker.Docker)
 
 	if !ok {
 		return pufferpanel.OperationResult{Error: pufferpanel.ErrEnvironmentNotSupported}
 	}
 
-	err := dockerEnv.PullImage(context.Background(), d.ImageName, true)
+	err := dockerEnv.PullImage(env, context.Background(), d.ImageName, true)
 	return pufferpanel.OperationResult{Error: err}
 }
diff --git a/operations/fabricdl/fabricdl.go b/operations/fabricdl/fabricdl.go
index 57cfa4f9..7830add3 100644
--- a/operations/fabricdl/fabricdl.go
+++ b/operations/fabricdl/fabricdl.go
@@ -39,11 +39,12 @@ func (f *Fabricdl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationRe
 	}
 
 	file, err := pufferpanel.DownloadViaMaven(metadata[0].Url, env)
+	defer utils.Close(file)
 	if err != nil {
 		return pufferpanel.OperationResult{Error: err}
 	}
 
-	err = files.CopyFile(file, path.Join(env.GetRootDirectory(), "fabric-installer.jar"))
+	err = files.WriteFile(file, path.Join(env.GetRootDirectory(), "fabric-installer.jar"))
 	if err != nil {
 		return pufferpanel.OperationResult{Error: err}
 	}
diff --git a/operations/forgedl/forgedl.go b/operations/forgedl/forgedl.go
index 1fdd8e2c..15985849 100644
--- a/operations/forgedl/forgedl.go
+++ b/operations/forgedl/forgedl.go
@@ -50,12 +50,13 @@ func (op ForgeDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationRes
 	jarDownload := strings.Replace(InstallerUrl, "${version}", op.Version, -1)
 
 	localFile, err := pufferpanel.DownloadViaMaven(jarDownload, env)
+	defer utils.Close(localFile)
 	if err != nil {
 		return pufferpanel.OperationResult{Error: err}
 	}
 
 	//copy from the cache
-	err = files.CopyFile(localFile, path.Join(env.GetRootDirectory(), op.Filename))
+	err = files.WriteFile(localFile, path.Join(env.GetRootDirectory(), op.Filename))
 	if err != nil {
 		return pufferpanel.OperationResult{Error: err}
 	}
diff --git a/operations/mojangdl/mojangdl.go b/operations/mojangdl/mojangdl.go
index ec74ef98..4549073d 100644
--- a/operations/mojangdl/mojangdl.go
+++ b/operations/mojangdl/mojangdl.go
@@ -61,7 +61,7 @@ func (op MojangDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationRe
 	return pufferpanel.OperationResult{Error: err}
 }
 
-func downloadServerFromJson(url, target string, env pufferpanel.Environment) error {
+func downloadServerFromJson(url, target string, env *pufferpanel.Environment) error {
 	response, err := pufferpanel.HttpGet(url)
 	defer utils.CloseResponse(response)
 	if err != nil {
diff --git a/operations/neoforge/factory.go b/operations/neoforgedl/factory.go
similarity index 100%
rename from operations/neoforge/factory.go
rename to operations/neoforgedl/factory.go
diff --git a/operations/neoforge/neoforgedl.go b/operations/neoforgedl/neoforgedl.go
similarity index 92%
rename from operations/neoforge/neoforgedl.go
rename to operations/neoforgedl/neoforgedl.go
index d83bad84..17fbae67 100644
--- a/operations/neoforge/neoforgedl.go
+++ b/operations/neoforgedl/neoforgedl.go
@@ -35,12 +35,13 @@ func (op NeoforgeDL) Run(args pufferpanel.RunOperatorArgs) pufferpanel.Operation
 	jarDownload := strings.Replace(InstallerUrl, "${version}", op.Version, -1)
 
 	localFile, err := pufferpanel.DownloadViaMaven(jarDownload, env)
+	defer utils.Close(localFile)
 	if err != nil {
 		return pufferpanel.OperationResult{Error: err}
 	}
 
 	//copy from the cache
-	err = files.CopyFile(localFile, path.Join(env.GetRootDirectory(), op.Filename))
+	err = files.WriteFile(localFile, path.Join(env.GetRootDirectory(), op.Filename))
 	if err != nil {
 		return pufferpanel.OperationResult{Error: err}
 	}
@@ -82,7 +83,7 @@ func getLatestForMCVersion(minecraftVersion string) (string, error) {
 	}
 
 	if topVersion == nil {
-		return "", errors.New("failed to find neoforge version for " + minecraftVersion)
+		return "", errors.New("failed to find neoforgedl version for " + minecraftVersion)
 	}
 
 	return topVersion.Original(), nil
diff --git a/operations/paperdl/factory.go b/operations/paperdl/factory.go
new file mode 100644
index 00000000..71bab0bb
--- /dev/null
+++ b/operations/paperdl/factory.go
@@ -0,0 +1,33 @@
+package paperdl
+
+import (
+	"errors"
+	"github.com/pufferpanel/pufferpanel/v3"
+	"github.com/spf13/cast"
+)
+
+type OperationFactory struct {
+	pufferpanel.OperationFactory
+}
+
+func (of OperationFactory) Create(op pufferpanel.CreateOperation) (pufferpanel.Operation, error) {
+	minecraftVersion := cast.ToString(op.OperationArgs["minecraftVersion"])
+	build := cast.ToString(op.OperationArgs["build"])
+	filename := cast.ToString(op.OperationArgs["target"])
+
+	if minecraftVersion == "" {
+		return nil, errors.New("missing minecraftVersion")
+	}
+
+	if build == "" {
+		return nil, errors.New("missing build")
+	}
+
+	return PaperDl{MinecraftVersion: minecraftVersion, Build: build, Filename: filename}, nil
+}
+
+func (of OperationFactory) Key() string {
+	return "paperdl"
+}
+
+var Factory OperationFactory
diff --git a/operations/paperdl/paperdl.go b/operations/paperdl/paperdl.go
new file mode 100644
index 00000000..a239e42d
--- /dev/null
+++ b/operations/paperdl/paperdl.go
@@ -0,0 +1,157 @@
+package paperdl
+
+import (
+	"crypto"
+	"encoding/json"
+	"errors"
+	"github.com/hashicorp/go-version"
+	"github.com/pufferpanel/pufferpanel/v3"
+	"github.com/pufferpanel/pufferpanel/v3/files"
+	"github.com/pufferpanel/pufferpanel/v3/logging"
+	"github.com/pufferpanel/pufferpanel/v3/utils"
+	"net/http"
+	"net/url"
+	"path"
+	"strings"
+)
+
+const VersionsUrl = "https://fill.papermc.io/v3/projects/paper/versions"
+const BuildUrl = "https://fill.papermc.io/v3/projects/paper/versions/${mcVersion}/builds/${build}"
+var UserAgent = pufferpanel.Display + " https://github.com/pufferpanel/pufferpanel"
+
+type PaperDl struct {
+	MinecraftVersion string
+	Build            string
+	Filename         string
+}
+
+func (op PaperDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationResult {
+	env := args.Environment
+
+	if op.MinecraftVersion == "latest" {
+		logging.Info.Printf("PaperDL got Minecraft version 'latest', looking up latest version supported by Paper")
+		mcVersion, err := getLatestMCVersion()
+		if err != nil {
+			return pufferpanel.OperationResult{Error: err}
+		}
+		op.MinecraftVersion = mcVersion
+	}
+
+	dlUrl, hash, err := op.getDownloadUrlAndHash(env)
+	if err != nil {
+		return pufferpanel.OperationResult{Error: err}
+	}
+
+	dl, err := pufferpanel.Download(dlUrl, hash, crypto.SHA256, true, env)
+	defer utils.Close(dl)
+	if err != nil {
+		return pufferpanel.OperationResult{Error: err}
+	}
+
+	err = files.WriteFile(dl, path.Join(env.GetRootDirectory(), op.Filename))
+	if err != nil {
+		return pufferpanel.OperationResult{Error: err}
+	}
+
+	return pufferpanel.OperationResult{Error: nil}
+}
+
+func getLatestMCVersion() (string, error) {
+	path, err := url.Parse(VersionsUrl)
+	if err != nil {
+		return "", err
+	}
+
+	request := &http.Request{
+		Method: "GET",
+		URL: path,
+		Header: http.Header{},
+	}
+	request.Header.Add("user-agent", UserAgent)
+
+	response, err := pufferpanel.Http().Do(request)
+	defer utils.CloseResponse(response)
+	if err != nil {
+		return "", err
+	}
+
+	var versions PaperVersionsResponse
+	err = json.NewDecoder(response.Body).Decode(&versions)
+	if err != nil {
+		return "", err
+	}
+
+	latest, _ := version.NewVersion("0.0")
+	for _, v := range versions.Versions {
+		if ver, err := version.NewVersion(v.VersionInfo.Id); err == nil && latest.LessThan(ver) {
+			latest = ver
+		} else if err != nil {
+			logging.Info.Printf("failed to parse version '%s', %s", v, err)
+		}
+	}
+
+	logging.Info.Printf("Latest Minecraft version supported by Paper is %s", latest.Original())
+	return latest.Original(), nil
+}
+
+func (op PaperDl) getDownloadUrlAndHash(env *pufferpanel.Environment) (string, string, error) {
+	path, err := url.Parse(strings.Replace(strings.Replace(BuildUrl, "${mcVersion}", op.MinecraftVersion, -1), "${build}", op.Build, -1))
+	if err != nil {
+		return "", "", err
+	}
+
+	request := &http.Request{
+		Method: "GET",
+		URL: path,
+		Header: http.Header{},
+	}
+	request.Header.Add("User-Agent", UserAgent)
+
+	response, err := pufferpanel.Http().Do(request)
+	defer utils.CloseResponse(response)
+	if err != nil {
+		return "", "", err
+	}
+
+	if response.StatusCode == 404 {
+		env.DisplayToConsole(true, "Invalid Minecraft version or Paper build\n")
+		return "", "", errors.New("Invalid minecraft version or paper build")
+	}
+
+	var build PaperBuild
+	err = json.NewDecoder(response.Body).Decode(&build)
+	if err != nil {
+		return "", "", err
+	}
+
+	return build.Downloads.Server.Url, build.Downloads.Server.Checksums.Sha256, nil
+}
+
+type PaperVersionInfo struct {
+	Id string `json:"id"`
+}
+
+type PaperVersion struct {
+	VersionInfo PaperVersionInfo `json:"version"`
+}
+
+type PaperVersionsResponse struct {
+	Versions []PaperVersion `json:"versions"`
+}
+
+type PaperChecksums struct {
+	Sha256 string `json:"sha256"`
+}
+
+type PaperServer struct {
+	Checksums PaperChecksums `json:"checksums"`
+	Url       string         `json:"url"`
+}
+
+type PaperDownload struct {
+	Server PaperServer `json:"server:default"`
+}
+
+type PaperBuild struct {
+	Downloads PaperDownload `json:"downloads"`
+}
diff --git a/operations/resolveneoforgeversion/resolveneoforgeversion.go b/operations/resolveneoforgeversion/resolveneoforgeversion.go
index f56495f1..c74e7d1a 100644
--- a/operations/resolveneoforgeversion/resolveneoforgeversion.go
+++ b/operations/resolveneoforgeversion/resolveneoforgeversion.go
@@ -34,7 +34,7 @@ type ResolveNeoForgeVersion struct {
 func (op ResolveNeoForgeVersion) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationResult {
 	//if a specific version wasn't specified, we have to dig around through the files....
 	if op.Version == "" {
-		dir := filepath.Join(args.Environment.GetRootDirectory(), "libraries", "net", "neoforged", "neoforge")
+		dir := filepath.Join(args.Environment.GetRootDirectory(), "libraries", "net", "neoforged", "neoforgedl")
 		folders, err := os.ReadDir(dir)
 		if os.IsNotExist(err) {
 			return pufferpanel.OperationResult{VariableOverrides: map[string]interface{}{
diff --git a/operations/spongedl/spongedl.go b/operations/spongedl/spongedl.go
index ca3002f2..f3828a11 100644
--- a/operations/spongedl/spongedl.go
+++ b/operations/spongedl/spongedl.go
@@ -106,12 +106,13 @@ func (op SpongeDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationRe
 			}
 
 			file, err := pufferpanel.DownloadViaMaven(url, env)
+			defer utils.Close(file)
 			if err != nil {
 				return pufferpanel.OperationResult{Error: err}
 			}
 
 			//going to stick the spongeforge rename in, to assist with those modpacks
-			err = files.CopyFile(file, path.Join(env.GetRootDirectory(), "mods", "_aspongeforge.jar"))
+			err = files.WriteFile(file, path.Join(env.GetRootDirectory(), "mods", "_aspongeforge.jar"))
 			if err != nil {
 				return pufferpanel.OperationResult{Error: err}
 			}
@@ -119,11 +120,12 @@ func (op SpongeDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationRe
 	case "spongevanilla":
 		{
 			file, err := pufferpanel.DownloadViaMaven(url, env)
+			defer utils.Close(file)
 			if err != nil {
 				return pufferpanel.OperationResult{Error: err}
 			}
 
-			err = files.CopyFile(file, path.Join(env.GetRootDirectory(), "server.jar"))
+			err = files.WriteFile(file, path.Join(env.GetRootDirectory(), "server.jar"))
 			if err != nil {
 				return pufferpanel.OperationResult{Error: err}
 			}
@@ -136,7 +138,7 @@ func (op SpongeDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.OperationRe
 	return pufferpanel.OperationResult{Error: nil}
 }
 
-func (op SpongeDl) getLatestVersion(env pufferpanel.Environment) (SpongeApiV2Versions, error) {
+func (op SpongeDl) getLatestVersion(env *pufferpanel.Environment) (SpongeApiV2Versions, error) {
 	var data SpongeApiV2Versions
 
 	var params = "?limit=1"
@@ -163,7 +165,7 @@ func (op SpongeDl) getLatestVersion(env pufferpanel.Environment) (SpongeApiV2Ver
 	return data, err
 }
 
-func (op SpongeDl) getSpecificVersion(env pufferpanel.Environment, version string) (SpongeApiV2Latest, error) {
+func (op SpongeDl) getSpecificVersion(env *pufferpanel.Environment, version string) (SpongeApiV2Latest, error) {
 	var data SpongeApiV2Latest
 
 	var url = SpongeApiBaseUrl + op.SpongeType + "/versions/" + version
diff --git a/operations/steamgamedl/steamgamedl.go b/operations/steamgamedl/steamgamedl.go
index 49ee17e5..ea8c6230 100644
--- a/operations/steamgamedl/steamgamedl.go
+++ b/operations/steamgamedl/steamgamedl.go
@@ -55,19 +55,21 @@ func (c SteamGameDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.Operation
 	manifestFolder := filepath.Join(env.GetRootDirectory(), ".manifest")
 	_ = os.RemoveAll(manifestFolder)
 
-	cmdArgs := []string{"-app", c.AppId, "-dir", ".manifest", "-loginid", loginId, "-manifest-only"}
+	cmdArgs := []string{filepath.Join(rootBinaryFolder, "depotdownloader", DepotDownloaderBinary), "-app", c.AppId, "-dir", ".manifest", "-loginid", loginId, "-manifest-only"}
 	if c.Username != "" {
 		cmdArgs = append(cmdArgs, "-username", c.Username, "-remember-password")
 		if c.Password != "" {
 			cmdArgs = append(cmdArgs, "-password", c.Password)
 		}
 	}
+	if !config.DepotDownloaderDisableLancache.Value() {
+		cmdArgs = append(cmdArgs, "-use-lancache")
+	}
 	cmdArgs = append(cmdArgs, c.ExtraArgs...)
 
 	ch := make(chan int, 1)
 	steps := pufferpanel.ExecutionData{
-		Command:   filepath.Join(rootBinaryFolder, "depotdownloader", DepotDownloaderBinary),
-		Arguments: cmdArgs,
+		Command: utils.MergeArguments(cmdArgs),
 		Callback: func(exitCode int) {
 			ch <- exitCode
 		},
@@ -83,7 +85,7 @@ func (c SteamGameDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.Operation
 	}
 
 	//download game itself now
-	cmdArgs = []string{"-app", c.AppId, "-dir", ".", "-loginid", loginId, "-validate"}
+	cmdArgs = []string{filepath.Join(rootBinaryFolder, "depotdownloader", DepotDownloaderBinary), "-app", c.AppId, "-dir", ".", "-loginid", loginId, "-validate"}
 	if c.Username != "" {
 		cmdArgs = append(cmdArgs, "-username", c.Username, "-remember-password")
 		if c.Password != "" {
@@ -100,8 +102,7 @@ func (c SteamGameDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.Operation
 	}
 
 	steps = pufferpanel.ExecutionData{
-		Command:   filepath.Join(rootBinaryFolder, "depotdownloader", DepotDownloaderBinary),
-		Arguments: cmdArgs,
+		Command: utils.MergeArguments(cmdArgs),
 		Callback: func(exitCode int) {
 			ch <- exitCode
 		},
@@ -138,7 +139,7 @@ func (c SteamGameDl) Run(args pufferpanel.RunOperatorArgs) pufferpanel.Operation
 	return pufferpanel.OperationResult{Error: nil}
 }
 
-func downloadMetadata(env pufferpanel.Environment) error {
+func downloadMetadata(env *pufferpanel.Environment) error {
 	response, err := pufferpanel.HttpGet(SteamMetadataLink)
 	defer utils.CloseResponse(response)
 	if err != nil {
diff --git a/servers/docker/docker.go b/servers/docker/docker.go
index e3346e79..e2fed645 100644
--- a/servers/docker/docker.go
+++ b/servers/docker/docker.go
@@ -31,8 +31,6 @@ import (
 )
 
 type Docker struct {
-	*pufferpanel.BaseEnvironment
-	ContainerId   string               `json:"-"`
 	ImageName     string               `json:"image"`
 	Binds         map[string]string    `json:"bindings,omitempty"`
 	Network       string               `json:"networkName,omitempty"`
@@ -50,7 +48,7 @@ type Docker struct {
 	lastStatTime     time.Time
 }
 
-func (d *Docker) dockerExecuteAsync(steps pufferpanel.ExecutionData) error {
+func (d *Docker) ExecuteAsyncImpl(environment *pufferpanel.Environment, steps pufferpanel.ExecutionData) error {
 	if d.downloadingImage {
 		return pufferpanel.ErrImageDownloading
 	}
@@ -64,7 +62,7 @@ func (d *Docker) dockerExecuteAsync(steps pufferpanel.ExecutionData) error {
 
 	ctx := context.Background()
 	//TODO: This logic may not work anymore, it's complicated to use an existing container with install/uninstall
-	exists, err := d.doesContainerExist(dockerClient, ctx)
+	exists, err := doesContainerExist(dockerClient, environment.ServerId, ctx)
 	if err != nil {
 		return err
 	}
@@ -73,7 +71,7 @@ func (d *Docker) dockerExecuteAsync(steps pufferpanel.ExecutionData) error {
 		return errors.New("docker container already exists")
 	}
 
-	err = d.createContainer(ctx, steps)
+	err = d.createContainer(environment, steps, ctx)
 	if err != nil {
 		return err
 	}
@@ -85,35 +83,35 @@ func (d *Docker) dockerExecuteAsync(steps pufferpanel.ExecutionData) error {
 		Stream: true,
 	}
 
-	d.connection, err = dockerClient.ContainerAttach(ctx, d.ContainerId, cfg)
+	d.connection, err = dockerClient.ContainerAttach(ctx, environment.ServerId, cfg)
 	if err != nil {
 		return err
 	}
 
-	d.Wait.Add(1)
+	environment.Wait.Add(1)
 
 	go func() {
 		defer d.connection.Close()
-		_, _ = io.Copy(d.Wrapper, d.connection.Reader)
+		_, _ = io.Copy(environment.Wrapper, d.connection.Reader)
 	}()
 
-	d.BaseEnvironment.CreateConsoleStdinProxy(steps.StdInConfig, d.connection.Conn)
-	d.BaseEnvironment.Console.Start()
+	environment.CreateConsoleStdinProxy(steps.StdInConfig, d.connection.Conn)
+	environment.Console.Start()
 
-	go d.handleClose(dockerClient, steps.Callback)
+	go d.handleClose(environment, dockerClient, steps.Callback)
 
 	startOpts := container.StartOptions{}
 
-	_ = d.StatusTracker.WriteMessage(pufferpanel.Transmission{
+	_ = environment.StatusTracker.WriteMessage(pufferpanel.Transmission{
 		Message: pufferpanel.ServerRunning{
 			Running:    true,
-			Installing: d.IsInstalling(),
+			Installing: environment.IsInstalling(),
 		},
 		Type: pufferpanel.MessageTypeStatus,
 	})
 
-	d.DisplayToConsole(true, "Starting container\n")
-	err = dockerClient.ContainerStart(ctx, d.ContainerId, startOpts)
+	environment.DisplayToConsole(true, "Starting container\n")
+	err = dockerClient.ContainerStart(ctx, environment.ServerId, startOpts)
 	if err != nil {
 		return err
 	}
@@ -121,25 +119,25 @@ func (d *Docker) dockerExecuteAsync(steps pufferpanel.ExecutionData) error {
 	return err
 }
 
-func (d *Docker) kill() (err error) {
-	running, err := d.IsRunning()
+func (d *Docker) KillImpl(environment *pufferpanel.Environment) error {
+	running, err := environment.IsRunning()
 	if err != nil {
 		return err
 	}
 
 	if !running {
-		return
+		return nil
 	}
 
 	dockerClient, err := d.getClient()
 	if err != nil {
 		return err
 	}
-	err = dockerClient.ContainerKill(context.Background(), d.ContainerId, "SIGKILL")
-	return
+	err = dockerClient.ContainerKill(context.Background(), environment.ServerId, "SIGKILL")
+	return err
 }
 
-func (d *Docker) isRunning() (bool, error) {
+func (d *Docker) IsRunningImpl(environment *pufferpanel.Environment) (bool, error) {
 	dockerClient, err := d.getClient()
 	if err != nil {
 		return false, err
@@ -147,20 +145,20 @@ func (d *Docker) isRunning() (bool, error) {
 
 	ctx := context.Background()
 
-	exists, err := d.doesContainerExist(dockerClient, ctx)
+	exists, err := doesContainerExist(dockerClient, environment.ServerId, ctx)
 	if !exists {
 		return false, err
 	}
 
-	stats, err := dockerClient.ContainerInspect(ctx, d.ContainerId)
+	stats, err := dockerClient.ContainerInspect(ctx, environment.ServerId)
 	if err != nil {
 		return false, err
 	}
 	return stats.State.Running, nil
 }
 
-func (d *Docker) GetStats() (*pufferpanel.ServerStats, error) {
-	running, err := d.IsRunning()
+func (d *Docker) GetStatsImpl(environment *pufferpanel.Environment) (*pufferpanel.ServerStats, error) {
+	running, err := environment.IsRunning()
 	if err != nil {
 		return nil, err
 	}
@@ -171,7 +169,7 @@ func (d *Docker) GetStats() (*pufferpanel.ServerStats, error) {
 			Memory: 0,
 		}
 
-		if d.Server.Stats.Type == "jcmd" {
+		if environment.Server.Stats.Type == "jcmd" {
 			stats.Jvm = &utils.JvmStats{}
 		}
 
@@ -193,7 +191,7 @@ func (d *Docker) GetStats() (*pufferpanel.ServerStats, error) {
 	}
 
 	ctx := context.Background()
-	res, err := dockerClient.ContainerStats(ctx, d.ContainerId, false)
+	res, err := dockerClient.ContainerStats(ctx, environment.ServerId, false)
 	defer func() {
 		if res.Body != nil {
 			utils.Close(res.Body)
@@ -217,13 +215,13 @@ func (d *Docker) GetStats() (*pufferpanel.ServerStats, error) {
 		Cpu:    calculateCPUPercent(data),
 	}
 
-	if d.Server.Stats.Type == "jcmd" {
-		cmd, _ := d.Server.Stats.Metadata["cmd"].(string)
+	if environment.Server.Stats.Type == "jcmd" {
+		cmd, _ := environment.Server.Stats.Metadata["cmd"].(string)
 		if cmd == "" {
 			cmd = "jcmd"
 		}
 
-		r, e := dockerClient.ContainerExecCreate(context.Background(), d.ContainerId, container.ExecOptions{
+		r, e := dockerClient.ContainerExecCreate(context.Background(), environment.ServerId, container.ExecOptions{
 			AttachStderr: true,
 			AttachStdout: true,
 			Cmd:          []string{cmd, "1", "GC.heap_info"},
@@ -270,13 +268,13 @@ func (d *Docker) getClient() (*client.Client, error) {
 	return d.cli, err
 }
 
-func (d *Docker) doesContainerExist(client *client.Client, ctx context.Context) (bool, error) {
+func doesContainerExist(client *client.Client, id string, ctx context.Context) (bool, error) {
 	opts := container.ListOptions{
 		Filters: filters.NewArgs(),
 	}
 
 	opts.All = true
-	opts.Filters.Add("name", d.ContainerId)
+	opts.Filters.Add("name", id)
 
 	existingContainers, err := client.ContainerList(ctx, opts)
 	if err != nil {
@@ -284,7 +282,7 @@ func (d *Docker) doesContainerExist(client *client.Client, ctx context.Context)
 	}
 
 	for _, v := range existingContainers {
-		if slices.Contains(v.Names, "/"+d.ContainerId) {
+		if slices.Contains(v.Names, "/"+id) {
 			return true, nil
 		}
 	}
@@ -292,7 +290,7 @@ func (d *Docker) doesContainerExist(client *client.Client, ctx context.Context)
 	return false, nil
 }
 
-func (d *Docker) PullImage(ctx context.Context, imageName string, force bool) error {
+func (d *Docker) PullImage(environment *pufferpanel.Environment, ctx context.Context, imageName string, force bool) error {
 	if d.downloadingImage {
 		return pufferpanel.ErrImageDownloading
 	}
@@ -328,7 +326,7 @@ func (d *Docker) PullImage(ctx context.Context, imageName string, force bool) er
 			}
 		}
 
-		d.Log(logging.Debug, "Does image %v exist? %v", imageName, exists)
+		environment.Log(logging.Debug, "Does image %v exist? %v", imageName, exists)
 
 		if exists {
 			return nil
@@ -337,8 +335,8 @@ func (d *Docker) PullImage(ctx context.Context, imageName string, force bool) er
 
 	op := image.PullOptions{}
 
-	d.Log(logging.Debug, "Downloading image %v", imageName)
-	d.DisplayToConsole(true, "Downloading image for container, please wait\n")
+	environment.Log(logging.Debug, "Downloading image %v", imageName)
+	environment.DisplayToConsole(true, "Downloading image for container, please wait\n")
 
 	d.downloadingImage = true
 	defer func() {
@@ -351,20 +349,20 @@ func (d *Docker) PullImage(ctx context.Context, imageName string, force bool) er
 		return err
 	}
 
-	w := &ImageWriter{Parent: d.ConsoleTracker}
+	w := &ImageWriter{Parent: environment.ConsoleTracker}
 	_, err = io.Copy(w, r)
 
 	if err != nil {
 		return err
 	}
 
-	d.Log(logging.Debug, "Downloaded image %v", imageName)
-	d.DisplayToConsole(true, "Downloaded image for container\n")
+	environment.Log(logging.Debug, "Downloaded image %v", imageName)
+	environment.DisplayToConsole(true, "Downloaded image for container\n")
 	return err
 }
 
-func (d *Docker) createContainer(ctx context.Context, data pufferpanel.ExecutionData) error {
-	d.Log(logging.Debug, "Creating container")
+func (d *Docker) createContainer(environment *pufferpanel.Environment, data pufferpanel.ExecutionData, ctx context.Context) error {
+	environment.Log(logging.Debug, "Creating container")
 	containerRoot := d.ContainerRoot
 	if containerRoot == "" {
 		containerRoot = "/pufferpanel"
@@ -378,24 +376,26 @@ func (d *Docker) createContainer(ctx context.Context, data pufferpanel.Execution
 
 	imageName := utils.ReplaceTokens(d.ImageName, data.Variables)
 
-	err := d.PullImage(ctx, imageName, false)
+	err := d.PullImage(environment, ctx, imageName, false)
 
 	if err != nil {
 		return err
 	}
 
+	cmd, args := utils.SplitArguments(data.Command)
+
 	cmdSlice := strslice.StrSlice{}
 	if data.Command != "" {
-		cmdSlice = append(cmdSlice, data.Command)
+		cmdSlice = append(cmdSlice, cmd)
 	}
-	for _, v := range data.Arguments {
+	for _, v := range args {
 		cmdSlice = append(cmdSlice, v)
 	}
 
-	d.Log(logging.Debug, "Container command: %s\n", cmdSlice)
+	environment.Log(logging.Debug, "Container command: %s\n", cmdSlice)
 
 	labels := map[string]string{
-		"pufferpanel.server": d.ContainerId,
+		"pufferpanel.server": environment.ServerId,
 	}
 
 	for k, v := range d.Labels {
@@ -445,7 +445,7 @@ func (d *Docker) createContainer(ctx context.Context, data pufferpanel.Execution
 
 	containerConfig.Env = make([]string, 0)
 	for k, v := range envVars {
-		containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s=%s", k, v))
+		containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s=%s", k, utils.ReplaceTokens(v, data.Variables)))
 	}
 
 	if len(containerConfig.Entrypoint) == 0 && len(cmdSlice) > 0 {
@@ -458,9 +458,9 @@ func (d *Docker) createContainer(ctx context.Context, data pufferpanel.Execution
 
 	var dir string
 	if containerMountSource != "" {
-		dir = filepath.Join(containerMountSource, "servers", d.ServerId)
+		dir = filepath.Join(containerMountSource, "servers", environment.ServerId)
 	} else {
-		dir = d.GetRootDirectory()
+		dir = environment.GetRootDirectory()
 	}
 
 	//convert root dir to a full path, so we can bind it
@@ -533,12 +533,12 @@ func (d *Docker) createContainer(ctx context.Context, data pufferpanel.Execution
 	networkConfig := &network.NetworkingConfig{}
 
 	//for now, default to linux across the board. This resolves problems that Windows has when you use it and docker
-	_, err = d.cli.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, &v1.Platform{OS: "linux"}, d.ContainerId)
+	_, err = d.cli.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, &v1.Platform{OS: "linux"}, environment.ServerId)
 	return err
 }
 
-func (d *Docker) SendCode(code int) error {
-	running, err := d.IsRunning()
+func (d *Docker) SendCodeImpl(environment *pufferpanel.Environment, code int) error {
+	running, err := environment.IsRunning()
 
 	if err != nil || !running {
 		return err
@@ -551,10 +551,10 @@ func (d *Docker) SendCode(code int) error {
 	}
 
 	ctx := context.Background()
-	return dockerClient.ContainerKill(ctx, d.ContainerId, cast.ToString(code))
+	return dockerClient.ContainerKill(ctx, environment.ServerId, cast.ToString(code))
 }
 
-func (d *Docker) GetUid() int {
+func (d *Docker) GetUidImpl(environment *pufferpanel.Environment) int {
 	user := d.Config.User
 	if user == "" {
 		return -1
@@ -562,7 +562,7 @@ func (d *Docker) GetUid() int {
 	return cast.ToInt(strings.Split(user, ":")[0])
 }
 
-func (d *Docker) GetGid() int {
+func (d *Docker) GetGidImpl(environment *pufferpanel.Environment) int {
 	user := d.Config.User
 	if user == "" {
 		return -1
@@ -570,38 +570,38 @@ func (d *Docker) GetGid() int {
 	return cast.ToInt(strings.Split(user, ":")[1])
 }
 
-func (d *Docker) handleClose(client *client.Client, callback func(int)) {
+func (d *Docker) handleClose(environment *pufferpanel.Environment, client *client.Client, callback func(int)) {
 	exitCode := -1
-	okChan, errChan := client.ContainerWait(context.Background(), d.ContainerId, container.WaitConditionRemoved)
+	okChan, errChan := client.ContainerWait(context.Background(), environment.ServerId, container.WaitConditionRemoved)
 
 	select {
 	case chanErr := <-errChan:
 		{
 			exitCode = -999
-			d.Log(logging.Error, "Error from error channel: %s\n", chanErr.Error())
+			environment.Log(logging.Error, "Error from error channel: %s\n", chanErr.Error())
 		}
 	case info := <-okChan:
 		{
 			exitCode = cast.ToInt(info.StatusCode)
 			if info.Error != nil {
-				d.Log(logging.Error, "Error from info channel: %s\n", info.Error.Message)
+				environment.Log(logging.Error, "Error from info channel: %s\n", info.Error.Message)
 			}
 		}
 	}
 
-	d.LastExitCode = exitCode
+	environment.LastExitCode = exitCode
 
-	d.Wait.Done()
+	environment.Wait.Done()
 
-	_ = d.StatusTracker.WriteMessage(pufferpanel.Transmission{
+	_ = environment.StatusTracker.WriteMessage(pufferpanel.Transmission{
 		Message: pufferpanel.ServerRunning{
 			Running:    false,
-			Installing: d.IsInstalling(),
+			Installing: environment.IsInstalling(),
 		},
 		Type: pufferpanel.MessageTypeStatus,
 	})
 
-	_ = d.BaseEnvironment.Console.Close()
+	_ = environment.Console.Close()
 
 	if callback != nil {
 		callback(exitCode)
@@ -624,9 +624,6 @@ func calculateMemoryPercent(v *container.StatsResponse) float64 {
 }
 
 func convertToBind(source string) string {
-	if runtime.GOOS != "windows" {
-		return source
-	}
 	fullPath, err := filepath.Abs(source)
 	if err != nil {
 		panic(err)
diff --git a/servers/docker/factory.go b/servers/docker/factory.go
index 17c34b98..28c12a22 100644
--- a/servers/docker/factory.go
+++ b/servers/docker/factory.go
@@ -8,25 +8,14 @@ type EnvironmentFactory struct {
 	pufferpanel.EnvironmentFactory
 }
 
-func (ef EnvironmentFactory) Create(id string) pufferpanel.Environment {
-	d := &Docker{
-		BaseEnvironment: &pufferpanel.BaseEnvironment{
-			Type:     ef.Key(),
-			ServerId: id,
-		},
-		ContainerId: id,
-		ImageName:   "pufferpanel/generic",
-		Network:     "host",
-		Ports:       make([]string, 0),
-		Binds:       make(map[string]string),
-		Labels:      make(map[string]string),
+func (ef EnvironmentFactory) Create() pufferpanel.EnvironmentImpl {
+	return &Docker{
+		ImageName: "pufferpanel/generic",
+		Network:   "host",
+		Ports:     make([]string, 0),
+		Binds:     make(map[string]string),
+		Labels:    make(map[string]string),
 	}
-
-	d.BaseEnvironment.Wrapper = d.CreateWrapper()
-	d.BaseEnvironment.ExecutionFunction = d.dockerExecuteAsync
-	d.BaseEnvironment.IsRunningFunc = d.isRunning
-	d.BaseEnvironment.KillFunc = d.kill
-	return d
 }
 
 func (ef EnvironmentFactory) Key() string {
diff --git a/servers/env_loader.go b/servers/env_loader.go
index 8a4c1eb5..2a51c29b 100644
--- a/servers/env_loader.go
+++ b/servers/env_loader.go
@@ -3,6 +3,8 @@ package servers
 import (
 	"fmt"
 	"github.com/pufferpanel/pufferpanel/v3"
+	"github.com/pufferpanel/pufferpanel/v3/servers/docker"
+	"github.com/pufferpanel/pufferpanel/v3/servers/tty"
 	"github.com/pufferpanel/pufferpanel/v3/utils"
 	"path/filepath"
 	"sync"
@@ -10,46 +12,47 @@ import (
 
 var envMapping = make(map[string]pufferpanel.EnvironmentFactory)
 
-func CreateEnvironment(environmentType, folder string, backupFolder string, server pufferpanel.Server) (pufferpanel.Environment, error) {
-	factory := envMapping[environmentType]
+func init() {
+	envMapping["host"] = tty.EnvironmentFactory{}
+	envMapping["tty"] = tty.EnvironmentFactory{}
+	envMapping["standard"] = tty.EnvironmentFactory{}
+	envMapping["docker"] = docker.EnvironmentFactory{}
+}
 
-	if factory == nil {
-		switch environmentType {
-		case "standard":
-			factory = envMapping["host"]
-		case "tty":
-			factory = envMapping["host"]
-		}
-	}
+func CreateEnvironment(environmentType, folder string, backupFolder string, server pufferpanel.Server) (*pufferpanel.Environment, error) {
+	factory := envMapping[environmentType]
 
 	if factory == nil {
 		return nil, fmt.Errorf("undefined environment: %s", environmentType)
 	}
 
-	item := factory.Create(server.Identifier)
+	item := &pufferpanel.Environment{
+		Type:            factory.Key(),
+		ServerId:        server.Identifier,
+		ConsoleTracker:  pufferpanel.CreateTracker(),
+		StatusTracker:   pufferpanel.CreateTracker(),
+		StatsTracker:    pufferpanel.CreateTracker(),
+		ConsoleBuffer:   pufferpanel.CreateCache(),
+		BackupDirectory: filepath.Join(backupFolder, server.Identifier),
+		Wait:            &sync.WaitGroup{},
+		Server:          server,
+	}
+	item.Implementation = factory.Create()
 	err := utils.UnmarshalTo(server.Environment.Metadata, item)
 	if err != nil {
 		return nil, err
 	}
 
-	serverRoot := filepath.Join(folder, server.Identifier)
-	backupDirectory := filepath.Join(backupFolder, server.Identifier)
-	envCache := pufferpanel.CreateCache()
-
-	e := item.GetBase()
-	if e.RootDirectory == "" {
-		e.RootDirectory = serverRoot
+	err = utils.UnmarshalTo(server.Environment.Metadata, item.Implementation)
+	if err != nil {
+		return nil, err
 	}
-	e.BackupDirectory = backupDirectory
 
-	e.ConsoleTracker = pufferpanel.CreateTracker()
-	e.StatusTracker = pufferpanel.CreateTracker()
-	e.StatsTracker = pufferpanel.CreateTracker()
+	if item.RootDirectory == "" {
+		item.RootDirectory = filepath.Join(folder, server.Identifier)
+	}
 
-	e.ConsoleBuffer = envCache
-	e.Wait = &sync.WaitGroup{}
-	e.Wrapper = e.CreateWrapper()
-	e.Server = server
+	item.CreateWrapper()
 
 	return item, nil
 }
diff --git a/servers/env_loader_docker.go b/servers/env_loader_docker.go
deleted file mode 100644
index a859df14..00000000
--- a/servers/env_loader_docker.go
+++ /dev/null
@@ -1,9 +0,0 @@
-//go:build !nodocker
-
-package servers
-
-import "github.com/pufferpanel/pufferpanel/v3/servers/docker"
-
-func init() {
-	envMapping["docker"] = docker.EnvironmentFactory{}
-}
diff --git a/servers/env_loader_standard.go b/servers/env_loader_standard.go
deleted file mode 100644
index f29e1876..00000000
--- a/servers/env_loader_standard.go
+++ /dev/null
@@ -1,10 +0,0 @@
-//go:build windows && !nohost
-
-package servers
-
-import "github.com/pufferpanel/pufferpanel/v3/servers/standard"
-
-func init() {
-	envMapping["host"] = standard.EnvironmentFactory{}
-	envMapping["standard"] = standard.EnvironmentFactory{}
-}
diff --git a/servers/env_loader_tty.go b/servers/env_loader_tty.go
deleted file mode 100644
index 7b4624d1..00000000
--- a/servers/env_loader_tty.go
+++ /dev/null
@@ -1,11 +0,0 @@
-//go:build !windows && !nohost
-
-package servers
-
-import "github.com/pufferpanel/pufferpanel/v3/servers/tty"
-
-func init() {
-	envMapping["host"] = tty.EnvironmentFactory{}
-	envMapping["tty"] = tty.EnvironmentFactory{}
-	envMapping["standard"] = tty.EnvironmentFactory{}
-}
diff --git a/servers/operation_functions.go b/servers/operation_functions.go
index b1620571..0086f1ca 100644
--- a/servers/operation_functions.go
+++ b/servers/operation_functions.go
@@ -12,7 +12,7 @@ import (
 	"path/filepath"
 )
 
-func CreateFunctions(env pufferpanel.Environment) []cel.EnvOption {
+func CreateFunctions(env *pufferpanel.Environment) []cel.EnvOption {
 	return []cel.EnvOption{
 		cel.Function("file_exists",
 			cel.Overload("file_exists_string_bool",
@@ -34,7 +34,7 @@ func CreateFunctions(env pufferpanel.Environment) []cel.EnvOption {
 	}
 }
 
-func cel_file_exists(env pufferpanel.Environment) functions.UnaryOp {
+func cel_file_exists(env *pufferpanel.Environment) functions.UnaryOp {
 	return func(fileName ref.Val) ref.Val {
 		fullPath := filepath.Join(env.GetRootDirectory(), fileName.Value().(string))
 		_, err := os.Stat(fullPath)
@@ -42,14 +42,14 @@ func cel_file_exists(env pufferpanel.Environment) functions.UnaryOp {
 	}
 }
 
-func cel_in_path(env pufferpanel.Environment) functions.UnaryOp {
+func cel_in_path(env *pufferpanel.Environment) functions.UnaryOp {
 	return func(fileName ref.Val) ref.Val {
 		_, err := exec.LookPath(fileName.Value().(string))
 		return types.Bool(err == nil || errors.Is(err, exec.ErrDot))
 	}
 }
 
-func cel_is_server_running(env pufferpanel.Environment) functions.FunctionOp {
+func cel_is_server_running(env *pufferpanel.Environment) functions.FunctionOp {
 	return func(values ...ref.Val) ref.Val {
 		r, err := env.IsRunning()
 		return types.Bool(err == nil && r)
diff --git a/servers/operation_process.go b/servers/operation_process.go
index 50ca21ed..605bf076 100644
--- a/servers/operation_process.go
+++ b/servers/operation_process.go
@@ -18,7 +18,8 @@ import (
 	"github.com/pufferpanel/pufferpanel/v3/operations/mkdir"
 	"github.com/pufferpanel/pufferpanel/v3/operations/mojangdl"
 	"github.com/pufferpanel/pufferpanel/v3/operations/move"
-	neoforgedl "github.com/pufferpanel/pufferpanel/v3/operations/neoforge"
+	"github.com/pufferpanel/pufferpanel/v3/operations/neoforgedl"
+	"github.com/pufferpanel/pufferpanel/v3/operations/paperdl"
 	"github.com/pufferpanel/pufferpanel/v3/operations/resolveforgeversion"
 	"github.com/pufferpanel/pufferpanel/v3/operations/resolveneoforgeversion"
 	"github.com/pufferpanel/pufferpanel/v3/operations/sleep"
@@ -47,6 +48,7 @@ var factories = []pufferpanel.OperationFactory{
 	mojangdl.Factory,
 	move.Factory,
 	neoforgedl.Factory,
+	paperdl.Factory,
 	resolveforgeversion.Factory,
 	resolveneoforgeversion.Factory,
 	sleep.Factory,
@@ -62,7 +64,7 @@ func init() {
 	}
 }
 
-func GenerateProcess(directions []pufferpanel.ConditionalMetadataType, environment pufferpanel.Environment, dataMapping map[string]interface{}, env map[string]string) (OperationProcess, error) {
+func GenerateProcess(directions []pufferpanel.ConditionalMetadataType, environment *pufferpanel.Environment, dataMapping map[string]interface{}, env map[string]string) (OperationProcess, error) {
 	dataMap := make(map[string]interface{})
 	for k, v := range dataMapping {
 		dataMap[k] = v
diff --git a/servers/server.go b/servers/server.go
index dbb03ca0..66438353 100644
--- a/servers/server.go
+++ b/servers/server.go
@@ -28,9 +28,9 @@ type Server struct {
 	pufferpanel.DaemonServer
 	pufferpanel.Server
 
-	CrashCounter       int                     `json:"-"`
-	RunningEnvironment pufferpanel.Environment `json:"-"`
-	Scheduler          *Scheduler              `json:"-"`
+	CrashCounter       int                      `json:"-"`
+	RunningEnvironment *pufferpanel.Environment `json:"-"`
+	Scheduler          *Scheduler               `json:"-"`
 	stopChan           chan bool
 	waitForConsole     sync.Locker
 	fileServer         files.FileServer
@@ -250,11 +250,8 @@ func (p *Server) Start() error {
 	data := p.DataToMap()
 
 	commandLine := utils.ReplaceTokens(command.Command, data)
-
-	cmd, args := utils.SplitArguments(commandLine)
 	err = p.RunningEnvironment.ExecuteAsync(pufferpanel.ExecutionData{
-		Command:     cmd,
-		Arguments:   args,
+		Command:     commandLine,
 		Environment: utils.ReplaceTokensInMap(p.Execution.EnvironmentVariables, data),
 		Variables:   p.DataToMap(),
 		Callback:    p.afterExit,
@@ -428,7 +425,7 @@ func (p *Server) Execute(command string) (err error) {
 	return
 }
 
-func (p *Server) SetEnvironment(environment pufferpanel.Environment) (err error) {
+func (p *Server) SetEnvironment(environment *pufferpanel.Environment) (err error) {
 	p.RunningEnvironment = environment
 	return
 }
@@ -437,7 +434,7 @@ func (p *Server) Id() string {
 	return p.Identifier
 }
 
-func (p *Server) GetEnvironment() pufferpanel.Environment {
+func (p *Server) GetEnvironment() *pufferpanel.Environment {
 	return p.RunningEnvironment
 }
 
@@ -794,7 +791,7 @@ func (p *Server) Log(l *log.Logger, format string, obj ...interface{}) {
 
 func (p *Server) RunCondition(condition string, extraData map[string]interface{}) (bool, error) {
 	data := map[string]interface{}{
-		conditions.VariableEnv:      p.RunningEnvironment.GetBase().Type,
+		conditions.VariableEnv:      p.RunningEnvironment.Type,
 		conditions.VariableServerId: p.Id(),
 	}
 
diff --git a/servers/server_loader.go b/servers/server_loader.go
index d207df57..2242df9f 100644
--- a/servers/server_loader.go
+++ b/servers/server_loader.go
@@ -30,7 +30,7 @@ func LoadFromFolder() {
 		if element.IsDir() || !strings.HasSuffix(element.Name(), ".json") {
 			continue
 		}
-		logging.Info.Printf("Attempting to load " + element.Name())
+		logging.Info.Printf("Attempting to load %s\n", element.Name())
 		id := strings.TrimSuffix(element.Name(), filepath.Ext(element.Name()))
 		program, err = Load(id)
 		if err != nil {
diff --git a/servers/standard/factory.go b/servers/standard/factory.go
deleted file mode 100644
index d1c1f4fb..00000000
--- a/servers/standard/factory.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package standard
-
-import (
-	"github.com/pufferpanel/pufferpanel/v3"
-)
-
-type EnvironmentFactory struct {
-	pufferpanel.EnvironmentFactory
-}
-
-func (ef EnvironmentFactory) Create(id string) pufferpanel.Environment {
-	s := &standard{
-		BaseEnvironment: &pufferpanel.BaseEnvironment{Type: ef.Key(), ServerId: id},
-	}
-
-	s.BaseEnvironment.Wrapper = s.CreateWrapper()
-	s.BaseEnvironment.ExecutionFunction = s.standardExecuteAsync
-	s.BaseEnvironment.IsRunningFunc = s.isRunning
-	s.BaseEnvironment.KillFunc = s.kill
-	return s
-}
-
-func (ef EnvironmentFactory) Key() string {
-	return "host"
-}
diff --git a/servers/standard/standard.go b/servers/standard/standard.go
deleted file mode 100644
index b427c99a..00000000
--- a/servers/standard/standard.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package standard
-
-import (
-	"errors"
-	"fmt"
-	"github.com/pufferpanel/pufferpanel/v3"
-	"github.com/pufferpanel/pufferpanel/v3/logging"
-	"github.com/pufferpanel/pufferpanel/v3/utils"
-	"github.com/shirou/gopsutil/process"
-	"github.com/spf13/cast"
-	"os"
-	"os/exec"
-	"runtime"
-	"strings"
-	"syscall"
-	"time"
-)
-
-type standard struct {
-	*pufferpanel.BaseEnvironment
-	mainProcess *exec.Cmd
-}
-
-func (s *standard) standardExecuteAsync(steps pufferpanel.ExecutionData) (err error) {
-	s.Wait.Add(1)
-	s.mainProcess = exec.Command(steps.Command, steps.Arguments...)
-	s.mainProcess.Dir = s.GetRootDirectory()
-
-	var envVars = make(map[string]string)
-
-	for _, v := range os.Environ() {
-		key, value, valid := strings.Cut(v, "=")
-		if !valid {
-			continue
-		}
-		if strings.HasPrefix(key, "PUFFER_") {
-			continue
-		}
-		envVars[key] = value
-	}
-	envVars["HOME"] = s.GetRootDirectory()
-	envVars["TERM"] = "xterm-256color"
-	for k, v := range steps.Environment {
-		envVars[k] = v
-	}
-
-	for k, v := range envVars {
-		s.mainProcess.Env = append(s.mainProcess.Env, fmt.Sprintf("%s=%s", k, v))
-	}
-
-	s.mainProcess.Stdout = s.Wrapper
-	s.mainProcess.Stderr = s.Wrapper
-
-	pipe, err := s.mainProcess.StdinPipe()
-	if err != nil {
-		s.Wait.Done()
-		return err
-	}
-
-	s.BaseEnvironment.CreateConsoleStdinProxy(steps.StdInConfig, pipe)
-	s.BaseEnvironment.Console.Start()
-
-	s.Log(logging.Info, "Starting process: %s %s", s.mainProcess.Path, strings.Join(s.mainProcess.Args[1:], " "))
-	s.DisplayToConsole(true, "Starting process: %s %s", s.mainProcess.Path, strings.Join(s.mainProcess.Args[1:], " "))
-
-	_ = s.StatusTracker.WriteMessage(pufferpanel.Transmission{
-		Message: pufferpanel.ServerRunning{
-			Running:    true,
-			Installing: s.IsInstalling(),
-		},
-		Type: pufferpanel.MessageTypeStatus,
-	})
-
-	err = s.mainProcess.Start()
-	if err != nil && err.Error() != "exit status 1" {
-		s.Wait.Done()
-		_ = s.StatusTracker.WriteMessage(pufferpanel.Transmission{
-			Message: pufferpanel.ServerRunning{
-				Running:    false,
-				Installing: s.IsInstalling(),
-			},
-			Type: pufferpanel.MessageTypeStatus,
-		})
-		s.Log(logging.Info, "Process failed to start: %s", err)
-		return
-	} else {
-		s.Log(logging.Info, "Process started (%d)", s.mainProcess.Process.Pid)
-	}
-
-	go s.handleClose(steps.Callback)
-	return
-}
-
-func (s *standard) kill() (err error) {
-	running, err := s.IsRunning()
-	if err != nil {
-		return err
-	}
-	if !running {
-		return
-	}
-	return s.mainProcess.Process.Kill()
-}
-
-func (s *standard) isRunning() (isRunning bool, err error) {
-	isRunning = s.mainProcess != nil && s.mainProcess.Process != nil
-	if isRunning {
-		pr, pErr := os.FindProcess(s.mainProcess.Process.Pid)
-		if pr == nil || pErr != nil {
-			isRunning = false
-		} else if runtime.GOOS != "windows" && pr.Signal(syscall.Signal(0)) != nil {
-			isRunning = false
-		}
-	}
-	return
-}
-
-func (s *standard) GetStats() (*pufferpanel.ServerStats, error) {
-	running, err := s.IsRunning()
-	if err != nil {
-		return nil, err
-	}
-	if !running {
-		stats := &pufferpanel.ServerStats{
-			Cpu:    0,
-			Memory: 0,
-		}
-
-		if s.Server.Stats.Type == "jcmd" {
-			stats.Jvm = &utils.JvmStats{}
-		}
-
-		return stats, nil
-	}
-
-	pr, err := process.NewProcess(int32(s.mainProcess.Process.Pid))
-	if err != nil {
-		return nil, err
-	}
-
-	memMap, _ := pr.MemoryInfo()
-	cpu, _ := pr.Percent(time.Second * 1)
-
-	return &pufferpanel.ServerStats{
-		Cpu:    cpu,
-		Memory: cast.ToFloat64(memMap.RSS),
-	}, nil
-}
-
-func (s *standard) SendCode(code int) error {
-	running, err := s.IsRunning()
-
-	if err != nil || !running {
-		return err
-	}
-
-	return s.mainProcess.Process.Signal(syscall.Signal(code))
-}
-
-func (s *standard) GetUid() int {
-	return -1
-}
-
-func (s *standard) GetGid() int {
-	return -1
-}
-
-func (s *standard) handleClose(callback func(exitCode int)) {
-	err := s.mainProcess.Wait()
-
-	_ = s.StatusTracker.WriteMessage(pufferpanel.Transmission{
-		Message: pufferpanel.ServerRunning{
-			Running: false,
-		},
-		Type: pufferpanel.MessageTypeStatus,
-	})
-
-	_ = s.Console.Close()
-
-	var exitCode int
-	if s.mainProcess == nil || s.mainProcess.ProcessState == nil || err != nil {
-		var psErr *exec.ExitError
-		if errors.As(err, &psErr) {
-			exitCode = psErr.ExitCode()
-		} else {
-			exitCode = 1
-		}
-	} else {
-		exitCode = s.mainProcess.ProcessState.ExitCode()
-	}
-	s.LastExitCode = exitCode
-
-	if s.mainProcess != nil && s.mainProcess.Process != nil {
-		_ = s.mainProcess.Process.Release()
-	}
-
-	s.mainProcess = nil
-
-	s.Wait.Done()
-
-	if callback != nil {
-		callback(exitCode)
-	}
-}
diff --git a/servers/tty/factory.go b/servers/tty/factory.go
index 835606fd..7ded4ed6 100644
--- a/servers/tty/factory.go
+++ b/servers/tty/factory.go
@@ -1,5 +1,3 @@
-//go:build !windows
-
 package tty
 
 import (
@@ -10,17 +8,8 @@ type EnvironmentFactory struct {
 	pufferpanel.EnvironmentFactory
 }
 
-func (ef EnvironmentFactory) Create(id string) pufferpanel.Environment {
-	t := &tty{
-		BaseEnvironment: &pufferpanel.BaseEnvironment{Type: ef.Key(), ServerId: id},
-	}
-
-	t.BaseEnvironment.Wrapper = t.CreateWrapper()
-	t.BaseEnvironment.ExecutionFunction = t.ttyExecuteAsync
-	t.BaseEnvironment.IsRunningFunc = t.isRunning
-	t.BaseEnvironment.KillFunc = t.kill
-
-	return t
+func (ef EnvironmentFactory) Create() pufferpanel.EnvironmentImpl {
+	return &tty{}
 }
 
 func (ef EnvironmentFactory) Key() string {
diff --git a/servers/tty/tty.go b/servers/tty/tty.go
index a45d73bd..e5ec0a76 100644
--- a/servers/tty/tty.go
+++ b/servers/tty/tty.go
@@ -1,5 +1,3 @@
-//go:build !windows
-
 package tty
 
 import (
@@ -7,6 +5,7 @@ import (
 	"fmt"
 	"github.com/creack/pty"
 	"github.com/pufferpanel/pufferpanel/v3"
+	"github.com/pufferpanel/pufferpanel/v3/config"
 	"github.com/pufferpanel/pufferpanel/v3/logging"
 	"github.com/pufferpanel/pufferpanel/v3/utils"
 	"github.com/shirou/gopsutil/process"
@@ -15,6 +14,7 @@ import (
 	"net"
 	"os"
 	"os/exec"
+	"runtime"
 	"strings"
 	"sync"
 	"syscall"
@@ -22,19 +22,25 @@ import (
 )
 
 type tty struct {
-	*pufferpanel.BaseEnvironment
-	mainProcess *exec.Cmd
-
-	statLocker   sync.Mutex
-	lastStats    *pufferpanel.ServerStats
-	lastStatTime time.Time
+	mainProcess         *exec.Cmd
+	statLocker          sync.Mutex
+	lastStats           *pufferpanel.ServerStats
+	lastStatTime        time.Time
+	disableStdin        bool
+	disableSpecialStats bool
+
+	DisableUnshare bool     `json:"disableUnshare"`
+	Mounts         []string `json:"mounts"`
 }
 
-func (t *tty) ttyExecuteAsync(steps pufferpanel.ExecutionData) (err error) {
-	t.Wait.Add(1)
+func (t *tty) ExecuteAsyncImpl(environment *pufferpanel.Environment, steps pufferpanel.ExecutionData) (err error) {
+	environment.Wait.Add(1)
 
-	pr := exec.Command(steps.Command, steps.Arguments...)
-	pr.Dir = t.GetRootDirectory()
+	pr, err := t.createCmd(environment.GetRootDirectory(), steps.Command)
+	if err != nil {
+		return err
+	}
+	pr.Dir = environment.GetRootDirectory()
 
 	var envVars = make(map[string]string)
 
@@ -48,7 +54,7 @@ func (t *tty) ttyExecuteAsync(steps pufferpanel.ExecutionData) (err error) {
 		}
 		envVars[key] = value
 	}
-	envVars["HOME"] = t.GetRootDirectory()
+	envVars["HOME"] = environment.GetRootDirectory()
 	envVars["TERM"] = "xterm-256color"
 	for k, v := range steps.Environment {
 		envVars[k] = v
@@ -58,38 +64,43 @@ func (t *tty) ttyExecuteAsync(steps pufferpanel.ExecutionData) (err error) {
 		pr.Env = append(pr.Env, fmt.Sprintf("%s=%s", k, v))
 	}
 
-	pr.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
 	t.mainProcess = pr
-	t.DisplayToConsole(true, "Starting process: %s %s", t.mainProcess.Path, strings.Join(t.mainProcess.Args[1:], " "))
-	t.Log(logging.Info, "Starting process: %s %s", t.mainProcess.Path, strings.Join(t.mainProcess.Args[1:], " "))
+	environment.DisplayToConsole(true, "Starting process: %s %s", t.mainProcess.Path, strings.Join(t.mainProcess.Args[1:], " "))
+	environment.Log(logging.Info, "Starting process: %s %s", t.mainProcess.Path, strings.Join(t.mainProcess.Args[1:], " "))
 
-	_ = t.StatusTracker.WriteMessage(pufferpanel.Transmission{
+	_ = environment.StatusTracker.WriteMessage(pufferpanel.Transmission{
 		Message: pufferpanel.ServerRunning{
 			Running:    true,
-			Installing: t.IsInstalling(),
+			Installing: environment.IsInstalling(),
 		},
 		Type: pufferpanel.MessageTypeStatus,
 	})
 
+	t.disableSpecialStats = steps.DisableStats
+	t.disableStdin = steps.DisableStdin
+
 	processTty, err := pty.Start(pr)
 	if err != nil {
-		t.Wait.Done()
+		environment.Wait.Done()
 		return
 	}
 
-	t.BaseEnvironment.CreateConsoleStdinProxy(steps.StdInConfig, processTty)
-	t.BaseEnvironment.Console.Start()
+	if !t.disableStdin {
+		environment.CreateConsoleStdinProxy(steps.StdInConfig, processTty)
+	}
+
+	environment.Console.Start()
 
 	go func(proxy io.Writer) {
 		_, _ = io.Copy(proxy, processTty)
-	}(t.Wrapper)
+	}(environment.Wrapper)
 
-	go t.handleClose(steps.Callback)
+	go t.handleClose(environment, steps.Callback)
 	return
 }
 
-func (t *tty) kill() (err error) {
-	running, err := t.IsRunning()
+func (t *tty) KillImpl(environment *pufferpanel.Environment) (err error) {
+	running, err := environment.IsRunning()
 	if err != nil {
 		return
 	}
@@ -99,8 +110,8 @@ func (t *tty) kill() (err error) {
 	return t.mainProcess.Process.Kill()
 }
 
-func (t *tty) GetStats() (*pufferpanel.ServerStats, error) {
-	running, err := t.IsRunning()
+func (t *tty) GetStatsImpl(environment *pufferpanel.Environment) (*pufferpanel.ServerStats, error) {
+	running, err := environment.IsRunning()
 	if err != nil {
 		return nil, err
 	}
@@ -110,7 +121,7 @@ func (t *tty) GetStats() (*pufferpanel.ServerStats, error) {
 			Memory: 0,
 		}
 
-		if t.Server.Stats.Type == "jcmd" {
+		if environment.Server.Stats.Type == "jcmd" {
 			stats.Jvm = &utils.JvmStats{}
 		}
 
@@ -138,7 +149,7 @@ func (t *tty) GetStats() (*pufferpanel.ServerStats, error) {
 		Memory: cast.ToFloat64(memMap.RSS),
 	}
 
-	if t.Server.Stats.Type == "jcmd" {
+	if !t.disableSpecialStats && environment.Server.Stats.Type == "jcmd" {
 		var socket *net.UnixConn
 		if socket, err = t.initiateJCMD(); err == nil && socket != nil {
 			for _, s := range []string{"1", "\x00", "jcmd", "\x00", "GC.heap_info", "\x00", "\x00", "\x00"} {
@@ -169,8 +180,8 @@ func (t *tty) GetStats() (*pufferpanel.ServerStats, error) {
 	return stats, nil
 }
 
-func (t *tty) SendCode(code int) error {
-	running, err := t.IsRunning()
+func (t *tty) SendCodeImpl(environment *pufferpanel.Environment, code int) error {
+	running, err := environment.IsRunning()
 
 	if err != nil || !running {
 		return err
@@ -179,15 +190,15 @@ func (t *tty) SendCode(code int) error {
 	return t.mainProcess.Process.Signal(syscall.Signal(code))
 }
 
-func (t *tty) GetUid() int {
+func (t *tty) GetUidImpl(*pufferpanel.Environment) int {
 	return -1
 }
 
-func (t *tty) GetGid() int {
+func (t *tty) GetGidImpl(*pufferpanel.Environment) int {
 	return -1
 }
 
-func (t *tty) isRunning() (isRunning bool, err error) {
+func (t *tty) IsRunningImpl(*pufferpanel.Environment) (isRunning bool, err error) {
 	isRunning = t.mainProcess != nil && t.mainProcess.Process != nil
 	if isRunning {
 		pr, pErr := os.FindProcess(t.mainProcess.Process.Pid)
@@ -200,10 +211,10 @@ func (t *tty) isRunning() (isRunning bool, err error) {
 	return
 }
 
-func (t *tty) handleClose(callback func(exitCode int)) {
+func (t *tty) handleClose(environment *pufferpanel.Environment, callback func(exitCode int)) {
 	err := t.mainProcess.Wait()
 
-	_ = t.Console.Close()
+	_ = environment.Console.Close()
 
 	var exitCode int
 	if t.mainProcess.ProcessState == nil || err != nil {
@@ -216,14 +227,14 @@ func (t *tty) handleClose(callback func(exitCode int)) {
 	} else {
 		exitCode = t.mainProcess.ProcessState.ExitCode()
 	}
-	t.LastExitCode = exitCode
+	environment.LastExitCode = exitCode
 
 	if err != nil {
-		t.Log(logging.Error, "Error waiting on process: %s\n", err)
+		environment.Log(logging.Error, "Error waiting on process: %s\n", err)
 	}
 
 	if t.mainProcess != nil && t.mainProcess.ProcessState != nil {
-		t.Log(logging.Debug, "%s\n", t.mainProcess.ProcessState.String())
+		environment.Log(logging.Debug, "%s\n", t.mainProcess.ProcessState.String())
 	}
 
 	if t.mainProcess != nil && t.mainProcess.Process != nil {
@@ -233,18 +244,29 @@ func (t *tty) handleClose(callback func(exitCode int)) {
 	t.statLocker.Lock()
 	t.statLocker.Unlock()
 
+	//if we are using unshare AND we're in tmp, we can nuke the workspace at this point
+	if !t.DisableUnshare && strings.HasPrefix(t.mainProcess.Dir, os.TempDir()) {
+		err = os.RemoveAll(t.mainProcess.Dir)
+		if err != nil {
+			logging.Debug.Printf("Failed to delete %s: %s", t.mainProcess.Dir, err.Error())
+		}
+	}
+
 	t.mainProcess = nil
 
-	t.Wait.Done()
+	environment.Wait.Done()
 
-	_ = t.StatusTracker.WriteMessage(pufferpanel.Transmission{
+	_ = environment.StatusTracker.WriteMessage(pufferpanel.Transmission{
 		Message: pufferpanel.ServerRunning{
 			Running:    false,
-			Installing: t.IsInstalling(),
+			Installing: environment.IsInstalling(),
 		},
 		Type: pufferpanel.MessageTypeStatus,
 	})
 
+	t.disableStdin = false
+	t.disableSpecialStats = false
+
 	if callback != nil {
 		callback(exitCode)
 	}
@@ -312,3 +334,93 @@ func (t *tty) initiateJCMD() (*net.UnixConn, error) {
 
 	return net.DialUnix("unix", nil, addr)
 }
+
+var cmdList = []string{
+	"mkdir -p {dev,bin,usr,lib,etc,tmp,proc}",
+	"mount -t tmpfs -o size=50m tmpfs tmp",
+	"mount --bind /bin bin",
+	"mount --bind /lib lib",
+	"mount --rbind /usr usr",
+	"mount --rbind /etc etc",
+	"mount --rbind /dev dev",
+	"mount --rbind /proc proc",
+}
+
+func (t *tty) createCmd(workDir, cmd string) (pr *exec.Cmd, err error) {
+	if t.DisableUnshare || config.SecurityDisableUnshare.Value() {
+		c, args := utils.SplitArguments(cmd)
+		pr = exec.Command(c, args...)
+		pr.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
+		pr.Dir = workDir
+		return
+	} else {
+		workDirMount := removeRoot(workDir)
+		binaryFolderMount := removeRoot(config.BinariesFolder.Value())
+		cacheFolderMount := removeRoot(config.CacheFolder.Value())
+
+		mountFolders := []string{workDirMount, binaryFolderMount, cacheFolderMount}
+		for _, v := range t.Mounts {
+			mountFolders = append(mountFolders, removeRoot(v))
+		}
+
+		unshareArgs := make([]string, len(cmdList))
+		copy(unshareArgs, cmdList)
+
+		if runtime.GOARCH == "amd64" {
+			unshareArgs = append(unshareArgs,
+				"mkdir -p lib64",
+				"mount --bind /lib64 lib64",
+			)
+		}
+
+		unshareArgs = append(unshareArgs,
+			fmt.Sprintf("mkdir -p {%s}", strings.Join(mountFolders, ",")),
+			fmt.Sprintf("mount --bind %s %s", workDir, workDirMount),
+			fmt.Sprintf("mount --bind %s %s", config.BinariesFolder.Value(), binaryFolderMount),
+			fmt.Sprintf("mount --bind %s %s", config.CacheFolder.Value(), cacheFolderMount),
+		)
+
+		for _, v := range t.Mounts {
+			unshareArgs = append(unshareArgs, fmt.Sprintf("mount --bind %s %s", v, removeRoot(v)))
+		}
+
+		unshareArgs = append(unshareArgs, "mount --rbind / .",
+			fmt.Sprintf("unshare -UR . -w %s --map-user=%d --map-group=%d %s", workDir, os.Getuid(), os.Getgid(), cmd))
+
+		pr = exec.Command("bash", "-c", strings.Join(unshareArgs, " && "))
+		pr.Dir, err = os.MkdirTemp("", "unshare-pp-")
+		if err != nil {
+			return
+		}
+		pr.SysProcAttr = &syscall.SysProcAttr{
+			Setctty: true,
+			Setsid:  true,
+			Unshareflags: syscall.CLONE_NEWUSER |
+				syscall.CLONE_NEWNS |
+				syscall.CLONE_FILES |
+				syscall.CLONE_NEWCGROUP |
+				syscall.CLONE_NEWIPC |
+				syscall.CLONE_NEWUTS,
+			Credential: &syscall.Credential{Uid: 0, Gid: 0, NoSetGroups: true},
+			UidMappings: []syscall.SysProcIDMap{
+				{
+					ContainerID: 0,
+					HostID:      os.Getuid(),
+					Size:        1,
+				},
+			},
+			GidMappings: []syscall.SysProcIDMap{
+				{
+					ContainerID: 0,
+					HostID:      os.Getgid(),
+					Size:        1,
+				},
+			},
+		}
+	}
+	return
+}
+
+func removeRoot(path string) string {
+	return strings.TrimPrefix(path, "/")
+}
diff --git a/services/templates.go b/services/templates.go
index fbba419e..9d590053 100644
--- a/services/templates.go
+++ b/services/templates.go
@@ -244,7 +244,7 @@ func (t *Template) AddRepo(repo *models.TemplateRepo) error {
 }
 
 func (t *Template) DeleteRepo(id uint) error {
-	return t.DB.Where(&models.TemplateRepo{ID: id}).Error
+	return t.DB.Where(&models.TemplateRepo{ID: id}).Delete(&models.TemplateRepo{ID: id}).Error
 }
 
 func readTemplateFromDisk(name, path string) (*models.Template, error) {
diff --git a/services/user.go b/services/user.go
index 0148d589..cd76eb38 100644
--- a/services/user.go
+++ b/services/user.go
@@ -245,6 +245,10 @@ func (us *User) Search(usernameFilter, emailFilter string, pageSize, page uint)
 	return users, count, res.Error
 }
 
-func (us *User) IsSecurePassword(password string) bool {
-	return len(password) >= 8
+func (us *User) IsSecurePassword(password string) error {
+	//TODO: Change to use validator
+	if len(password) < 8 {
+		return pufferpanel.ErrFieldLength("password", 8, 100)
+	}
+	return nil
 }
diff --git a/systemd/scripts/postinstall.sh b/systemd/scripts/postinstall.sh
index 5b7e0937..ca8a33d4 100644
--- a/systemd/scripts/postinstall.sh
+++ b/systemd/scripts/postinstall.sh
@@ -7,8 +7,16 @@ if [ ! -f "/var/lib/pufferpanel/database.db" ]; then
   touch /var/lib/pufferpanel/database.db
 fi
 
-pufferpanel --config=/etc/pufferpanel/config.json dbmigrate
+pufferpanel --config=/etc/pufferpanel/config.json db upgrade
 exitCode=$?
 [ $exitCode -eq 0 ] || [ $exitCode -eq 9 ] || exit $exitCode
 
 chown -R pufferpanel:pufferpanel /etc/pufferpanel /var/log/pufferpanel /var/lib/pufferpanel /var/www/pufferpanel
+
+if command -v apparmor_parser >/dev/null 2>&1
+then
+    apparmor_parser -r /etc/apparmor.d/pufferpanel
+fi
+
+chmod o-rx /etc/pufferpanel
+chmod o-rx /var/lib/pufferpanel
diff --git a/systemd/scripts/postupgrade.sh b/systemd/scripts/postupgrade.sh
index ff4cd31f..9c2071c8 100644
--- a/systemd/scripts/postupgrade.sh
+++ b/systemd/scripts/postupgrade.sh
@@ -2,10 +2,6 @@
 
 systemctl daemon-reload
 
-if [ -f "/var/lib/pufferpanel/database-RESTORE.db" ]; then
-  mv /var/lib/pufferpanel/database-RESTORE.db /var/lib/pufferpanel/database.db
-fi
-
 if [ ! -f "/var/lib/pufferpanel/database.db" ]; then
   touch /var/lib/pufferpanel/database.db
 fi
@@ -15,7 +11,7 @@ wasRunning=$?
 
 systemctl stop pufferpanel
 
-pufferpanel --config=/etc/pufferpanel/config.json dbmigrate
+pufferpanel --config=/etc/pufferpanel/config.json db upgrade
 exitCode=$?
 [ $exitCode -eq 0 ] || [ $exitCode -eq 9 ] || exit $exitCode
 
@@ -30,3 +26,10 @@ fi
 exitCode=$?
 [ $exitCode -eq 0 ] || [ $exitCode -eq 9 ] || exit $exitCode
 
+if command -v apparmor_parser >/dev/null 2>&1
+then
+    apparmor_parser -r /etc/apparmor.d/pufferpanel
+fi
+
+chmod o-rx /etc/pufferpanel
+chmod o-rx /var/lib/pufferpanel
diff --git a/systemd/scripts/preupgrade.sh b/systemd/scripts/preupgrade.sh
index 018a03f8..7c434eab 100644
--- a/systemd/scripts/preupgrade.sh
+++ b/systemd/scripts/preupgrade.sh
@@ -1,5 +1,5 @@
 #!/usr/bin/env sh
 
 if [ -f "/var/lib/pufferpanel/database.db" ]; then
-  mv /var/lib/pufferpanel/database.db /var/lib/pufferpanel/database-RESTORE.db
+  cp -f /var/lib/pufferpanel/database.db /var/lib/pufferpanel/database-RESTORE.db
 fi
diff --git a/systemd/servicefiles/apparmor.conf b/systemd/servicefiles/apparmor.conf
new file mode 100644
index 00000000..c2941931
--- /dev/null
+++ b/systemd/servicefiles/apparmor.conf
@@ -0,0 +1,27 @@
+# Last Modified: Sat Jun 14 14:12:25 2025
+abi <abi/3.0>,
+
+include <tunables/global>
+
+/usr/sbin/pufferpanel flags=(unconfined) {
+  #include <abstractions/base>
+
+  #/etc/pufferpanel/* rw,
+  #/var/lib/pufferpanel/* rw,
+  #/var/log/pufferpanel/* rw,
+  #/var/lib/pufferpanel/binaries/* ux,
+
+  #capability setgid,
+  #capability setuid,
+  #capability sys_admin,
+
+  #mount options=(rprivate, rw) -> /,
+
+  #/usr/sbin/pufferpanel mr,
+  #/usr/bin/* ux,
+  #owner /proc/*/gid_map w,
+  #owner /proc/*/setgroups w,
+  #owner /proc/*/uid_map w,
+
+  #/dev/tty r,
+}
diff --git a/tools/unshare/main.go b/tools/unshare/main.go
new file mode 100644
index 00000000..eb8a1c01
--- /dev/null
+++ b/tools/unshare/main.go
@@ -0,0 +1,150 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+	"syscall"
+)
+
+// This is a test package to see how unshare/chroot works in the Golang work
+// if this works well, this wil be merged into the main codebase
+
+//things we need to do:
+//configure apparmor to permit us to create user namespaces
+/* /etc/apparmor.d/unshare
+abi <abi/4.0>,
+include <tunables/global>
+
+profile lordralex /usr/bin/unshare flags=(unconfined) {
+  userns,
+}
+*/ //sudo apparmor_parser -r /etc/apparmor.d/unshare
+
+//after more testing, AppArmor can shove it. They are causing our issues. SOMEHOW EVEN THOUGH THEY DONT GIVE A DAMN INDICATION
+// - /etc/default/grub -> GRUB_CMDLINE_LINUX="apparmor=0"
+// - update-grub
+// - reboot
+
+//ensure uidmap is installed (?) - removing it works so far
+
+/*
+mkdir -p {tmp,proc,run/systemd/resolve}
+unshare --mount-proc=proc --map-users 1000:1000:1 -muipfCr bash -c 'mkdir -p {pufferpanel,dev,bin,usr,lib,lib64,etc}; mount --bind /bin bin; mount --bind /usr usr; mount --bind /lib lib; mount --bind /lib64 lib64; mount --rbind /etc etc; mount --rbind /dev dev; mount --rbind /run/systemd/resolve run/systemd/resolve; mount -t tmpfs -o size=100m tmpfs tmp; mount --rbind / .; unshare -UR . -w pufferpanel bash'
+*/
+
+///etc/apparmor.d/pufferpanel
+//# Last Modified: Sat Jun 14 14:12:25 2025
+//abi <abi/3.0>,
+//
+//include <tunables/global>
+//
+///home/lordralex/.cache/JetBrains/GoLand2025.1/tmp/GoLand/___1go_build_github_com_pufferpanel_pufferpanel_v3_tools_unshare {
+//  include <abstractions/base>
+//
+//  capability setgid,
+//  capability setuid,
+//  capability sys_admin,
+//
+//  mount options=(rprivate, rw) -> /,
+//
+//  /home/lordralex/.cache/JetBrains/GoLand2025.1/tmp/GoLand/___1go_build_github_com_pufferpanel_pufferpanel_v3_tools_unshare mr,
+//  /usr/bin/* ux,
+//  owner /proc/*/gid_map w,
+//  owner /proc/*/setgroups w,
+//  owner /proc/*/uid_map w,
+//}
+
+//what we've ended up with is a nightmare. But, a working one
+//the current thing works if we remove new PID (this causes issues with the forking because Go can't do the -f flag)
+//we also had to disable the net namespace because we could not actually use the network, unsure why (but I bet... go)
+//we store our working folder into /tmp so that it's cleaner where we actually persist files
+//this defers back to our "regular" username so we should be okay there
+//move the dir we're "chrooted" in to the arguments so we can test it elsewhere
+//long term, we might need to look at persistence, and see if we can "persist" these so we don't need to keep remaking it
+//this required several rounds of coffee, mountain dew, and midori
+
+func main() {
+	dir, _ := os.Getwd()
+	if len(os.Args) > 1 {
+		dir = os.Args[1]
+	}
+
+	//as soon as you add the third command, this no longer functions
+	unshare(dir, "pwd")                        //are we in the right place
+	unshare(dir, "ls", "-l")                   //do we see anything
+	unshare(dir, "whoami")                     //are we the correct user
+	unshare(dir, "touch", "test")              //can we write and it persist
+	unshare(dir, "curl", "1.1.1.1")            //can we access an IP
+	unshare(dir, "curl", "google.com")         //does DNS work
+	unshare(dir, "curl", "https://google.com") //does SSL work
+	unshare(dir, "ls", "-l", "/")              //is root clean
+
+	//now let's test the servers!
+	//unshare(dir, "ls", "-l", "/usr/lib/jvm/java-21-openjdk-amd64/lib")
+	//unshare(dir, "env")
+	unshare(dir, "java", "-Xmx4G", "-jar", "server.jar", "nogui")
+}
+
+var cmdList = []string{
+	"mkdir -p {dev,bin,usr,lib,lib64,etc,tmp,proc}",
+	"mount -t tmpfs -o size=50m tmpfs tmp",
+	"mount --bind /bin bin",
+	"mount --bind /lib lib",
+	"mount --bind /lib64 lib64",
+	"mount --rbind /usr usr",
+	"mount --rbind /etc etc",
+	"mount --rbind /dev dev",
+	"mount --rbind /proc proc",
+}
+
+func unshare(dir, cmd string, args ...string) {
+	var err error
+
+	c := exec.Command("bash", "-c",
+		strings.Join(append(cmdList,
+			"mkdir -p "+strings.TrimPrefix(dir, "/"),
+			"mount --bind "+dir+" "+strings.TrimPrefix(dir, "/"),
+			"mount --rbind / .",
+			fmt.Sprintf("unshare -UR . -w %s --map-user=%d --map-group=%d %s %s", dir, os.Getuid(), os.Getgid(), cmd, strings.Join(args, " ")),
+		), " && "))
+	c.Dir, err = os.MkdirTemp("", "unshare-pp-")
+	defer func() {
+		_ = os.RemoveAll(c.Dir)
+	}()
+	if err != nil {
+		panic(err)
+	}
+	c.SysProcAttr = &syscall.SysProcAttr{
+		Unshareflags: syscall.CLONE_NEWUSER |
+			syscall.CLONE_NEWNS |
+			syscall.CLONE_FILES |
+			syscall.CLONE_NEWCGROUP |
+			syscall.CLONE_NEWIPC |
+			//syscall.CLONE_NEWNET |
+			syscall.CLONE_NEWUTS, //|
+		//syscall.CLONE_NEWPID,
+		Credential: &syscall.Credential{Uid: 0, Gid: 0, NoSetGroups: true},
+		UidMappings: []syscall.SysProcIDMap{
+			{
+				ContainerID: 0,
+				HostID:      os.Getuid(),
+				Size:        1,
+			},
+		},
+		GidMappings: []syscall.SysProcIDMap{
+			{
+				ContainerID: 0,
+				HostID:      os.Getgid(),
+				Size:        1,
+			},
+		},
+	}
+	fmt.Println(c.String())
+	output, err := c.CombinedOutput()
+	fmt.Printf("%s\n", output)
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/utils/arguments.go b/utils/arguments.go
index a6a41507..7bcbd028 100644
--- a/utils/arguments.go
+++ b/utils/arguments.go
@@ -81,3 +81,18 @@ func SplitArguments(source string) (cmd string, arguments []string) {
 	arguments = results[1:]
 	return
 }
+
+func MergeArguments(arguments []string) string {
+	var result string
+	for _, v := range arguments {
+		if result != "" {
+			result += " "
+		}
+		if strings.Contains(v, " ") && !strings.HasPrefix(v, "\"") {
+			result += "\"" + v + "\""
+		} else {
+			result += v
+		}
+	}
+	return result
+}
diff --git a/utils/kernel_linux.go b/utils/kernel_tests.go
similarity index 100%
rename from utils/kernel_linux.go
rename to utils/kernel_tests.go
diff --git a/utils/kernel_windows.go b/utils/kernel_windows.go
deleted file mode 100644
index 4a1335d9..00000000
--- a/utils/kernel_windows.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package utils
-
-func testOpenat2() {
-	useOpenat2 = false
-}
diff --git a/web/api/self.go b/web/api/self.go
index 240df732..2d5d263a 100644
--- a/web/api/self.go
+++ b/web/api/self.go
@@ -97,7 +97,7 @@ func updateSelf(c *gin.Context) {
 
 	passwordChanged := false
 	if viewModel.NewPassword != "" {
-		if !us.IsSecurePassword(viewModel.NewPassword) {
+		if us.IsSecurePassword(viewModel.NewPassword) != nil {
 			response.HandleError(c, pufferpanel.ErrPasswordRequirements, http.StatusBadRequest)
 			return
 		}
diff --git a/web/api/servers.go b/web/api/servers.go
index c47d5ddc..ab858787 100644
--- a/web/api/servers.go
+++ b/web/api/servers.go
@@ -789,9 +789,8 @@ func editServerUser(c *gin.Context) {
 	if scopes.ContainsScope(currentPerms.Scopes, scopes.ScopeServerAdmin) || scopes.ContainsScope(currentGlobalPerms.Scopes, scopes.ScopeServerAdmin) || scopes.ContainsScope(currentGlobalPerms.Scopes, scopes.ScopeAdmin) {
 		existing.Scopes = perms.Scopes
 	} else {
-		allowedScopes := utils.Union(existing.Scopes, currentPerms.Scopes)
 		//update perms to match this "setup", but not stomp over what the user can't change
-		replacement := scopes.UpdateScopesWhereGranted(existing.Scopes, allowedScopes, currentPerms.Scopes)
+		replacement := scopes.UpdateScopesWhereGranted(existing.Scopes, perms.Scopes, currentPerms.Scopes)
 		existing.Scopes = replacement
 	}
 
diff --git a/web/api/templates.go b/web/api/templates.go
index 9678cad3..a8470aa5 100644
--- a/web/api/templates.go
+++ b/web/api/templates.go
@@ -84,8 +84,8 @@ func getsTemplatesForRepo(c *gin.Context) {
 // @Router /api/templates [post]
 // @Security OAuth2Application[templates.repo.create]
 func addRepo(c *gin.Context) {
-	var repo *models.TemplateRepo
-	err := c.BindJSON(repo)
+	var repo models.TemplateRepo
+	err := c.BindJSON(&repo)
 
 	if response.HandleError(c, err, http.StatusBadRequest) {
 		return
@@ -99,7 +99,7 @@ func addRepo(c *gin.Context) {
 	db := middleware.GetDatabase(c)
 	ts := &services.Template{DB: db}
 
-	err = ts.AddRepo(repo)
+	err = ts.AddRepo(&repo)
 
 	if response.HandleError(c, err, http.StatusInternalServerError) {
 		return
diff --git a/web/auth/register.go b/web/auth/register.go
index 7b442772..e45131e1 100644
--- a/web/auth/register.go
+++ b/web/auth/register.go
@@ -37,7 +37,7 @@ func RegisterPost(c *gin.Context) {
 		return
 	}
 
-	if !us.IsSecurePassword(request.Password) {
+	if us.IsSecurePassword(request.Password) != nil {
 		response.HandleError(c, pufferpanel.ErrPasswordRequirements, http.StatusBadRequest)
 		return
 	}
diff --git a/web/daemon/root.go b/web/daemon/root.go
index 396c6beb..bffda3d3 100644
--- a/web/daemon/root.go
+++ b/web/daemon/root.go
@@ -61,6 +61,7 @@ func getFeatures(c *gin.Context) {
 		envs = utils.Remove(envs, "host")
 		envs = utils.Remove(envs, "tty")
 		envs = utils.Remove(envs, "standard")
+		envs = utils.Remove(envs, "bubblewrap")
 	}
 
 	c.JSON(http.StatusOK, Features{Features: features, Environments: envs, OS: runtime.GOOS, Arch: runtime.GOARCH, Version: pufferpanel.Version})



Test patch:
diff --git a/operations/neoforge/neoforgedl_test.go b/operations/neoforgedl/neoforgedl_test.go
similarity index 100%
rename from operations/neoforge/neoforgedl_test.go
rename to operations/neoforgedl/neoforgedl_test.go
diff --git a/tools/templatetester/flags.go b/tools/templatetester/flags.go
new file mode 100644
index 00000000..3fa3c900
--- /dev/null
+++ b/tools/templatetester/flags.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+	"flag"
+	"strings"
+)
+
+var CmdFlags = &Flags{}
+
+func init() {
+	var skip string
+	var required string
+	var files string
+
+	flag.StringVar(&CmdFlags.GitRef, "gitRef", "refs/heads/v3", "")
+	flag.StringVar(&skip, "skip", "", "")
+	flag.StringVar(&required, "require", "*", "")
+	flag.StringVar(&CmdFlags.TemplateFolder, "path", "", "")
+	flag.BoolVar(&CmdFlags.DeleteTemp, "delete", true, "")
+	flag.StringVar(&CmdFlags.WorkingDir, "workDir", "", "")
+	flag.BoolVar(&CmdFlags.Reuse, "reuse", false, "")
+	flag.StringVar(&files, "files", "", "")
+	flag.StringVar(&CmdFlags.PufferpanelBinary, "binary", "pufferpanel", "")
+	flag.StringVar(&CmdFlags.Host, "host", "http://127.0.0.1:8080", "")
+	flag.BoolVar(&CmdFlags.PrintTests, "print", false, "")
+	flag.Parse()
+
+	if skip != "" {
+		CmdFlags.Skip = strings.Split(skip, ",")
+	} else {
+		CmdFlags.Skip = make([]string, 0)
+	}
+
+	if required != "" {
+		CmdFlags.Required = strings.Split(required, ",")
+	} else {
+		CmdFlags.Required = make([]string, 0)
+	}
+
+	if files != "" {
+		CmdFlags.Files = strings.Split(files, ",")
+	} else {
+		CmdFlags.Files = make([]string, 0)
+	}
+}
+
+type Flags struct {
+	GitRef            string
+	Skip              []string
+	Required          []string
+	TemplateFolder    string
+	DeleteTemp        bool
+	WorkingDir        string
+	Reuse             bool
+	Files             []string
+	PufferpanelBinary string
+	Host              string
+	PrintTests        bool
+}
diff --git a/tools/templatetester/main.go b/tools/templatetester/main.go
index 94a99a7e..6f563845 100644
--- a/tools/templatetester/main.go
+++ b/tools/templatetester/main.go
@@ -1,37 +1,30 @@
 package main
 
 import (
-	"bufio"
 	"bytes"
-	"context"
 	"encoding/json"
 	"errors"
-	"flag"
 	"fmt"
-	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/api/types/filters"
-	"github.com/docker/docker/client"
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/pufferpanel/pufferpanel/v3"
-	"github.com/pufferpanel/pufferpanel/v3/config"
-	"github.com/pufferpanel/pufferpanel/v3/logging"
-	"github.com/pufferpanel/pufferpanel/v3/servers"
-	docker2 "github.com/pufferpanel/pufferpanel/v3/servers/docker"
-	"github.com/pufferpanel/pufferpanel/v3/utils"
-	"github.com/spf13/cast"
+	"github.com/pufferpanel/pufferpanel/v3/models"
+	"github.com/pufferpanel/pufferpanel/v3/scopes"
+	"github.com/pufferpanel/pufferpanel/v3/services"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"io"
 	"io/fs"
 	"log"
+	"math"
+	"net"
+	"net/http"
+	"net/url"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
 	"time"
 )
 
-var templatesToSkip []string
-var mustTest []string
-var filesToTest []string
-
 // The purpose of this is to simply test all templates in our repo to the best of our ability
 // This will download all the templates, spin up a fake server using it, and attempt to run everything
 // For now, it will just test that we can create, install, start, and stop the server
@@ -40,42 +33,18 @@ var filesToTest []string
 // Arguments are templates to ignore, for ones which require data that we cannot actually safely test
 // such as ones which need Steam credentials or to actually own the game
 func main() {
-	var gitRef string
-	var skipStr string
-	var requiredStr string
-	var templateFolder string
-	var deleteTemp bool
-	var workingDir string
-	var reuse bool
-	var filesArg string
-	flag.StringVar(&gitRef, "gitRef", "refs/heads/v3", "")
-	flag.StringVar(&skipStr, "skip", "", "")
-	flag.StringVar(&requiredStr, "require", "", "")
-	flag.StringVar(&templateFolder, "path", "", "")
-	flag.BoolVar(&deleteTemp, "delete", true, "")
-	flag.StringVar(&workingDir, "workDir", "", "")
-	flag.BoolVar(&reuse, "reuse", false, "")
-	flag.StringVar(&filesArg, "files", "", "")
-	flag.Parse()
-
-	//just forces the config init to run, so we can then load the config values from ENV
-	_ = config.LoadConfigFile("")
-
-	if skipStr != "" {
-		templatesToSkip = strings.Split(skipStr, ",")
-		fmt.Printf("Skip rules: %s\n", strings.Join(templatesToSkip, " "))
+	if len(CmdFlags.Skip) != 0 {
+		log.Printf("Skip rules: %s", strings.Join(CmdFlags.Skip, " "))
 	}
-	if requiredStr != "" {
-		mustTest = strings.Split(requiredStr, ",")
-		fmt.Printf("Require rules: %s\n", strings.Join(mustTest, " "))
+	if len(CmdFlags.Required) != 0 {
+		log.Printf("Require rules: %s", strings.Join(CmdFlags.Required, " "))
 	}
-	if filesArg != "" {
-		filesToTest = strings.Split(filesArg, ",")
-		fmt.Printf("Files to test rules: %s\n", strings.Join(filesToTest, " "))
+	if len(CmdFlags.Files) != 0 {
+		log.Printf("Files to test rules: %s", strings.Join(CmdFlags.Files, " "))
 	}
 
 	var err error
-	if workingDir == "" {
+	if CmdFlags.WorkingDir == "" {
 		tmpDir := os.TempDir()
 		pattern := "puffertemplatetest"
 
@@ -85,11 +54,11 @@ func main() {
 			panicIf(err)
 		}
 
-		workingDir, err = os.MkdirTemp("", pattern)
+		CmdFlags.WorkingDir, err = os.MkdirTemp("", pattern)
 		panicIf(err)
 	} else {
-		err = filepath.WalkDir(workingDir, func(path string, info fs.DirEntry, err error) error {
-			if path == workingDir {
+		err = filepath.WalkDir(CmdFlags.WorkingDir, func(path string, info fs.DirEntry, err error) error {
+			if path == CmdFlags.WorkingDir {
 				return err
 			}
 			return os.RemoveAll(path)
@@ -99,389 +68,332 @@ func main() {
 		}
 	}
 
-	if deleteTemp {
+	if CmdFlags.DeleteTemp {
 		defer func() {
-			_ = os.RemoveAll(workingDir)
+			_ = os.RemoveAll(CmdFlags.WorkingDir)
 		}()
 	}
 
-	_ = config.DataRootFolder.Set(workingDir, false)
-	_ = config.LogsFolder.Set(filepath.Join(workingDir, "logs"), false)
-	_ = config.DatabaseDialect.Set("sqlite3", false)
-	_ = config.DatabaseUrl.Set("file:test.db?cache=shared&mode=memory", false)
-	_ = config.ConsoleForward.Set(true, false)
-
-	err = os.MkdirAll(config.ServersFolder.Value(), 0755)
-	panicIf(err)
-	err = os.MkdirAll(config.BinariesFolder.Value(), 0755)
-	panicIf(err)
-	err = os.MkdirAll(config.CacheFolder.Value(), 0755)
-	panicIf(err)
-	err = os.MkdirAll(config.LogsFolder.Value(), 0755)
-	panicIf(err)
+	var tests = buildTests()
 
-	newPath := os.Getenv("PATH")
-	fullPath, _ := filepath.Abs(config.BinariesFolder.Value())
-	if !strings.Contains(newPath, fullPath) {
-		_ = os.Setenv("PATH", newPath+":"+fullPath)
+	if len(tests) == 0 {
+		log.Printf("No tests were found")
+		return
 	}
 
-	logging.Initialize(false)
-	_ = docker2.InitContainerMountSource()
-
-	//this may require a DB, so we are going to pretend we have one
-	//because of how code works, we're going to abuse our own system
-	//db, err := database.GetConnection()
-	//panicIf(err)
-
-	//get all templates
-	if templateFolder == "" {
-		if templateFolder = os.Getenv("TEMPLATE_PATH"); templateFolder == "" {
-			templateFolder = filepath.Join(workingDir, "templates")
-			err = os.MkdirAll(templateFolder, 0755)
+	if CmdFlags.PrintTests {
+		data := make([]string, 0)
+		for _, v := range tests {
+			data = append(data, "\""+v.Name+"\"")
+		}
+		msg := fmt.Sprintf("TEMPLATES=%s\n", strings.Join(data, ","))
+		envFile := os.ExpandEnv("$GITHUB_ENV")
+		log.Println(msg)
+		if envFile != "" {
+			log.Printf("Writing data to %s", envFile)
+			file, err := os.OpenFile(envFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 			panicIf(err)
-
-			log.Printf("Cloning template repo")
-			_, err = git.PlainClone(templateFolder, false, &git.CloneOptions{
-				URL:           "https://github.com/PufferPanel/templates",
-				ReferenceName: plumbing.ReferenceName(gitRef),
-				SingleBranch:  true,
-				Depth:         1,
-			})
+			defer func() {
+				_ = file.Close()
+			}()
+			_, err = file.WriteString(msg)
 			panicIf(err)
 		}
+		return
 	}
 
-	var templateFolders []os.DirEntry
-	templateFolders, err = os.ReadDir(templateFolder)
+	//we have our test set, let's kick off a panel instance
+	//for this, we're going to run the binary, and wait for the "service" to start up (using the unix socket)
+	//once that's done, we'll then create our servers
+	//this is the best way to truly "model" what's going on
+
+	waiter := make(chan bool, 1)
+
+	var unixSocketPath = fmt.Sprintf("/tmp/pufferpanel-%d.sock", time.Now().Unix())
+	_ = os.Remove(unixSocketPath)
+	listener, err := net.ListenPacket("unixgram", unixSocketPath)
 	panicIf(err)
 
-	testScenarios := make([]*TestScenario, 0)
+	var dbConn = "file:" + filepath.Join(CmdFlags.WorkingDir, "test.db") + "?cache=shared"
 
-	for _, folder := range templateFolders {
-		if !folder.IsDir() || strings.HasPrefix(folder.Name(), ".") {
-			continue
-		}
+	go socketListener(listener, waiter)
+
+	c := exec.Command(CmdFlags.PufferpanelBinary, "runService")
 
-		if _, err = os.Stat(filepath.Join(templateFolder, folder.Name(), ".skip")); err == nil {
-			continue
+	defer func() {
+		if c.Process.Pid != 0 {
+			_ = c.Process.Kill()
 		}
+		_ = listener.Close()
+		_ = os.Remove(unixSocketPath)
+	}()
 
-		var files []os.DirEntry
-		files, err = os.ReadDir(filepath.Join(templateFolder, folder.Name()))
+	go func() {
+		c.Dir = CmdFlags.WorkingDir
+		c.Env = append(
+			os.Environ(),
+			"PUFFER_PANEL_DATABASE_URL="+dbConn,
+			"PUFFER_LOGS=logs",
+			"PUFFER_DAEMON_CONSOLE_FORWARD=true",
+			"PUFFER_DAEMON_DATA_ROOT="+CmdFlags.WorkingDir,
+			"PUFFER_WEB_HOST=0.0.0.0:8080",
+			"NOTIFY_SOCKET="+unixSocketPath,
+			"GIN_MODE=release",
+		)
+		stdout, err := c.StdoutPipe()
+		panicIf(err)
+		stderr, err := c.StderrPipe()
 		panicIf(err)
 
-		for _, file := range files {
-			if file.Name() == "data.json" {
-				continue
-			}
+		panicIf(c.Start())
 
-			if len(filesToTest) > 0 {
-				test := false
-				for _, t := range filesToTest {
-					if t == file.Name() {
-						test = true
-						break
-					}
-				}
-				if !test {
-					continue
-				}
-			}
+		go ioCopy(os.Stdout, stdout)
+		go ioCopy(os.Stderr, stderr)
 
-			filePath := filepath.Join(templateFolder, folder.Name(), file.Name())
-			if strings.HasSuffix(file.Name(), ".json") {
-				tmp := &TestTemplate{}
-				tmp.Name = strings.TrimSuffix(file.Name(), ".json")
-
-				tmp.Template, err = os.ReadFile(filePath)
-				panicIf(err)
-
-				_, err = os.Stat(filepath.Join(templateFolder, folder.Name(), "data.txt"))
-				if err == nil {
-					tmp.Variables, err = readDataTxtFile(filepath.Join(templateFolder, folder.Name(), "data.txt"))
-					panicIf(err)
-				} else if !os.IsNotExist(err) {
-					panicIf(err)
-				}
-
-				_, err = os.Stat(filepath.Join(templateFolder, folder.Name(), "data.json"))
-				if err == nil {
-					tests, err := readDataJsonFile(filepath.Join(templateFolder, folder.Name(), "data.json"))
-					for _, v := range tests {
-						testScenarios = append(testScenarios, &TestScenario{
-							Name: v.Name,
-							Test: &TestTemplate{
-								Template:           tmp.Template,
-								Name:               tmp.Name,
-								Variables:          v.Variables,
-								Environment:        v.Environment,
-								IgnoreExitCode:     v.IgnoreExitCode,
-								RuntimeRequirement: v.RuntimeRequirement,
-							},
-						})
-					}
-					panicIf(err)
-				} else if !os.IsNotExist(err) {
-					panicIf(err)
-				} else {
-					//no data json, which means it's a single test
-					//but, each template could support envs, so auto-process each
-
-					template := pufferpanel.Server{}
-					err = json.NewDecoder(bytes.NewReader(tmp.Template)).Decode(&template)
-					panicIf(err)
-
-					if len(template.SupportedEnvironments) > 0 {
-						for _, v := range template.SupportedEnvironments {
-							z := &TestTemplate{
-								Template:           tmp.Template,
-								Name:               tmp.Name,
-								Environment:        make(map[string]interface{}),
-								Variables:          make(map[string]interface{}),
-								RuntimeRequirement: tmp.RuntimeRequirement,
-							}
-
-							scenario := &TestScenario{
-								Name: z.Name,
-								Test: z,
-							}
-							if v.Type != "host" {
-								scenario.Name = scenario.Name + "-" + v.Type
-							}
-
-							for r, p := range v.Metadata {
-								scenario.Test.Environment[r] = p
-							}
-
-							scenario.Test.Environment["type"] = v.Type
-
-							testScenarios = append(testScenarios, scenario)
-						}
-					} else {
-						testScenarios = append(testScenarios, &TestScenario{
-							Name: tmp.Name,
-							Test: tmp,
-						})
-					}
-				}
+		e := c.Wait()
+		var d *exec.ExitError
+		if errors.As(e, &d) {
+			if d.Error() == "signal: killed" {
+				e = nil
 			}
 		}
-	}
+		panicIf(e)
+	}()
 
-	var docker *client.Client
-	ctx := context.Background()
+	//wait for panel to be up, so the db is fully created and we're good to go
+	<-waiter
 
-	finalScenarioList := make([]*TestScenario, 0)
-	for _, scenario := range testScenarios {
-		skip := false
-		for _, v := range templatesToSkip {
-			if utils.CompareWildcard(scenario.Name, v) {
-				skip = true
-				break
-			}
-		}
-		if skip {
-			for _, v := range mustTest {
-				if utils.CompareWildcard(scenario.Name, v) {
-					skip = false
-					break
-				}
-			}
+	//now we can inject our admin user in, so we can proceed to spin up the servers
+	log.Println("Starting database edits")
+	db, err := gorm.Open(sqlite.Open(dbConn))
+	panicIf(err)
+	panicIf(initLoginAdminUser(db))
 
-			if skip {
-				log.Printf("Skipping %s", scenario.Name)
-				continue
-			}
-		}
-		log.Printf("Will run test for: %s", scenario.Name)
-		finalScenarioList = append(finalScenarioList, scenario)
-	}
+	//now, start the web calls
+	//the concern is the session length, we'll "force" it to last for 24 hours. if it expires, then the tests should
+	//be failed anyways
+	client := &http.Client{}
+	session, err := createSession(db)
+	panicIf(err)
 
-	//now... we can create servers from each one of them
-	for _, scenario := range finalScenarioList {
-		log.Printf("Starting test for %s", scenario.Name)
+	log.Println("Now starting tests")
 
-		template := scenario.Test
+	for i := range tests {
+		runTest(client, session, tests[i])
+	}
+}
 
-		if strings.HasSuffix(scenario.Name, "-docker") {
-			//kill off any existing docker containers
-			if docker == nil {
-				docker, err = client.NewClientWithOpts(client.FromEnv)
-				panicIf(err)
-				docker.NegotiateAPIVersion(ctx)
-			}
+func runTest(client *http.Client, session string, test *TestScenario) {
+	log.Println("\nStarting: " + test.Name)
 
-			opts := container.ListOptions{
-				Filters: filters.NewArgs(),
-			}
+	template := pufferpanel.Server{}
+	err := json.NewDecoder(bytes.NewReader(test.Test.Template)).Decode(&template)
+	panicIf(err)
+	if err = template.Requirements.Test(template); err != nil {
+		log.Printf("Template requirements failed: %s", err)
+		return
+	}
 
-			opts.All = true
-			opts.Filters.Add("name", scenario.Name)
+	urlPrefix := CmdFlags.Host + "/api/servers/" + test.Name
 
-			existingContainers, err := docker.ContainerList(ctx, opts)
-			panicIf(err)
-			if len(existingContainers) > 0 {
-				err = docker.ContainerRemove(ctx, scenario.Name, container.RemoveOptions{
-					Force: true,
-				})
-				panicIf(err)
-			}
-		}
+	var data []byte
 
-		buf := bytes.NewReader(template.Template)
+	//create server
+	_, err = call(client, &http.Request{
+		Method: "PUT",
+		URL:    createUrl(urlPrefix),
+		Header: createHeaders(session),
+		Body:   createCreateBody(test),
+	})
+	panicIf(err)
 
-		log.Printf("Creating server")
-		prg := servers.CreateProgram()
-		err = json.NewDecoder(buf).Decode(prg)
+	//install server
+	_, err = call(client, &http.Request{
+		Method: "POST",
+		URL:    createUrl(urlPrefix + "/install"),
+		Header: createHeaders(session),
+	})
+	panicIf(err)
+
+	//wait for install to complete
+	for {
+		time.Sleep(10 * time.Second)
+		data, err = call(client, &http.Request{
+			Method: "GET",
+			URL:    createUrl(urlPrefix + "/status"),
+			Header: createHeaders(session),
+		})
 		panicIf(err)
-		prg.Identifier = strings.ReplaceAll(scenario.Name, "+", "")
 
-		if template.Variables != nil {
-			for k, v := range template.Variables {
-				variable := prg.Variables[k]
-				variable.Value = v
-				prg.Variables[k] = variable
-			}
-		}
+		var status pufferpanel.ServerRunning
+		err = json.NewDecoder(bytes.NewReader(data)).Decode(&status)
+		panicIf(err)
 
-		if len(template.Environment) > 0 {
-			prg.Environment = pufferpanel.MetadataType{
-				Type:     cast.ToString(template.Environment["type"]),
-				Metadata: template.Environment,
-			}
+		if !status.Installing {
+			break
 		}
+	}
 
-		if reuse {
-			_ = os.Remove(filepath.Join(config.ServersFolder.Value(), prg.Identifier+".json"))
-		} else {
-			_ = os.RemoveAll(filepath.Join(config.ServersFolder.Value(), prg.Identifier))
-		}
+	//start server
+	_, err = call(client, &http.Request{
+		Method: "POST",
+		URL:    createUrl(urlPrefix + "/start"),
+		Header: createHeaders(session),
+	})
+	panicIf(err)
 
-		prg, err = servers.Create(prg)
+	//wait for 5 minutes
+	started := time.Now()
+	for {
+		time.Sleep(1 * time.Minute)
+		data, err = call(client, &http.Request{
+			Method: "GET",
+			URL:    createUrl(urlPrefix + "/status"),
+			Header: createHeaders(session),
+		})
 		panicIf(err)
 
-		err = prg.Install()
+		var status pufferpanel.ServerRunning
+		err = json.NewDecoder(bytes.NewReader(data)).Decode(&status)
 		panicIf(err)
 
-		err = runServer(prg, template.RuntimeRequirement)
+		if !status.Running {
+			panicIf(errors.New("server did not run for 5 minutes"))
+		}
+		if math.Abs(time.Since(started).Seconds()) >= 360 {
+			break
+		}
+	}
+
+	//stop server
+	_, err = call(client, &http.Request{
+		Method: "POST",
+		URL:    createUrl(urlPrefix + "/stop"),
+		Header: createHeaders(session),
+	})
+	panicIf(err)
+
+	//wait for the stop
+	started = time.Now()
+	for {
+		time.Sleep(1 * time.Minute)
+		data, err = call(client, &http.Request{
+			Method: "GET",
+			URL:    createUrl(urlPrefix + "/status"),
+			Header: createHeaders(session),
+		})
 		panicIf(err)
 
-		running, err := prg.IsRunning()
+		var status pufferpanel.ServerRunning
+		err = json.NewDecoder(bytes.NewReader(data)).Decode(&status)
 		panicIf(err)
 
-		if running {
-			panic(errors.New("server is still running"))
+		if !status.Running {
+			break
 		}
-
-		if !template.IgnoreExitCode && prg.RunningEnvironment.GetLastExitCode() != prg.Execution.ExpectedExitCode {
-			panicIf(fmt.Errorf("exit code status %d", prg.RunningEnvironment.GetLastExitCode()))
+		if math.Abs(time.Since(started).Seconds()) >= 360 {
+			panicIf(errors.New("server did not stop after 360 seconds"))
 		}
+	}
+
+	//delete server
+	_, err = call(client, &http.Request{
+		Method: "DELETE",
+		URL:    createUrl(urlPrefix),
+		Header: createHeaders(session),
+	})
+
+	panicIf(err)
+}
 
-		err = prg.Destroy()
+func socketListener(listener net.PacketConn, ch chan bool) {
+	defer func() {
+		_ = listener.Close()
+	}()
+
+	const NotifyReady = "READY=1"
+	const NotifyShutdown = "STOPPING=1"
+
+	for {
+		buf := make([]byte, 16)
+		n, _, err := listener.ReadFrom(buf)
+		if err != nil {
+			return
+		}
 		panicIf(err)
 
-		//force delete files
-		if reuse {
-			_ = os.Remove(filepath.Join(config.ServersFolder.Value(), prg.Identifier+".json"))
-		} else {
-			_ = os.RemoveAll(filepath.Join(config.ServersFolder.Value(), prg.Identifier))
+		str := string(buf[:n])
+
+		if str == NotifyReady {
+			ch <- true
+		} else if str == NotifyShutdown {
+			break
 		}
 	}
 }
 
-func readDataTxtFile(fileName string) (map[string]interface{}, error) {
-	file, err := os.Open(fileName)
-	if err != nil {
-		return nil, err
-	}
-	defer utils.Close(file)
-
-	result := make(map[string]interface{})
-	scanner := bufio.NewScanner(file)
-	for scanner.Scan() {
-		line := scanner.Text()
-		parts := strings.Split(line, "=")
-		key := parts[0]
-		value := parts[1]
-		result[key] = value
-	}
-	return result, nil
+var loginAdminUser = &models.User{
+	Username:       "loginAdminUser",
+	Email:          "admin@example.com",
+	OtpActive:      false,
+	HashedPassword: "asdf",
 }
 
-func readDataJsonFile(fileName string) ([]*TestData, error) {
-	file, err := os.Open(fileName)
+func initLoginAdminUser(db *gorm.DB) error {
+	err := db.Create(loginAdminUser).Error
 	if err != nil {
-		return nil, err
+		return err
 	}
-	defer utils.Close(file)
 
-	result := make([]*TestData, 0)
-	err = json.NewDecoder(file).Decode(&result)
-	return result, err
+	perms := &models.Permissions{
+		UserId: &loginAdminUser.ID,
+		Scopes: []*scopes.Scope{scopes.ScopeAdmin},
+	}
+	err = db.Create(perms).Error
+	return err
 }
 
-func runServer(prg *servers.Server, waitFor int) (err error) {
-	if waitFor == 0 {
-		waitFor = 5
+func createSession(db *gorm.DB) (string, error) {
+	ss := &services.Session{DB: db}
+	token, err := ss.CreateForUser(loginAdminUser)
+	if err != nil {
+		return "", err
 	}
+	err = db.Model(&models.Session{}).Where("token = ?", token).Update("expiration_time", time.Now().Add(time.Hour*24)).Error
+	return token, err
+}
 
-	err = prg.Start()
-	panicIf(err)
-
-	c := make(chan error, 1)
-	go func() {
-		c <- prg.RunningEnvironment.WaitForMainProcess()
+func call(client *http.Client, request *http.Request) (data []byte, err error) {
+	response, err := client.Do(request)
+	defer func() {
+		if response != nil && response.Body != nil {
+			_ = response.Body.Close()
+		}
 	}()
-	t := time.After(time.Minute * time.Duration(waitFor))
-
-	//we need to make sure we were running for a minute
-	//if we did not, something went wrong
-	running, err := prg.IsRunning()
-	panicIf(err)
 
-	if !running {
-		panic(errors.New("server did not run for a minute"))
+	if err != nil {
+		return
 	}
 
-	select {
-	case <-t:
-		break
-	case err = <-c:
-		panicIf(err)
-		break
+	data, err = io.ReadAll(response.Body)
+	if err == io.EOF {
+		err = nil
 	}
 
-	err = prg.Stop()
-	panicIf(err)
-
-	return prg.GetEnvironment().WaitForMainProcessFor(5 * time.Minute)
-}
-
-func panicIf(err error) {
-	if err != nil {
-		panic(err)
+	if err != nil || response.StatusCode < 200 || response.StatusCode >= 400 {
+		err = fmt.Errorf("failed to create server (%d)\n%s", response.StatusCode, data)
 	}
+	return
 }
 
-type TestScenario struct {
-	Name string
-	Test *TestTemplate
-}
-
-type TestTemplate struct {
-	Template           []byte
-	Name               string
-	Variables          map[string]interface{}
-	Environment        map[string]interface{}
-	IgnoreExitCode     bool
-	RuntimeRequirement int
+func createUrl(str string) *url.URL {
+	u, err := url.Parse(str)
+	panicIf(err)
+	return u
 }
 
-type TestData struct {
-	Name               string                 `json:"name"`
-	Variables          map[string]interface{} `json:"variables"`
-	Environment        map[string]interface{} `json:"environment"`
-	IgnoreExitCode     bool                   `json:"ignoreExitCode"`
-	RuntimeRequirement int                    `json:"runtimeRequirement"`
+func createHeaders(session string) http.Header {
+	headers := http.Header{}
+	headers.Add("Authorization", "Bearer "+session)
+	return headers
 }
diff --git a/tools/templatetester/models.go b/tools/templatetester/models.go
new file mode 100644
index 00000000..65f1a998
--- /dev/null
+++ b/tools/templatetester/models.go
@@ -0,0 +1,23 @@
+package main
+
+type TestScenario struct {
+	Name string
+	Test *TestTemplate
+}
+
+type TestTemplate struct {
+	Template           []byte
+	Name               string
+	Variables          map[string]interface{}
+	Environment        map[string]interface{}
+	IgnoreExitCode     bool
+	RuntimeRequirement int
+}
+
+type TestData struct {
+	Name               string                 `json:"name"`
+	Variables          map[string]interface{} `json:"variables"`
+	Environment        map[string]interface{} `json:"environment"`
+	IgnoreExitCode     bool                   `json:"ignoreExitCode"`
+	RuntimeRequirement int                    `json:"runtimeRequirement"`
+}
diff --git a/tools/templatetester/util.go b/tools/templatetester/util.go
new file mode 100644
index 00000000..cd8cf429
--- /dev/null
+++ b/tools/templatetester/util.go
@@ -0,0 +1,271 @@
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/pufferpanel/pufferpanel/v3"
+	"github.com/pufferpanel/pufferpanel/v3/models"
+	"github.com/pufferpanel/pufferpanel/v3/utils"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func panicIf(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+func readDataTxtFile(fileName string) (map[string]interface{}, error) {
+	file, err := os.Open(fileName)
+	if err != nil {
+		return nil, err
+	}
+	defer utils.Close(file)
+
+	result := make(map[string]interface{})
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		parts := strings.Split(line, "=")
+		key := parts[0]
+		value := parts[1]
+		result[key] = value
+	}
+	return result, nil
+}
+
+func readDataJsonFile(fileName string) ([]*TestData, error) {
+	file, err := os.Open(fileName)
+	if err != nil {
+		return nil, err
+	}
+	defer utils.Close(file)
+
+	result := make([]*TestData, 0)
+	err = json.NewDecoder(file).Decode(&result)
+	return result, err
+}
+
+func buildTests() []*TestScenario {
+	var templateFolder = CmdFlags.TemplateFolder
+
+	if templateFolder == "" {
+		if templateFolder = os.Getenv("TEMPLATE_PATH"); templateFolder == "" {
+			templateFolder = filepath.Join(CmdFlags.WorkingDir, "templates")
+			err := os.MkdirAll(templateFolder, 0755)
+			panicIf(err)
+
+			log.Printf("Cloning template repo")
+			_, err = git.PlainClone(templateFolder, false, &git.CloneOptions{
+				URL:           "https://github.com/PufferPanel/templates",
+				ReferenceName: plumbing.ReferenceName(CmdFlags.GitRef),
+				SingleBranch:  true,
+				Depth:         1,
+			})
+			panicIf(err)
+		}
+	}
+
+	templateFolders, err := os.ReadDir(templateFolder)
+	panicIf(err)
+
+	testScenarios := make([]*TestScenario, 0)
+
+	for _, folder := range templateFolders {
+		if !folder.IsDir() || strings.HasPrefix(folder.Name(), ".") {
+			continue
+		}
+
+		log.Printf("Scanning %s", folder.Name())
+
+		if _, err = os.Stat(filepath.Join(templateFolder, folder.Name(), ".skip")); err == nil {
+			log.Printf("  Skipping as skip file present")
+			continue
+		}
+
+		var files []os.DirEntry
+		files, err = os.ReadDir(filepath.Join(templateFolder, folder.Name()))
+		panicIf(err)
+
+		for _, file := range files {
+			if file.Name() == "data.json" {
+				continue
+			}
+
+			log.Printf("  Scanning %s", file.Name())
+
+			if len(CmdFlags.Files) > 0 {
+				test := false
+				for _, t := range CmdFlags.Files {
+					if t == file.Name() {
+						test = true
+						break
+					}
+				}
+				if !test {
+					log.Printf("  Skipping %s as not in files", file.Name())
+					continue
+				}
+			}
+
+			filePath := filepath.Join(templateFolder, folder.Name(), file.Name())
+			if strings.HasSuffix(file.Name(), ".json") {
+				tmp := &TestTemplate{}
+				tmp.Name = strings.TrimSuffix(file.Name(), ".json")
+
+				tmp.Template, err = os.ReadFile(filePath)
+				panicIf(err)
+
+				_, err = os.Stat(filepath.Join(templateFolder, folder.Name(), "data.txt"))
+				if err == nil {
+					tmp.Variables, err = readDataTxtFile(filepath.Join(templateFolder, folder.Name(), "data.txt"))
+					panicIf(err)
+				} else if !os.IsNotExist(err) {
+					panicIf(err)
+				}
+
+				template := pufferpanel.Server{}
+				err = json.NewDecoder(bytes.NewReader(tmp.Template)).Decode(&template)
+				panicIf(err)
+
+				_, err = os.Stat(filepath.Join(templateFolder, folder.Name(), "data.json"))
+				if err == nil {
+					tests, err := readDataJsonFile(filepath.Join(templateFolder, folder.Name(), "data.json"))
+					for _, v := range tests {
+						log.Printf("  Considering %s", v.Name)
+						testScenarios = append(testScenarios, &TestScenario{
+							Name: v.Name,
+							Test: &TestTemplate{
+								Template:           tmp.Template,
+								Name:               tmp.Name,
+								Variables:          v.Variables,
+								Environment:        v.Environment,
+								IgnoreExitCode:     v.IgnoreExitCode,
+								RuntimeRequirement: v.RuntimeRequirement,
+							},
+						})
+					}
+					panicIf(err)
+				} else if !os.IsNotExist(err) {
+					panicIf(err)
+				} else {
+					//no data json, which means it's a single test
+					//but, each template could support envs, so auto-process each
+					if len(template.SupportedEnvironments) > 0 {
+						for _, v := range template.SupportedEnvironments {
+							z := &TestTemplate{
+								Template:           tmp.Template,
+								Name:               tmp.Name,
+								Environment:        make(map[string]interface{}),
+								Variables:          make(map[string]interface{}),
+								RuntimeRequirement: tmp.RuntimeRequirement,
+							}
+
+							scenario := &TestScenario{
+								Name: z.Name,
+								Test: z,
+							}
+							if v.Type != "host" {
+								scenario.Name = scenario.Name + "-" + v.Type
+							}
+
+							for r, p := range v.Metadata {
+								scenario.Test.Environment[r] = p
+							}
+
+							scenario.Test.Environment["type"] = v.Type
+							log.Printf("  Considering %s", scenario.Name)
+							testScenarios = append(testScenarios, scenario)
+						}
+					} else {
+						log.Printf("  Considering %s", tmp.Name)
+						testScenarios = append(testScenarios, &TestScenario{
+							Name: tmp.Name,
+							Test: tmp,
+						})
+					}
+				}
+			}
+		}
+	}
+
+	finalScenarioList := make([]*TestScenario, 0)
+	for _, scenario := range testScenarios {
+		skip := false
+		for _, v := range CmdFlags.Skip {
+			if utils.CompareWildcard(scenario.Name, v) {
+				skip = true
+				break
+			}
+		}
+		if skip {
+			for _, v := range CmdFlags.Required {
+				if utils.CompareWildcard(scenario.Name, v) {
+					skip = false
+					break
+				}
+			}
+
+			if skip {
+				log.Printf("Skipping %s", scenario.Name)
+				continue
+			}
+		}
+		log.Printf("Will run test for: %s", scenario.Name)
+		finalScenarioList = append(finalScenarioList, scenario)
+	}
+
+	return finalScenarioList
+}
+
+func ioCopy(dest io.Writer, src io.Reader) {
+	var buf = make([]byte, 1024)
+	var readErr error
+	var n int
+
+	for readErr != io.EOF {
+		n, readErr = src.Read(buf)
+		_, _ = dest.Write(buf[:n])
+	}
+}
+
+func createCreateBody(scenario *TestScenario) io.ReadCloser {
+	model := &models.ServerCreation{
+		Server: pufferpanel.Server{},
+		NodeId: 0,
+		Name:   scenario.Name,
+	}
+
+	_ = json.NewDecoder(bytes.NewReader(scenario.Test.Template)).Decode(&model.Server)
+
+	for k, v := range scenario.Test.Variables {
+		val, ok := model.Variables[k]
+		if ok {
+			val.Value = v
+			model.Variables[k] = val
+		}
+	}
+
+	envType := scenario.Test.Environment["type"].(string)
+
+	for _, v := range model.SupportedEnvironments {
+		if v.Type == envType {
+			model.Environment = v
+		}
+	}
+
+	for k, v := range scenario.Test.Environment {
+		model.Environment.Metadata[k] = v
+	}
+
+	buf := &bytes.Buffer{}
+	_ = json.NewEncoder(buf).Encode(model)
+	return io.NopCloser(buf)
+}
diff --git a/utils/arguments_test.go b/utils/arguments_test.go
index 99a6ed67..f5edf2d1 100644
--- a/utils/arguments_test.go
+++ b/utils/arguments_test.go
@@ -19,6 +19,25 @@ func TestReplaceTokens(t *testing.T) {
 	assert.Equal(t, "TEST val1", resultTest)
 }
 
+func TestReplaceTokensInMaps(t *testing.T) {
+	mapping := createSourceMap()
+	data := []string{
+		"TEST ${val1}",
+		"TEST2",
+		"TEST ${1234567}",
+		"${int}",
+	}
+	expected := []string{
+		"TEST RESULT1",
+		"TEST2",
+		"TEST RESULT3",
+		"436",
+	}
+
+	resultTest := ReplaceTokensInArr(data, mapping)
+	assert.Equal(t, expected, resultTest)
+}
+
 func createSourceMap() map[string]interface{} {
 	source := make(map[string]interface{})
 
@@ -91,3 +110,39 @@ func TestSplitArguments(t *testing.T) {
 		})
 	}
 }
+
+func TestMergeArguments(t *testing.T) {
+	tests := []struct {
+		args     []string
+		expected string
+	}{
+		{
+			expected: "java -jar test.jar",
+			args:     []string{"java", "-jar", "test.jar"},
+		},
+		{
+			expected: "java -jar \"test.jar\"",
+			args:     []string{"java", "-jar", "\"test.jar\""},
+		},
+		{
+			expected: "java -jar \"test this.jar\"",
+			args:     []string{"java", "-jar", "test this.jar"},
+		},
+		{
+			expected: "java -jar \"test this.jar\" noGui",
+			args:     []string{"java", "-jar", "test this.jar", "noGui"},
+		},
+		{
+			expected: "\"C:\\\\Program Files\\\\Java\\\\bin\\\\java.exe\" -jar \"test this.jar\" noGui",
+			args:     []string{"\"C:\\\\Program Files\\\\Java\\\\bin\\\\java.exe\"", "-jar", "test this.jar", "noGui"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run("", func(t *testing.T) {
+			result := MergeArguments(tt.args)
+			if result != tt.expected {
+				t.Errorf("MergeArguments() got = %v, want %v", result, tt.expected)
+			}
+		})
+	}
+}
diff --git a/web/tests/entry_test.go b/web/tests/entry_test.go
index 7289f5cc..5b7e29e8 100644
--- a/web/tests/entry_test.go
+++ b/web/tests/entry_test.go
@@ -55,7 +55,7 @@ func TestMain(m *testing.M) {
 		panic(err)
 	}
 
-	err = database.Migrate(db)
+	err = database.Upgrade(db, false)
 	if err != nil {
 		panic(err)
 	}
diff --git a/web/tests/root_test.go b/web/tests/root_test.go
index 3887360c..6edbcdc3 100644
--- a/web/tests/root_test.go
+++ b/web/tests/root_test.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"github.com/pufferpanel/pufferpanel/v3"
 	"github.com/pufferpanel/pufferpanel/v3/config"
-	"github.com/pufferpanel/pufferpanel/v3/utils"
 	"github.com/pufferpanel/pufferpanel/v3/web/daemon"
 	"github.com/stretchr/testify/assert"
 	"net/http"
@@ -46,10 +45,6 @@ func Test_getFeatures(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			_ = config.DockerDisallowHost.Set(tt.dockerFlag, false)
 
-			if runtime.GOOS == "windows" {
-				tt.expected.Environments = utils.Remove(tt.expected.Environments, "tty")
-			}
-
 			result := CallAPI("GET", "/daemon/features", nil, "")
 			if !assert.Equal(t, http.StatusOK, result.Code) {
 				return
diff --git a/web/tests/server_test.go b/web/tests/server_test.go
index 46dd215b..4d999a4a 100644
--- a/web/tests/server_test.go
+++ b/web/tests/server_test.go
@@ -25,7 +25,6 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 	"testing"
 	"time"
@@ -295,7 +294,6 @@ func TestServers(t *testing.T) {
 				if !assert.Equal(t, http.StatusOK, response.Code) {
 					return
 				}
-				//TODO: Check to make sure our user above was added
 				var data []*models.UserPermissionsView
 				err := json.NewDecoder(response.Body).Decode(&data)
 				if !assert.NoError(t, err) {
@@ -323,6 +321,88 @@ func TestServers(t *testing.T) {
 				}
 			})
 
+			t.Run("GrantUserPermissions", func(t *testing.T) {
+				var data = []byte(`{"scopes": ["server.view", "server.data.view", "server.start", "server.users.view", "server.users.edit"]}`)
+				response := CallAPIRaw("PUT", "/api/servers/"+ServerId+"/user/"+loginNoLoginUser.Email, data, session)
+				if !assert.Equal(t, http.StatusNoContent, response.Code) {
+					return
+				}
+
+				response = CallAPIRaw("GET", "/api/servers/"+ServerId+"/user", nil, session)
+				if !assert.Equal(t, http.StatusOK, response.Code) {
+					return
+				}
+				var perms []*models.UserPermissionsView
+				err := json.NewDecoder(response.Body).Decode(&perms)
+				if !assert.NoError(t, err) {
+					return
+				}
+
+				if assert.NotEmpty(t, perms) {
+					return
+				}
+				found := false
+				for _, v := range perms {
+					if v.Email == loginNoLoginUser.Email {
+						var expectedScopes = []*scopes.Scope{
+							scopes.ScopeServerView, scopes.ScopeServerViewData, scopes.ScopeServerStart, scopes.ScopeServerUserView, scopes.ScopeServerUserEdit,
+						}
+						if !assert.Equal(t, expectedScopes, v.Scopes) {
+							return
+						}
+						found = true
+					}
+				}
+
+				if !found {
+					assert.Fail(t, "Failed to locate user")
+				}
+			})
+
+			t.Run("SubuserGrantUserPermissions", func(t *testing.T) {
+				//first get a session for the fake user
+				testSession, err := createSession(db, loginNoLoginUser)
+				if !assert.NoError(t, err) {
+					return
+				}
+
+				var data = []byte(`{"scopes": ["server.view", "server.start", "server.stop"]}`)
+				response := CallAPIRaw("PUT", "/api/servers/"+ServerId+"/user/"+loginNoAdminWithServersUser.Email, data, testSession)
+				if !assert.Equal(t, http.StatusNoContent, response.Code) {
+					return
+				}
+
+				response = CallAPIRaw("GET", "/api/servers/"+ServerId+"/user", nil, session)
+				if !assert.Equal(t, http.StatusOK, response.Code) {
+					return
+				}
+				var perms []*models.UserPermissionsView
+				err = json.NewDecoder(response.Body).Decode(&perms)
+				if !assert.NoError(t, err) {
+					return
+				}
+
+				if assert.NotEmpty(t, perms) {
+					return
+				}
+				found := false
+				for _, v := range perms {
+					if v.Email == loginNoAdminWithServersUser.Email {
+						var expectedScopes = []*scopes.Scope{
+							scopes.ScopeServerView, scopes.ScopeServerStart,
+						}
+						if !assert.Equal(t, expectedScopes, v.Scopes) {
+							return
+						}
+						found = true
+					}
+				}
+
+				if !found {
+					assert.Fail(t, "Failed to locate user")
+				}
+			})
+
 			t.Run("UpdateVariable", func(t *testing.T) {
 				motd := "This is a changed MOTD"
 				var variables = []byte(`{"motd": "` + motd + `" }`)
@@ -722,11 +802,6 @@ func TestServers(t *testing.T) {
 				})
 
 				t.Run("DeleteFileWithURIEncoding", func(t *testing.T) {
-					if runtime.GOOS == "windows" {
-						t.Skipf("Windows doesn't support file with URI encoding")
-						return
-					}
-
 					filename := "file.delete.test?id=12345"
 
 					fileLocation := filepath.Join(serverDir, filename)