Browse Source

chore: merge pull request #175

Northon Torga 10 months ago
parent
commit
49edee48f1

+ 8 - 2
.air.toml

@@ -4,10 +4,16 @@ tmp_dir = "tmp"
 
 [build]
 bin = "bin/os"
-cmd = "CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/os ./os.go"
+cmd = "make build"
 delay = 1000
 exclude_dir = [".git", "bin", "logs", "tmp", "vendor", "src/devUtils"]
 exclude_regex = [".*_templ.go"]
 follow_symlink = false
 ignore = ["*_test.go", "testHelpers.go"]
-include_ext = ["go", "tpl", "tmpl", "templ", "html"]
+include_ext = ["go", "tpl", "tmpl", "templ", "html", "js", "css"]
+
+[log]
+main_only = true
+
+[screen]
+clear_on_rebuild = true

+ 3 - 1
.vscode/settings.json

@@ -24,6 +24,7 @@
     "grpcs",
     "gsub",
     "Hostnames",
+    "htmx",
     "installables",
     "Installables",
     "ioncube",
@@ -83,5 +84,6 @@
     "webserver",
     "zerolog",
     "Zerolog"
-  ]
+  ],
+  "editor.formatOnSave": true
 }

+ 6 - 1
CHANGELOG.md

@@ -1,10 +1,15 @@
 # Changelog
 
 ```log
-0.0.9 - 2024/XX/XX
+0.0.9 - 2024/10/04
 refactor(front): runtime page with HTMX+Alpine.js
 refactor(front): ssls page with HTMX+Alpine.js
 feat: chown default dirs after service install/add
+feat: add jsonAjax helper
+feat: add dev-build.sh script and make file
+fix: adjust mapping layout for lower resolutions
+fix: bug on match pattern on mappings
+fix: bug on error level and error log php update
 
 0.0.8 - 2024/09/23
 refactor(front): databases page with HTMX+Alpine.js

+ 13 - 0
Makefile

@@ -0,0 +1,13 @@
+SHELL=/bin/bash
+UI_DIR=src/presentation/ui
+
+dev:
+	air serve
+
+build:
+	templ generate -path $(UI_DIR)
+	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/os ./os.go
+	if podman ps | grep -q "os"; then podman exec os /bin/bash -c "supervisorctl restart os-api"; fi
+
+run:
+	/var/infinite/os serve

+ 31 - 6
README.md

@@ -66,8 +66,8 @@ When running in production, the `/speedia/.env` file is only used if the environ
 Speedia OS commands can harm your system, so it's important to run the unit tests in a proper container:
 
 ```
-podman build -t sos-unit-test:latest -f Containerfile.test .
-podman run --rm -it sos-unit-test:latest
+podman build -t os-unit-test:latest -f Containerfile.test .
+podman run --rm -it os-unit-test:latest
 ```
 
 Make sure you have the `.env` file in the root of the git directory before running the tests.
@@ -87,20 +87,45 @@ For instance there you'll find a `testHelpers.go` file that is used to read the
 
 ### Building
 
+#### Simple Build
+
 To build the project, run the command below. It takes two minutes to build the project at first. After that, it takes less than 10 seconds to build.
 
 ```
-podman build -t sos:latest .
+podman build -t os:latest .
 ```
 
 To run the project you may use the following command:
 
 ```
-podman run --name sos --env 'PRIMARY_VHOST=speedia.net' --rm -p 1618:1618 -it sos:latest
+podman run --name os --env 'PRIMARY_VHOST=speedia.net' --rm -p 1618:1618 -it os:latest
 ```
 
 When testing, consider publishing port 80 and 443 to the host so that you don't need to use a reverse proxy. You should also consider using `--env 'LOG_LEVEL=debug'` to increase the log verbosity.
 
+#### Development Build
+
+When developing the project, you may want to use a script to automate the build process. The `dev-build.sh` script is available in the root of the project and it will take care of all the steps needed to build and run the container.
+
+To run the script you can simply use `bash dev-build.sh` (bash may be replaced by zsh or similar). By default, the script will expose the port 1618 to the host which is used by the API and the dashboard.
+
+- If you pass the `http` argument, it will also expose the ports 80 and 443 to the host;
+- If you pass the `ols` argument, it will expose port 7080 (used by OpenLiteSpeed admin);
+- If you pass the `no-cache` argument, it will remove the image cache and rebuild the image from scratch;
+
+The script will also create a `dev` account with the password `123456` so you can access the dashboard.
+
+When you need to stop the container, just CTRL+C to stop and remove it. If you don't want to remove it, just ditch the `--rm` flag from the `podman run` command in the script.
+
+If you look closely at the script, you'll see that it mounts the project's `bin` directory to the container `/speedia/bin` path. This is done to allow the container to access the binary file generated by Air on the host. The script then replace the binary file that comes with the container with the one on the `/bin` directory.
+
+With this approach you don't need to rebuild the container every time you change the code. Although sometimes you may want to restart the container to apply some changes, specially when changing the dependencies or system configurations. In this case, just hit CTRL+C to stop the container and run the script again.
+
+**Notes:**
+
+1. You must run the script from the project's root directory;
+2. Until Echo v4.13.0 is released, you'll need to refresh the browser page during development to see the changes in the dashboard as we're not able to use the `DEV_MODE` auto refresh websocket trick for now. To understand how this trick used to work, check the UI router and main layout files.
+
 ### Web UIs
 
 This project has two web UIs, the previous Vue.js frontend and the new [Templ](https://templ.guide/) + [Alpine.js](https://alpinejs.dev/) + [HTMX](https://htmx.org/docs/) frontend. The Vue.js frontend is deprecated and will be removed in the future. It's available at `/_/` and the [Templ](https://templ.guide/) + [Alpine.js](https://alpinejs.dev/) + [HTMX](https://htmx.org/docs/) frontend is available at `/`.
@@ -113,9 +138,9 @@ For the interface code to be read and rendered by Go, we need to convert all `.t
 templ generate -path src/presentation/api
 ```
 
-With this, Go will be able to provide the entire application interface at the previously indicated route (`/`).
+It is important that this is done before using Air to create the binary; otherwise, the Web UI will not be embedded, and you will not be able to use it.
 
-**NOTE:** It is important that this is done before using Air to create the binary; otherwise, the Web UI will not be embedded, and you will not be able to use it.
+**NOTE:** If you are using the `dev-build.sh` script, you don't need to run the `templ generate` command (or Air for that matter) since the script will take care of everything for you.
 
 ### VSCode Extensions
 

+ 66 - 0
dev-build.sh

@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# Define the execution arguments.
+ports=(-p 1618:1618)
+case ${1} in
+http)
+  sudo sysctl net.ipv4.ip_unprivileged_port_start=80
+  ports+=(-p 80:80 -p 443:443)
+  ;;
+ols)
+  ports+=(-p 7080:7080)
+  ;;
+no-cache)
+  podman image prune -a
+  podman rmi localhost/os -f
+  ;;
+esac
+
+echo "=> Building the container..."
+make build
+podman build -t os:latest .
+# TODO: Re-add --env 'DEV_MODE=true' after Echo v4.13.0 release.
+podman run --name os -d \
+  --env 'LOG_LEVEL=debug' --env 'PRIMARY_VHOST=speedia.cloud' \
+  --hostname=speedia.cloud --cpus=2 --memory=2g --rm \
+  --volume "$(pwd)/bin:/speedia/bin:Z,ro,bind,slave" \
+  "${ports[@]}" -it os:latest
+
+echo "=> Waiting for the container to start..."
+sleep 5
+
+echo "=> Replacing the standard binary with the development binary..."
+podman exec os /bin/bash -c 'rm -f os && ln -s bin/os os && supervisorctl restart os-api'
+
+echo "=> Creating a development account..."
+podman exec os /bin/bash -c 'os account create -u dev -p 123456'
+
+echo
+echo "<<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>"
+echo
+echo "=> Starting the development build..."
+echo "Any changes to the code will trigger a rebuild automatically."
+echo "Please, ignore the 'Only root can run SOS' message."
+echo
+echo "<<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>"
+echo
+sleep 3
+
+stopDevBuild() {
+  kill $airPid
+  kill $podmanPid
+  echo
+  echo "=> Development build stopped."
+  echo
+  exit
+}
+
+trap stopDevBuild SIGINT
+
+air &
+airPid=$!
+podman attach os &
+podmanPid=$!
+
+wait $airPid
+wait $podmanPid

+ 22 - 24
go.mod

@@ -1,36 +1,36 @@
 module github.com/speedianet/os
 
-go 1.22
+go 1.23
 
 require (
 	github.com/alecthomas/chroma v0.10.0
 	github.com/alessio/shellescape v1.4.2
-	github.com/dgraph-io/badger/v4 v4.2.0
+	github.com/dgraph-io/badger/v4 v4.3.0
 	github.com/glebarez/sqlite v1.11.0
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/google/uuid v1.6.0
 	github.com/joho/godotenv v1.5.1
 	github.com/labstack/echo/v4 v4.12.0
 	github.com/rs/zerolog v1.33.0
-	github.com/samber/slog-zerolog/v2 v2.6.0
+	github.com/samber/slog-zerolog/v2 v2.7.0
 	github.com/shirou/gopsutil v3.21.11+incompatible
 	github.com/spf13/cobra v1.8.1
 	github.com/swaggo/echo-swagger v1.4.1
 	github.com/swaggo/swag v1.16.3
-	golang.org/x/crypto v0.25.0
-	golang.org/x/exp v0.0.0-20240707233637-46b078467d37
-	golang.org/x/net v0.27.0
-	golang.org/x/term v0.22.0
+	golang.org/x/crypto v0.28.0
+	golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6
+	golang.org/x/net v0.30.0
+	golang.org/x/term v0.25.0
 	gopkg.in/yaml.v3 v3.0.1
-	gorm.io/gorm v1.25.10
+	gorm.io/gorm v1.25.12
 )
 
 require (
 	github.com/KyleBanks/depth v1.2.1 // indirect
-	github.com/a-h/templ v0.2.747
+	github.com/a-h/templ v0.2.778
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/dgraph-io/ristretto v0.1.1 // indirect
-	github.com/dlclark/regexp2 v1.4.0 // indirect
+	github.com/dgraph-io/ristretto v1.0.0 // indirect
+	github.com/dlclark/regexp2 v1.11.4 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/glebarez/go-sqlite v1.22.0 // indirect
@@ -40,16 +40,14 @@ require (
 	github.com/go-openapi/spec v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.2.1 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/flatbuffers v24.3.25+incompatible // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
-	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/klauspost/compress v1.17.10 // indirect
 	github.com/labstack/gommon v0.4.2 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
@@ -57,24 +55,24 @@ require (
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	github.com/samber/lo v1.44.0 // indirect
-	github.com/samber/slog-common v0.17.0 // indirect
+	github.com/samber/lo v1.47.0 // indirect
+	github.com/samber/slog-common v0.17.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/swaggo/files/v2 v2.0.0 // indirect
+	github.com/swaggo/files/v2 v2.0.1 // indirect
 	github.com/tklauser/go-sysconf v0.3.14 // indirect
-	github.com/tklauser/numcpus v0.8.0 // indirect
+	github.com/tklauser/numcpus v0.9.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	golang.org/x/sys v0.22.0 // indirect
-	golang.org/x/text v0.16.0
-	golang.org/x/time v0.5.0 // indirect
-	golang.org/x/tools v0.23.0 // indirect
+	golang.org/x/sys v0.26.0 // indirect
+	golang.org/x/text v0.19.0
+	golang.org/x/time v0.7.0 // indirect
+	golang.org/x/tools v0.26.0 // indirect
 	google.golang.org/protobuf v1.34.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	modernc.org/libc v1.54.2 // indirect
+	modernc.org/libc v1.61.0 // indirect
 	modernc.org/mathutil v1.6.0 // indirect
 	modernc.org/memory v1.8.0 // indirect
-	modernc.org/sqlite v1.30.1 // indirect
+	modernc.org/sqlite v1.33.1 // indirect
 )

+ 52 - 60
go.sum

@@ -2,14 +2,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
 github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
-github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
-github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
+github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
+github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
 github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
 github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
 github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
 github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -19,15 +18,15 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
-github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
-github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
-github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
-github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
+github.com/dgraph-io/badger/v4 v4.3.0 h1:lcsCE1/1qrRhqP+zYx6xDZb8n7U+QlwNicpc676Ub40=
+github.com/dgraph-io/badger/v4 v4.3.0/go.mod h1:Sc0T595g8zqAQRDf44n+z3wG4BOqLwceaFntt8KPxUM=
+github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
+github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
+github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -57,8 +56,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
-github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -74,8 +71,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
 github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -103,8 +98,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 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.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
+github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -136,12 +131,12 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
 github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA=
-github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
-github.com/samber/slog-common v0.17.0 h1:HdRnk7QQTa9ByHlLPK3llCBo8ZSX3F/ZyeqVI5dfMtI=
-github.com/samber/slog-common v0.17.0/go.mod h1:mZSJhinB4aqHziR0SKPqpVZjJ0JO35JfH+dDIWqaCBk=
-github.com/samber/slog-zerolog/v2 v2.6.0 h1:S7Q7fvV6HB7NSa7WnI/7ymuVkQZg5XhNXM1ltmAOvGc=
-github.com/samber/slog-zerolog/v2 v2.6.0/go.mod h1:vGzG7VhveVOnyHEpr7LpIuw28QxEOfV/dQxphJRB4iY=
+github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
+github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/samber/slog-common v0.17.1 h1:jTqqLBgoJshpoxlPSGiypyOanjH6tY+i9bwyYmIbjhI=
+github.com/samber/slog-common v0.17.1/go.mod h1:mZSJhinB4aqHziR0SKPqpVZjJ0JO35JfH+dDIWqaCBk=
+github.com/samber/slog-zerolog/v2 v2.7.0 h1:VWJNhvoR3bf+SDEO89BmahAnz6w5l+NGbPBcnMUqO2g=
+github.com/samber/slog-zerolog/v2 v2.7.0/go.mod h1:vGzG7VhveVOnyHEpr7LpIuw28QxEOfV/dQxphJRB4iY=
 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/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -151,7 +146,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 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/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 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=
@@ -160,14 +154,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
 github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
-github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
-github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
+github.com/swaggo/files/v2 v2.0.1 h1:XCVJO/i/VosCDsJu1YLpdejGsGnBE9deRMpjN4pJLHk=
+github.com/swaggo/files/v2 v2.0.1/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
 github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
 github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
 github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
 github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
-github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
-github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
+github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
+github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -181,18 +175,18 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
-golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
-golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw=
+golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -202,36 +196,35 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
-golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -240,8 +233,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
 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=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
-golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
 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=
@@ -270,26 +263,25 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
-gorm.io/gorm v1.25.10/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=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
 modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.19.0 h1:f9K5VdC0nVhHKTFMvhjtZ8TbRgFQbASvE5yO1zs8eC0=
-modernc.org/ccgo/v4 v4.19.0/go.mod h1:CfpAl+673iXNwMG/aqcQn+vDcu4Es/YLya7+9RHjTa4=
+modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
+modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
 modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
 modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
-modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
-modernc.org/libc v1.54.2 h1:9ymAodb+3v85YfBIZqn62BGgO4L9zF2Hx4LNb6dSU/Q=
-modernc.org/libc v1.54.2/go.mod h1:B0D6klDmSmnq26T1iocn9kzyX6NtbzjuI3+oX/xfvng=
+modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
+modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
+modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
 modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
 modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
 modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@@ -298,8 +290,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
 modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
 modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
 modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
-modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
+modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
+modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
 modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
 modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

+ 19 - 4
src/infra/helper/reloadWebServer.go

@@ -1,14 +1,29 @@
 package infraHelper
 
-import "errors"
+import (
+	"errors"
+	"strings"
+	"time"
+)
 
 func ReloadWebServer() error {
-	_, err := RunCmdWithSubShell(
-		"nginx -t && nginx -s reload && sleep 2",
-	)
+	wsConfigTestResult, err := RunCmd("/usr/sbin/nginx", "-t")
+	if err != nil {
+		if wsConfigTestResult != "" {
+			return errors.New("NginxConfigTestFailed: " + err.Error())
+		}
+
+		if !strings.Contains(wsConfigTestResult, "test is successful") {
+			return errors.New("NginxConfigTestFailed: " + wsConfigTestResult)
+		}
+	}
+
+	_, err = RunCmd("/usr/sbin/nginx", "-s", "reload", "-c", "/etc/nginx/nginx.conf")
 	if err != nil {
 		return errors.New("NginxReloadFailed: " + err.Error())
 	}
 
+	time.Sleep(2 * time.Second)
+
 	return nil
 }

+ 13 - 6
src/infra/runtime/runtimeCmdRepo.go

@@ -2,7 +2,7 @@ package runtimeInfra
 
 import (
 	"errors"
-	"log"
+	"log/slog"
 	"os"
 	"strings"
 
@@ -76,19 +76,26 @@ func (repo *RuntimeCmdRepo) UpdatePhpSettings(
 	if err != nil {
 		return err
 	}
+	phpConfigFilePathStr := phpConfFilePath.String()
 
 	for _, setting := range settings {
-		name := setting.Name.String()
-		value := setting.Value.String()
+		settingName := setting.Name.String()
+		settingValue := setting.Value.String()
 		if setting.Value.GetType() == "string" {
-			value = "\"" + value + "\""
+			settingValue = "\"" + settingValue + "\""
+			settingValue = strings.Replace(settingValue, "|", "\\|", -1)
 		}
 
 		_, err := infraHelper.RunCmd(
-			"sed", "-i", "s/"+name+" .*/"+name+" "+value+"/g", phpConfFilePath.String(),
+			"sed", "-i", "s|"+settingName+" .*|"+settingName+" "+settingValue+"|g", phpConfigFilePathStr,
 		)
 		if err != nil {
-			log.Printf("(%s) UpdatePhpSettingFailed: %s", name, err.Error())
+			slog.Debug(
+				"UpdatePhpSettingFailed",
+				slog.String("settingName", settingName),
+				slog.String("settingValue", settingValue),
+				slog.Any("error", err),
+			)
 			continue
 		}
 	}

+ 1 - 1
src/presentation/http.go

@@ -47,7 +47,7 @@ func HttpServerInit(
 			os.Exit(1)
 		}
 
-		aliases := []string{}
+		aliases := []string{"localhost", "127.0.0.1"}
 		err = infraHelper.CreateSelfSignedSsl(pkiDir, "os", aliases)
 		if err != nil {
 			slog.Error("GenerateSelfSignedCertFailed", slog.Any("error", err))

+ 29 - 0
src/presentation/ui/assets/additional.js

@@ -0,0 +1,29 @@
+"use strict";
+
+document.addEventListener("alpine:init", () => {
+  async function jsonAjax(method, url, payload) {
+    const loadingOverlayElement = document.getElementById("loading-overlay");
+    loadingOverlayElement.classList.add("htmx-request");
+
+    await fetch(url, {
+      method: method,
+      headers: {
+        Accept: "application/json",
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(payload),
+    })
+      .then((response) => {
+        loadingOverlayElement.classList.remove("htmx-request");
+        return response.json();
+      })
+      .then((parsedResponse) => {
+        Alpine.store("toast").displayToast(parsedResponse.body, "success");
+      })
+      .catch((parsedResponse) => {
+        Alpine.store("toast").displayToast(parsedResponse.body, "danger");
+      });
+  }
+
+  window.jsonAjax = jsonAjax;
+});

+ 1 - 1
src/presentation/ui/component/mappings/matchPatternSelectInput.templ

@@ -11,6 +11,6 @@ templ MatchPatternSelectInput(id, label, bindValuePath string) {
 			{Label: "Contains", Value: "contains"},
 			{Label: "Equals", Value: "equals"},
 			{Label: "Ends With", Value: "ends-with"},
-		}, true,
+		}, false,
 	)
 }

+ 3 - 3
src/presentation/ui/component/mappings/vhostMappingsList.templ

@@ -12,9 +12,9 @@ templ VhostMappingsList(
 	deleteOnClick string,
 ) {
 	<!-- VhostMappingsList -->
-	<div class="bg-os-500 p-4 flex-col rounded mb-1.5">
-		<div class="flex justify-between items-center">
-			<div class="flex space-x-3">
+	<div class="bg-os-500 flex-col rounded-md p-3">
+		<div class="flex items-center justify-between">
+			<div class="flex gap-4">
 				<span>{ mapping.Path.String() }</span>
 				switch mapping.TargetType.String() {
 					case "service":

+ 1 - 1
src/presentation/ui/component/structural/serviceNotInstalledWarningForm.templ

@@ -14,7 +14,7 @@ templ ServiceNotInstalledWarningForm(serviceName string) {
 		<span class="mr-3">The <span class="text-speedia-300 font-bold">{ serviceName }</span> service is not installed yet.</span>
 		@componentForm.SubmitButton(
 			"schedule-service-installation",
-			"Schedule "+serviceName+" service installation", "ph-caret-line-down",
+			"Schedule "+serviceName+" service installation", "ph-queue",
 			"", true,
 		)
 	</form>

+ 7 - 7
src/presentation/ui/component/structural/tag.templ

@@ -2,19 +2,19 @@ package componentStructural
 
 templ Tag(highlightedIcon, highlightedLabel, tagValue, tagColor string) {
 	<!-- Tag -->
-	<div class={ "flex rounded-md border-solid border-2 border-" + tagColor + " text-sm w-fit" }>
-		<div class={ "bg-" + tagColor }>
-			<div class="mx-2 flex items-center space-x-1">
+	<div class={ "flex rounded-md border-solid border-2.5 border-" + tagColor + " text-sm" }>
+		<div class={ "bg-" + tagColor + " flex items-center" }>
+			<div class="flex items-center px-1">
 				if highlightedIcon != "" {
 					<i class={ "ph-duotone " + highlightedIcon + " text-lg" }></i>
 				}
 				if highlightedLabel != "" {
-					<span class="mb-0.3 text-neutral-50">{ highlightedLabel }</span>
+					<span class="max-w-64 truncate">{ highlightedLabel }</span>
 				}
 			</div>
 		</div>
-		<div class="flex items-center">
-			<span class="mb-0.2 mx-2">{ tagValue }</span>
-		</div>
+		<span class={ "leading-normal ring-" + tagColor + " ring-offset-" + tagColor + " rounded-md px-1 py-0.5 ring-1 ring-offset-1 max-w-64 truncate" }>
+			@templ.Raw(tagValue)
+		</span>
 	</div>
 }

+ 1 - 1
src/presentation/ui/component/util/toast.templ

@@ -27,7 +27,7 @@ script ToastGlobalState() {
 			return;
 		}
 
-	    const responseData = event.detail.xhr.responseText;
+	  const responseData = event.detail.xhr.responseText;
 		if (responseData === '') {
 			return;
 		}

+ 3 - 20
src/presentation/ui/layout/main.templ

@@ -42,12 +42,7 @@ templ MainLayout(pageContent templ.Component, currentUrl string) {
 				rel="stylesheet"
 				href="/assets/additional.css"
 			/>
-			<style>
-                ::-webkit-scrollbar {
-                    width: 0;
-                    height: 0;
-                }
-            </style>
+			<style>::-webkit-scrollbar {width: 0;height: 0;}</style>
 			<script type="text/javascript">
 				window.__unocss = {
 					theme: {
@@ -84,21 +79,9 @@ templ MainLayout(pageContent templ.Component, currentUrl string) {
 			</script>
 			<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime@0.61.5/uno.global.min.js" integrity="sha256-miwoG1k3DDK5ai24d7edKuvz3xRNHkUNcX8zl6qru5U=" crossorigin="anonymous"></script>
 			<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.1/dist/htmx.min.js" integrity="sha256-bUqqSw0+i0yR+Nl7kqNhoZsb1FRN6j9mj9w+YqY5ld8=" crossorigin="anonymous"></script>
+			<script defer src="/assets/additional.js"></script>
 			<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.14.1/dist/cdn.min.js" integrity="sha256-jFBwr6faTqqhp3sVi4/VTxJ0FpaF9YGZN1ZGLl/5QYM=" crossorigin="anonymous"></script>
 			<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js" integrity="sha256-NY2a+7GrW++i9IBhowd25bzXcH9BCmBrqYX5i8OxwDQ=" crossorigin="anonymous"></script>
-			<script>
-				htmx.defineExtension('encoding-request-as-json', {
-					onEvent: function (eventName, eventContent) {
-						if (eventName === 'htmx:configRequest') {
-							eventContent.detail.headers['Content-Type'] = 'application/json';
-						}
-					},
-					encodeParameters : function(xhr, requestParams) {
-						xhr.overrideMimeType('text/json');
-						return (JSON.stringify(requestParams));
-					}
-				})
-			</script>
 			if isDevMode, _ := voHelper.InterfaceToBool(os.Getenv("DEV_MODE")); isDevMode {
 				@DevWsHotReload()
 			}
@@ -107,7 +90,7 @@ templ MainLayout(pageContent templ.Component, currentUrl string) {
 			@componentUtil.LoadingOverlay()
 			@Sidebar(currentUrl)
 			<!-- Page Content -->
-			<div class="w-full overflow-x-hidden h-screen overflow-h-auto p-6">
+			<div class="overflow-h-auto h-screen w-full overflow-x-hidden p-6">
 				@pageContent
 			</div>
 			@componentUtil.Toast()

+ 1 - 1
src/presentation/ui/middleware/auth.go

@@ -39,7 +39,7 @@ func getAccountIdFromAccessToken(
 }
 
 func shouldSkipUiAuthentication(req *http.Request) bool {
-	urlSkipRegex := regexp.MustCompile(`^/(api|\_|login)/`)
+	urlSkipRegex := regexp.MustCompile(`^/(api|\_|login|dev)/`)
 	return urlSkipRegex.MatchString(req.URL.Path)
 }
 

+ 46 - 43
src/presentation/ui/page/mappings.templ

@@ -20,14 +20,14 @@ script MappingsIndexLocalState() {
 			resetPrimaryStates() {
 				this.virtualHost = {
 					'hostname': '',
-					'type': '',
+					'type': 'top-level',
 					'rootDirectory': '',
 					'parentHostname': ''
 				}
 				this.mapping = {
 					'id': 0,
 					'path': '',
-					'matchPattern': '',
+					'matchPattern': 'begins-with',
 					'targetType': 'url',
 					'targetValue': '',
 					'targetHttpResponseCode': ''
@@ -143,15 +143,15 @@ templ MappingsIndex(
 ) {
 	@MappingsIndexLocalState()
 	<div x-data="mappings">
-		<div class="mb-6 flex flex-row items-center justify-between">
-			<div class="basis-[70%]">
+		<div class="mb-6 flex flex-row items-center justify-between gap-3">
+			<div class="basis-[65%]">
 				@componentStructural.PageTitle(
 					"Mappings",
-					"Configure how the server handles different URLs and paths. You can specify where static files are located, set responses for HTTP status codes, redirect requests to different URLs, add inline HTML content, and adjust how the server interacts with services like PHP.",
+					"Configure how the server handles different URLs and paths. You can configure static files location, responses codes, redirect URLs, add inline HTML content, and proxy the traffic to services such as PHP, Node etc.",
 					"ph-graph",
 				)
 			</div>
-			<div class="my-4 flex space-x-5">
+			<div class="flex basis-[35%] gap-3">
 				@componentForm.SubmitButton(
 					"create-vhost-button", "create virtual host", "ph-plus-square",
 					"openCreateVhostModal()", false,
@@ -171,9 +171,7 @@ templ MappingsIndex(
 			hx-swap="outerHTML transition:true"
 			class="flex flex-col"
 		>
-			<div id="mappings-tables" class="card w-full">
-				@MappingsFormTable(vhostsWithMappings)
-			</div>
+			@MappingsFormTable(vhostsWithMappings)
 			@CreateVhostModal()
 			@componentStructural.DeleteModal(
 				"isDeleteVhostModalOpen", "closeDeleteVhostModal()", "deleteVhostElement()",
@@ -190,42 +188,45 @@ templ MappingsIndex(
 }
 
 templ MappingsFormTable(vhostsWithMappings []dto.VirtualHostWithMappings) {
-	<table
-		class="w-full table-auto border-collapse rounded-md transition-all duration-300 ease-in-out"
-	>
-		<div class="bg-os-800 rounded-md p-4">
-			for _, vhostWithMappings := range vhostsWithMappings {
-				<div class="bg-os-500 mb-4 flex rounded p-4">
-					<!-- Vhosts Column -->
-					<div class="bg-os-800 w-3/8 min-h-56 flex flex-col rounded p-4">
-						<div class="flex items-center justify-between">
-							<span class="text-xl">{ vhostWithMappings.Hostname.String() }</span>
-							<div class="flex space-x-2">
-								@componentStructural.CircularIconButtonWithTooltip("ph-plus", "speedia-500", "speedia-300", "openCreateMappingFromVhostModal('"+vhostWithMappings.Hostname.String()+"')", "create mapping", "os-200")
-								if vhostWithMappings.Type.String() != "primary" {
-									@componentStructural.CircularIconButtonWithTooltip("ph-trash", "red-800", "red-600", "openDeleteVhostModal('"+vhostWithMappings.Hostname.String()+"')", "delete virtual host", "red-500")
-								}
-							</div>
-						</div>
-						<div class="mt-4 flex space-x-3">
-							@componentStructural.Tag("ph-folder-open", "", vhostWithMappings.RootDirectory.String(), "speedia-300")
-							@componentStructural.Tag("ph-file-text", "", vhostWithMappings.Type.String(), "speedia-300")
+	<div id="mappings-form-table" class="w-full space-y-3">
+		for _, vhostWithMappings := range vhostsWithMappings {
+			<div class="bg-os-800 mb-4 flex space-x-3 rounded-md p-3">
+				<!-- VirtualHosts Column -->
+				<div class="bg-os-500 min-w-2/8 max-2/8 min-h-56 flex flex-col rounded p-3">
+					<div class="flex flex-wrap items-center justify-between gap-2">
+						<span class="truncate text-lg font-bold">{ vhostWithMappings.Hostname.String() }</span>
+						<div class="flex space-x-2">
+							@componentStructural.CircularIconButtonWithTooltip(
+								"ph-plus", "speedia-500", "speedia-300",
+								"openCreateMappingFromVhostModal('"+vhostWithMappings.Hostname.String()+"')",
+								"create mapping", "os-200",
+							)
+							if vhostWithMappings.Type.String() != "primary" {
+								@componentStructural.CircularIconButtonWithTooltip(
+									"ph-trash", "red-800", "red-600",
+									"openDeleteVhostModal('"+vhostWithMappings.Hostname.String()+"')",
+									"delete virtual host", "red-500",
+								)
+							}
 						</div>
 					</div>
-					<div class="w-4"></div>
-					<!-- Mappings Column -->
-					<div class="bg-os-800 w-1/1 flex-col rounded p-4">
-						for _, mapping := range vhostWithMappings.Mappings {
-							@componentMappings.VhostMappingsList(
-								mapping, vhostWithMappings.RootDirectory,
-								"openDeleteMappingModal("+mapping.Id.String()+", '"+mapping.Path.String()+"')",
-							)
-						}
+					<div class="mt-4 flex flex-wrap gap-3">
+						@componentStructural.Tag("ph-folder-open", "", vhostWithMappings.RootDirectory.String(), "speedia-300")
+						@componentStructural.Tag("ph-file-text", "", vhostWithMappings.Type.String(), "speedia-300")
 					</div>
 				</div>
-			}
-		</div>
-	</table>
+				<!-- Mappings Column -->
+				<div class="w-full flex-col space-y-3">
+					for _, mapping := range vhostWithMappings.Mappings {
+						@componentMappings.VhostMappingsList(
+							mapping, vhostWithMappings.RootDirectory,
+							"openDeleteMappingModal("+mapping.Id.String()+", '"+mapping.Path.String()+"')",
+						)
+					}
+				</div>
+			</div>
+		}
+	</div>
 }
 
 templ CreateVhostForm() {
@@ -238,7 +239,7 @@ templ CreateVhostForm() {
 		<div class="my-6">
 			@componentForm.SelectInput(
 				"type", "Type", "virtualHost.type", "",
-				valueObject.AvailableVirtualHostsTypes, true,
+				valueObject.AvailableVirtualHostsTypes, false,
 			)
 		</div>
 		<template x-if="virtualHost.type == 'top-level'">
@@ -331,7 +332,9 @@ templ CreateMappingForm(vhostsHostnames []string) {
 					<i class="ph-bold ph-plus absolute right-0 top-1.5 text-xs transition duration-300 group-open:rotate-45"></i>
 				</summary>
 				<div class="mt-5 py-2">
-					@componentMappings.MatchPatternSelectInput("matchPattern", "Match Pattern", "mapping.matchPattern")
+					@componentMappings.MatchPatternSelectInput(
+						"matchPattern", "Match Pattern", "mapping.matchPattern",
+					)
 				</div>
 			</details>
 		</section>

+ 13 - 12
src/presentation/ui/page/runtimes.templ

@@ -7,9 +7,10 @@ import (
 	presenterDto "github.com/speedianet/os/src/presentation/ui/presenter/dto"
 )
 
-script RuntimesIndexLocalState(defaultVhostHostname, defaultRuntimeType string) {
+script RuntimesIndexLocalState(selectedVhostHostname, selectedRuntimeType string) {
     document.addEventListener('alpine:init', () => {
 		Alpine.data('runtimes', () => ({
+			vhostHostname: selectedVhostHostname,
 			reloadRuntimePageContent(vhostHostname, runtimeType) {
 				htmx.ajax(
 					'GET',
@@ -22,11 +23,11 @@ script RuntimesIndexLocalState(defaultVhostHostname, defaultRuntimeType string)
 					},
 				);
 			},
-			updateSelectedVhostHostname(selectedVhostHostname) {
-				this.reloadRuntimePageContent(selectedVhostHostname, defaultRuntimeType);
+			updateSelectedVhostHostname(vhostHostname) {
+				this.reloadRuntimePageContent(vhostHostname, selectedRuntimeType);
 			},
-			updateSelectedRuntimeType(selectedRuntimeType) {
-				this.reloadRuntimePageContent(defaultVhostHostname, selectedRuntimeType);
+			updateSelectedRuntimeType(runtimeType) {
+				this.reloadRuntimePageContent(selectedVhostHostname, runtimeType);
 			},
 		}));
 	});
@@ -36,6 +37,10 @@ templ RuntimesIndex(
 	runtimeOverview presenterDto.RuntimeOverview,
 	vhostsHostnames []string,
 ) {
+	@RuntimesIndexLocalState(
+		runtimeOverview.VirtualHostHostname.String(),
+		runtimeOverview.Type.String(),
+	)
 	<div class="flex flex-col" x-data="runtimes">
 		<div class="mb-6 flex flex-row items-center justify-between">
 			@componentStructural.PageTitle(
@@ -47,16 +52,12 @@ templ RuntimesIndex(
 		<div
 			id="runtimes-page-content"
 			hx-get={ "/runtimes/?vhostHostname=" + runtimeOverview.VirtualHostHostname.String() + "&runtimeType=" + runtimeOverview.Type.String() }
-			hx-trigger="submit from:form delay:500ms"
+			hx-trigger="submit from:form delay:500ms, refresh:runtimes-page-content from:window delay:250ms"
 			hx-select="#runtimes-page-content"
 			hx-target="#runtimes-page-content"
 			hx-swap="outerHTML transition:true"
 			class="card w-full"
 		>
-			@RuntimesIndexLocalState(
-				runtimeOverview.VirtualHostHostname.String(),
-				runtimeOverview.Type.String(),
-			)
 			<div id="runtimes-tabs">
 				@RuntimesTabs(runtimeOverview, vhostsHostnames)
 			</div>
@@ -64,7 +65,7 @@ templ RuntimesIndex(
 	</div>
 }
 
-func readRuntimesTabHeaderItems(
+func transformRuntimeTypesIntoTabHeaderItems(
 	selectedRuntimeType valueObject.RuntimeType,
 ) []componentStructural.HorizontalTabHeaderItem {
 	tabHeaderItems := []componentStructural.HorizontalTabHeaderItem{
@@ -89,7 +90,7 @@ templ RuntimesTabs(
 	vhostsHostnames []string,
 ) {
 	@componentStructural.HorizontalTabHeader(
-		readRuntimesTabHeaderItems(runtimeOverview.Type), "updateSelectedRuntimeType",
+		transformRuntimeTypesIntoTabHeaderItems(runtimeOverview.Type), "updateSelectedRuntimeType",
 	)
 	if runtimeOverview.Type.String() == "php" {
 		@pageRuntimes.PhpRuntimeHorizontalTabContent(runtimeOverview, vhostsHostnames)

+ 166 - 203
src/presentation/ui/page/runtimes/phpRuntimeHorizontalTabContent.templ

@@ -6,80 +6,198 @@ import (
 	componentForm "github.com/speedianet/os/src/presentation/ui/component/form"
 	componentStructural "github.com/speedianet/os/src/presentation/ui/component/structural"
 	presenterDto "github.com/speedianet/os/src/presentation/ui/presenter/dto"
+	"strconv"
 )
 
-script PhpRuntimeHorizontalTabContentLocalState(defaultVhostHostname string) {
+script PhpRuntimeHorizontalTabContentLocalState() {
 	document.addEventListener('alpine:init', () => {
 		Alpine.data('php', () => ({
-			// Primary states
+			// Primary States
 			phpConfigs: {},
-			get phpModulesAsApiFormat() {
-				const modulesAsObjectsArray = []
-				for (const moduleName of Object.keys(this.phpConfigs.modules)) {
-					modulesAsObjectsArray.push({
-						name: moduleName,
-						status: this.phpConfigs.modules[moduleName]
-					});
+			resetPrimaryStates() {
+				phpConfigsElement = document.getElementById('phpConfigs');
+				if (!phpConfigsElement) {
+					return;
 				}
-				return modulesAsObjectsArray;
-			},
-			get phpSettingsAsApiFormat() {
-				const settingsAsObjectsArray = []
-				for (const settingName of Object.keys(this.phpConfigs.settings)) {
-					settingsAsObjectsArray.push({
-						name: settingName,
-						value: this.phpConfigs.settings[settingName]
-					});
-				}
-				return settingsAsObjectsArray;
+				this.phpConfigs = JSON.parse(phpConfigsElement.textContent);
 			},
 			init() {
-				this.phpConfigs = {
-					vhostHostname: defaultVhostHostname,
-					version: document.getElementById('phpVersion').textContent,
-					modules: Object.fromEntries(
-						JSON.parse(
-							document.getElementById('phpModulesCheckInputsSwitchToggles').textContent
-						).map(item => [item.name, item.status])
-					),
-					settings: Object.fromEntries(
-						JSON.parse(
-							document.getElementById('phpSettingsInputs').textContent
-						).map(item => [item.name, item.value])
-					),
-				};
+				this.resetPrimaryStates();
+			},
+			async updatePhpConfigs() {
+				await jsonAjax(
+					'PUT', '/api/v1/runtime/php/' + this.vhostHostname + '/',
+					{
+						version: this.phpConfigs.version.value,
+						modules: this.phpConfigs.modules,
+						settings: this.phpConfigs.settings,
+					},
+				);
 			},
 
-			// Auxiliary states
+			// Auxiliary States
 			selectedPhpVerticalTab: 'modules',
 			updateSelectedPhpVerticalTab(tabName) {
 				this.selectedPhpVerticalTab = tabName;
 			},
 
-			// Modal states
+			// Modal States
 			isUpdatePhpVersionModalOpen: false,
-			openUpdatePhpVersionModal(phpVersion) {
-				this.phpConfigs.version = phpVersion;
+			openUpdatePhpVersionModal() {
 				this.isUpdatePhpVersionModalOpen = true;
 			},
 			closeUpdatePhpVersionModal() {
 				this.isUpdatePhpVersionModalOpen = false;
 			},
-			updateVersion() {
+			updatePhpVersion() {
+				this.closeUpdatePhpVersionModal();
 				htmx.ajax(
-					'PUT',
-					'/api/v1/runtime/php/' + this.phpConfigs.vhostHostname + '/',
+					'PUT', '/api/v1/runtime/php/' + this.vhostHostname + '/',
 					{
 						swap: 'none',
-						values: { version: this.phpConfigs.version },
+						values: { version: this.phpConfigs.version.value },
 					},
-				);
-				this.closeUpdatePhpVersionModal();
+				).then(() => {
+					this.$dispatch('refresh:runtimes-page-content');
+				});
 			},
 		}));
 	});
 }
 
+templ PhpRuntimeHorizontalTabContent(
+	runtimeOverview presenterDto.RuntimeOverview,
+	vhostsHostnames []string,
+) {
+	<!-- PhpRuntimeHorizontalTabContent JavaScript -->
+	@PhpRuntimeHorizontalTabContentLocalState()
+	<!-- PhpRuntimeHorizontalTabContent HTML -->
+	<div class="bg-os-800 -mt-4 rounded-b-xl rounded-r-xl p-4" x-data="php">
+		<div class="bg-os-500 rounded-b-lg rounded-r-lg p-6">
+			if runtimeOverview.IsInstalled {
+				<div class="lg:max-w-1/3">
+					@componentForm.SelectInput(
+						"virtualHost", "Virtual Host Hostname", "vhostHostname",
+						"updateSelectedVhostHostname(vhostHostname)",
+						vhostsHostnames, false,
+					)
+				</div>
+				if runtimeOverview.IsVirtualHostUsingRuntime {
+					@FunctionalPhpRuntimeContent(runtimeOverview, vhostsHostnames)
+					@UpdatePhpVersionModal()
+				} else {
+					@CreatePhpMappingForm(runtimeOverview.VirtualHostHostname)
+				}
+			} else {
+				@componentStructural.ServiceNotInstalledWarningForm("php")
+			}
+		</div>
+	</div>
+}
+
+templ PhpModulesCheckboxInputsSwitchToggles(phpConfigs *entity.PhpConfigs) {
+	<!-- PhpModulesCheckboxInputsSwitchToggles -->
+	<div
+		x-show="selectedPhpVerticalTab == 'modules'"
+		class="gap-7.5 grid grid-cols-5 lg:grid-cols-6"
+	>
+		// Using the index as the key is not recommended, but in this case, the entities
+		// list used here is the exact same as the one that will be on Alpine.js, so
+		// the position of the entities will also be the same. Since we need Alpine to
+		// control the state of the checkboxes, we have to use the index as the key.
+		for moduleIndex, moduleEntity := range phpConfigs.Modules {
+			@componentForm.CheckboxInputSwitchToggle(
+				"", moduleEntity.Name.String(),
+				"phpConfigs.modules["+strconv.FormatInt(int64(moduleIndex), 10)+"].status",
+				"",
+			)
+		}
+	</div>
+}
+
+func transformPhpSettingsOptionsIntoStringSlice(
+	options []valueObject.PhpSettingOption,
+) []string {
+	optionsStrSlice := []string{}
+	for _, phpOption := range options {
+		optionsStrSlice = append(optionsStrSlice, phpOption.String())
+	}
+
+	return optionsStrSlice
+}
+
+templ PhpSettingsInputs(phpConfigs *entity.PhpConfigs) {
+	<!-- PhpSettingsInputs -->
+	<div
+		x-show="selectedPhpVerticalTab == 'settings'"
+		class="grid grid-cols-3 gap-7"
+	>
+		for settingIndex, settingEntity := range phpConfigs.Settings {
+			if settingEntity.Type.String() == "text" {
+				@componentForm.InputField(
+					settingEntity.Value.GetType(), "", settingEntity.Name.String(),
+					"phpConfigs.settings["+strconv.FormatInt(int64(settingIndex), 10)+"].value",
+					false,
+				)
+			} else {
+				@componentForm.SelectInput(
+					"", settingEntity.Name.String(),
+					"phpConfigs.settings["+strconv.FormatInt(int64(settingIndex), 10)+"].value",
+					"", transformPhpSettingsOptionsIntoStringSlice(settingEntity.Options), false,
+				)
+			}
+		}
+	</div>
+}
+
+func transformPhpVersionOptionsIntoStringSlice(
+	versionOptions []valueObject.PhpVersion,
+) []string {
+	versionOptionsStrSlice := []string{}
+	for _, versionOption := range versionOptions {
+		versionOptionsStrSlice = append(versionOptionsStrSlice, versionOption.String())
+	}
+
+	return versionOptionsStrSlice
+}
+
+templ FunctionalPhpRuntimeContent(
+	runtimeOverview presenterDto.RuntimeOverview,
+	vhostsHostnames []string,
+) {
+	<!-- FunctionalPhpRuntimeContent JavaScript -->
+	@templ.JSONScript("phpConfigs", runtimeOverview.PhpConfigs)
+	<!-- FunctionalPhpRuntimeContent HTML -->
+	<div class="mt-6">
+		<div class="lg:max-w-1/3">
+			@componentForm.SelectInput(
+				"version", "Version", "phpConfigs.version.value",
+				"openUpdatePhpVersionModal()",
+				transformPhpVersionOptionsIntoStringSlice(runtimeOverview.PhpConfigs.Version.Options),
+				false,
+			)
+		</div>
+		<div class="mt-6 flex justify-stretch">
+			@componentStructural.VerticalTabHeader(
+				[]componentStructural.VerticalTabHeaderItem{
+					{Label: "Modules", Icon: "ph-puzzle-piece", ComponentValue: "modules"},
+					{Label: "Settings", Icon: "ph-gear", ComponentValue: "settings"},
+				}, "updateSelectedPhpVerticalTab",
+			)
+			<div id="php-vertical-tab-content" class="w-full p-8">
+				@PhpModulesCheckboxInputsSwitchToggles(runtimeOverview.PhpConfigs)
+				@PhpSettingsInputs(runtimeOverview.PhpConfigs)
+				<div class="max-w-1/3 mt-12">
+					@componentForm.SubmitButton(
+						"apply-php-runtime-configs-changes", "Apply changes",
+						"ph-check-fat", "updatePhpConfigs()", false,
+					)
+				</div>
+			</div>
+		</div>
+	</div>
+}
+
 templ CreatePhpMappingForm(selectedVhostHostname valueObject.Fqdn) {
 	<!-- CreatePhpMappingForm -->
 	<form
@@ -87,6 +205,7 @@ templ CreatePhpMappingForm(selectedVhostHostname valueObject.Fqdn) {
 		hx-post="/api/v1/vhosts/mapping/"
 		hx-indicator="#loading-overlay"
 		hx-swap="none"
+		class="mt-4 p-4"
 	>
 		<h1 class="flex text-3xl">The Selected Virtual Host Doesn't Map to PHP Yet</h1>
 		<p class="mt-2 text-justify">
@@ -123,177 +242,21 @@ templ CreatePhpMappingForm(selectedVhostHostname valueObject.Fqdn) {
 	</form>
 }
 
-func transformPhpVersionOptionsIntoStringSlice(
-	versionOptions []valueObject.PhpVersion,
-) []string {
-	versionOptionsStrSlice := []string{}
-	for _, versionOption := range versionOptions {
-		versionOptionsStrSlice = append(versionOptionsStrSlice, versionOption.String())
-	}
-
-	return versionOptionsStrSlice
-}
-
-templ PhpModulesCheckboxInputsSwitchToggles(
-	selectedPhpVersion entity.PhpVersion,
-	modules []entity.PhpModule,
-) {
-	<!-- PhpModulesCheckboxInputsSwitchToggles JavaScript -->
-	@templ.JSONScript("phpModulesCheckInputsSwitchToggles", modules)
-	<!-- PhpModulesCheckboxInputsSwitchToggles HTML -->
-	<div
-		x-show="selectedPhpVerticalTab == 'modules'"
-		class="grid grid-cols-9 gap-10"
-	>
-		for _, module := range modules {
-			@componentForm.CheckboxInputSwitchToggle(
-				"", module.Name.String(),
-				"phpConfigs.modules['"+module.Name.String()+"']", "",
-			)
-		}
-	</div>
-	<input name="modules" type="hidden" x-bind:value="phpModulesAsApiFormat"/>
-}
-
-func transformPhpSettingsOptionsIntoStringSlice(
-	options []valueObject.PhpSettingOption,
-) []string {
-	optionsStrSlice := []string{}
-	for _, phpOption := range options {
-		optionsStrSlice = append(optionsStrSlice, phpOption.String())
-	}
-
-	return optionsStrSlice
-}
-
-templ PhpSettingsInputs(
-	selectedPhpVersion entity.PhpVersion,
-	settings []entity.PhpSetting,
-) {
-	<!-- PhpSettingsInputs JavaScript -->
-	@templ.JSONScript("phpSettingsInputs", settings)
-	<!-- PhpSettingsInputs HTML -->
-	<div
-		x-show="selectedPhpVerticalTab == 'settings'"
-		class="grid grid-cols-4 gap-7"
-	>
-		for _, setting := range settings {
-			if setting.Type.String() == "text" {
-				@componentForm.InputField(
-					setting.Value.GetType(), "",
-					setting.Name.String(),
-					"phpConfigs.settings['"+setting.Name.String()+"']",
-					false,
-				)
-			} else {
-				@componentForm.SelectInput(
-					"", setting.Name.String(),
-					"phpConfigs.settings['"+setting.Name.String()+"']", "",
-					transformPhpSettingsOptionsIntoStringSlice(setting.Options),
-					false,
-				)
-			}
-		}
-	</div>
-	<input name="settings" type="hidden" x-bind:value="phpSettingsAsApiFormat"/>
-}
-
 templ UpdatePhpVersionWarningContent() {
 	<h3 class="text-pretty mb-3 text-xl font-bold leading-relaxed">
 		Do you really want to change the PHP version?
 	</h3>
-	<strong>Before changing, make sure your application is compatible with PHP version <span x-text="phpConfigs.version"></span>.</strong>
-	<p>Remember to then enable/disable the desired modules and adjust the settings in the new version, since the modules/settings of the previous version may be different.</p>
+	<p class="font-bold">Make sure your application is compatible with PHP <span x-text="phpConfigs.version.value"></span> before proceeding.</p>
+	<p class="mt-4 text-sm">You must also remember to enable/disable any modules and adjust the settings in the new version, as the modules/settings of the previous version will not be automatically transferred.</p>
 }
 
 templ UpdatePhpVersionModal() {
 	<!-- UpdatePhpVersionModal -->
 	@componentStructural.WarningModal(
 		"isUpdatePhpVersionModalOpen", "closeUpdatePhpVersionModal()",
-		"Cancel", "updateVersion()",
+		"Cancel", "updatePhpVersion()",
 		"update-version-button", "ph-swap", "Yes, change version",
 	) {
 		@UpdatePhpVersionWarningContent()
 	}
 }
-
-templ InstalledPhpRuntimeContent(
-	runtimeOverview presenterDto.RuntimeOverview,
-	vhostsHostnames []string,
-) {
-	<div class="p-4">
-		<div class="float-left w-1/5">
-			@componentForm.SelectInput(
-				"virtualHost", "Virtual Host Hostname",
-				"phpConfigs.vhostHostname",
-				"const vhostHostname = $event.target.value; updateSelectedVhostHostname(vhostHostname)",
-				vhostsHostnames, false,
-			)
-		</div>
-		if runtimeOverview.CanVirtualHostHostnameAccessRuntime {
-			<form
-				id="update-php-runtime-configs-form"
-				hx-put={ "/api/v1/runtime/php/" + runtimeOverview.VirtualHostHostname.String() + "/" }
-				hx-ext="encoding-request-as-json"
-				hx-swap="none"
-			>
-				<div class="w-1/7 flex pl-5">
-					<p id="phpVersion" class="hidden">{ runtimeOverview.PhpConfigs.Version.Value.String() }</p>
-					@componentForm.SelectInput(
-						"version", "Version", "phpConfigs.version",
-						"const phpVersion = $event.target.value; openUpdatePhpVersionModal(phpVersion)",
-						transformPhpVersionOptionsIntoStringSlice(runtimeOverview.PhpConfigs.Version.Options),
-						false,
-					)
-				</div>
-				<div class="mt-6 flex">
-					@componentStructural.VerticalTabHeader(
-						[]componentStructural.VerticalTabHeaderItem{
-							{Label: "Modules", Icon: "ph-puzzle-piece", ComponentValue: "modules"},
-							{Label: "Settings", Icon: "ph-gear", ComponentValue: "settings"},
-						}, "updateSelectedPhpVerticalTab",
-					)
-					<div class="px-10 py-4">
-						@PhpModulesCheckboxInputsSwitchToggles(
-							runtimeOverview.PhpConfigs.Version,
-							runtimeOverview.PhpConfigs.Modules,
-						)
-						@PhpSettingsInputs(
-							runtimeOverview.PhpConfigs.Version,
-							runtimeOverview.PhpConfigs.Settings,
-						)
-					</div>
-				</div>
-				<div class="mt-5 flex justify-end">
-					<div class="w-1/4">
-						@componentForm.SubmitButton(
-							"apply-php-runtime-configs-changes", "Apply changes",
-							"ph-check-fat", "", false,
-						)
-					</div>
-				</div>
-			</form>
-		} else {
-			<div class="mb-6 flex w-full"></div>
-			@CreatePhpMappingForm(runtimeOverview.VirtualHostHostname)
-		}
-	</div>
-}
-
-templ PhpRuntimeHorizontalTabContent(
-	runtimeOverview presenterDto.RuntimeOverview,
-	vhostsHostnames []string,
-) {
-	<!-- PhpRuntimeHorizontalTabContent -->
-	@PhpRuntimeHorizontalTabContentLocalState(runtimeOverview.VirtualHostHostname.String())
-	<div class="bg-os-800 -mt-4 rounded-b-xl rounded-r-xl p-4" x-data="php">
-		<div class="bg-os-500 rounded-b-lg rounded-r-lg px-4 py-2">
-			if runtimeOverview.IsInstalled {
-				@InstalledPhpRuntimeContent(runtimeOverview, vhostsHostnames)
-			} else {
-				@componentStructural.ServiceNotInstalledWarningForm("php")
-			}
-		</div>
-		@UpdatePhpVersionModal()
-	</div>
-}

+ 11 - 11
src/presentation/ui/presenter/dto/runtimeOverview.go

@@ -6,24 +6,24 @@ import (
 )
 
 type RuntimeOverview struct {
-	VirtualHostHostname                 valueObject.Fqdn        `json:"vhostHostname"`
-	Type                                valueObject.RuntimeType `json:"type"`
-	IsInstalled                         bool                    `json:"-"`
-	CanVirtualHostHostnameAccessRuntime bool                    `json:"-"`
-	PhpConfigs                          *entity.PhpConfigs      `json:"phpConfigs"`
+	VirtualHostHostname       valueObject.Fqdn        `json:"vhostHostname"`
+	Type                      valueObject.RuntimeType `json:"type"`
+	IsInstalled               bool                    `json:"-"`
+	IsVirtualHostUsingRuntime bool                    `json:"-"`
+	PhpConfigs                *entity.PhpConfigs      `json:"phpConfigs"`
 }
 
 func NewRuntimeOverview(
 	virtualHostHostname valueObject.Fqdn,
 	runtimeType valueObject.RuntimeType,
-	isInstalled, canVirtualHostHostnameAccessRuntime bool,
+	isInstalled, isVirtualHostUsingRuntime bool,
 	phpConfigs *entity.PhpConfigs,
 ) RuntimeOverview {
 	return RuntimeOverview{
-		VirtualHostHostname:                 virtualHostHostname,
-		Type:                                runtimeType,
-		IsInstalled:                         isInstalled,
-		CanVirtualHostHostnameAccessRuntime: canVirtualHostHostnameAccessRuntime,
-		PhpConfigs:                          phpConfigs,
+		VirtualHostHostname:       virtualHostHostname,
+		Type:                      runtimeType,
+		IsInstalled:               isInstalled,
+		IsVirtualHostUsingRuntime: isVirtualHostUsingRuntime,
+		PhpConfigs:                phpConfigs,
 	}
 }

+ 13 - 7
src/presentation/ui/presenter/runtimes.go

@@ -58,7 +58,7 @@ func (presenter *RuntimesPresenter) runtimeOverviewFactory(
 	selectedVhostHostname valueObject.Fqdn,
 ) (runtimeOverview presenterDto.RuntimeOverview, err error) {
 	isInstalled := false
-	canVirtualHostHostnameAccessRuntime := false
+	isVirtualHostUsingRuntime := false
 
 	var phpConfigsPtr *entity.PhpConfigs
 	if runtimeType.String() == "php" {
@@ -66,9 +66,9 @@ func (presenter *RuntimesPresenter) runtimeOverviewFactory(
 		responseOutput := presenter.runtimeService.ReadPhpConfigs(requestBody)
 
 		isInstalled = true
-		canVirtualHostHostnameAccessRuntime = true
+		isVirtualHostUsingRuntime = true
 		if responseOutput.Status != service.Success {
-			canVirtualHostHostnameAccessRuntime = false
+			isVirtualHostUsingRuntime = false
 			responseOutputBodyStr, assertOk := responseOutput.Body.(string)
 			if assertOk {
 				isInstalled = responseOutputBodyStr != "ServiceUnavailable"
@@ -85,7 +85,7 @@ func (presenter *RuntimesPresenter) runtimeOverviewFactory(
 
 	return presenterDto.NewRuntimeOverview(
 		selectedVhostHostname, runtimeType, isInstalled,
-		canVirtualHostHostnameAccessRuntime, phpConfigsPtr,
+		isVirtualHostUsingRuntime, phpConfigsPtr,
 	), nil
 }
 
@@ -96,16 +96,22 @@ func (presenter *RuntimesPresenter) Handler(c echo.Context) error {
 	}
 	runtimeType, err := valueObject.NewRuntimeType(rawRuntimeType)
 	if err != nil {
+		slog.Error("InvalidRuntimeType", slog.Any("err", err))
 		return nil
 	}
 
-	selectedVhostHostname, err := valueObject.NewFqdn(c.QueryParam("vhostHostname"))
+	primaryVhostHostname, err := infraHelper.GetPrimaryVirtualHost()
 	if err != nil {
-		primaryVhostHostname, err := infraHelper.GetPrimaryVirtualHost()
+		slog.Error("ReadPrimaryVirtualHost", slog.Any("err", err))
+		return nil
+	}
+	selectedVhostHostname := primaryVhostHostname
+	if c.QueryParam("vhostHostname") != "" {
+		selectedVhostHostname, err = valueObject.NewFqdn(c.QueryParam("vhostHostname"))
 		if err != nil {
+			slog.Error("InvalidVhostHostname", slog.Any("err", err))
 			return nil
 		}
-		selectedVhostHostname = primaryVhostHostname
 	}
 
 	runtimeOverview, err := presenter.runtimeOverviewFactory(