Kaynağa Gözat

fmt: Code style and linter

jamesread 1 ay önce
ebeveyn
işleme
25b80e1460
66 değiştirilmiş dosya ile 4128 ekleme ve 2841 silme
  1. 1 0
      .github/PULL_REQUEST_TEMPLATE.md
  2. 6 5
      .pre-commit-config.yaml
  3. 1 0
      CONTRIBUTING.adoc
  4. 12 2
      frontend/.eslintrc.json
  5. 2 2
      frontend/Makefile
  6. 371 187
      frontend/package-lock.json
  7. 7 6
      frontend/package.json
  8. 219 183
      frontend/resources/vue/ActionButton.vue
  9. 418 326
      frontend/resources/vue/App.vue
  10. 239 186
      frontend/resources/vue/Dashboard.vue
  11. 24 22
      frontend/resources/vue/ExecutionButton.vue
  12. 4 1
      frontend/resources/vue/components/ActionGroupLimitsLabel.vue
  13. 36 30
      frontend/resources/vue/components/ActionIconGlyph.vue
  14. 60 56
      frontend/resources/vue/components/ActionStatusDisplay.vue
  15. 44 39
      frontend/resources/vue/components/Breadcrumbs.vue
  16. 24 11
      frontend/resources/vue/components/ChoiceChecklist.vue
  17. 25 19
      frontend/resources/vue/components/ChoiceCombobox.vue
  18. 19 7
      frontend/resources/vue/components/ConnectionBanner.vue
  19. 39 23
      frontend/resources/vue/components/DashboardComponent.vue
  20. 29 22
      frontend/resources/vue/components/DashboardComponentDirectory.vue
  21. 11 8
      frontend/resources/vue/components/DashboardComponentDisplay.vue
  22. 14 8
      frontend/resources/vue/components/DashboardComponentMostRecentExecution.vue
  23. 187 160
      frontend/resources/vue/components/EntityDefinitionSection.vue
  24. 75 72
      frontend/resources/vue/components/EntityInstancesTable.vue
  25. 45 25
      frontend/resources/vue/components/EntityListFilter.vue
  26. 4 1
      frontend/resources/vue/components/LogActionTitle.vue
  27. 15 18
      frontend/resources/vue/router.js
  28. 6 6
      frontend/resources/vue/utils/choiceChecklistHelpers.js
  29. 3 3
      frontend/resources/vue/utils/justificationTemplate.js
  30. 12 12
      frontend/resources/vue/utils/prefilledArguments.js
  31. 221 140
      frontend/resources/vue/views/ActionDetailsView.vue
  32. 92 22
      frontend/resources/vue/views/ActionExecConditionsView.vue
  33. 152 93
      frontend/resources/vue/views/ArgumentForm.vue
  34. 68 36
      frontend/resources/vue/views/DiagnosticsView.vue
  35. 44 41
      frontend/resources/vue/views/EntitiesView.vue
  36. 168 122
      frontend/resources/vue/views/EntityDetailsView.vue
  37. 170 118
      frontend/resources/vue/views/ExecutionView.vue
  38. 67 22
      frontend/resources/vue/views/LoginView.vue
  39. 33 20
      frontend/resources/vue/views/LogsCalendarView.vue
  40. 216 120
      frontend/resources/vue/views/LogsListView.vue
  41. 64 16
      frontend/resources/vue/views/LogsQueueView.vue
  42. 12 6
      frontend/resources/vue/views/NotFoundView.vue
  43. 52 22
      frontend/resources/vue/views/UserControlPanel.vue
  44. 4 4
      integration-tests/package-lock.json
  45. 1 1
      integration-tests/package.json
  46. 3 3
      lang/go.mod
  47. 4 16
      lang/go.sum
  48. 1 1
      scripts/devcheck.py
  49. 25 0
      service/.golangci.yml
  50. 3 6
      service/Makefile
  51. 210 62
      service/go.mod
  52. 512 467
      service/go.sum
  53. 2 2
      service/internal/acl/acl.go
  54. 5 9
      service/internal/auth/otjwt/jwt_test.go
  55. 2 2
      service/internal/auth/otoauth2/restapi_auth_oauth2.go
  56. 8 8
      service/internal/config/config_reloader_user_test.go
  57. 1 1
      service/internal/executor/executor.go
  58. 0 10
      service/internal/executor/group_concurrency.go
  59. 1 1
      service/internal/executor/timeout_context.go
  60. 1 1
      service/internal/filehelper/file_change_notify.go
  61. 1 2
      service/internal/filehelper/file_write.go
  62. 1 1
      service/internal/installationinfo/runtimeinfo.go
  63. 0 23
      service/internal/servicehost/log_directory.go
  64. 30 0
      service/internal/servicehost/log_directory_windows.go
  65. 1 1
      service/scripts/find-flakey-tests-inf/main.go
  66. 1 2
      service/tools.go

+ 1 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -18,6 +18,7 @@ Please put a X in the boxes as evidence of reading through the checklist.
   - [ ] I followed the "1 logical change" rule.
 - [ ] I have forked the project, and raised this PR on a feature branch.
 - [ ] I ran the `pre-commit` hooks, and my commit message was validated.
+- [ ] I installed the pre-push hook (`pre-commit install --hook-type pre-push`) and integration tests pass on push when relevant files changed.
 - [ ] `make -wC service compile` runs without any issues.
 - [ ] `make -wC service codestyle` runs without any issues.
 - [ ] `make -wC service unittests` runs without any issues.

+ 6 - 5
.pre-commit-config.yaml

@@ -38,32 +38,33 @@ repos:
         entry: make service-codestyle
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^(service/|proto/|lang/)
 
       - id: frontend-codestyle
         name: frontend-codestyle
         entry: make frontend-codestyle
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^frontend/
 
       - id: service-unittests
         name: service-unittests
         entry: make service-unittests
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^(service/|proto/|lang/)
 
       - id: service-build
         name: service-build
         entry: make service
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^(service/|proto/|lang/)
 
       - id: it
         name: integration-tests
         entry: make it
         language: system
         pass_filenames: false
-        always_run: true
+        stages: [pre-push]
+        files: ^(service/|frontend/|integration-tests/|proto/|Makefile)

+ 1 - 0
CONTRIBUTING.adoc

@@ -43,6 +43,7 @@ choco install git go protoc make python nodejs-lts -y && pip install pre-commit
 git clone https://github.com/OliveTin/OliveTin.git
 cd OliveTin
 pre-commit install
+pre-commit install --hook-type pre-push
 
 # Step3: compile binary for current dev env (OS, ARCH)
 # `make proto` will also run `make go-tools`, which installs "buf". This binary

+ 12 - 2
frontend/.eslintrc.json

@@ -4,12 +4,22 @@
         "es2021": true
     },
     "extends": [
+        "plugin:vue/recommended",
         "standard"
     ],
+    "parser": "vue-eslint-parser",
     "parserOptions": {
         "ecmaVersion": 12,
-        "sourceType": "module"
+        "sourceType": "module",
+        "parser": {
+            "js": "espree"
+        }
     },
     "rules": {
+        "vue/multi-word-component-names": "off",
+        "vue/require-default-prop": "off",
+        "vue/no-v-html": "warn",
+        "no-tabs": "off",
+        "no-mixed-spaces-and-tabs": "off"
     }
-}
+}

+ 2 - 2
frontend/Makefile

@@ -4,7 +4,7 @@ endef
 
 codestyle:
 	npm install
-	npx eslint --fix main.js js/*
+	npx eslint --fix main.js js/* resources/vue
 	npx stylelint style.css
 
 clean:
@@ -18,4 +18,4 @@ build:
 
 dist: deps clean build
 
-.PHONY: codestyle
+.PHONY: codestyle

+ 371 - 187
frontend/package-lock.json

@@ -11,24 +11,25 @@
 			"dependencies": {
 				"@connectrpc/connect": "^2.1.2",
 				"@connectrpc/connect-web": "^2.1.2",
-				"@hugeicons/core-free-icons": "^4.2.1",
-				"@hugeicons/vue": "^1.0.6",
+				"@hugeicons/core-free-icons": "^4.2.2",
+				"@hugeicons/vue": "^1.0.7",
 				"@vitejs/plugin-vue": "^6.0.7",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
-				"picocrank": "^1.17.0",
+				"picocrank": "^1.18.2",
 				"standard": "^17.1.2",
 				"unplugin-vue-components": "^32.1.0",
-				"vite": "^8.0.16",
-				"vue": "^3.5.38",
+				"vite": "^8.1.3",
+				"vue": "^3.5.39",
 				"vue-i18n": "^11.4.6",
 				"vue-router": "^5.1.0"
 			},
 			"devDependencies": {
+				"eslint-plugin-vue": "^10.9.2",
 				"process": "^0.11.10",
-				"stylelint": "^17.13.0",
+				"stylelint": "^17.14.0",
 				"stylelint-config-standard": "^40.0.0"
 			},
 			"engines": {
@@ -36,13 +37,13 @@
 			}
 		},
 		"node_modules/@babel/code-frame": {
-			"version": "7.29.0",
-			"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
-			"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+			"version": "7.29.7",
+			"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+			"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
 			"dev": true,
 			"license": "MIT",
 			"dependencies": {
-				"@babel/helper-validator-identifier": "^7.28.5",
+				"@babel/helper-validator-identifier": "^7.29.7",
 				"js-tokens": "^4.0.0",
 				"picocolors": "^1.1.1"
 			},
@@ -409,20 +410,20 @@
 			}
 		},
 		"node_modules/@emnapi/core": {
-			"version": "1.10.0",
-			"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
-			"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+			"version": "1.11.1",
+			"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+			"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
 			"license": "MIT",
 			"optional": true,
 			"dependencies": {
-				"@emnapi/wasi-threads": "1.2.1",
+				"@emnapi/wasi-threads": "1.2.2",
 				"tslib": "^2.4.0"
 			}
 		},
 		"node_modules/@emnapi/runtime": {
-			"version": "1.10.0",
-			"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
-			"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+			"version": "1.11.1",
+			"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+			"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
 			"license": "MIT",
 			"optional": true,
 			"dependencies": {
@@ -430,9 +431,9 @@
 			}
 		},
 		"node_modules/@emnapi/wasi-threads": {
-			"version": "1.2.1",
-			"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
-			"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+			"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
 			"license": "MIT",
 			"optional": true,
 			"dependencies": {
@@ -941,15 +942,15 @@
 			}
 		},
 		"node_modules/@hugeicons/core-free-icons": {
-			"version": "4.2.1",
-			"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.1.tgz",
-			"integrity": "sha512-75jYZKYyA9VwS35YRmmGUGzFedbY+Fl0Vxx5FzXR2CGDlIhNRumFeVqaaKoClf2MeYEJwPAVMEL9RwCYtOgnSw==",
+			"version": "4.2.2",
+			"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz",
+			"integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==",
 			"license": "MIT"
 		},
 		"node_modules/@hugeicons/vue": {
-			"version": "1.0.6",
-			"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.6.tgz",
-			"integrity": "sha512-T1Wbuk2qDdoE2xcY3CDtwMOKewwIYDE7JoMVLeuAV4cbOx5CFtToHCQAmLgVPIFGGZUsV2nN52lBkts9upvePw==",
+			"version": "1.0.7",
+			"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.7.tgz",
+			"integrity": "sha512-oUE0ztUNYbsMFpMgbV3bsxQBObNkeouomUThFuFPzYC5eV++VTYonStsltJu2MoLMkxV8mzpoVvIbuMPBZSN0w==",
 			"license": "MIT",
 			"peerDependencies": {
 				"vue": "^2.6.0 || ^3.0.0"
@@ -1110,13 +1111,13 @@
 			"license": "MIT"
 		},
 		"node_modules/@napi-rs/wasm-runtime": {
-			"version": "1.1.5",
-			"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
-			"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+			"version": "1.1.6",
+			"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+			"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
 			"license": "MIT",
 			"optional": true,
 			"dependencies": {
-				"@tybys/wasm-util": "^0.10.2"
+				"@tybys/wasm-util": "^0.10.3"
 			},
 			"funding": {
 				"type": "github",
@@ -1160,18 +1161,18 @@
 			}
 		},
 		"node_modules/@oxc-project/types": {
-			"version": "0.133.0",
-			"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
-			"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+			"version": "0.139.0",
+			"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+			"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
 			"license": "MIT",
 			"funding": {
 				"url": "https://github.com/sponsors/Boshen"
 			}
 		},
 		"node_modules/@rolldown/binding-android-arm64": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
-			"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+			"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
 			"cpu": [
 				"arm64"
 			],
@@ -1185,9 +1186,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-darwin-arm64": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
-			"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+			"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
 			"cpu": [
 				"arm64"
 			],
@@ -1201,9 +1202,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-darwin-x64": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
-			"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+			"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
 			"cpu": [
 				"x64"
 			],
@@ -1217,9 +1218,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-freebsd-x64": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
-			"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+			"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
 			"cpu": [
 				"x64"
 			],
@@ -1233,9 +1234,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
-			"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+			"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
 			"cpu": [
 				"arm"
 			],
@@ -1249,9 +1250,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-arm64-gnu": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
-			"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+			"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
 			"cpu": [
 				"arm64"
 			],
@@ -1265,9 +1266,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-arm64-musl": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
-			"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+			"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
 			"cpu": [
 				"arm64"
 			],
@@ -1281,9 +1282,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-ppc64-gnu": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
-			"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+			"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
 			"cpu": [
 				"ppc64"
 			],
@@ -1297,9 +1298,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-s390x-gnu": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
-			"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+			"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
 			"cpu": [
 				"s390x"
 			],
@@ -1313,9 +1314,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-x64-gnu": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
-			"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+			"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
 			"cpu": [
 				"x64"
 			],
@@ -1329,9 +1330,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-linux-x64-musl": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
-			"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+			"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
 			"cpu": [
 				"x64"
 			],
@@ -1345,9 +1346,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-openharmony-arm64": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
-			"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+			"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
 			"cpu": [
 				"arm64"
 			],
@@ -1361,27 +1362,27 @@
 			}
 		},
 		"node_modules/@rolldown/binding-wasm32-wasi": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
-			"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+			"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
 			"cpu": [
 				"wasm32"
 			],
 			"license": "MIT",
 			"optional": true,
 			"dependencies": {
-				"@emnapi/core": "1.10.0",
-				"@emnapi/runtime": "1.10.0",
-				"@napi-rs/wasm-runtime": "^1.1.4"
+				"@emnapi/core": "1.11.1",
+				"@emnapi/runtime": "1.11.1",
+				"@napi-rs/wasm-runtime": "^1.1.6"
 			},
 			"engines": {
 				"node": "^20.19.0 || >=22.12.0"
 			}
 		},
 		"node_modules/@rolldown/binding-win32-arm64-msvc": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
-			"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+			"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
 			"cpu": [
 				"arm64"
 			],
@@ -1395,9 +1396,9 @@
 			}
 		},
 		"node_modules/@rolldown/binding-win32-x64-msvc": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
-			"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+			"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
 			"cpu": [
 				"x64"
 			],
@@ -1436,15 +1437,31 @@
 			}
 		},
 		"node_modules/@tybys/wasm-util": {
-			"version": "0.10.2",
-			"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
-			"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+			"version": "0.10.3",
+			"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+			"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
 			"license": "MIT",
 			"optional": true,
 			"dependencies": {
 				"tslib": "^2.4.0"
 			}
 		},
+		"node_modules/@types/esrecurse": {
+			"version": "4.3.1",
+			"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+			"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+			"dev": true,
+			"license": "MIT",
+			"peer": true
+		},
+		"node_modules/@types/estree": {
+			"version": "1.0.9",
+			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+			"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+			"dev": true,
+			"license": "MIT",
+			"peer": true
+		},
 		"node_modules/@types/jsesc": {
 			"version": "2.5.1",
 			"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
@@ -1507,39 +1524,39 @@
 			}
 		},
 		"node_modules/@vue/compiler-core": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
-			"integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
+			"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
 			"license": "MIT",
 			"dependencies": {
 				"@babel/parser": "^7.29.7",
-				"@vue/shared": "3.5.38",
+				"@vue/shared": "3.5.39",
 				"entities": "^7.0.1",
 				"estree-walker": "^2.0.2",
 				"source-map-js": "^1.2.1"
 			}
 		},
 		"node_modules/@vue/compiler-dom": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
-			"integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
+			"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/compiler-core": "3.5.38",
-				"@vue/shared": "3.5.38"
+				"@vue/compiler-core": "3.5.39",
+				"@vue/shared": "3.5.39"
 			}
 		},
 		"node_modules/@vue/compiler-sfc": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
-			"integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
+			"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
 			"license": "MIT",
 			"dependencies": {
 				"@babel/parser": "^7.29.7",
-				"@vue/compiler-core": "3.5.38",
-				"@vue/compiler-dom": "3.5.38",
-				"@vue/compiler-ssr": "3.5.38",
-				"@vue/shared": "3.5.38",
+				"@vue/compiler-core": "3.5.39",
+				"@vue/compiler-dom": "3.5.39",
+				"@vue/compiler-ssr": "3.5.39",
+				"@vue/shared": "3.5.39",
 				"estree-walker": "^2.0.2",
 				"magic-string": "^0.30.21",
 				"postcss": "^8.5.15",
@@ -1547,13 +1564,13 @@
 			}
 		},
 		"node_modules/@vue/compiler-ssr": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
-			"integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
+			"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/compiler-dom": "3.5.38",
-				"@vue/shared": "3.5.38"
+				"@vue/compiler-dom": "3.5.39",
+				"@vue/shared": "3.5.39"
 			}
 		},
 		"node_modules/@vue/devtools-api": {
@@ -1581,53 +1598,53 @@
 			"license": "MIT"
 		},
 		"node_modules/@vue/reactivity": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
-			"integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
+			"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/shared": "3.5.38"
+				"@vue/shared": "3.5.39"
 			}
 		},
 		"node_modules/@vue/runtime-core": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
-			"integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
+			"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/reactivity": "3.5.38",
-				"@vue/shared": "3.5.38"
+				"@vue/reactivity": "3.5.39",
+				"@vue/shared": "3.5.39"
 			}
 		},
 		"node_modules/@vue/runtime-dom": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
-			"integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
+			"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/reactivity": "3.5.38",
-				"@vue/runtime-core": "3.5.38",
-				"@vue/shared": "3.5.38",
+				"@vue/reactivity": "3.5.39",
+				"@vue/runtime-core": "3.5.39",
+				"@vue/shared": "3.5.39",
 				"csstype": "^3.2.3"
 			}
 		},
 		"node_modules/@vue/server-renderer": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
-			"integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
+			"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/compiler-ssr": "3.5.38",
-				"@vue/shared": "3.5.38"
+				"@vue/compiler-ssr": "3.5.39",
+				"@vue/shared": "3.5.39"
 			},
 			"peerDependencies": {
-				"vue": "3.5.38"
+				"vue": "3.5.39"
 			}
 		},
 		"node_modules/@vue/shared": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
-			"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
+			"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
 			"license": "MIT"
 		},
 		"node_modules/@xterm/addon-fit": {
@@ -1949,6 +1966,13 @@
 				"url": "https://github.com/sponsors/antfu"
 			}
 		},
+		"node_modules/boolbase": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+			"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+			"dev": true,
+			"license": "ISC"
+		},
 		"node_modules/brace-expansion": {
 			"version": "1.1.14",
 			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
@@ -2138,9 +2162,9 @@
 			"license": "MIT"
 		},
 		"node_modules/cosmiconfig": {
-			"version": "9.0.1",
-			"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
-			"integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
+			"version": "9.0.2",
+			"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
+			"integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
 			"dev": true,
 			"license": "MIT",
 			"dependencies": {
@@ -2990,6 +3014,51 @@
 				"url": "https://github.com/sponsors/ljharb"
 			}
 		},
+		"node_modules/eslint-plugin-vue": {
+			"version": "10.9.2",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz",
+			"integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==",
+			"dev": true,
+			"license": "MIT",
+			"dependencies": {
+				"@eslint-community/eslint-utils": "^4.4.0",
+				"natural-compare": "^1.4.0",
+				"nth-check": "^2.1.1",
+				"postcss-selector-parser": "^7.1.0",
+				"semver": "^7.6.3",
+				"xml-name-validator": "^4.0.0"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"peerDependencies": {
+				"@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
+				"@typescript-eslint/parser": "^7.0.0 || ^8.0.0",
+				"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+				"vue-eslint-parser": "^10.3.0"
+			},
+			"peerDependenciesMeta": {
+				"@stylistic/eslint-plugin": {
+					"optional": true
+				},
+				"@typescript-eslint/parser": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/eslint-plugin-vue/node_modules/semver": {
+			"version": "7.8.5",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+			"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+			"dev": true,
+			"license": "ISC",
+			"bin": {
+				"semver": "bin/semver.js"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
 		"node_modules/eslint-scope": {
 			"version": "7.2.2",
 			"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -4897,6 +4966,19 @@
 				"node": ">=0.10.0"
 			}
 		},
+		"node_modules/nth-check": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+			"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"boolbase": "^1.0.0"
+			},
+			"funding": {
+				"url": "https://github.com/fb55/nth-check?sponsor=1"
+			}
+		},
 		"node_modules/object-assign": {
 			"version": "4.1.1",
 			"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5186,19 +5268,19 @@
 			"license": "ISC"
 		},
 		"node_modules/picocrank": {
-			"version": "1.17.0",
-			"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.17.0.tgz",
-			"integrity": "sha512-EIUSI26elt6ulloN74CT2sX5tBxS2wjdV4A+eXHYNtHbH0hM2iKkYzpKKWOEr+yUTfrvjah6VuqieedFw/9R0w==",
+			"version": "1.18.2",
+			"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz",
+			"integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==",
 			"license": "ISC",
 			"dependencies": {
-				"@hugeicons/core-free-icons": "^4.1.1",
-				"@hugeicons/vue": "^1.0.5",
-				"@vitejs/plugin-vue": "^6.0.6",
+				"@hugeicons/core-free-icons": "^4.2.2",
+				"@hugeicons/vue": "^1.0.7",
+				"@vitejs/plugin-vue": "^6.0.7",
 				"femtocrank": "^2.5.0",
-				"unplugin-vue-components": "^32.0.0",
-				"vite": "^8.0.10",
-				"vue": "^3.5.33",
-				"vue-router": "^5.0.6"
+				"unplugin-vue-components": "^32.1.0",
+				"vite": "^8.1.2",
+				"vue": "^3.5.39",
+				"vue-router": "^5.1.0"
 			}
 		},
 		"node_modules/picomatch": {
@@ -5318,9 +5400,9 @@
 			}
 		},
 		"node_modules/postcss": {
-			"version": "8.5.15",
-			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
-			"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+			"version": "8.5.16",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+			"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
 			"funding": [
 				{
 					"type": "opencollective",
@@ -5373,9 +5455,9 @@
 			}
 		},
 		"node_modules/postcss-selector-parser": {
-			"version": "7.1.1",
-			"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
-			"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+			"version": "7.1.4",
+			"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
+			"integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
 			"dev": true,
 			"license": "MIT",
 			"dependencies": {
@@ -5620,12 +5702,12 @@
 			}
 		},
 		"node_modules/rolldown": {
-			"version": "1.0.3",
-			"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
-			"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+			"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
 			"license": "MIT",
 			"dependencies": {
-				"@oxc-project/types": "=0.133.0",
+				"@oxc-project/types": "=0.139.0",
 				"@rolldown/pluginutils": "^1.0.0"
 			},
 			"bin": {
@@ -5635,21 +5717,21 @@
 				"node": "^20.19.0 || >=22.12.0"
 			},
 			"optionalDependencies": {
-				"@rolldown/binding-android-arm64": "1.0.3",
-				"@rolldown/binding-darwin-arm64": "1.0.3",
-				"@rolldown/binding-darwin-x64": "1.0.3",
-				"@rolldown/binding-freebsd-x64": "1.0.3",
-				"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
-				"@rolldown/binding-linux-arm64-gnu": "1.0.3",
-				"@rolldown/binding-linux-arm64-musl": "1.0.3",
-				"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
-				"@rolldown/binding-linux-s390x-gnu": "1.0.3",
-				"@rolldown/binding-linux-x64-gnu": "1.0.3",
-				"@rolldown/binding-linux-x64-musl": "1.0.3",
-				"@rolldown/binding-openharmony-arm64": "1.0.3",
-				"@rolldown/binding-wasm32-wasi": "1.0.3",
-				"@rolldown/binding-win32-arm64-msvc": "1.0.3",
-				"@rolldown/binding-win32-x64-msvc": "1.0.3"
+				"@rolldown/binding-android-arm64": "1.1.5",
+				"@rolldown/binding-darwin-arm64": "1.1.5",
+				"@rolldown/binding-darwin-x64": "1.1.5",
+				"@rolldown/binding-freebsd-x64": "1.1.5",
+				"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+				"@rolldown/binding-linux-arm64-gnu": "1.1.5",
+				"@rolldown/binding-linux-arm64-musl": "1.1.5",
+				"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+				"@rolldown/binding-linux-s390x-gnu": "1.1.5",
+				"@rolldown/binding-linux-x64-gnu": "1.1.5",
+				"@rolldown/binding-linux-x64-musl": "1.1.5",
+				"@rolldown/binding-openharmony-arm64": "1.1.5",
+				"@rolldown/binding-wasm32-wasi": "1.1.5",
+				"@rolldown/binding-win32-arm64-msvc": "1.1.5",
+				"@rolldown/binding-win32-x64-msvc": "1.1.5"
 			}
 		},
 		"node_modules/run-parallel": {
@@ -6151,9 +6233,9 @@
 			}
 		},
 		"node_modules/stylelint": {
-			"version": "17.13.0",
-			"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.13.0.tgz",
-			"integrity": "sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==",
+			"version": "17.14.0",
+			"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
+			"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
 			"dev": true,
 			"funding": [
 				{
@@ -6169,13 +6251,13 @@
 			"dependencies": {
 				"@csstools/css-calc": "^3.2.1",
 				"@csstools/css-parser-algorithms": "^4.0.0",
-				"@csstools/css-syntax-patches-for-csstree": "^1.1.4",
+				"@csstools/css-syntax-patches-for-csstree": "^1.1.5",
 				"@csstools/css-tokenizer": "^4.0.0",
 				"@csstools/media-query-list-parser": "^5.0.0",
 				"@csstools/selector-resolve-nested": "^4.0.0",
 				"@csstools/selector-specificity": "^6.0.0",
 				"colord": "^2.9.3",
-				"cosmiconfig": "^9.0.1",
+				"cosmiconfig": "^9.0.2",
 				"css-functions-list": "^3.3.3",
 				"css-tree": "^3.2.1",
 				"debug": "^4.4.3",
@@ -6195,7 +6277,7 @@
 				"picocolors": "^1.1.1",
 				"postcss": "^8.5.15",
 				"postcss-safe-parser": "^7.0.1",
-				"postcss-selector-parser": "^7.1.1",
+				"postcss-selector-parser": "^7.1.4",
 				"postcss-value-parser": "^4.2.0",
 				"string-width": "^8.2.1",
 				"supports-hyperlinks": "^4.4.0",
@@ -6790,15 +6872,15 @@
 			}
 		},
 		"node_modules/vite": {
-			"version": "8.0.16",
-			"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
-			"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
+			"version": "8.1.3",
+			"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
+			"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
 			"license": "MIT",
 			"dependencies": {
 				"lightningcss": "^1.32.0",
 				"picomatch": "^4.0.4",
-				"postcss": "^8.5.15",
-				"rolldown": "1.0.3",
+				"postcss": "^8.5.16",
+				"rolldown": "~1.1.3",
 				"tinyglobby": "^0.2.17"
 			},
 			"bin": {
@@ -6815,7 +6897,7 @@
 			},
 			"peerDependencies": {
 				"@types/node": "^20.19.0 || >=22.12.0",
-				"@vitejs/devtools": "^0.1.18",
+				"@vitejs/devtools": "^0.3.0",
 				"esbuild": "^0.27.0 || ^0.28.0",
 				"jiti": ">=1.21.0",
 				"less": "^4.0.0",
@@ -6879,16 +6961,16 @@
 			}
 		},
 		"node_modules/vue": {
-			"version": "3.5.38",
-			"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
-			"integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
+			"version": "3.5.39",
+			"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
+			"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
 			"license": "MIT",
 			"dependencies": {
-				"@vue/compiler-dom": "3.5.38",
-				"@vue/compiler-sfc": "3.5.38",
-				"@vue/runtime-dom": "3.5.38",
-				"@vue/server-renderer": "3.5.38",
-				"@vue/shared": "3.5.38"
+				"@vue/compiler-dom": "3.5.39",
+				"@vue/compiler-sfc": "3.5.39",
+				"@vue/runtime-dom": "3.5.39",
+				"@vue/server-renderer": "3.5.39",
+				"@vue/shared": "3.5.39"
 			},
 			"peerDependencies": {
 				"typescript": "*"
@@ -6899,6 +6981,98 @@
 				}
 			}
 		},
+		"node_modules/vue-eslint-parser": {
+			"version": "10.4.1",
+			"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz",
+			"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
+			"dev": true,
+			"license": "MIT",
+			"peer": true,
+			"dependencies": {
+				"debug": "^4.4.0",
+				"eslint-scope": "^8.2.0 || ^9.0.0",
+				"eslint-visitor-keys": "^4.2.0 || ^5.0.0",
+				"espree": "^10.3.0 || ^11.0.0",
+				"esquery": "^1.6.0",
+				"semver": "^7.6.3"
+			},
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/mysticatea"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
+			}
+		},
+		"node_modules/vue-eslint-parser/node_modules/eslint-scope": {
+			"version": "9.1.2",
+			"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+			"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"peer": true,
+			"dependencies": {
+				"@types/esrecurse": "^4.3.1",
+				"@types/estree": "^1.0.8",
+				"esrecurse": "^4.3.0",
+				"estraverse": "^5.2.0"
+			},
+			"engines": {
+				"node": "^20.19.0 || ^22.13.0 || >=24"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+			"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"peer": true,
+			"engines": {
+				"node": "^20.19.0 || ^22.13.0 || >=24"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/vue-eslint-parser/node_modules/espree": {
+			"version": "11.2.0",
+			"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+			"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+			"dev": true,
+			"license": "BSD-2-Clause",
+			"peer": true,
+			"dependencies": {
+				"acorn": "^8.16.0",
+				"acorn-jsx": "^5.3.2",
+				"eslint-visitor-keys": "^5.0.1"
+			},
+			"engines": {
+				"node": "^20.19.0 || ^22.13.0 || >=24"
+			},
+			"funding": {
+				"url": "https://opencollective.com/eslint"
+			}
+		},
+		"node_modules/vue-eslint-parser/node_modules/semver": {
+			"version": "7.8.5",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+			"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+			"dev": true,
+			"license": "ISC",
+			"peer": true,
+			"bin": {
+				"semver": "bin/semver.js"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
 		"node_modules/vue-i18n": {
 			"version": "11.4.6",
 			"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
@@ -7145,6 +7319,16 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/xml-name-validator": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+			"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+			"dev": true,
+			"license": "Apache-2.0",
+			"engines": {
+				"node": ">=12"
+			}
+		},
 		"node_modules/yaml": {
 			"version": "2.9.0",
 			"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",

+ 7 - 6
frontend/package.json

@@ -5,8 +5,9 @@
 	"repository": "https://github.com/OliveTin/OliveTin",
 	"source": "index.html",
 	"devDependencies": {
+		"eslint-plugin-vue": "^10.9.2",
 		"process": "^0.11.10",
-		"stylelint": "^17.13.0",
+		"stylelint": "^17.14.0",
 		"stylelint-config-standard": "^40.0.0"
 	},
 	"scripts": {
@@ -24,18 +25,18 @@
 	"dependencies": {
 		"@connectrpc/connect": "^2.1.2",
 		"@connectrpc/connect-web": "^2.1.2",
-		"@hugeicons/core-free-icons": "^4.2.1",
-		"@hugeicons/vue": "^1.0.6",
+		"@hugeicons/core-free-icons": "^4.2.2",
+		"@hugeicons/vue": "^1.0.7",
 		"@vitejs/plugin-vue": "^6.0.7",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
-		"picocrank": "^1.17.0",
+		"picocrank": "^1.18.2",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^32.1.0",
-		"vite": "^8.0.16",
-		"vue": "^3.5.38",
+		"vite": "^8.1.3",
+		"vue": "^3.5.39",
 		"vue-i18n": "^11.4.6",
 		"vue-router": "^5.1.0"
 	},

+ 219 - 183
frontend/resources/vue/ActionButton.vue

@@ -1,36 +1,73 @@
 <template>
-	<div :id="`actionButton-${bindingId}`" role="none" class="action-button" @contextmenu.prevent="openActionDetails">
-		<span
-			v-if="showExecutionIndicator"
-			class="execution-indicator"
-			:class="executionIndicatorClass"
-			:title="executionIndicatorTitle"
-			aria-hidden="true"
-		></span>
-		<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
-													  :class="combinedClasses" @click="handleClick">
-
-			<div v-if="showNavigateOnStartIcons" class="navigate-on-start-container">
-				<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
-					<HugeiconsIcon :icon="ComputerTerminal01Icon" />
-				</div>
-				<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
-					<HugeiconsIcon :icon="TypeCursorIcon" />
-				</div>
-				<div v-if="navigateOnStart == 'hist'" class="navigate-on-start" title="Opens action execution history on start">
-					<HugeiconsIcon :icon="WorkHistoryIcon" />
-				</div>
-				<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
-					<HugeiconsIcon :icon="WorkoutRunIcon" />
-				</div>
-			</div>
-
-			<ActionIconGlyph class="icon" :glyph="actionGlyph" />
-			<span class="title" aria-live="polite">{{ displayTitle }}
-			</span>
-			<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
-		</button>
-	</div>
+  <div
+    :id="`actionButton-${bindingId}`"
+    role="none"
+    class="action-button"
+    @contextmenu.prevent="openActionDetails"
+  >
+    <span
+      v-if="showExecutionIndicator"
+      class="execution-indicator"
+      :class="executionIndicatorClass"
+      :title="executionIndicatorTitle"
+      aria-hidden="true"
+    />
+    <button
+      :id="`actionButtonInner-${bindingId}`"
+      :title="title"
+      :disabled="!canExec || isDisabled"
+      :class="combinedClasses"
+      @click="handleClick"
+    >
+      <div
+        v-if="showNavigateOnStartIcons"
+        class="navigate-on-start-container"
+      >
+        <div
+          v-if="navigateOnStart == 'pop'"
+          class="navigate-on-start"
+          title="Opens a popup dialog on start"
+        >
+          <HugeiconsIcon :icon="ComputerTerminal01Icon" />
+        </div>
+        <div
+          v-if="navigateOnStart == 'arg'"
+          class="navigate-on-start"
+          title="Opens an argument form on start"
+        >
+          <HugeiconsIcon :icon="TypeCursorIcon" />
+        </div>
+        <div
+          v-if="navigateOnStart == 'hist'"
+          class="navigate-on-start"
+          title="Opens action execution history on start"
+        >
+          <HugeiconsIcon :icon="WorkHistoryIcon" />
+        </div>
+        <div
+          v-if="navigateOnStart == ''"
+          class="navigate-on-start"
+          title="Run in the background"
+        >
+          <HugeiconsIcon :icon="WorkoutRunIcon" />
+        </div>
+      </div>
+
+      <ActionIconGlyph
+        class="icon"
+        :glyph="actionGlyph"
+      />
+      <span
+        class="title"
+        aria-live="polite"
+      >{{ displayTitle }}
+      </span>
+      <span
+        v-if="rateLimitMessage"
+        class="rate-limit-message"
+      >{{ rateLimitMessage }}</span>
+    </button>
+  </div>
 </template>
 
 <script setup>
@@ -59,18 +96,18 @@ const navigateOnStart = ref('')
 
 const props = defineProps({
   actionData: {
-	type: Object,
-	required: true
+    type: Object,
+    required: true
   },
   cssClass: {
-	type: String,
-	required: false,
-	default: ''
+    type: String,
+    required: false,
+    default: ''
   },
   prefilledArguments: {
-	type: Object,
-	required: false,
-	default: () => ({})
+    type: Object,
+    required: false,
+    default: () => ({})
   }
 })
 
@@ -84,7 +121,6 @@ const displayTitle = ref('')
 
 // State
 const isDisabled = ref(false)
-const showArgumentForm = ref(false)
 
 // Rate limiting
 const rateLimitExpires = ref(0)
@@ -99,7 +135,7 @@ const flashedTrackingIds = new Set()
 
 // Show navigate on start icons - defaults to true if not set
 const showNavigateOnStartIcons = computed(() => {
-	return window.initResponse?.showNavigateOnStartIcons ?? true
+  return window.initResponse?.showNavigateOnStartIcons ?? true
 })
 
 const actionGlyph = computed(() => props.actionData?.icon ?? '')
@@ -107,63 +143,63 @@ const glyph = ref('')
 
 // Combined classes including custom cssClass
 const combinedClasses = computed(() => {
-	const classes = [...buttonClasses.value]
-	if (props.cssClass) {
-		classes.push(props.cssClass)
-	}
-	return classes
+  const classes = [...buttonClasses.value]
+  if (props.cssClass) {
+    classes.push(props.cssClass)
+  }
+  return classes
 })
 
 const hasRunningInstance = computed(() => {
-	const id = bindingId.value
-	return !!(id && bindingExecutionState[id]?.hasRunning)
+  const id = bindingId.value
+  return !!(id && bindingExecutionState[id]?.hasRunning)
 })
 
 const hasQueuedInstance = computed(() => {
-	const id = bindingId.value
-	return !!(id && bindingExecutionState[id]?.hasQueued)
+  const id = bindingId.value
+  return !!(id && bindingExecutionState[id]?.hasQueued)
 })
 
 const showExecutionIndicator = computed(() => {
-	return hasRunningInstance.value || hasQueuedInstance.value
+  return hasRunningInstance.value || hasQueuedInstance.value
 })
 
 const executionIndicatorClass = computed(() => {
-	if (hasRunningInstance.value) {
-		return 'execution-indicator-running'
-	}
-	if (hasQueuedInstance.value) {
-		return 'execution-indicator-queued'
-	}
-	return ''
+  if (hasRunningInstance.value) {
+    return 'execution-indicator-running'
+  }
+  if (hasQueuedInstance.value) {
+    return 'execution-indicator-queued'
+  }
+  return ''
 })
 
 const executionIndicatorTitle = computed(() => {
-	if (hasRunningInstance.value) {
-		return 'Running'
-	}
-	if (hasQueuedInstance.value) {
-		return 'Queued'
-	}
-	return ''
+  if (hasRunningInstance.value) {
+    return 'Running'
+  }
+  if (hasQueuedInstance.value) {
+    return 'Queued'
+  }
+  return ''
 })
 
 function consumeAndFlashPendingResult () {
-	const id = bindingId.value
-	if (!id) {
-		return
-	}
+  const id = bindingId.value
+  if (!id) {
+    return
+  }
 
-	const pending = consumePendingBindingFlash(id)
-	if (pending) {
-		onExecutionFinished(pending)
-	}
+  const pending = consumePendingBindingFlash(id)
+  if (pending) {
+    onExecutionFinished(pending)
+  }
 }
 
 // Timestamps
 const updateIterationTimestamp = ref(0)
 
-function constructFromJson(json) {
+function constructFromJson (json) {
   updateIterationTimestamp.value = 0
 
   updateFromJson(json)
@@ -174,11 +210,11 @@ function constructFromJson(json) {
   popupOnStart.value = json.popupOnStart
 
   if (popupOnStart.value.includes('execution-dialog')) {
-	navigateOnStart.value = 'pop'
+    navigateOnStart.value = 'pop'
   } else if (popupOnStart.value === 'history') {
-	navigateOnStart.value = 'hist'
+    navigateOnStart.value = 'hist'
   } else if (needsArgumentForm(props.actionData)) {
-	navigateOnStart.value = 'arg'
+    navigateOnStart.value = 'arg'
   }
 
   isDisabled.value = !json.canExec
@@ -186,111 +222,111 @@ function constructFromJson(json) {
   glyph.value = json.icon ?? ''
   // Initialize rate limit from action data (parse datetime string)
   if (json.datetimeRateLimitExpires) {
-	const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
-	rateLimitExpires.value = date.getTime() / 1000
+    const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
+    rateLimitExpires.value = date.getTime() / 1000
   } else {
-	rateLimitExpires.value = 0
+    rateLimitExpires.value = 0
   }
   // Also initialize the store so the watch picks it up
   if (bindingId.value) {
-	rateLimits[bindingId.value] = rateLimitExpires.value
-	setBindingExecutionState(
+    rateLimits[bindingId.value] = rateLimitExpires.value
+    setBindingExecutionState(
 	  bindingId.value,
 	  !!json.hasRunningInstance,
 	  !!json.hasQueuedInstance
-	)
+    )
   }
   updateRateLimitStatus()
 }
 
-function updateFromJson(json) {
+function updateFromJson (json) {
   // Fields that should not be updated
   // title - as the callback URL relies on it
 
   // Update rate limiting if changed (parse datetime string)
   if (json.datetimeRateLimitExpires) {
-	const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
-	rateLimitExpires.value = date.getTime() / 1000
-	updateRateLimitStatus()
+    const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
+    rateLimitExpires.value = date.getTime() / 1000
+    updateRateLimitStatus()
   } else if (json.datetimeRateLimitExpires === '') {
-	// Explicitly clear if empty string
-	rateLimitExpires.value = 0
-	updateRateLimitStatus()
+    // Explicitly clear if empty string
+    rateLimitExpires.value = 0
+    updateRateLimitStatus()
   }
 }
 
-function updateRateLimitStatus() {
+function updateRateLimitStatus () {
   if (rateLimitExpires.value === 0) {
-	isRateLimited.value = false
-	rateLimitMessage.value = ''
-	if (rateLimitInterval.value) {
+    isRateLimited.value = false
+    rateLimitMessage.value = ''
+    if (rateLimitInterval.value) {
 	  clearInterval(rateLimitInterval.value)
 	  rateLimitInterval.value = null
-	}
-	return
+    }
+    return
   }
 
   const now = Math.floor(Date.now() / 1000)
   const expires = rateLimitExpires.value
 
   if (now >= expires) {
-	// Rate limit has expired
-	isRateLimited.value = false
-	rateLimitMessage.value = ''
-	rateLimitExpires.value = 0
-	if (rateLimitInterval.value) {
+    // Rate limit has expired
+    isRateLimited.value = false
+    rateLimitMessage.value = ''
+    rateLimitExpires.value = 0
+    if (rateLimitInterval.value) {
 	  clearInterval(rateLimitInterval.value)
 	  rateLimitInterval.value = null
-	}
+    }
   } else {
-	// Still rate limited
-	isRateLimited.value = true
-	const secondsRemaining = expires - now
-	rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
+    // Still rate limited
+    isRateLimited.value = true
+    const secondsRemaining = expires - now
+    rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
 
-	// Set up interval to update every second
-		if (!rateLimitInterval.value) {
+    // Set up interval to update every second
+    if (!rateLimitInterval.value) {
 	  rateLimitInterval.value = setInterval(() => {
-		updateRateLimitStatus()
+        updateRateLimitStatus()
 	  }, 1000)
-	}
+    }
   }
 }
 
-function openActionDetails() {
+function openActionDetails () {
   const id = props.actionData?.bindingId
   if (!id) {
-	return
+    return
   }
   router.push(`/action/${id}`)
 }
 
-async function handleClick() {
+async function handleClick () {
   if (popupOnStart.value === 'history') {
-	openActionDetails()
-	return
+    openActionDetails()
+    return
   }
   if (needsArgumentForm(props.actionData)) {
-	const bindingId = props.actionData.bindingId
-	const prefilled = props.prefilledArguments || {}
-	if (Object.keys(prefilled).length > 0) {
+    const bindingId = props.actionData.bindingId
+    const prefilled = props.prefilledArguments || {}
+    if (Object.keys(prefilled).length > 0) {
 	  router.push({
-		path: `/actionBinding/${bindingId}/argumentForm`,
-		state: { prefilledArguments: prefilled }
+        path: `/actionBinding/${bindingId}/argumentForm`,
+        state: { prefilledArguments: prefilled }
 	  })
-	} else {
+    } else {
 	  router.push(`/actionBinding/${bindingId}/argumentForm`)
-	}
+    }
   } else {
-	await startAction()
+    await startAction()
   }
 }
 
-function getUniqueId() {
+function getUniqueId () {
   if (window.isSecureContext) {
-	return window.crypto.randomUUID()
+    return window.crypto.randomUUID()
   } else {
-	return Date.now().toString()
+    return Date.now().toString()
   }
 }
 
@@ -323,122 +359,122 @@ async function pollExecutionUntilDone (trackingId) {
   }
 }
 
-async function startAction(actionArgs) {
+async function startAction (actionArgs) {
   buttonClasses.value = [] // Removes old animation classes
 
   if (actionArgs === undefined) {
-	actionArgs = []
+    actionArgs = []
   }
 
   // UUIDs are create client side, so that we can setup a "execution-button"
   // to track the execution before we send the request to the server.
   const startActionArgs = {
-	bindingId: props.actionData.bindingId,
-	arguments: actionArgs,
-	uniqueTrackingId: getUniqueId()
+    bindingId: props.actionData.bindingId,
+    arguments: actionArgs,
+    uniqueTrackingId: getUniqueId()
   }
 
   console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
 
   watch(
-	() => buttonResults[startActionArgs.uniqueTrackingId],
-	(newResult, oldResult) => {
+    () => buttonResults[startActionArgs.uniqueTrackingId],
+    (newResult, oldResult) => {
 	  onLogEntryChanged(newResult)
-	}
+    }
   )
 
   requestReconnectNow()
 
   try {
-	const response = await window.client.startAction(startActionArgs)
-	const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
+    const response = await window.client.startAction(startActionArgs)
+    const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
 
-	if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
+    if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
 	  router.push(`/logs/${trackingId}`)
-	}
+    }
 
-	if (!connectionState.connected) {
+    if (!connectionState.connected) {
 	  await pollExecutionUntilDone(trackingId)
-	}
+    }
   } catch (err) {
-	console.error('Failed to start action:', err)
+    console.error('Failed to start action:', err)
   }
 }
 
-function onLogEntryChanged(logEntry) {
+function onLogEntryChanged (logEntry) {
   if (logEntry.executionFinished) {
-	onExecutionFinished(logEntry)
+    onExecutionFinished(logEntry)
   } else if (logEntry.queued && !logEntry.executionStarted) {
-	onExecutionQueued(logEntry)
+    onExecutionQueued(logEntry)
   } else {
-	onExecutionStarted(logEntry)
+    onExecutionStarted(logEntry)
   }
 }
 
-function onExecutionQueued(_logEntry) {
+function onExecutionQueued (_logEntry) {
   isDisabled.value = true
   updateDom('action-queued', '[Queued]')
 }
 
-function onExecutionStarted(logEntry) {
+function onExecutionStarted (logEntry) {
   if (
-	popupOnStart.value &&
+    popupOnStart.value &&
 	popupOnStart.value.includes('execution-dialog') &&
 	!shouldSuppressPopupOnStartNavigation(router)
   ) {
-	router.push(`/logs/${logEntry.executionTrackingId}`)
+    router.push(`/logs/${logEntry.executionTrackingId}`)
   }
 
   isDisabled.value = true
   updateDom(null, title.value)
 }
 
-function onExecutionFinished(logEntry) {
+function onExecutionFinished (logEntry) {
   const trackingId = logEntry.executionTrackingId
   if (trackingId) {
-	if (flashedTrackingIds.has(trackingId)) {
-	  return
-	}
-	flashedTrackingIds.add(trackingId)
+    if (flashedTrackingIds.has(trackingId)) {
+      return
+    }
+    flashedTrackingIds.add(trackingId)
   }
 
   // Local no-arg watches and the binding-scoped pending flash can both
   // observe the same finished execution; consume so only one path flashes.
   if (bindingId.value) {
-	consumePendingBindingFlash(bindingId.value)
+    consumePendingBindingFlash(bindingId.value)
   }
 
   if (logEntry.timedOut) {
-	renderExecutionResult('action-timeout', 'Timed out')
+    renderExecutionResult('action-timeout', 'Timed out')
   } else if (logEntry.blocked) {
-	renderExecutionResult('action-blocked', 'Blocked!')
+    renderExecutionResult('action-blocked', 'Blocked!')
   } else if (logEntry.exitCode !== 0) {
-	renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
+    renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
   } else {
-	renderExecutionResult('action-success', 'Success!')
+    renderExecutionResult('action-success', 'Success!')
   }
 }
 
-function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
+function renderExecutionResult (resultCssClass, temporaryStatusMessage) {
   updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
   onExecStatusChanged()
 }
 
-function updateDom(resultCssClass, newTitle) {
+function updateDom (resultCssClass, newTitle) {
   if (resultCssClass == null) {
-	buttonClasses.value = []
+    buttonClasses.value = []
   } else {
-	buttonClasses.value = [resultCssClass]
+    buttonClasses.value = [resultCssClass]
   }
 
   displayTitle.value = newTitle
 }
 
-function onExecStatusChanged() {
+function onExecStatusChanged () {
   isDisabled.value = false
 
   setTimeout(() => {
-	updateDom(null, title.value)
+    updateDom(null, title.value)
   }, 2000)
 }
 
@@ -448,47 +484,47 @@ onMounted(() => {
   // Watch the central rate limit store for updates to this button's bindingId
   // Watch the entire rateLimits object to ensure reactivity with dynamic keys
   watch(
-	rateLimits,
-	() => {
+    rateLimits,
+    () => {
 	  const id = bindingId.value
 	  if (id && rateLimits[id] !== undefined) {
-		const newExpires = rateLimits[id]
-		if (newExpires !== rateLimitExpires.value) {
+        const newExpires = rateLimits[id]
+        if (newExpires !== rateLimitExpires.value) {
 		  rateLimitExpires.value = newExpires
 		  updateRateLimitStatus()
-		}
+        }
 	  }
-	},
-	{ deep: true }
+    },
+    { deep: true }
   )
 
   // Binding-scoped flash survives argument-form navigation/remount (#920).
   watch(
-	() => pendingBindingFlash[bindingId.value],
-	(pending) => {
+    () => pendingBindingFlash[bindingId.value],
+    (pending) => {
 	  if (pending) {
-		consumeAndFlashPendingResult()
+        consumeAndFlashPendingResult()
 	  }
-	},
-	{ immediate: true }
+    },
+    { immediate: true }
   )
 })
 
 onUnmounted(() => {
   isComponentMounted.value = false
   if (rateLimitInterval.value) {
-	clearInterval(rateLimitInterval.value)
-	rateLimitInterval.value = null
+    clearInterval(rateLimitInterval.value)
+    rateLimitInterval.value = null
   }
 })
 
 watch(
   () => props.actionData,
   (newData) => {
-	updateFromJson(newData)
-	if (newData?.icon !== undefined) {
+    updateFromJson(newData)
+    if (newData?.icon !== undefined) {
 	  glyph.value = newData.icon ?? ''
-	}
+    }
   },
   { deep: true }
 )

+ 418 - 326
frontend/resources/vue/App.vue

@@ -1,126 +1,218 @@
 <template>
-    <Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation">
-        <template #toolbar>
-            <div id="banner" v-if="bannerMessage" :style="bannerCss">
-                <p>{{ bannerMessage }}</p>
-            </div>
-        </template>
-
-        <template #user-info>
-            <ConnectionBanner />
-            <div class="flex-row user-info" style="gap: .5em;">
-                <span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
-                <router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
-                    <span id="username-text">{{ username }}</span>
-                </router-link>
-                <HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
-            </div>
-
-        </template>
-    </Header>
-
-    <div id="layout">
-        <Navigation ref="navigation">
-            <Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/>
-        </Navigation>
-
-		<div id="content" initial-martial-complete="{{ hasLoaded }}">
-            <main title="Main content">
-                <router-view :key="$route.fullPath" />
-            </main>
-
-            <footer title="footer" v-if="showFooter">
-                <p>
-                    <img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
-                    OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
-                </p>
-                <p>
-                    <span>
-                        <a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a>
-                    </span>
-
-                    <span>
-                        <a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a>
-                    </span>
-
-                    <span>
-                        <a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
-                    </span>
-
-                    <span v-if="availableThemes.length > 1">
-                        <a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
-                    </span>
-                </p>
-                <p v-if="showVersionNumber">
-                    <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
-                </p>
-            </footer>
-        </div>
+  <Header
+    :title="pageTitle"
+    :logo-url="logoUrl"
+    :sidebar-enabled="sidebarEnabled"
+    :top-bar-enabled="topbarEnabled"
+    :navigation="navigation"
+    @toggle-sidebar="toggleSidebar"
+  >
+    <template #toolbar>
+      <div
+        v-if="bannerMessage"
+        id="banner"
+        :style="bannerCss"
+      >
+        <p>{{ bannerMessage }}</p>
+      </div>
+    </template>
+
+    <template #user-info>
+      <ConnectionBanner />
+      <div
+        class="flex-row user-info"
+        style="gap: .5em;"
+      >
+        <span
+          v-if="!isLoggedIn && showLoginLink"
+          id="link-login"
+        ><router-link to="/login">{{ t('login-button') }}</router-link></span>
+        <router-link
+          v-else-if="isLoggedIn"
+          to="/user"
+          class="user-link"
+        >
+          <span id="username-text">{{ username }}</span>
+        </router-link>
+        <HugeiconsIcon
+          v-if="isLoggedIn"
+          :icon="UserCircle02Icon"
+          width="1.5em"
+          height="1.5em"
+        />
+      </div>
+    </template>
+  </Header>
+
+  <div id="layout">
+    <Navigation ref="navigation">
+      <Sidebar
+        v-if="sidebarEnabled && showNavigation"
+        id="mainnav"
+        ref="sidebar"
+      />
+    </Navigation>
+
+    <div
+      id="content"
+      :initial-martial-complete="hasLoaded"
+    >
+      <main title="Main content">
+        <router-view :key="$route.fullPath" />
+      </main>
+
+      <footer
+        v-if="showFooter"
+        title="footer"
+      >
+        <p>
+          <img
+            title="application icon"
+            :src="logoUrl"
+            alt="OliveTin logo"
+            style="height: 1em;"
+            class="logo"
+          >
+          OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
+        </p>
+        <p>
+          <span>
+            <a
+              href="https://docs.olivetin.app"
+              target="_new"
+            >{{ t('docs') }}</a>
+          </span>
+
+          <span>
+            <a
+              href="https://github.com/OliveTin/OliveTin/issues/new/choose"
+              target="_new"
+            >{{ t('raise-issue') }}</a>
+          </span>
+
+          <span>
+            <a
+              href="#"
+              @click.prevent="openLanguageDialog"
+            >{{ currentLanguageName }}</a>
+          </span>
+
+          <span v-if="availableThemes.length > 1">
+            <a
+              href="#"
+              @click.prevent="openThemeDialog"
+            >{{ currentThemeName }}</a>
+          </span>
+        </p>
+        <p v-if="showVersionNumber">
+          <a
+            id="available-version"
+            href="http://olivetin.app"
+            target="_blank"
+            hidden
+          >?</a>
+        </p>
+      </footer>
     </div>
-
-    <dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick">
-        <div class="dialog-content" @click.stop>
-            <h2>{{ t('language-dialog.title') }}</h2>
-            <select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
-                <option v-for="(name, code) in availableLanguages" :key="code" :value="code">
-                    {{ code === 'auto' ? name : `${name} (${code})` }}
-                </option>
-            </select>
-            <p class="browser-languages">
-                {{ t('language-dialog.browser-languages') }}:
-                <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
-                <span v-else>{{ t('language-dialog.not-available') }}</span>
-            </p>
-            <div class="dialog-buttons">
-                <button @click="closeLanguageDialog">{{ t('language-dialog.close') }}</button>
-            </div>
-        </div>
-    </dialog>
-
-    <dialog ref="themeDialog" class="theme-dialog" @click="handleThemeDialogClick">
-        <div class="dialog-content" @click.stop>
-            <h2>{{ t('theme-dialog.title') }}</h2>
-            <select v-model="selectedTheme" @change="changeTheme" class="language-select">
-                <option value="">{{ t('theme-dialog.default') }}</option>
-                <option v-for="theme in availableThemes" :key="theme" :value="theme">
-                    {{ theme }}
-                </option>
-            </select>
-            <div class="dialog-buttons">
-                <button @click="closeThemeDialog">{{ t('theme-dialog.close') }}</button>
-            </div>
-        </div>
-    </dialog>
+  </div>
+
+  <dialog
+    ref="languageDialog"
+    class="language-dialog"
+    @click="handleLanguageDialogClick"
+  >
+    <div
+      class="dialog-content"
+      @click.stop
+    >
+      <h2>{{ t('language-dialog.title') }}</h2>
+      <select
+        v-model="selectedLanguage"
+        class="language-select"
+        @change="changeLanguage"
+      >
+        <option
+          v-for="(name, code) in availableLanguages"
+          :key="code"
+          :value="code"
+        >
+          {{ code === 'auto' ? name : `${name} (${code})` }}
+        </option>
+      </select>
+      <p class="browser-languages">
+        {{ t('language-dialog.browser-languages') }}:
+        <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
+        <span v-else>{{ t('language-dialog.not-available') }}</span>
+      </p>
+      <div class="dialog-buttons">
+        <button @click="closeLanguageDialog">
+          {{ t('language-dialog.close') }}
+        </button>
+      </div>
+    </div>
+  </dialog>
+
+  <dialog
+    ref="themeDialog"
+    class="theme-dialog"
+    @click="handleThemeDialogClick"
+  >
+    <div
+      class="dialog-content"
+      @click.stop
+    >
+      <h2>{{ t('theme-dialog.title') }}</h2>
+      <select
+        v-model="selectedTheme"
+        class="language-select"
+        @change="changeTheme"
+      >
+        <option value="">
+          {{ t('theme-dialog.default') }}
+        </option>
+        <option
+          v-for="theme in availableThemes"
+          :key="theme"
+          :value="theme"
+        >
+          {{ theme }}
+        </option>
+      </select>
+      <div class="dialog-buttons">
+        <button @click="closeThemeDialog">
+          {{ t('theme-dialog.close') }}
+        </button>
+      </div>
+    </div>
+  </dialog>
 </template>
 
 <script setup>
-import { ref, onMounted, computed } from 'vue';
-import { useRouter } from 'vue-router';
-import Sidebar from 'picocrank/vue/components/Sidebar.vue';
-import Navigation from 'picocrank/vue/components/Navigation.vue';
-import Header from 'picocrank/vue/components/Header.vue';
-import ConnectionBanner from './components/ConnectionBanner.vue';
-import { connectEventStreamIfNeeded } from '../../js/websocket.js';
+import { ref, onMounted, computed } from 'vue'
+import { useRouter } from 'vue-router'
+import Sidebar from 'picocrank/vue/components/Sidebar.vue'
+import Navigation from 'picocrank/vue/components/Navigation.vue'
+import Header from 'picocrank/vue/components/Header.vue'
+import ConnectionBanner from './components/ConnectionBanner.vue'
+import { connectEventStreamIfNeeded } from '../../js/websocket.js'
 import { HugeiconsIcon } from '@hugeicons/vue'
-import { Menu01Icon } from '@hugeicons/core-free-icons'
-import { UserCircle02Icon } from '@hugeicons/core-free-icons'
-import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
-import logoUrl from '../../OliveTinLogo.png';
-import { useI18n } from 'vue-i18n';
-import combinedTranslations from '../../../lang/combined_output.json';
-const { t, locale } = useI18n();
-
-const router = useRouter();
-
-const sidebar = ref(null);
-const navigation = ref(null);
-const username = ref('notset');
-const isLoggedIn = ref(false);
-const currentVersion = ref('?');
-const pageTitle = ref('OliveTin');
-const bannerMessage = ref('');
-const bannerCss = ref('');
-const hasLoaded = ref(false);
+import { UserCircle02Icon, DashboardSquare01Icon } from '@hugeicons/core-free-icons'
+import logoUrl from '../../OliveTinLogo.png'
+import { useI18n } from 'vue-i18n'
+import combinedTranslations from '../../../lang/combined_output.json'
+const { t } = useI18n()
+
+const router = useRouter()
+
+const sidebar = ref(null)
+const navigation = ref(null)
+const username = ref('notset')
+const isLoggedIn = ref(false)
+const currentVersion = ref('?')
+const pageTitle = ref('OliveTin')
+const bannerMessage = ref('')
+const bannerCss = ref('')
+const hasLoaded = ref(false)
 const showFooter = ref(true)
 const showNavigation = ref(true)
 const showLogs = ref(true)
@@ -144,279 +236,279 @@ const selectedTheme = ref(themePreference.value)
 
 // Available languages with display names
 const availableLanguages = {
-    'auto': 'Browser Language',
-    'en': 'English',
-    'de-DE': 'Deutsch',
-    'es-ES': 'Español',
-    'it-IT': 'Italiano',
-    'zh-Hans-CN': '简体中文'
+  auto: 'Browser Language',
+  en: 'English',
+  'de-DE': 'Deutsch',
+  'es-ES': 'Español',
+  'it-IT': 'Italiano',
+  'zh-Hans-CN': '简体中文'
 }
 
 // Computed property to get current language display name
 const currentLanguageName = computed(() => {
-    if (languagePreference.value === 'auto') {
-        return availableLanguages['auto']
-    }
+  if (languagePreference.value === 'auto') {
+    return availableLanguages.auto
+  }
 
-    return availableLanguages[languagePreference.value] || languagePreference.value
+  return availableLanguages[languagePreference.value] || languagePreference.value
 })
 
 // Computed property to get current theme display name
 const currentThemeName = computed(() => {
-    if (!themePreference.value || themePreference.value === '') {
-        return t('theme-dialog.default')
-    }
-    return themePreference.value
+  if (!themePreference.value || themePreference.value === '') {
+    return t('theme-dialog.default')
+  }
+  return themePreference.value
 })
 
 // Computed properties for navigation style
 const topbarEnabled = computed(() => {
-    return sectionNavigationStyle.value === 'topbar'
+  return sectionNavigationStyle.value === 'topbar'
 })
 
 const sidebarEnabled = computed(() => {
-    return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
+  return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
 })
 
-function normalizeBrowserLanguage() {
-    const available = Object.keys(combinedTranslations.messages || {})
-
-    if (navigator.languages && navigator.languages.length > 0) {
-        for (const candidate of navigator.languages) {
-            const lowerCandidate = candidate.toLowerCase()
-
-            // Try exact match (case-insensitive)
-            const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
-            if (exact) {
-                return exact
-            }
-
-            // Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
-            const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
-            if (prefix) {
-                return prefix
-            }
-        }
-    }
-
-    return 'en'
-}
+function normalizeBrowserLanguage () {
+  const available = Object.keys(combinedTranslations.messages || {})
 
-function toggleSidebar() {
-    if (sidebar.value && showNavigation.value) {
-        sidebar.value.toggle()
-    }
-}
+  if (navigator.languages && navigator.languages.length > 0) {
+    for (const candidate of navigator.languages) {
+      const lowerCandidate = candidate.toLowerCase()
 
-function updateHeaderFromInit() {
-    if (!window.initResponse) {
-        return
-    }
+      // Try exact match (case-insensitive)
+      const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
+      if (exact) {
+        return exact
+      }
 
-    username.value = window.initResponse.authenticatedUser
-    isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
-    currentVersion.value = window.initResponse.currentVersion
-    pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
-    bannerMessage.value = window.initResponse.bannerMessage || ''
-    bannerCss.value = window.initResponse.bannerCss || ''
-    showFooter.value = window.initResponse.showFooter
-    showNavigation.value = window.initResponse.showNavigation
-    showLogs.value = window.initResponse.showLogList
-    showDiagnostics.value = window.initResponse.showDiagnostics
-    showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
-    sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
-    availableThemes.value = window.initResponse.availableThemes || []
-
-    if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
-        showLoginLink.value = false
+      // Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
+      const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
+      if (prefix) {
+        return prefix
+      }
     }
+  }
 
-    applyStyleMods()
-    loadCustomJsIfEnabled()
-
-    renderNavigation()
-    applyTheme()
-
-    if (window.initResponse.loginRequired) {
-        connectEventStreamIfNeeded()
-        router.push('/login')
-        return
-    }
-
-    connectEventStreamIfNeeded()
+  return 'en'
 }
 
-function renderNavigation() {
-    if (!navigation.value) {
-        return
-    }
-
-    const rootDashboards = window.initResponse?.rootDashboards || []
-
-    if (typeof navigation.value.clear === 'function') {
-        navigation.value.clear()
-    }
-
-    for (const rootDashboard of rootDashboards) {
-        navigation.value.addNavigationLink({
-            id: rootDashboard,
-            name: rootDashboard,
-            title: rootDashboard,
-            path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
-            icon: DashboardSquare01Icon,
-        })
-    }
-
-    navigation.value.addSeparator()
-    navigation.value.addRouterLink('Entities', t('nav.entities'))
-
-    if (showLogs.value) {
-        navigation.value.addRouterLink('Logs', t('nav.logs'))
-    }
-
-    if (showDiagnostics.value) {
-        navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
-    }
+function toggleSidebar () {
+  if (sidebar.value && showNavigation.value) {
+    sidebar.value.toggle()
+  }
 }
 
-function openLanguageDialog() {
-    selectedLanguage.value = languagePreference.value
-
-    if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
-        browserLanguages.value = navigator.languages
-    } else {
-        browserLanguages.value = []
-    }
+function updateHeaderFromInit () {
+  if (!window.initResponse) {
+    return
+  }
+
+  username.value = window.initResponse.authenticatedUser
+  isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
+  currentVersion.value = window.initResponse.currentVersion
+  pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
+  bannerMessage.value = window.initResponse.bannerMessage || ''
+  bannerCss.value = window.initResponse.bannerCss || ''
+  showFooter.value = window.initResponse.showFooter
+  showNavigation.value = window.initResponse.showNavigation
+  showLogs.value = window.initResponse.showLogList
+  showDiagnostics.value = window.initResponse.showDiagnostics
+  showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
+  sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
+  availableThemes.value = window.initResponse.availableThemes || []
+
+  if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
+    showLoginLink.value = false
+  }
+
+  applyStyleMods()
+  loadCustomJsIfEnabled()
+
+  renderNavigation()
+  applyTheme()
+
+  if (window.initResponse.loginRequired) {
+    connectEventStreamIfNeeded()
+    router.push('/login')
+    return
+  }
 
-    if (languageDialog.value) {
-        languageDialog.value.showModal()
-    }
+  connectEventStreamIfNeeded()
 }
 
-function closeLanguageDialog() {
-    if (languageDialog.value) {
-        languageDialog.value.close()
-    }
+function renderNavigation () {
+  if (!navigation.value) {
+    return
+  }
+
+  const rootDashboards = window.initResponse?.rootDashboards || []
+
+  if (typeof navigation.value.clear === 'function') {
+    navigation.value.clear()
+  }
+
+  for (const rootDashboard of rootDashboards) {
+    navigation.value.addNavigationLink({
+      id: rootDashboard,
+      name: rootDashboard,
+      title: rootDashboard,
+      path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
+      icon: DashboardSquare01Icon
+    })
+  }
+
+  navigation.value.addSeparator()
+  navigation.value.addRouterLink('Entities', t('nav.entities'))
+
+  if (showLogs.value) {
+    navigation.value.addRouterLink('Logs', t('nav.logs'))
+  }
+
+  if (showDiagnostics.value) {
+    navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
+  }
 }
 
-function changeLanguage() {
-    if (!window.i18n || !selectedLanguage.value) {
-        return
-    }
-
-    if (selectedLanguage.value === 'auto') {
-        localStorage.removeItem('olivetin-language')
-        languagePreference.value = 'auto'
-        window.i18n.locale.value = normalizeBrowserLanguage()
-    } else {
-        window.i18n.locale.value = selectedLanguage.value
-        localStorage.setItem('olivetin-language', selectedLanguage.value)
-        languagePreference.value = selectedLanguage.value
-    }
+function openLanguageDialog () {
+  selectedLanguage.value = languagePreference.value
 
-    // Update navigation with new translations
-    if (navigation.value) {
-        renderNavigation()
-    }
+  if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
+    browserLanguages.value = navigator.languages
+  } else {
+    browserLanguages.value = []
+  }
 
-    closeLanguageDialog()
+  if (languageDialog.value) {
+    languageDialog.value.showModal()
+  }
 }
 
-function handleLanguageDialogClick(event) {
-    // Close dialog when clicking on the backdrop
-    if (event.target === languageDialog.value) {
-        closeLanguageDialog()
-    }
+function closeLanguageDialog () {
+  if (languageDialog.value) {
+    languageDialog.value.close()
+  }
 }
 
-function openThemeDialog() {
-    selectedTheme.value = themePreference.value || ''
+function changeLanguage () {
+  if (!window.i18n || !selectedLanguage.value) {
+    return
+  }
+
+  if (selectedLanguage.value === 'auto') {
+    localStorage.removeItem('olivetin-language')
+    languagePreference.value = 'auto'
+    window.i18n.locale.value = normalizeBrowserLanguage()
+  } else {
+    window.i18n.locale.value = selectedLanguage.value
+    localStorage.setItem('olivetin-language', selectedLanguage.value)
+    languagePreference.value = selectedLanguage.value
+  }
+
+  // Update navigation with new translations
+  if (navigation.value) {
+    renderNavigation()
+  }
 
-    if (themeDialog.value) {
-        themeDialog.value.showModal()
-    }
+  closeLanguageDialog()
 }
 
-function closeThemeDialog() {
-    if (themeDialog.value) {
-        themeDialog.value.close()
-    }
+function handleLanguageDialogClick (event) {
+  // Close dialog when clicking on the backdrop
+  if (event.target === languageDialog.value) {
+    closeLanguageDialog()
+  }
 }
 
-function changeTheme() {
-    if (!selectedTheme.value || selectedTheme.value === '') {
-        localStorage.removeItem('olivetin-theme')
-        themePreference.value = ''
-    } else {
-        localStorage.setItem('olivetin-theme', selectedTheme.value)
-        themePreference.value = selectedTheme.value
-    }
+function openThemeDialog () {
+  selectedTheme.value = themePreference.value || ''
 
-    applyTheme()
-    closeThemeDialog()
+  if (themeDialog.value) {
+    themeDialog.value.showModal()
+  }
 }
 
-function applyTheme() {
-    let themeStyle = document.getElementById('theme-style')
+function closeThemeDialog () {
+  if (themeDialog.value) {
+    themeDialog.value.close()
+  }
+}
 
-    if (!themeStyle) {
-        themeStyle = document.createElement('style')
-        themeStyle.id = 'theme-style'
-        themeStyle.type = 'text/css'
-        document.head.appendChild(themeStyle)
-    }
+function changeTheme () {
+  if (!selectedTheme.value || selectedTheme.value === '') {
+    localStorage.removeItem('olivetin-theme')
+    themePreference.value = ''
+  } else {
+    localStorage.setItem('olivetin-theme', selectedTheme.value)
+    themePreference.value = selectedTheme.value
+  }
+
+  applyTheme()
+  closeThemeDialog()
+}
 
-    // Load theme into @layer theme so it takes precedence over @layer components
-    if (themePreference.value && themePreference.value !== '') {
-        themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
-    } else {
-        themeStyle.textContent = `@import url('/theme.css') layer(theme);`
-    }
+function applyTheme () {
+  let themeStyle = document.getElementById('theme-style')
+
+  if (!themeStyle) {
+    themeStyle = document.createElement('style')
+    themeStyle.id = 'theme-style'
+    themeStyle.type = 'text/css'
+    document.head.appendChild(themeStyle)
+  }
+
+  // Load theme into @layer theme so it takes precedence over @layer components
+  if (themePreference.value && themePreference.value !== '') {
+    themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
+  } else {
+    themeStyle.textContent = '@import url(\'/theme.css\') layer(theme);'
+  }
 }
 
-function loadCustomJsIfEnabled() {
-    if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) {
-        return
-    }
-    const script = document.createElement('script')
-    script.src = '/custom-webui/custom.js'
-    script.async = true
-    script.id = 'olivetin-custom-js'
-    document.head.appendChild(script)
+function loadCustomJsIfEnabled () {
+  if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) {
+    return
+  }
+  const script = document.createElement('script')
+  script.src = '/custom-webui/custom.js'
+  script.async = true
+  script.id = 'olivetin-custom-js'
+  document.head.appendChild(script)
 }
 
-function applyStyleMods() {
-    if (!window.initResponse || !window.initResponse.styleMods) {
-        return
-    }
+function applyStyleMods () {
+  if (!window.initResponse || !window.initResponse.styleMods) {
+    return
+  }
 
-    for (const styleMod of window.initResponse.styleMods) {
-        if (styleMod) {
-            document.body.classList.add(styleMod)
-        }
+  for (const styleMod of window.initResponse.styleMods) {
+    if (styleMod) {
+      document.body.classList.add(styleMod)
     }
+  }
 }
 
-function handleThemeDialogClick(event) {
-    if (event.target === themeDialog.value) {
-        closeThemeDialog()
-    }
+function handleThemeDialogClick (event) {
+  if (event.target === themeDialog.value) {
+    closeThemeDialog()
+  }
 }
 
 window.updateHeaderFromInit = updateHeaderFromInit
 
 onMounted(() => {
-    updateHeaderFromInit()
+  updateHeaderFromInit()
 
-    // Initialize selected language from stored preference
-    selectedLanguage.value = languagePreference.value
+  // Initialize selected language from stored preference
+  selectedLanguage.value = languagePreference.value
 
-    // Initialize selected theme from stored preference
-    selectedTheme.value = themePreference.value || ''
+  // Initialize selected theme from stored preference
+  selectedTheme.value = themePreference.value || ''
 
-    if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
-        browserLanguages.value = navigator.languages
-    }
+  if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
+    browserLanguages.value = navigator.languages
+  }
 })
 </script>
 

+ 239 - 186
frontend/resources/vue/Dashboard.vue

@@ -1,58 +1,112 @@
 <template>
-    <section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;">
-        <HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" />
-        <p>Loading dashboard...</p>
-        <p style="color: var(--fg2);">{{ loadingTime }}s</p>
+  <section
+    v-if="!dashboard && !initError"
+    style="text-align: center; padding: 2em;"
+  >
+    <HugeiconsIcon
+      :icon="Loading03Icon"
+      width="3em"
+      height="3em"
+      style="animation: spin 1s linear infinite;"
+    />
+    <p>Loading dashboard...</p>
+    <p style="color: var(--fg2);">
+      {{ loadingTime }}s
+    </p>
+  </section>
+  <section
+    v-if="initError"
+    style="text-align: center; padding: 2em;"
+    class="bad"
+  >
+    <h2 style="color: var(--error);">
+      Initialization Failed
+    </h2>
+    <p>{{ initError }}</p>
+    <p style="color: var(--fg2);">
+      Please check your configuration and try again.
+    </p>
+  </section>
+  <template v-else-if="dashboard">
+    <section v-if="dashboard.contents.length == 0">
+      <div
+        v-if="isDirectory"
+        class="back-button-container"
+      >
+        <button
+          class="back-button"
+          @click="goBack"
+        >
+          <HugeiconsIcon
+            :icon="ArrowLeftIcon"
+            width="1.2em"
+            height="1.2em"
+          />
+          <span>Back</span>
+        </button>
+      </div>
+      <h2>{{ dashboard.title }}</h2>
+      <p
+        style="text-align: center"
+        class="padding"
+      >
+        This dashboard is empty.
+      </p>
     </section>
-    <section v-if="initError" style="text-align: center; padding: 2em;" class = "bad">
-        <h2 style="color: var(--error);">Initialization Failed</h2>
-        <p>{{ initError }}</p>
-        <p style="color: var(--fg2);">Please check your configuration and try again.</p>
+
+    <section
+      v-else
+      class="transparent"
+    >
+      <div
+        v-if="isDirectory"
+        class="back-button-container"
+      >
+        <button
+          class="back-button"
+          @click="goBack"
+        >
+          <HugeiconsIcon
+            :icon="ArrowLeftIcon"
+            width="1.2em"
+            height="1.2em"
+          />
+          <span>Back</span>
+        </button>
+      </div>
+      <div
+        v-for="component in dashboard.contents"
+        :key="component.title"
+        class="dashboard-row"
+      >
+        <h2 v-if="dashboard.title != 'Default'">
+          <router-link
+            v-if="component.entityType && component.entityKey"
+            :to="{
+              name: 'EntityDetails',
+              params: {
+                entityType: component.entityType,
+                entityKey: component.entityKey
+              }
+            }"
+            class="entity-link"
+          >
+            {{ component.title }}
+          </router-link>
+          <span v-else>{{ component.title }}</span>
+        </h2>
+
+        <fieldset :class="component.cssClass">
+          <template
+            v-for="subcomponent in component.contents"
+            :key="subcomponent.title || subcomponent.type"
+          >
+            <DashboardComponent :component="subcomponent" />
+          </template>
+        </fieldset>
+      </div>
     </section>
-    <template v-else-if="dashboard">
-        <section v-if="dashboard.contents.length == 0">
-            <div class="back-button-container" v-if="isDirectory">
-                <button @click="goBack" class="back-button">
-                    <HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
-                    <span>Back</span>
-                </button>
-            </div>
-            <h2>{{ dashboard.title }}</h2>
-            <p style = "text-align: center" class = "padding">This dashboard is empty.</p>
-        </section>
-
-        <section class="transparent" v-else>
-            <div class="back-button-container" v-if="isDirectory">
-                <button @click="goBack" class="back-button">
-                    <HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
-                    <span>Back</span>
-                </button>
-            </div>
-            <div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title">
-                <h2 v-if = "dashboard.title != 'Default'">
-                    <router-link
-                        v-if="component.entityType && component.entityKey"
-                        :to="{
-                            name: 'EntityDetails',
-                            params: {
-                                entityType: component.entityType,
-                                entityKey: component.entityKey
-                            }
-                        }"
-                        class="entity-link">
-                        {{ component.title }}
-                    </router-link>
-                    <span v-else>{{ component.title }}</span>
-                </h2>
-
-                <fieldset :class="component.cssClass">
-                    <template v-for="subcomponent in component.contents">
-                        <DashboardComponent :component="subcomponent" />
-                    </template>
-                </fieldset>
-            </div>
-        </section>
-    </template>
+  </template>
 </template>
 
 <script setup>
@@ -63,18 +117,18 @@ import { HugeiconsIcon } from '@hugeicons/vue'
 import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
 
 const props = defineProps({
-    title: {
-        type: String,
-        required: false
-    },
-    entityType: {
-        type: String,
-        required: false
-    },
-    entityKey: {
-        type: String,
-        required: false
-    }
+  title: {
+    type: String,
+    required: false
+  },
+  entityType: {
+    type: String,
+    required: false
+  },
+  entityKey: {
+    type: String,
+    required: false
+  }
 })
 
 const router = useRouter()
@@ -86,166 +140,166 @@ let checkInitInterval = null
 let dashboardRequestId = 0
 
 const isDirectory = computed(() => {
-    if (!dashboard.value || !window.initResponse) {
-        return false
-    }
-    const rootDashboards = window.initResponse.rootDashboards || []
-    return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
+  if (!dashboard.value || !window.initResponse) {
+    return false
+  }
+  const rootDashboards = window.initResponse.rootDashboards || []
+  return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
 })
 
-function goBack() {
-    if (window.history.length > 1) {
-        router.back()
+function goBack () {
+  if (window.history.length > 1) {
+    router.back()
+  } else {
+    const rootDashboards = window.initResponse?.rootDashboards || []
+    if (rootDashboards.length > 0) {
+      router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
     } else {
-        const rootDashboards = window.initResponse?.rootDashboards || []
-        if (rootDashboards.length > 0) {
-            router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
-        } else {
-            router.push({ name: 'Actions' })
-        }
+      router.push({ name: 'Actions' })
     }
+  }
 }
 
-async function getDashboard() {
-    const requestId = ++dashboardRequestId
-    let title = props.title
+async function getDashboard () {
+  const requestId = ++dashboardRequestId
+  let title = props.title
 
-    // If no specific title was provided or it's the placeholder 'default',
-    // prefer the first configured root dashboard (e.g., "Test").
-    if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
-        title = window.initResponse.rootDashboards[0]
-    }
+  // If no specific title was provided or it's the placeholder 'default',
+  // prefer the first configured root dashboard (e.g., "Test").
+  if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
+    title = window.initResponse.rootDashboards[0]
+  }
 
-    try {
-        const request = {
-            title: title,
-        }
-
-        if (props.entityType && props.entityKey) {
-            request.entityType = props.entityType
-            request.entityKey = props.entityKey
-        }
+  try {
+    const request = {
+      title
+    }
 
-        const ret = await window.client.getDashboard(request)
+    if (props.entityType && props.entityKey) {
+      request.entityType = props.entityType
+      request.entityKey = props.entityKey
+    }
 
-        if (requestId !== dashboardRequestId) {
-            return
-        }
+    const ret = await window.client.getDashboard(request)
 
-        if (!ret || !ret.dashboard) {
-            throw new Error('No dashboard found')
-        }
+    if (requestId !== dashboardRequestId) {
+      return
+    }
 
-        dashboard.value = ret.dashboard
-        const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
-        document.title = ret.dashboard.title + ' - ' + pageTitle
+    if (!ret || !ret.dashboard) {
+      throw new Error('No dashboard found')
+    }
 
-        // Clear any previous init error since we successfully loaded
-        initError.value = null
+    dashboard.value = ret.dashboard
+    const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
+    document.title = ret.dashboard.title + ' - ' + pageTitle
 
-        // Stop the loading timer once dashboard is loaded
-        if (loadingTimer) {
-            clearInterval(loadingTimer)
-            loadingTimer = null
-        }
+    // Clear any previous init error since we successfully loaded
+    initError.value = null
 
-        // Set attribute to indicate dashboard is loaded successfully
-        document.body.setAttribute('loaded-dashboard', title || 'default')
-    } catch (e) {
-        if (requestId !== dashboardRequestId) {
-            return
-        }
+    // Stop the loading timer once dashboard is loaded
+    if (loadingTimer) {
+      clearInterval(loadingTimer)
+      loadingTimer = null
+    }
 
-        // On error, provide a safe fallback state
-        console.error('Failed to load dashboard', e)
-        dashboard.value = { title: title || 'Default', contents: [] }
-        const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
-        document.title = 'Error - ' + pageTitle
+    // Set attribute to indicate dashboard is loaded successfully
+    document.body.setAttribute('loaded-dashboard', title || 'default')
+  } catch (e) {
+    if (requestId !== dashboardRequestId) {
+      return
+    }
 
-        // Stop the loading timer on error
-        if (loadingTimer) {
-            clearInterval(loadingTimer)
-            loadingTimer = null
-        }
+    // On error, provide a safe fallback state
+    console.error('Failed to load dashboard', e)
+    dashboard.value = { title: title || 'Default', contents: [] }
+    const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
+    document.title = 'Error - ' + pageTitle
 
-        // Set attribute even on error so tests can proceed
-        document.body.setAttribute('loaded-dashboard', title || 'error')
+    // Stop the loading timer on error
+    if (loadingTimer) {
+      clearInterval(loadingTimer)
+      loadingTimer = null
     }
-}
 
-function waitForInitAndLoadDashboard() {
-    document.body.removeAttribute('loaded-dashboard')
+    // Set attribute even on error so tests can proceed
+    document.body.setAttribute('loaded-dashboard', title || 'error')
+  }
+}
 
+function waitForInitAndLoadDashboard () {
+  document.body.removeAttribute('loaded-dashboard')
+
+  if (loadingTimer) {
+    clearInterval(loadingTimer)
+    loadingTimer = null
+  }
+  if (checkInitInterval) {
+    clearInterval(checkInitInterval)
+    checkInitInterval = null
+  }
+
+  loadingTime.value = 0
+  loadingTimer = setInterval(() => {
+    loadingTime.value++
+  }, 1000)
+
+  // Check if init has completed successfully
+  if (window.initResponse) {
+    getDashboard()
+  } else if (window.initError) {
+    // Init failed, show error immediately
+    initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
+    // Stop the loading timer since we're showing an error
     if (loadingTimer) {
-        clearInterval(loadingTimer)
-        loadingTimer = null
+      clearInterval(loadingTimer)
+      loadingTimer = null
     }
-    if (checkInitInterval) {
+  } else {
+    // Init hasn't completed yet, poll for completion
+    checkInitInterval = setInterval(() => {
+      if (window.initResponse) {
         clearInterval(checkInitInterval)
         checkInitInterval = null
-    }
-
-    loadingTime.value = 0
-    loadingTimer = setInterval(() => {
-        loadingTime.value++
-    }, 1000)
-
-    // Check if init has completed successfully
-    if (window.initResponse) {
         getDashboard()
-    } else if (window.initError) {
-        // Init failed, show error immediately
+      } else if (window.initError) {
+        clearInterval(checkInitInterval)
+        checkInitInterval = null
         initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
         // Stop the loading timer since we're showing an error
         if (loadingTimer) {
-            clearInterval(loadingTimer)
-            loadingTimer = null
+          clearInterval(loadingTimer)
+          loadingTimer = null
         }
-    } else {
-        // Init hasn't completed yet, poll for completion
-        checkInitInterval = setInterval(() => {
-            if (window.initResponse) {
-                clearInterval(checkInitInterval)
-                checkInitInterval = null
-                getDashboard()
-            } else if (window.initError) {
-                clearInterval(checkInitInterval)
-                checkInitInterval = null
-                initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
-                // Stop the loading timer since we're showing an error
-                if (loadingTimer) {
-                    clearInterval(loadingTimer)
-                    loadingTimer = null
-                }
-            }
-        }, 100) // Check every 100ms
-    }
+      }
+    }, 100) // Check every 100ms
+  }
 }
 
 onMounted(() => {
-    waitForInitAndLoadDashboard()
+  waitForInitAndLoadDashboard()
 })
 
 watch(
-    () => [props.title, props.entityType, props.entityKey],
-    () => {
-        dashboard.value = null
-        waitForInitAndLoadDashboard()
-    }
+  () => [props.title, props.entityType, props.entityKey],
+  () => {
+    dashboard.value = null
+    waitForInitAndLoadDashboard()
+  }
 )
 
 onUnmounted(() => {
-    document.body.removeAttribute('loaded-dashboard')
-
-    // Clean up the timers when component is unmounted
-    if (loadingTimer) {
-        clearInterval(loadingTimer)
-        loadingTimer = null
-    }
-    if (checkInitInterval) {
-        clearInterval(checkInitInterval)
-        checkInitInterval = null
-    }
+  document.body.removeAttribute('loaded-dashboard')
+
+  // Clean up the timers when component is unmounted
+  if (loadingTimer) {
+    clearInterval(loadingTimer)
+    loadingTimer = null
+  }
+  if (checkInitInterval) {
+    clearInterval(checkInitInterval)
+    checkInitInterval = null
+  }
 })
 
 </script>
@@ -279,7 +333,6 @@ fieldset {
 	place-items: stretch;
 }
 
-
 @keyframes spin {
     from {
         transform: rotate(0deg);

+ 24 - 22
frontend/resources/vue/ExecutionButton.vue

@@ -1,6 +1,6 @@
 <template>
-  <div 
-    :id="`execution-${executionTrackingId}`"
+  <div
+    :id="`execution-${trackingId}`"
     class="execution-button"
   >
     <button
@@ -13,25 +13,26 @@
 </template>
 
 <script>
-//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
+// import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
 
 export default {
   name: 'ExecutionButton',
-//  mixins: [ExecutionFeedbackButton],
+  //  mixins: [ExecutionFeedbackButton],
   props: {
     executionTrackingId: {
       type: String,
       required: true
     }
   },
-  data() {
+  data () {
     return {
+      trackingId: '',
       ellapsed: 0,
       isWaiting: true
     }
   },
   computed: {
-    buttonText() {
+    buttonText () {
       if (this.isWaiting) {
         return 'Executing...'
       } else {
@@ -39,33 +40,34 @@ export default {
       }
     }
   },
-  mounted() {
+  mounted () {
+    this.trackingId = this.executionTrackingId
     this.constructFromJson(this.executionTrackingId)
   },
   methods: {
-    constructFromJson(json) {
-      this.executionTrackingId = json
+    constructFromJson (json) {
+      this.trackingId = json
       this.ellapsed = 0
       this.isWaiting = true
     },
-    
-    show() {
+
+    show () {
       this.$emit('show')
-      
+
       if (window.executionDialog) {
         window.executionDialog.reset()
         window.executionDialog.show()
-        window.executionDialog.fetchExecutionResult(this.executionTrackingId)
+        window.executionDialog.fetchExecutionResult(this.trackingId)
       }
     },
-    
-    onExecStatusChanged() {
+
+    onExecStatusChanged () {
       this.isWaiting = false
       this.domTitle = this.ellapsed + 's'
     },
-    
+
     // Override from ExecutionFeedbackButton
-    onExecutionFinished(logEntry) {
+    onExecutionFinished (logEntry) {
       if (logEntry.timedOut) {
         this.renderExecutionResult('action-timeout', 'Timed out')
       } else if (logEntry.blocked) {
@@ -77,13 +79,13 @@ export default {
         this.renderExecutionResult('action-success', 'Success!')
       }
     },
-    
-    renderExecutionResult(resultCssClass, temporaryStatusMessage) {
+
+    renderExecutionResult (resultCssClass, temporaryStatusMessage) {
       this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
       this.onExecStatusChanged()
     },
-    
-    updateDom(resultCssClass, title) {
+
+    updateDom (resultCssClass, title) {
       // For execution button, we don't need to update classes as much
       // since it's a simpler component
       if (resultCssClass) {
@@ -138,4 +140,4 @@ export default {
   border-color: #c3e6cb;
   color: #155724;
 }
-</style> 
+</style>

+ 4 - 1
frontend/resources/vue/components/ActionGroupLimitsLabel.vue

@@ -1,5 +1,8 @@
 <template>
-  <span v-if="showLimits" class="action-group-limit">
+  <span
+    v-if="showLimits"
+    class="action-group-limit"
+  >
     {{ t('logs.action-group-limits', { concurrent: maxConcurrent, queueSize }) }}
   </span>
 </template>

+ 36 - 30
frontend/resources/vue/components/ActionIconGlyph.vue

@@ -1,15 +1,21 @@
 <template>
-	<span class="action-icon-glyph">
-		<HugeiconsIcon
-			v-if="hugeiconsModel"
-			:icon="hugeiconsModel"
-			width="1em"
-			height="1em"
-			class="action-icon-glyph-svg"
-		/>
-		<span v-else-if="decodedTextGlyphIsHtml" v-html="decodedTextGlyph"></span>
-		<span v-else v-text="decodedTextGlyph"></span>
-	</span>
+  <span class="action-icon-glyph">
+    <HugeiconsIcon
+      v-if="hugeiconsModel"
+      :icon="hugeiconsModel"
+      width="1em"
+      height="1em"
+      class="action-icon-glyph-svg"
+    />
+    <span
+      v-else-if="decodedTextGlyphIsHtml"
+      v-html="decodedTextGlyph"
+    />
+    <span
+      v-else
+      v-text="decodedTextGlyph"
+    />
+  </span>
 </template>
 
 <script setup>
@@ -22,38 +28,38 @@ const hugeiconsPrefix = 'hugeicons:'
 
 /** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
 const hugeiconsRegistry = {
-	CommandLineIcon,
+  CommandLineIcon
 }
 
 const props = defineProps({
-	glyph: {
-		type: String,
-		required: false,
-		default: '',
-	},
+  glyph: {
+    type: String,
+    required: false,
+    default: ''
+  }
 })
 
 const hugeiconsModel = computed(() => {
-	if (!props.glyph) {
-		return CommandLineIcon
-	}
+  if (!props.glyph) {
+    return CommandLineIcon
+  }
 
-	if (!props.glyph.startsWith(hugeiconsPrefix)) {
-		return null
-	}
+  if (!props.glyph.startsWith(hugeiconsPrefix)) {
+    return null
+  }
 
-	const name = props.glyph.slice(hugeiconsPrefix.length)
-	const iconModel = hugeiconsRegistry[name]
+  const name = props.glyph.slice(hugeiconsPrefix.length)
+  const iconModel = hugeiconsRegistry[name]
 
-	return iconModel ?? CommandLineIcon
+  return iconModel ?? CommandLineIcon
 })
 
 const decodedTextGlyph = computed(() => {
-	if (hugeiconsModel.value) {
-		return ''
-	}
+  if (hugeiconsModel.value) {
+    return ''
+  }
 
-	return decodeHtmlEntities(props.glyph)
+  return decodeHtmlEntities(props.glyph)
 })
 
 const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))

+ 60 - 56
frontend/resources/vue/components/ActionStatusDisplay.vue

@@ -1,86 +1,91 @@
 <template>
-    <div :class = "statusClass + ' annotation'">
-        <router-link v-if="showQueueLink" to="/logs/queue" class="queue-status-link">{{ statusText }}</router-link>
-        <span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
-    </div>
-
+  <div :class="statusClass + ' annotation'">
+    <router-link
+      v-if="showQueueLink"
+      to="/logs/queue"
+      class="queue-status-link"
+    >
+      {{ statusText }}
+    </router-link>
+    <span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
+  </div>
 </template>
 
 <script setup>
 import { computed } from 'vue'
 
 const props = defineProps({
-    logEntry: {
-        type: Object,
-        required: true
-    },
-    linkQueuedStatus: {
-        type: Boolean,
-        default: false
-    }
+  logEntry: {
+    type: Object,
+    required: true
+  },
+  linkQueuedStatus: {
+    type: Boolean,
+    default: false
+  }
 })
 
-function isWaitingInQueue(logEntry) {
-    return logEntry &&
+function isWaitingInQueue (logEntry) {
+  return logEntry &&
         !logEntry.executionFinished &&
         !logEntry.executionStarted
 }
 
 const statusText = computed(() => {
-    const logEntry = props.logEntry
-    if (!logEntry) return 'unknown'
-
-    if (logEntry.executionFinished) {
-        if (logEntry.blocked) {
-            return 'Blocked'
-        } else if (logEntry.timedOut) {
-            return 'Timed out'
-        } else {
-            return 'Completed'
-        }
+  const logEntry = props.logEntry
+  if (!logEntry) return 'unknown'
+
+  if (logEntry.executionFinished) {
+    if (logEntry.blocked) {
+      return 'Blocked'
+    } else if (logEntry.timedOut) {
+      return 'Timed out'
+    } else {
+      return 'Completed'
     }
+  }
 
-    if (isWaitingInQueue(logEntry)) {
-        return 'Queued'
-    }
+  if (isWaitingInQueue(logEntry)) {
+    return 'Queued'
+  }
 
-    return 'Still running...'
+  return 'Still running...'
 })
 
 const exitCodeText = computed(() => {
-    const logEntry = props.logEntry
-    if (!logEntry) return ''
-    if (logEntry.exitCode === 0) {
-        return ''
-    }
-    if (logEntry.executionFinished) {
-        if (logEntry.blocked || logEntry.timedOut) {
-            return ''
-        }
-        return ' (Exit code: ' + logEntry.exitCode + ')'
-    }
+  const logEntry = props.logEntry
+  if (!logEntry) return ''
+  if (logEntry.exitCode === 0) {
     return ''
+  }
+  if (logEntry.executionFinished) {
+    if (logEntry.blocked || logEntry.timedOut) {
+      return ''
+    }
+    return ' (Exit code: ' + logEntry.exitCode + ')'
+  }
+  return ''
 })
 
 const showQueueLink = computed(() => {
-    return props.linkQueuedStatus && isWaitingInQueue(props.logEntry)
+  return props.linkQueuedStatus && isWaitingInQueue(props.logEntry)
 })
 
 const statusClass = computed(() => {
-    const logEntry = props.logEntry
-    if (!logEntry) return ''
-    if (logEntry.executionFinished) {
-        if (logEntry.blocked) {
-            return 'status-blocked'
-        } else if (logEntry.timedOut) {
-            return 'status-timeout'
-        } else if (logEntry.exitCode === 0) {
-            return 'status-success'
-        } else {
-            return 'status-nonzero-exit'
-        }
+  const logEntry = props.logEntry
+  if (!logEntry) return ''
+  if (logEntry.executionFinished) {
+    if (logEntry.blocked) {
+      return 'status-blocked'
+    } else if (logEntry.timedOut) {
+      return 'status-timeout'
+    } else if (logEntry.exitCode === 0) {
+      return 'status-success'
+    } else {
+      return 'status-nonzero-exit'
     }
-    return ''
+  }
+  return ''
 })
 </script>
 
@@ -110,5 +115,4 @@ const statusClass = computed(() => {
   text-decoration: underline;
 }
 
-
 </style>

+ 44 - 39
frontend/resources/vue/components/Breadcrumbs.vue

@@ -1,14 +1,50 @@
 <template>
-    <div id = "breadcrumbs">
-        <template v-for="(link, index) in links" :key="link.name">
-            <router-link :to="link.href">{{ link.name }}</router-link>
-            <span v-if="index < links.length - 1" class="separator">
-                &raquo;
-            </span>
-        </template>
-    </div>
+  <div id="breadcrumbs">
+    <template
+      v-for="(link, index) in links"
+      :key="link.name"
+    >
+      <router-link :to="link.href">
+        {{ link.name }}
+      </router-link>
+      <span
+        v-if="index < links.length - 1"
+        class="separator"
+      >
+        &raquo;
+      </span>
+    </template>
+  </div>
 </template>
 
+<script setup>
+import { ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+const links = ref([])
+
+watch(() => route.matched, (matched) => {
+  links.value = []
+  matched.forEach((record) => {
+    if (!record) return
+    if (record.meta && record.meta.breadcrumb) {
+      record.meta.breadcrumb.forEach((item) => {
+        links.value.push({
+          name: item.name,
+          href: item.href || record.path || '/'
+        })
+      })
+    } else if (record.name) {
+      links.value.push({
+        name: record.name,
+        href: record.path || '/'
+      })
+    }
+  })
+}, { immediate: true })
+</script>
+
 <style scoped>
 span {
     color: #bbb;
@@ -26,34 +62,3 @@ a:hover {
 }
 
 </style>
-
-
-<script setup>
-    import { ref } from 'vue';
-    import { watch } from 'vue';
-    import { useRoute } from 'vue-router';
-
-    const route = useRoute();
-    const links = ref([]);
-
-    watch(() => route.matched, (matched) => {
-
-        links.value = [];
-        matched.forEach((record) => {
-            if (!record) return;
-            if (record.meta && record.meta.breadcrumb) {
-                record.meta.breadcrumb.forEach((item) => {
-                    links.value.push({
-                        name: item.name,
-                        href: item.href || record.path || '/'
-                    });
-                });
-            } else if (record.name) {
-                links.value.push({
-                    name: record.name,
-                    href: record.path || '/'
-                });
-            }
-        });
-    }, { immediate: true });
-</script>

+ 24 - 11
frontend/resources/vue/components/ChoiceChecklist.vue

@@ -1,15 +1,28 @@
 <template>
-  <div class="choice-checklist" :id="wrapperId">
+  <div
+    :id="wrapperId"
+    class="choice-checklist"
+  >
     <div class="choice-checklist-controls">
-      <button type="button" class="choice-checklist-control" @click="selectAll">
+      <button
+        type="button"
+        class="choice-checklist-control"
+        @click="selectAll"
+      >
         Select all
       </button>
-      <button type="button" class="choice-checklist-control" @click="selectNone">
+      <button
+        type="button"
+        class="choice-checklist-control"
+        @click="selectNone"
+      >
         Select none
       </button>
     </div>
     <fieldset class="choice-checklist-fieldset">
-      <legend class="visually-hidden">{{ label || name }}</legend>
+      <legend class="visually-hidden">
+        {{ label || name }}
+      </legend>
       <label
         v-for="(choice, index) in choices"
         :key="choice.value"
@@ -21,7 +34,7 @@
           type="checkbox"
           :checked="isSelected(choice.value)"
           @change="handleToggle(choice.value)"
-        />
+        >
         <span>{{ choiceLabel(choice) }}</span>
       </label>
     </fieldset>
@@ -34,7 +47,7 @@
       :required="required"
       tabindex="-1"
       aria-hidden="true"
-    />
+    >
   </div>
 </template>
 
@@ -86,23 +99,23 @@ function optionId (index) {
   return argumentFieldOptionId(props.name, index)
 }
 
-function isSelected(value) {
+function isSelected (value) {
   return selectedValues.value.includes(value)
 }
 
-function emitSelection(selected) {
+function emitSelection (selected) {
   emit('update:modelValue', formatChecklistValue(selected))
 }
 
-function handleToggle(value) {
+function handleToggle (value) {
   emitSelection(toggleChoice(selectedValues.value, value))
 }
 
-function selectAll() {
+function selectAll () {
   emitSelection(allChoiceValues(props.choices))
 }
 
-function selectNone() {
+function selectNone () {
   emitSelection([])
 }
 </script>

+ 25 - 19
frontend/resources/vue/components/ChoiceCombobox.vue

@@ -1,8 +1,11 @@
 <template>
-  <div class="choice-combobox" ref="rootRef">
+  <div
+    ref="rootRef"
+    class="choice-combobox"
+  >
     <input
-      ref="searchInputRef"
       :id="id"
+      ref="searchInputRef"
       type="text"
       class="choice-combobox-input"
       role="combobox"
@@ -17,12 +20,12 @@
       @input="handleSearchInput"
       @keydown="handleKeydown"
       @blur="handleBlur"
-    />
+    >
     <input
       :name="name"
       type="hidden"
       :value="modelValue"
-    />
+    >
     <ul
       v-if="isOpen && filteredChoices.length > 0"
       :id="listboxId"
@@ -44,7 +47,10 @@
         {{ choiceLabel(choice) }}
       </li>
     </ul>
-    <div v-else-if="isOpen && query" class="choice-combobox-list choice-combobox-empty">
+    <div
+      v-else-if="isOpen && query"
+      class="choice-combobox-list choice-combobox-empty"
+    >
       No matching options
     </div>
   </div>
@@ -140,11 +146,11 @@ watch([() => props.modelValue, () => props.choices], () => {
   }
 }, { immediate: true })
 
-function choiceLabel(choice) {
+function choiceLabel (choice) {
   return choiceDisplayLabel(choice)
 }
 
-function syncFromModelValue() {
+function syncFromModelValue () {
   const next = syncStateFromModelValue(props.choices, props.modelValue)
   query.value = next.query
 
@@ -153,12 +159,12 @@ function syncFromModelValue() {
   }
 }
 
-function selectedChoiceIndex(choices) {
+function selectedChoiceIndex (choices) {
   const index = choices.findIndex(choice => choice.value === props.modelValue)
   return index >= 0 ? index : 0
 }
 
-function openList() {
+function openList () {
   document.dispatchEvent(new CustomEvent(closeOthersEvent, { detail: { id: props.id } }))
   const wasClosed = !isOpen.value
   isOpen.value = true
@@ -167,23 +173,23 @@ function openList() {
   }
 }
 
-function closeList() {
+function closeList () {
   isOpen.value = false
   isUserFiltering.value = false
   syncFromModelValue()
 }
 
-function emitValue(value) {
+function emitValue (value) {
   emit('update:modelValue', value)
 }
 
-function selectChoice(choice) {
+function selectChoice (choice) {
   emitValue(choice.value)
   query.value = choiceLabel(choice)
   isOpen.value = false
 }
 
-function handleFocus() {
+function handleFocus () {
   if (!isOpen.value) {
     syncFromModelValue()
     isUserFiltering.value = false
@@ -192,14 +198,14 @@ function handleFocus() {
   openList()
 }
 
-function handleSearchInput(event) {
+function handleSearchInput (event) {
   isUserFiltering.value = true
   query.value = event.target.value
   openList()
   highlightedIndex.value = 0
 }
 
-function moveHighlight(delta) {
+function moveHighlight (delta) {
   if (filteredChoices.value.length === 0) {
     return
   }
@@ -218,7 +224,7 @@ function moveHighlight(delta) {
   highlightedIndex.value = nextIndex
 }
 
-function handleKeydown(event) {
+function handleKeydown (event) {
   if (event.key === 'ArrowDown') {
     event.preventDefault()
     const wasOpen = isOpen.value
@@ -258,17 +264,17 @@ function handleKeydown(event) {
   }
 }
 
-function handleBlur() {
+function handleBlur () {
   closeList()
 }
 
-function handleCloseOthers(event) {
+function handleCloseOthers (event) {
   if (event.detail.id !== props.id) {
     closeList()
   }
 }
 
-function handleOutsideMouseDown(event) {
+function handleOutsideMouseDown (event) {
   if (!isOpen.value || rootRef.value?.contains(event.target)) {
     return
   }

+ 19 - 7
frontend/resources/vue/components/ConnectionBanner.vue

@@ -1,10 +1,22 @@
 <template>
-    <span id="connection-banner" v-if="connectionState.showDisconnectedBanner" class="inline-notification critical user-info-connection">
-        <span class="connection-banner-sr-only" role="status">{{ staticAnnouncement }}</span>
-        <span aria-hidden="true">
-            <a :href="websocketDocsUrl" target="_blank" rel="noopener noreferrer" class="connection-banner-link">{{ linkText }}</a>{{ bannerSuffix }}
-        </span>
+  <span
+    v-if="connectionState.showDisconnectedBanner"
+    id="connection-banner"
+    class="inline-notification critical user-info-connection"
+  >
+    <span
+      class="connection-banner-sr-only"
+      role="status"
+    >{{ staticAnnouncement }}</span>
+    <span aria-hidden="true">
+      <a
+        :href="websocketDocsUrl"
+        target="_blank"
+        rel="noopener noreferrer"
+        class="connection-banner-link"
+      >{{ linkText }}</a>{{ bannerSuffix }}
     </span>
+  </span>
 </template>
 
 <script setup>
@@ -18,7 +30,7 @@ const websocketDocsUrl = 'https://docs.olivetin.app/troubleshooting/err-websocke
 
 const linkText = computed(() => t('disconnected-banner-link-text'))
 
-function formatShortRelative(ms) {
+function formatShortRelative (ms) {
   if (ms < 0) return '0s'
   const secs = Math.floor(ms / 1000)
   const mins = Math.floor(secs / 60)
@@ -28,7 +40,7 @@ function formatShortRelative(ms) {
   return `${secs}s`
 }
 
-function formatShortTime(ts) {
+function formatShortTime (ts) {
   if (ts == null) return '--:--'
   return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
 }

+ 39 - 23
frontend/resources/vue/components/DashboardComponent.vue

@@ -1,23 +1,39 @@
 <template>
-    <ActionButton v-if="component.type == 'link'" :actionData="component.action" :cssClass="component.cssClass" :key="component.title" />
-
-    <DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
-
-    <DashboardComponentDisplay v-else-if="component.type == 'display'" :component="component" />
-
-    <DashboardComponentMostRecentExecution v-else-if="component.type == 'stdout-most-recent-execution'" :component="component" />
-
-    <template v-else-if="component.type == 'fieldset'">
-        <template v-for="subcomponent in component.contents" :key="subcomponent.title">
-            <DashboardComponent :component="subcomponent" />
-        </template>
+  <ActionButton
+    v-if="component.type == 'link'"
+    :key="component.title"
+    :action-data="component.action"
+    :css-class="component.cssClass"
+  />
+
+  <DashboardComponentDirectory
+    v-else-if="component.type == 'directory'"
+    :component="component"
+  />
+
+  <DashboardComponentDisplay
+    v-else-if="component.type == 'display'"
+    :component="component"
+  />
+
+  <DashboardComponentMostRecentExecution
+    v-else-if="component.type == 'stdout-most-recent-execution'"
+    :component="component"
+  />
+
+  <template v-else-if="component.type == 'fieldset'">
+    <template
+      v-for="subcomponent in component.contents"
+      :key="subcomponent.title"
+    >
+      <DashboardComponent :component="subcomponent" />
     </template>
+  </template>
 
-    <div v-else>
-        OTHER: {{ component.type }}
-        {{ component }}
-    </div>
-
+  <div v-else>
+    OTHER: {{ component.type }}
+    {{ component }}
+  </div>
 </template>
 
 <script setup>
@@ -26,10 +42,10 @@ import DashboardComponentMostRecentExecution from './DashboardComponentMostRecen
 import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
 import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
 
-const props = defineProps({
-    component: {
-        type: Object,
-        required: true
-    }
+defineProps({
+  component: {
+    type: Object,
+    required: true
+  }
 })
-</script>
+</script>

+ 29 - 22
frontend/resources/vue/components/DashboardComponentDirectory.vue

@@ -1,8 +1,15 @@
 <template>
-    <button class="directory-button" :class="component.cssClass" @click="navigateToDirectory">
-        <span class="icon" v-html="unicodeIcon"></span>
-        <span class="title">{{ component.title }}</span>
-    </button>
+  <button
+    class="directory-button"
+    :class="component.cssClass"
+    @click="navigateToDirectory"
+  >
+    <span
+      class="icon"
+      v-html="unicodeIcon"
+    />
+    <span class="title">{{ component.title }}</span>
+  </button>
 </template>
 
 <script setup>
@@ -12,33 +19,33 @@ import { computed } from 'vue'
 const router = useRouter()
 
 const props = defineProps({
-    component: {
-        type: Object,
-        required: true
-    }
+  component: {
+    type: Object,
+    required: true
+  }
 })
 
-function getUnicodeIcon(icon) {
-    if (icon === '' || !icon) {
-        return '&#x1f4c1;' // Default folder icon
-    } else {
-        return unescape(icon)
-    }
+function getUnicodeIcon (icon) {
+  if (icon === '' || !icon) {
+    return '&#x1f4c1;' // Default folder icon
+  } else {
+    return unescape(icon)
+  }
 }
 
 const unicodeIcon = computed(() => {
-    return getUnicodeIcon(props.component.icon)
+  return getUnicodeIcon(props.component.icon)
 })
 
-function navigateToDirectory() {
-    const params = { title: props.component.title }
+function navigateToDirectory () {
+  const params = { title: props.component.title }
 
-    if (props.component.entityType && props.component.entityKey) {
-        params.entityType = props.component.entityType
-        params.entityKey = props.component.entityKey
-    }
+  if (props.component.entityType && props.component.entityKey) {
+    params.entityType = props.component.entityType
+    params.entityKey = props.component.entityKey
+  }
 
-    router.push({ name: 'Dashboard', params })
+  router.push({ name: 'Dashboard', params })
 }
 </script>
 

+ 11 - 8
frontend/resources/vue/components/DashboardComponentDisplay.vue

@@ -1,15 +1,18 @@
 <template>
-    <div class="display" :class="component.cssClass">
-        <div v-html="component.title" />
-    </div>
+  <div
+    class="display"
+    :class="component.cssClass"
+  >
+    <div v-html="component.title" />
+  </div>
 </template>
 
 <script setup>
-const props = defineProps({
-    component: {
-        type: Object,
-        required: true
-    }
+defineProps({
+  component: {
+    type: Object,
+    required: true
+  }
 })
 </script>
 

+ 14 - 8
frontend/resources/vue/components/DashboardComponentMostRecentExecution.vue

@@ -1,13 +1,19 @@
 <template>
-  <div class="mre-container" :class="component.cssClass">
+  <div
+    class="mre-container"
+    :class="component.cssClass"
+  >
     <router-link
-        v-if="executionTrackingId"
-        :to="`/logs/${executionTrackingId}`"
-        class="mre-link"
+      v-if="executionTrackingId"
+      :to="`/logs/${executionTrackingId}`"
+      class="mre-link"
     >
-        <pre class="mre-output">{{ output }}</pre>
+      <pre class="mre-output">{{ output }}</pre>
     </router-link>
-    <pre v-else class="mre-output fg-important">{{ output }}</pre>
+    <pre
+      v-else
+      class="mre-output fg-important"
+    >{{ output }}</pre>
   </div>
 </template>
 
@@ -26,7 +32,7 @@ const output = ref('Waiting...')
 const executionTrackingId = ref(null)
 let unwatchButtonResults = null
 
-function updateFromLogEntry(logEntry) {
+function updateFromLogEntry (logEntry) {
   if (logEntry) {
     if (logEntry.output !== undefined) {
       output.value = logEntry.output
@@ -39,7 +45,7 @@ function updateFromLogEntry(logEntry) {
   }
 }
 
-async function fetchMostRecentExecution() {
+async function fetchMostRecentExecution () {
   if (!props.component.title) {
     output.value = 'Error: No action ID specified'
     executionTrackingId.value = null

+ 187 - 160
frontend/resources/vue/components/EntityDefinitionSection.vue

@@ -1,167 +1,194 @@
 <template>
-	<Section :padding="!hasTable">
-		<template #title>
-			<span class="section-title-with-icon">
-				Entity:
-				<ActionIconGlyph v-if="definition.icon" class="entity-title-icon" :glyph="definition.icon" />
-				{{ definition.title }}
-			</span>
-		</template>
-		<template v-if="hasTable" #toolbar>
-			<EntityListFilter v-model="searchText" />
-		</template>
-
-		<p v-if="!hasTable">{{ definition.instances.length }} instances.</p>
-
-		<template v-if="hasTable">
-			<p v-if="tableError" class="table-error padding" role="alert">{{ tableError }}</p>
-			<EntityInstancesTable
-				v-else
-				:instances="tableInstances"
-				:properties="definition.properties"
-				:total-instances="totalInstances"
-				v-model:page="currentPage"
-				v-model:page-size="pageSize"
-			/>
-		</template>
-
-		<ul v-else>
-			<li v-for="inst in definition.instances" :key="inst.uniqueKey">
-				<router-link :to="entityDetailsRoute(inst)">
-					{{ inst.title }}
-				</router-link>
-			</li>
-		</ul>
-
-		<div v-if="usedDashboards.length > 0" :class="{ padding: hasTable }">
-			<h3>Used on Dashboards:</h3>
-			<ul>
-				<li v-for="dash in usedDashboards" :key="dash">
-					<template v-if="isEntityDirectory(dash)">
-						{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
-					</template>
-					<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }">
-						{{ getDashboardTitle(dash) }}
-					</router-link>
-					<span v-else>{{ dash }}</span>
-				</li>
-			</ul>
-		</div>
-	</Section>
+  <Section :padding="!hasTable">
+    <template #title>
+      <span class="section-title-with-icon">
+        Entity:
+        <ActionIconGlyph
+          v-if="definition.icon"
+          class="entity-title-icon"
+          :glyph="definition.icon"
+        />
+        {{ definition.title }}
+      </span>
+    </template>
+    <template
+      v-if="hasTable"
+      #toolbar
+    >
+      <EntityListFilter v-model="searchText" />
+    </template>
+
+    <p v-if="!hasTable">
+      {{ definition.instances.length }} instances.
+    </p>
+
+    <template v-if="hasTable">
+      <p
+        v-if="tableError"
+        class="table-error padding"
+        role="alert"
+      >
+        {{ tableError }}
+      </p>
+      <EntityInstancesTable
+        v-else
+        v-model:page="currentPage"
+        v-model:page-size="pageSize"
+        :instances="tableInstances"
+        :properties="definition.properties"
+        :total-instances="totalInstances"
+      />
+    </template>
+
+    <ul v-else>
+      <li
+        v-for="inst in definition.instances"
+        :key="inst.uniqueKey"
+      >
+        <router-link :to="entityDetailsRoute(inst)">
+          {{ inst.title }}
+        </router-link>
+      </li>
+    </ul>
+
+    <div
+      v-if="usedDashboards.length > 0"
+      :class="{ padding: hasTable }"
+    >
+      <h3>Used on Dashboards:</h3>
+      <ul>
+        <li
+          v-for="dash in usedDashboards"
+          :key="dash"
+        >
+          <template v-if="isEntityDirectory(dash)">
+            {{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
+          </template>
+          <router-link
+            v-else-if="!dash.includes('entity:')"
+            :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }"
+          >
+            {{ getDashboardTitle(dash) }}
+          </router-link>
+          <span v-else>{{ dash }}</span>
+        </li>
+      </ul>
+    </div>
+  </Section>
 </template>
 
 <script setup>
-	import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
-	import Section from 'picocrank/vue/components/Section.vue'
-	import ActionIconGlyph from './ActionIconGlyph.vue'
-	import EntityInstancesTable from './EntityInstancesTable.vue'
-	import EntityListFilter from './EntityListFilter.vue'
-	import { entityDetailsRoute } from '../utils/entityRoutes.js'
-
-	const props = defineProps({
-		definition: {
-			type: Object,
-			required: true
-		}
-	})
-
-	const searchText = ref('')
-	const tableInstances = ref([])
-	const totalInstances = ref(0)
-	const currentPage = ref(1)
-	const pageSize = ref(10)
-	const tableError = ref('')
-	let fetchTimer = null
-	let fetchSequence = 0
-
-	const hasTable = computed(() => (props.definition.properties?.length ?? 0) > 0)
-
-	const usedDashboards = computed(() => filteredDashboards(props.definition.usedOnDashboards ?? []))
-
-	watch(searchText, () => {
-		currentPage.value = 1
-		scheduleFetchTableInstances()
-	})
-
-	watch([currentPage, pageSize], () => {
-		scheduleFetchTableInstances()
-	})
-
-	function filteredDashboards(dashboards) {
-		return dashboards.filter(d => d && !d.includes('{{'))
-	}
-
-	function isEntityDirectory(dashboardTitle) {
-		return dashboardTitle.endsWith(' [Entity Directory]')
-	}
-
-	function getDashboardTitle(dashboardTitle) {
-		if (isEntityDirectory(dashboardTitle)) {
-			return dashboardTitle.slice(0, -' [Entity Directory]'.length)
-		}
-		return dashboardTitle
-	}
-
-	function scheduleFetchTableInstances() {
-		if (!hasTable.value) {
-			return
-		}
-
-		if (fetchTimer) {
-			clearTimeout(fetchTimer)
-		}
-
-		fetchTimer = setTimeout(() => {
-			fetchTableInstances()
-		}, 250)
-	}
-
-	async function fetchTableInstances() {
-		if (!hasTable.value) {
-			return
-		}
-
-		const requestId = ++fetchSequence
-		tableError.value = ''
-		try {
-			const response = await window.client.getEntities({
-				entityType: props.definition.title,
-				filter: searchText.value.trim(),
-				page: currentPage.value,
-				pageSize: pageSize.value
-			})
-
-			if (requestId !== fetchSequence) {
-				return
-			}
-
-			const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
-			tableInstances.value = definition?.instances ?? []
-			totalInstances.value = definition?.totalInstances ?? 0
-		} catch (err) {
-			if (requestId !== fetchSequence) {
-				return
-			}
-
-			console.error('Failed to fetch entity instances:', err)
-			tableError.value = 'Failed to load entity instances.'
-			tableInstances.value = []
-			totalInstances.value = 0
-		}
-	}
-
-	onMounted(() => {
-		if (hasTable.value) {
-			fetchTableInstances()
-		}
-	})
-
-	onBeforeUnmount(() => {
-		if (fetchTimer) {
-			clearTimeout(fetchTimer)
-			fetchTimer = null
-		}
-	})
+import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
+import Section from 'picocrank/vue/components/Section.vue'
+import ActionIconGlyph from './ActionIconGlyph.vue'
+import EntityInstancesTable from './EntityInstancesTable.vue'
+import EntityListFilter from './EntityListFilter.vue'
+import { entityDetailsRoute } from '../utils/entityRoutes.js'
+
+const props = defineProps({
+  definition: {
+    type: Object,
+    required: true
+  }
+})
+
+const searchText = ref('')
+const tableInstances = ref([])
+const totalInstances = ref(0)
+const currentPage = ref(1)
+const pageSize = ref(10)
+const tableError = ref('')
+let fetchTimer = null
+let fetchSequence = 0
+
+const hasTable = computed(() => (props.definition.properties?.length ?? 0) > 0)
+
+const usedDashboards = computed(() => filteredDashboards(props.definition.usedOnDashboards ?? []))
+
+watch(searchText, () => {
+  currentPage.value = 1
+  scheduleFetchTableInstances()
+})
+
+watch([currentPage, pageSize], () => {
+  scheduleFetchTableInstances()
+})
+
+function filteredDashboards (dashboards) {
+  return dashboards.filter(d => d && !d.includes('{{'))
+}
+
+function isEntityDirectory (dashboardTitle) {
+  return dashboardTitle.endsWith(' [Entity Directory]')
+}
+
+function getDashboardTitle (dashboardTitle) {
+  if (isEntityDirectory(dashboardTitle)) {
+    return dashboardTitle.slice(0, -' [Entity Directory]'.length)
+  }
+  return dashboardTitle
+}
+
+function scheduleFetchTableInstances () {
+  if (!hasTable.value) {
+    return
+  }
+
+  if (fetchTimer) {
+    clearTimeout(fetchTimer)
+  }
+
+  fetchTimer = setTimeout(() => {
+    fetchTableInstances()
+  }, 250)
+}
+
+async function fetchTableInstances () {
+  if (!hasTable.value) {
+    return
+  }
+
+  const requestId = ++fetchSequence
+  tableError.value = ''
+  try {
+    const response = await window.client.getEntities({
+      entityType: props.definition.title,
+      filter: searchText.value.trim(),
+      page: currentPage.value,
+      pageSize: pageSize.value
+    })
+
+    if (requestId !== fetchSequence) {
+      return
+    }
+
+    const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
+    tableInstances.value = definition?.instances ?? []
+    totalInstances.value = definition?.totalInstances ?? 0
+  } catch (err) {
+    if (requestId !== fetchSequence) {
+      return
+    }
+
+    console.error('Failed to fetch entity instances:', err)
+    tableError.value = 'Failed to load entity instances.'
+    tableInstances.value = []
+    totalInstances.value = 0
+  }
+}
+
+onMounted(() => {
+  if (hasTable.value) {
+    fetchTableInstances()
+  }
+})
+
+onBeforeUnmount(() => {
+  if (fetchTimer) {
+    clearTimeout(fetchTimer)
+    fetchTimer = null
+  }
+})
 </script>
 
 <style scoped>

+ 75 - 72
frontend/resources/vue/components/EntityInstancesTable.vue

@@ -1,87 +1,90 @@
 <template>
-	<Table
-		:data="tableRows"
-		:headers="headers"
-		:show-pagination="false"
-	>
-		<template #cell-title="{ row, value }">
-			<router-link :to="entityDetailsRoute(row)">
-				{{ value }}
-			</router-link>
-		</template>
-	</Table>
+  <Table
+    :data="tableRows"
+    :headers="headers"
+    :show-pagination="false"
+  >
+    <template #cell-title="{ row, value }">
+      <router-link :to="entityDetailsRoute(row)">
+        {{ value }}
+      </router-link>
+    </template>
+  </Table>
 
-	<div v-if="totalInstances > 0" class="padding">
-		<Pagination
-			:total="totalInstances"
-			v-model:page="currentPageModel"
-			v-model:page-size="pageSizeModel"
-			item-title="entities"
-		/>
-	</div>
+  <div
+    v-if="totalInstances > 0"
+    class="padding"
+  >
+    <Pagination
+      v-model:page="currentPageModel"
+      v-model:page-size="pageSizeModel"
+      :total="totalInstances"
+      item-title="entities"
+    />
+  </div>
 </template>
 
 <script setup>
-	import { computed } from 'vue'
-	import Table from 'picocrank/vue/components/Table.vue'
-	import Pagination from 'picocrank/vue/components/Pagination.vue'
-	import { entityDetailsRoute } from '../utils/entityRoutes.js'
+import { computed } from 'vue'
+import Table from 'picocrank/vue/components/Table.vue'
+import Pagination from 'picocrank/vue/components/Pagination.vue'
+import { entityDetailsRoute } from '../utils/entityRoutes.js'
 
-	const props = defineProps({
-		instances: {
-			type: Array,
-			required: true
-		},
-		properties: {
-			type: Array,
-			required: true
-		},
-		totalInstances: {
-			type: Number,
-			default: 0
-		},
-		page: {
-			type: Number,
-			default: 1
-		},
-		pageSize: {
-			type: Number,
-			default: 10
-		}
-	})
+const props = defineProps({
+  instances: {
+    type: Array,
+    required: true
+  },
+  properties: {
+    type: Array,
+    required: true
+  },
+  totalInstances: {
+    type: Number,
+    default: 0
+  },
+  page: {
+    type: Number,
+    default: 1
+  },
+  pageSize: {
+    type: Number,
+    default: 10
+  }
+})
 
-	const emit = defineEmits(['update:page', 'update:pageSize'])
+const emit = defineEmits(['update:page', 'update:pageSize'])
 
-	const headers = computed(() => {
-		const propertyHeaders = props.properties.map(property => ({
-			key: property.name,
-			label: property.title
-		}))
+const headers = computed(() => {
+  const propertyHeaders = props.properties.map(property => ({
+    key: property.name,
+    label: property.title
+  }))
 
-		return [
-			{ key: 'title', label: 'Name' },
-			...propertyHeaders
-		]
-	})
+  return [
+    { key: 'title', label: 'Name' },
+    ...propertyHeaders
+  ]
+})
 
-	const tableRows = computed(() =>
-		props.instances.map(instance => ({
-			...instance.fields,
-			title: instance.title,
-			type: instance.type,
-			uniqueKey: instance.uniqueKey
-		}))
-	)
+const tableRows = computed(() =>
+  props.instances.map(instance => ({
+    ...instance.fields,
+    title: instance.title,
+    type: instance.type,
+    uniqueKey: instance.uniqueKey
+  }))
+)
 
-	const currentPageModel = computed({
-		get: () => props.page,
-		set: value => emit('update:page', value)
-	})
+const currentPageModel = computed({
+  get: () => props.page,
+  set: value => emit('update:page', value)
+})
 
-	const pageSizeModel = computed({
-		get: () => props.pageSize,
-		set: value => emit('update:pageSize', value)
-	})
+const pageSizeModel = computed({
+  get: () => props.pageSize,
+  set: value => emit('update:pageSize', value)
+})
 
 </script>
 

+ 45 - 25
frontend/resources/vue/components/EntityListFilter.vue

@@ -1,33 +1,53 @@
 <template>
-	<label class="input-with-icons">
-		<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
-			<path fill="currentColor"
-				d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
-		</svg>
-		<input
-			:value="modelValue"
-			aria-label="Filter entities"
-			placeholder="Filter entities..."
-			@input="$emit('update:modelValue', $event.target.value)"
-		/>
-		<button title="Clear search filter" :disabled="!modelValue" @click="$emit('update:modelValue', '')">
-			<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
-				<path fill="currentColor"
-					d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
-			</svg>
-		</button>
-	</label>
+  <label class="input-with-icons">
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="1em"
+      height="1em"
+      viewBox="0 0 24 24"
+      aria-hidden="true"
+    >
+      <path
+        fill="currentColor"
+        d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
+      />
+    </svg>
+    <input
+      :value="modelValue"
+      aria-label="Filter entities"
+      placeholder="Filter entities..."
+      @input="$emit('update:modelValue', $event.target.value)"
+    >
+    <button
+      title="Clear search filter"
+      :disabled="!modelValue"
+      @click="$emit('update:modelValue', '')"
+    >
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="1em"
+        height="1em"
+        viewBox="0 0 24 24"
+        aria-hidden="true"
+      >
+        <path
+          fill="currentColor"
+          d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
+        />
+      </svg>
+    </button>
+  </label>
 </template>
 
 <script setup>
-	defineProps({
-		modelValue: {
-			type: String,
-			default: ''
-		}
-	})
+defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  }
+})
 
-	defineEmits(['update:modelValue'])
+defineEmits(['update:modelValue'])
 </script>
 
 <style scoped>

+ 4 - 1
frontend/resources/vue/components/LogActionTitle.vue

@@ -1,7 +1,10 @@
 <template>
   <span class="log-action-title">
     <slot>{{ actionTitle }}</slot>
-    <span v-if="justification" class="log-justification">— {{ justification }}</span>
+    <span
+      v-if="justification"
+      class="log-justification"
+    >— {{ justification }}</span>
   </span>
 </template>
 

+ 15 - 18
frontend/resources/vue/router.js

@@ -2,10 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
 
 import Dashboard from './Dashboard.vue'
 
-import { Wrench01Icon } from '@hugeicons/core-free-icons'
-import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
-import { CellsIcon } from '@hugeicons/core-free-icons'
-import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
+import { Wrench01Icon, LeftToRightListDashIcon, CellsIcon, DashboardSquare01Icon } from '@hugeicons/core-free-icons'
 
 const routes = [
   {
@@ -44,8 +41,8 @@ const routes = [
     meta: {
       title: 'Logs Calendar',
       breadcrumb: [
-        { name: "Logs", href: "/logs" },
-        { name: "Calendar" },
+        { name: 'Logs', href: '/logs' },
+        { name: 'Calendar' }
       ]
     }
   },
@@ -56,8 +53,8 @@ const routes = [
     meta: {
       title: 'Execution Queue',
       breadcrumb: [
-        { name: "Logs", href: "/logs" },
-        { name: "Queue" },
+        { name: 'Logs', href: '/logs' },
+        { name: 'Queue' }
       ]
     }
   },
@@ -78,8 +75,8 @@ const routes = [
     meta: {
       title: 'OliveTin - Entity Details',
       breadcrumb: [
-        { name: "Entities", href: "/entities" },
-        { name: "Entity Details" }
+        { name: 'Entities', href: '/entities' },
+        { name: 'Entity Details' }
       ]
     }
   },
@@ -91,8 +88,8 @@ const routes = [
     meta: {
       title: 'Execution',
       breadcrumb: [
-        { name: "Logs", href: "/logs" },
-        { name: "Execution" },
+        { name: 'Logs', href: '/logs' },
+        { name: 'Execution' }
       ]
     }
   },
@@ -104,8 +101,8 @@ const routes = [
     meta: {
       title: 'Action Details',
       breadcrumb: [
-        { name: "Actions", href: "/" },
-        { name: "Action Details" },
+        { name: 'Actions', href: '/' },
+        { name: 'Action Details' }
       ]
     }
   },
@@ -117,8 +114,8 @@ const routes = [
     meta: {
       title: 'Execution conditions',
       breadcrumb: [
-        { name: "Actions", href: "/" },
-        { name: "Execution conditions" },
+        { name: 'Actions', href: '/' },
+        { name: 'Execution conditions' }
       ]
     }
   },
@@ -155,7 +152,7 @@ const routes = [
 const router = createRouter({
   history: createWebHistory(),
   routes,
-  scrollBehavior(to, from, savedPosition) {
+  scrollBehavior (to, from, savedPosition) {
     if (savedPosition) {
       return savedPosition
     } else {
@@ -168,7 +165,7 @@ const router = createRouter({
 router.beforeEach((to) => {
   if (to.meta && to.meta.title) {
     const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
-    document.title = to.meta.title + " - " + pageTitle
+    document.title = to.meta.title + ' - ' + pageTitle
   }
 })
 

+ 6 - 6
frontend/resources/vue/utils/choiceChecklistHelpers.js

@@ -1,8 +1,8 @@
-function parseLegacyChecklistValue(value) {
+function parseLegacyChecklistValue (value) {
   return value.split(',').map((segment) => segment.trim()).filter((segment) => segment !== '')
 }
 
-export function parseChecklistValue(value) {
+export function parseChecklistValue (value) {
   if (!value || value === '') {
     return []
   }
@@ -24,7 +24,7 @@ export function parseChecklistValue(value) {
   return parseLegacyChecklistValue(value)
 }
 
-export function formatChecklistValue(selected) {
+export function formatChecklistValue (selected) {
   if (!Array.isArray(selected) || selected.length === 0) {
     return ''
   }
@@ -32,7 +32,7 @@ export function formatChecklistValue(selected) {
   return JSON.stringify(selected)
 }
 
-export function toggleChoice(selected, value) {
+export function toggleChoice (selected, value) {
   const current = Array.isArray(selected) ? [...selected] : []
   const index = current.indexOf(value)
 
@@ -45,10 +45,10 @@ export function toggleChoice(selected, value) {
   return current
 }
 
-export function choiceLabel(choice) {
+export function choiceLabel (choice) {
   return choice.title || choice.value || ''
 }
 
-export function allChoiceValues(choices) {
+export function allChoiceValues (choices) {
   return choices.map((choice) => choice.value)
 }

+ 3 - 3
frontend/resources/vue/utils/justificationTemplate.js

@@ -1,4 +1,4 @@
-export function applyArgumentTemplate(template, args) {
+export function applyArgumentTemplate (template, args) {
   if (!template) {
     return ''
   }
@@ -6,10 +6,10 @@ export function applyArgumentTemplate(template, args) {
   return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, name) => args[name] ?? '')
 }
 
-export function actionRequiresJustification(justification) {
+export function actionRequiresJustification (justification) {
   return (justification ?? '').length > 0
 }
 
-export function actionJustificationTemplate(justification) {
+export function actionJustificationTemplate (justification) {
   return justification ?? ''
 }

+ 12 - 12
frontend/resources/vue/utils/prefilledArguments.js

@@ -1,20 +1,20 @@
 export function readPrefilledArgumentsFromNavigation (historyState = globalThis.window?.history?.state) {
-	if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
-		return { ...historyState.prefilledArguments }
-	}
+  if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
+    return { ...historyState.prefilledArguments }
+  }
 
-	return {}
+  return {}
 }
 
 export function getInitialArgumentValue (paramName, prefilledArguments = {}, search = globalThis.window?.location?.search ?? '') {
-	const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
-		? prefilledArguments
-		: {}
+  const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
+    ? prefilledArguments
+    : {}
 
-	if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) {
-		return safePrefilledArguments[paramName]
-	}
+  if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) {
+    return safePrefilledArguments[paramName]
+  }
 
-	const params = new URLSearchParams(search)
-	return params.get(paramName)
+  const params = new URLSearchParams(search)
+  return params.get(paramName)
 }

+ 221 - 140
frontend/resources/vue/views/ActionDetailsView.vue

@@ -1,130 +1,211 @@
 <template>
   <Section :padding="false">
-      <template #title>
-        <span class="section-title-with-icon">
-          Action Details:
-          <ActionIconGlyph v-if="action" class="action-title-icon" :glyph="action.icon" />
-          {{ actionTitle }}
-        </span>
-      </template>
-      <template #toolbar>
-        <div class="action-details-toolbar">
-          <button
-            v-for="dashboard in backToDashboards"
-            :key="dashboard.path"
-            @click="goToDashboard(dashboard.path)"
-            :title="'Back to ' + dashboard.title"
-            class="button neutral"
-          >
-            <HugeiconsIcon :icon="DashboardSquare01Icon" />
-            {{ dashboard.title }}
-          </button>
-          <button v-if="action" @click="startAction" title="Run this action" class="button neutral">
-            <HugeiconsIcon :icon="WorkoutRunIcon" />
-            Run
-          </button>
-          <router-link
-            v-if="action"
-            :to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
-            class="button neutral"
-            title="View configured automatic triggers and on-demand execution"
-          >
-            Execution conditions ({{ executionConditionCount }})
-          </router-link>
-        </div>
-      </template>
-
-      <div class = "flex-row padding" v-if="action">
-        <div class = "fg1">
-          <dl>
-            <dt>Timeout</dt>
-            <dd>{{ action.timeout }} seconds</dd>
-
-            <template v-if="actionGroups.length > 0">
-              <dt>
-                <router-link :to="{ name: 'LogsQueue' }" class="action-groups-link">Action groups</router-link>
-              </dt>
-              <dd>
-                <ul class="action-group-list">
-                  <li v-for="group in actionGroups" :key="group.name" class="action-group-row">
-                    <router-link :to="{ name: 'LogsQueue' }" class="action-groups-link action-group-name">{{ group.name }}</router-link><template v-if="group.maxConcurrent > 0 && group.queueSize > 0"> - </template><ActionGroupLimitsLabel
-                      :max-concurrent="group.maxConcurrent"
-                      :queue-size="group.queueSize"
-                    />
-                  </li>
-                </ul>
-              </dd>
-            </template>
-          </dl>
-          <p class = "fg1">
-            Execution history for this action. You can filter by execution tracking ID.
-          </p>
-        </div>
-        <div style = "align-self: start; text-align: right;">
-          <div class="filter-container">
-            <label class="input-with-icons">
-              <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-                <path fill="currentColor"
-                  d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
-              </svg>
-              <input placeholder="Filter current page" v-model="searchText" />
-              <button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
-                <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-                  <path fill="currentColor"
-                    d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
-                </svg>
-              </button>
-            </label>
-          </div>
-        </div>
+    <template #title>
+      <span class="section-title-with-icon">
+        Action Details:
+        <ActionIconGlyph
+          v-if="action"
+          class="action-title-icon"
+          :glyph="action.icon"
+        />
+        {{ actionTitle }}
+      </span>
+    </template>
+    <template #toolbar>
+      <div class="action-details-toolbar">
+        <button
+          v-for="dashboard in backToDashboards"
+          :key="dashboard.path"
+          :title="'Back to ' + dashboard.title"
+          class="button neutral"
+          @click="goToDashboard(dashboard.path)"
+        >
+          <HugeiconsIcon :icon="DashboardSquare01Icon" />
+          {{ dashboard.title }}
+        </button>
+        <button
+          v-if="action"
+          title="Run this action"
+          class="button neutral"
+          @click="startAction"
+        >
+          <HugeiconsIcon :icon="WorkoutRunIcon" />
+          Run
+        </button>
+        <router-link
+          v-if="action"
+          :to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
+          class="button neutral"
+          title="View configured automatic triggers and on-demand execution"
+        >
+          Execution conditions ({{ executionConditionCount }})
+        </router-link>
       </div>
-
-      <div v-show="filteredLogs.length > 0">
-        <table class="logs-table row-hover">
-          <thead>
-            <tr>
-              <th>Timestamp</th>
-              <th>Duration</th>
-              <th>Execution ID</th>
-              <th>Metadata</th>
-              <th>Status</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
-              <td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
-              <td class="duration">{{ formatExecutionDuration(log) }}</td>
-              <td>
-                <router-link :to="`/logs/${log.executionTrackingId}`">
-                  <LogActionTitle :justification="log.justification">
-                    {{ log.executionTrackingId }}
-                  </LogActionTitle>
-                </router-link>
-              </td>
-              <td class="tags">
-                <span class="annotation">
-                  <span class="annotation-key">User:</span>
-                  <span class="annotation-val">{{ log.user }}</span>
-                </span>
-                <span v-if="log.tags && log.tags.length > 0" class="tag-list">
-                  <span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
-                </span>
-              </td>
-              <td class="exit-code">
-                <ActionStatusDisplay :logEntry="log" :link-queued-status="true" />
-              </td>
-            </tr>
-          </tbody>
-        </table>
-
-        <Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class="padding"
-          @page-size-change="handlePageSizeChange" itemTitle="execution logs" />
+    </template>
+
+    <div
+      v-if="action"
+      class="flex-row padding"
+    >
+      <div class="fg1">
+        <dl>
+          <dt>Timeout</dt>
+          <dd>{{ action.timeout }} seconds</dd>
+
+          <template v-if="actionGroups.length > 0">
+            <dt>
+              <router-link
+                :to="{ name: 'LogsQueue' }"
+                class="action-groups-link"
+              >
+                Action groups
+              </router-link>
+            </dt>
+            <dd>
+              <ul class="action-group-list">
+                <li
+                  v-for="group in actionGroups"
+                  :key="group.name"
+                  class="action-group-row"
+                >
+                  <router-link
+                    :to="{ name: 'LogsQueue' }"
+                    class="action-groups-link action-group-name"
+                  >
+                    {{ group.name }}
+                  </router-link><template v-if="group.maxConcurrent > 0 && group.queueSize > 0">
+                    -
+                  </template><ActionGroupLimitsLabel
+                    :max-concurrent="group.maxConcurrent"
+                    :queue-size="group.queueSize"
+                  />
+                </li>
+              </ul>
+            </dd>
+          </template>
+        </dl>
+        <p class="fg1">
+          Execution history for this action. You can filter by execution tracking ID.
+        </p>
       </div>
-
-      <div v-show="logs.length === 0 && !loading" class="empty-state">
-        <p>This action has no execution history.</p>
-        <router-link to="/">Return to index</router-link>
+      <div style="align-self: start; text-align: right;">
+        <div class="filter-container">
+          <label class="input-with-icons">
+            <svg
+              xmlns="http://www.w3.org/2000/svg"
+              width="1em"
+              height="1em"
+              viewBox="0 0 24 24"
+            >
+              <path
+                fill="currentColor"
+                d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
+              />
+            </svg>
+            <input
+              v-model="searchText"
+              placeholder="Filter current page"
+            >
+            <button
+              title="Clear search filter"
+              :disabled="!searchText"
+              @click="clearSearch"
+            >
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                width="1em"
+                height="1em"
+                viewBox="0 0 24 24"
+              >
+                <path
+                  fill="currentColor"
+                  d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
+                />
+              </svg>
+            </button>
+          </label>
+        </div>
       </div>
+    </div>
+
+    <div v-show="filteredLogs.length > 0">
+      <table class="logs-table row-hover">
+        <thead>
+          <tr>
+            <th>Timestamp</th>
+            <th>Duration</th>
+            <th>Execution ID</th>
+            <th>Metadata</th>
+            <th>Status</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr
+            v-for="log in filteredLogs"
+            :key="log.executionTrackingId"
+            class="log-row"
+            :title="log.actionTitle"
+          >
+            <td class="timestamp">
+              {{ formatTimestamp(log.datetimeStarted) }}
+            </td>
+            <td class="duration">
+              {{ formatExecutionDuration(log) }}
+            </td>
+            <td>
+              <router-link :to="`/logs/${log.executionTrackingId}`">
+                <LogActionTitle :justification="log.justification">
+                  {{ log.executionTrackingId }}
+                </LogActionTitle>
+              </router-link>
+            </td>
+            <td class="tags">
+              <span class="annotation">
+                <span class="annotation-key">User:</span>
+                <span class="annotation-val">{{ log.user }}</span>
+              </span>
+              <span
+                v-if="log.tags && log.tags.length > 0"
+                class="tag-list"
+              >
+                <span
+                  v-for="tag in log.tags"
+                  :key="tag"
+                  class="tag"
+                >{{ tag }}</span>
+              </span>
+            </td>
+            <td class="exit-code">
+              <ActionStatusDisplay
+                :log-entry="log"
+                :link-queued-status="true"
+              />
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <Pagination
+        :page-size="pageSize"
+        :total="totalCount"
+        :current-page="currentPage"
+        :page="currentPage"
+        class="padding"
+        item-title="execution logs"
+        @page-change="handlePageChange"
+        @page-size-change="handlePageSizeChange"
+      />
+    </div>
+
+    <div
+      v-show="logs.length === 0 && !loading"
+      class="empty-state"
+    >
+      <p>This action has no execution history.</p>
+      <router-link to="/">
+        Return to index
+      </router-link>
+    </div>
   </Section>
 </template>
 
@@ -174,16 +255,16 @@ const executionConditionCount = computed(() => countExecutionConditions(action.v
 
 const actionGroups = computed(() => action.value?.groups ?? [])
 
-async function fetchActionLogs() {
+async function fetchActionLogs () {
   loading.value = true
   try {
     const actionId = route.params.actionId
     const startOffset = (currentPage.value - 1) * pageSize.value
 
     const args = {
-      "actionId": actionId,
-      "startOffset": BigInt(startOffset),
-      "pageSize": BigInt(Number(pageSize.value)),
+      actionId,
+      startOffset: BigInt(startOffset),
+      pageSize: BigInt(Number(pageSize.value))
     }
 
     const response = await window.client.getActionLogs(args)
@@ -203,11 +284,11 @@ async function fetchActionLogs() {
   }
 }
 
-async function fetchAction() {
+async function fetchAction () {
   try {
     const actionId = route.params.actionId
     const args = {
-      "bindingId": actionId
+      bindingId: actionId
     }
     const response = await window.client.getActionBinding(args)
     action.value = response.action
@@ -219,11 +300,11 @@ async function fetchAction() {
   }
 }
 
-function goToDashboard(path) {
+function goToDashboard (path) {
   router.push(path)
 }
 
-function resetState() {
+function resetState () {
   action.value = null
   backToDashboards.value = []
   actionTitle.value = 'Action Details'
@@ -235,11 +316,11 @@ function resetState() {
   syncDurationTicker()
 }
 
-function clearSearch() {
+function clearSearch () {
   searchText.value = ''
 }
 
-function formatTimestamp(timestamp) {
+function formatTimestamp (timestamp) {
   if (!timestamp) return 'Unknown'
   try {
     const date = new Date(timestamp)
@@ -249,11 +330,11 @@ function formatTimestamp(timestamp) {
   }
 }
 
-function plural(n, singular, pluralForm) {
+function plural (n, singular, pluralForm) {
   return n === 1 ? `1 ${singular}` : `${n} ${pluralForm}`
 }
 
-function formatDurationSimple(ms) {
+function formatDurationSimple (ms) {
   if (!Number.isFinite(ms) || ms < 0) {
     return '—'
   }
@@ -274,7 +355,7 @@ function formatDurationSimple(ms) {
   return parts.join(' ')
 }
 
-function formatExecutionDuration(log) {
+function formatExecutionDuration (log) {
   // Reading durationClock keeps this column reactive while executions are in progress.
   const clock = durationClock.value
 
@@ -300,7 +381,7 @@ function formatExecutionDuration(log) {
   return formatDurationSimple(endMs - started.getTime())
 }
 
-function syncDurationTicker() {
+function syncDurationTicker () {
   if (durationTicker != null) {
     clearInterval(durationTicker)
     durationTicker = null
@@ -314,18 +395,18 @@ function syncDurationTicker() {
   }, 1000)
 }
 
-function handlePageChange(page) {
+function handlePageChange (page) {
   currentPage.value = page
   fetchActionLogs()
 }
 
-function handlePageSizeChange(newPageSize) {
+function handlePageSizeChange (newPageSize) {
   pageSize.value = newPageSize
   currentPage.value = 1
   fetchActionLogs()
 }
 
-async function startAction() {
+async function startAction () {
   if (!action.value || !action.value.bindingId) {
     console.error('Cannot start action: no binding ID')
     return
@@ -351,7 +432,7 @@ async function startAction() {
   }
 }
 
-function onExecutionEvent(evt) {
+function onExecutionEvent (evt) {
   const logEntry = getExecutionLogEntry(evt)
   if (!logEntry || logEntry.bindingId !== route.params.actionId) {
     return

+ 92 - 22
frontend/resources/vue/views/ActionExecConditionsView.vue

@@ -1,12 +1,21 @@
 <template>
-  <Section :title="'Execution conditions: ' + actionTitle" :padding="false">
+  <Section
+    :title="'Execution conditions: ' + actionTitle"
+    :padding="false"
+  >
     <template #toolbar>
-      <router-link :to="{ name: 'ActionDetails', params: { actionId: route.params.actionId } }" class="button neutral">
+      <router-link
+        :to="{ name: 'ActionDetails', params: { actionId: route.params.actionId } }"
+        class="button neutral"
+      >
         Back to action details
       </router-link>
     </template>
 
-    <div v-if="action" class="padding content">
+    <div
+      v-if="action"
+      class="padding content"
+    >
       <p>
         These entries mirror the automatic triggers from your OliveTin configuration for this action.
         You can always run the action manually as well.
@@ -14,7 +23,12 @@
 
       <h3 class="exec-type-heading">
         On click
-        <a class="doc-link" :href="execConditionDocs.onDemand" target="_blank" rel="noopener noreferrer">Documentation</a>
+        <a
+          class="doc-link"
+          :href="execConditionDocs.onDemand"
+          target="_blank"
+          rel="noopener noreferrer"
+        >Documentation</a>
       </h3>
       <p>
         Manual execution from the web UI (dashboard or action details), or via the API (for example StartAction),
@@ -24,7 +38,12 @@
       <template v-if="action.execOnStartup">
         <h3 class="exec-type-heading">
           <code>execOnStartup</code>
-          <a class="doc-link" :href="execConditionDocs.startup" target="_blank" rel="noopener noreferrer">Documentation</a>
+          <a
+            class="doc-link"
+            :href="execConditionDocs.startup"
+            target="_blank"
+            rel="noopener noreferrer"
+          >Documentation</a>
         </h3>
         <p>Runs once when OliveTin starts.</p>
       </template>
@@ -32,37 +51,72 @@
       <template v-if="nonEmptyList(action.execOnCron)">
         <h3 class="exec-type-heading">
           <code>execOnCron</code>
-          <a class="doc-link" :href="execConditionDocs.cron" target="_blank" rel="noopener noreferrer">Documentation</a>
+          <a
+            class="doc-link"
+            :href="execConditionDocs.cron"
+            target="_blank"
+            rel="noopener noreferrer"
+          >Documentation</a>
         </h3>
         <ul>
-          <li v-for="(line, idx) in action.execOnCron" :key="'cron-' + idx"><code>{{ line }}</code></li>
+          <li
+            v-for="(line, idx) in action.execOnCron"
+            :key="'cron-' + idx"
+          >
+            <code>{{ line }}</code>
+          </li>
         </ul>
       </template>
 
       <template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
         <h3 class="exec-type-heading">
           <code>execOnFileCreatedInDir</code>
-          <a class="doc-link" :href="execConditionDocs.fileCreated" target="_blank" rel="noopener noreferrer">Documentation</a>
+          <a
+            class="doc-link"
+            :href="execConditionDocs.fileCreated"
+            target="_blank"
+            rel="noopener noreferrer"
+          >Documentation</a>
         </h3>
         <ul>
-          <li v-for="(dir, idx) in action.execOnFileCreatedInDir" :key="'created-' + idx"><code>{{ dir }}</code></li>
+          <li
+            v-for="(dir, idx) in action.execOnFileCreatedInDir"
+            :key="'created-' + idx"
+          >
+            <code>{{ dir }}</code>
+          </li>
         </ul>
       </template>
 
       <template v-if="nonEmptyList(action.execOnFileChangedInDir)">
         <h3 class="exec-type-heading">
           <code>execOnFileChangedInDir</code>
-          <a class="doc-link" :href="execConditionDocs.fileChanged" target="_blank" rel="noopener noreferrer">Documentation</a>
+          <a
+            class="doc-link"
+            :href="execConditionDocs.fileChanged"
+            target="_blank"
+            rel="noopener noreferrer"
+          >Documentation</a>
         </h3>
         <ul>
-          <li v-for="(dir, idx) in action.execOnFileChangedInDir" :key="'changed-' + idx"><code>{{ dir }}</code></li>
+          <li
+            v-for="(dir, idx) in action.execOnFileChangedInDir"
+            :key="'changed-' + idx"
+          >
+            <code>{{ dir }}</code>
+          </li>
         </ul>
       </template>
 
       <template v-if="action.execOnCalendarFile">
         <h3 class="exec-type-heading">
           <code>execOnCalendarFile</code>
-          <a class="doc-link" :href="execConditionDocs.calendar" target="_blank" rel="noopener noreferrer">Documentation</a>
+          <a
+            class="doc-link"
+            :href="execConditionDocs.calendar"
+            target="_blank"
+            rel="noopener noreferrer"
+          >Documentation</a>
         </h3>
         <p><code>{{ action.execOnCalendarFile }}</code></p>
       </template>
@@ -70,10 +124,18 @@
       <template v-if="nonEmptyList(action.execOnWebhooks)">
         <h3 class="exec-type-heading">
           <code>execOnWebhook</code>
-          <a class="doc-link" :href="execConditionDocs.webhook" target="_blank" rel="noopener noreferrer">Documentation</a>
+          <a
+            class="doc-link"
+            :href="execConditionDocs.webhook"
+            target="_blank"
+            rel="noopener noreferrer"
+          >Documentation</a>
         </h3>
         <ul class="webhook-list">
-          <li v-for="(wh, idx) in action.execOnWebhooks" :key="'wh-' + idx">
+          <li
+            v-for="(wh, idx) in action.execOnWebhooks"
+            :key="'wh-' + idx"
+          >
             <span v-if="wh.template">template: <code>{{ wh.template }}</code></span>
             <span v-if="wh.matchPath"> · matchPath: <code>{{ wh.matchPath }}</code></span>
             <span v-if="nonEmptyObject(wh.matchHeaders)"> · matchHeaders: <code>{{ wh.matchHeaders }}</code></span>
@@ -83,14 +145,22 @@
         </ul>
       </template>
 
-      <p v-if="!hasConfiguredTriggers" class="muted">
+      <p
+        v-if="!hasConfiguredTriggers"
+        class="muted"
+      >
         This action has no automatic triggers in configuration besides on-demand execution.
       </p>
     </div>
 
-    <div v-else-if="!loading" class="padding empty-state">
+    <div
+      v-else-if="!loading"
+      class="padding empty-state"
+    >
       <p>Could not load this action.</p>
-      <router-link :to="{ name: 'Actions' }">Return to index</router-link>
+      <router-link :to="{ name: 'Actions' }">
+        Return to index
+      </router-link>
     </div>
   </Section>
 </template>
@@ -112,18 +182,18 @@ const execConditionDocs = {
   fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
   fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
   calendar: 'https://docs.olivetin.app/action_execution/oncalendar.html',
-  webhook: 'https://docs.olivetin.app/action_execution/onwebhook.html',
+  webhook: 'https://docs.olivetin.app/action_execution/onwebhook.html'
 }
 
-function nonEmptyList(list) {
+function nonEmptyList (list) {
   return Array.isArray(list) && list.length > 0
 }
 
-function nonEmptyObject(object) {
+function nonEmptyObject (object) {
   return object && Object.keys(object).length > 0
 }
 
-function webhookHasCondition(webhook) {
+function webhookHasCondition (webhook) {
   return webhook.template || webhook.matchPath || nonEmptyObject(webhook.matchHeaders) || nonEmptyObject(webhook.matchQuery)
 }
 
@@ -147,7 +217,7 @@ const hasConfiguredTriggers = computed(() => {
   return false
 })
 
-async function fetchAction() {
+async function fetchAction () {
   loading.value = true
   try {
     const actionId = route.params.actionId

+ 152 - 93
frontend/resources/vue/views/ArgumentForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <section id = "argument-popup">
+  <section id="argument-popup">
     <div class="section-header">
       <h2>
         <span class="section-title-with-icon">
@@ -8,7 +8,11 @@
             :to="`/action/${bindingId}`"
             class="action-details-title-link"
           >
-            <ActionIconGlyph v-if="icon" class="action-title-icon" :glyph="icon" />
+            <ActionIconGlyph
+              v-if="icon"
+              class="action-title-icon"
+              :glyph="icon"
+            />
             {{ title }}
           </router-link>
         </span>
@@ -17,48 +21,96 @@
     <div class="section-content padding">
       <form @submit="handleSubmit">
         <template v-if="actionArguments.length > 0">
-
-          <template v-for="arg in actionArguments" :key="arg.name">
-              <label v-if="arg.type !== 'checklist'" :for="argumentFieldId(arg.name)">
-                {{ formatLabel(arg.title) }}
-              </label>
-              <div v-else class="argument-label">
-                {{ formatLabel(arg.title) }}
-              </div>
-
-              <datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="argumentFieldChoicesId(arg.name)">
-                <option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
-                  {{ suggestion }}
-                </option>
-                <option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
-                  {{ suggestion }}
-                </option>
-              </datalist>
-
-              <ChoiceCombobox v-if="getInputComponent(arg) === 'select'" :id="argumentFieldId(arg.name)" :name="arg.name"
-                :choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
-                @update:model-value="handleChoiceUpdate(arg, $event)" />
-
-              <ChoiceChecklist v-else-if="arg.type === 'checklist'" :name="arg.name"
-                :label="arg.title" :choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
-                @update:model-value="handleChoiceUpdate(arg, $event)" />
-
-              <component v-else :is="getInputComponent(arg)" :id="argumentFieldId(arg.name)" :name="arg.name"
-                :value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
-                :checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
-                :list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined"
-                :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
-                :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
-                :step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
-                @input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
-
-            <span class="argument-description" v-html="arg.description"></span>
+          <template
+            v-for="arg in actionArguments"
+            :key="arg.name"
+          >
+            <label
+              v-if="arg.type !== 'checklist'"
+              :for="argumentFieldId(arg.name)"
+            >
+              {{ formatLabel(arg.title) }}
+            </label>
+            <div
+              v-else
+              class="argument-label"
+            >
+              {{ formatLabel(arg.title) }}
+            </div>
+
+            <datalist
+              v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0"
+              :id="argumentFieldChoicesId(arg.name)"
+            >
+              <option
+                v-for="(suggestion, key) in arg.suggestions"
+                :key="key"
+                :value="key"
+              >
+                {{ suggestion }}
+              </option>
+              <option
+                v-for="(suggestion, index) in getBrowserSuggestions(arg)"
+                :key="`browser-${index}`"
+                :value="suggestion"
+              >
+                {{ suggestion }}
+              </option>
+            </datalist>
+
+            <ChoiceCombobox
+              v-if="getInputComponent(arg) === 'select'"
+              :id="argumentFieldId(arg.name)"
+              :name="arg.name"
+              :choices="arg.choices"
+              :model-value="getArgumentValue(arg)"
+              :required="arg.required"
+              @update:model-value="handleChoiceUpdate(arg, $event)"
+            />
+
+            <ChoiceChecklist
+              v-else-if="arg.type === 'checklist'"
+              :name="arg.name"
+              :label="arg.title"
+              :choices="arg.choices"
+              :model-value="getArgumentValue(arg)"
+              :required="arg.required"
+              @update:model-value="handleChoiceUpdate(arg, $event)"
+            />
+
+            <component
+              :is="getInputComponent(arg)"
+              v-else
+              :id="argumentFieldId(arg.name)"
+              :name="arg.name"
+              :value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
+              :checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
+              :list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined"
+              :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
+              :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
+              :step="arg.type === 'datetime' ? 1 : undefined"
+              :pattern="getPattern(arg)"
+              @input="handleInput(arg, $event)"
+              @change="handleChange(arg, $event)"
+            />
+
+            <span
+              class="argument-description"
+              v-html="arg.description"
+            />
           </template>
         </template>
 
         <template v-if="justificationRequired">
           <label for="justification">Justification:</label>
-          <input id="justification" name="justification" type="text" :value="justificationValue" required @input="handleJustificationInput" />
+          <input
+            id="justification"
+            name="justification"
+            type="text"
+            :value="justificationValue"
+            required
+            @input="handleJustificationInput"
+          >
         </template>
 
         <div v-if="actionArguments.length === 0 && !justificationRequired">
@@ -66,10 +118,18 @@
         </div>
 
         <div class="buttons">
-          <button name="start" type="submit" :disabled="!formReady || (hasConfirmation && !confirmationChecked)">
+          <button
+            name="start"
+            type="submit"
+            :disabled="!formReady || (hasConfirmation && !confirmationChecked)"
+          >
             Start
           </button>
-          <button name="cancel" type="button" @click="handleCancel">
+          <button
+            name="cancel"
+            type="button"
+            @click="handleCancel"
+          >
             Cancel
           </button>
         </div>
@@ -103,7 +163,7 @@ const router = useRouter()
 const dialog = ref(null)
 const title = ref('')
 const icon = ref('')
-//const arguments = ref([])
+// const arguments = ref([])
 const argValues = ref({})
 const confirmationChecked = ref(false)
 const hasConfirmation = ref(false)
@@ -128,7 +188,7 @@ const props = defineProps({
 })
 
 // Methods
-async function setup() {
+async function setup () {
   formReady.value = false
   document.body.removeAttribute('loaded-argument-form')
 
@@ -155,32 +215,32 @@ async function setup() {
 
     // Initialize values from navigation state, query params, or defaults
     actionArguments.value.forEach(arg => {
-    if (arg.type === 'confirmation') {
-      hasConfirmation.value = true
-      const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
-      let checkedValue = false
-      if (paramValue !== null) {
-        checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
-      } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
-        checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
-      }
-      argValues.value[arg.name] = checkedValue
-      confirmationChecked.value = checkedValue
-    } else {
-      const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
-      if (arg.type === 'checkbox') {
-        // For checkboxes, handle boolean default values properly
+      if (arg.type === 'confirmation') {
+        hasConfirmation.value = true
+        const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
+        let checkedValue = false
         if (paramValue !== null) {
-          argValues.value[arg.name] = paramValue === '1' || paramValue === 'true' || paramValue === true
+          checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
         } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
-          argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
-        } else {
-          argValues.value[arg.name] = false
+          checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
         }
+        argValues.value[arg.name] = checkedValue
+        confirmationChecked.value = checkedValue
       } else {
-        argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
+        const paramValue = getInitialArgumentValue(arg.name, prefilledArguments)
+        if (arg.type === 'checkbox') {
+        // For checkboxes, handle boolean default values properly
+          if (paramValue !== null) {
+            argValues.value[arg.name] = paramValue === '1' || paramValue === 'true' || paramValue === true
+          } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
+            argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
+          } else {
+            argValues.value[arg.name] = false
+          }
+        } else {
+          argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
+        }
       }
-    }
     })
 
     // Run initial validation on all fields after DOM is updated
@@ -202,7 +262,7 @@ async function setup() {
   }
 }
 
-function formatLabel(title) {
+function formatLabel (title) {
   const lastChar = title.charAt(title.length - 1)
   if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
     return title
@@ -210,7 +270,7 @@ function formatLabel(title) {
   return title + ':'
 }
 
-function getInputComponent(arg) {
+function getInputComponent (arg) {
   if (arg.type === 'html') {
     return 'div'
   } else if (arg.type === 'raw_string_multiline') {
@@ -222,7 +282,7 @@ function getInputComponent(arg) {
   }
 }
 
-function getInputType(arg) {
+function getInputType (arg) {
   if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
     return undefined
   }
@@ -242,33 +302,33 @@ function getInputType(arg) {
   return arg.type
 }
 
-function getPattern(arg) {
+function getPattern (arg) {
   if (arg.type && arg.type.startsWith('regex:')) {
     return arg.type.replace('regex:', '')
   }
   return undefined
 }
 
-function getArgumentValue(arg) {
+function getArgumentValue (arg) {
   if (arg.type === 'checkbox' || arg.type === 'confirmation') {
     return argValues.value[arg.name] === '1' || argValues.value[arg.name] === true || argValues.value[arg.name] === 'true'
   }
   return argValues.value[arg.name] || ''
 }
 
-function handleJustificationInput(event) {
+function handleJustificationInput (event) {
   justificationValue.value = event.target.value
   justificationEditedManually.value = true
 }
 
-function handleInput(arg, event) {
+function handleInput (arg, event) {
   const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
   argValues.value[arg.name] = value
   updateUrlWithArg(arg.name, value)
   updateJustificationFromTemplate()
 }
 
-function handleChange(arg, event) {
+function handleChange (arg, event) {
   if (arg.type === 'confirmation') {
     confirmationChecked.value = event.target.checked
     return
@@ -278,18 +338,18 @@ function handleChange(arg, event) {
   validateArgument(arg, event.target.value)
 }
 
-function getValidationElement(arg) {
+function getValidationElement (arg) {
   return document.getElementById(argumentFieldValidationElementId(arg))
 }
 
-function handleChoiceUpdate(arg, value) {
+function handleChoiceUpdate (arg, value) {
   argValues.value[arg.name] = value
   updateUrlWithArg(arg.name, value)
   validateArgument(arg, value)
   updateJustificationFromTemplate()
 }
 
-async function validateArgument(arg, value) {
+async function validateArgument (arg, value) {
   if (!arg.type || arg.type.startsWith('regex:')) {
     return
   }
@@ -316,7 +376,7 @@ async function validateArgument(arg, value) {
 
   try {
     const validateArgumentTypeArgs = {
-      value: value,
+      value,
       type: arg.type,
       bindingId: props.bindingId,
       argumentName: arg.name
@@ -348,7 +408,7 @@ async function validateArgument(arg, value) {
   }
 }
 
-function updateUrlWithArg(name, value) {
+function updateUrlWithArg (name, value) {
   if (name && value !== undefined) {
     const url = new URL(window.location.href)
 
@@ -363,7 +423,7 @@ function updateUrlWithArg(name, value) {
   }
 }
 
-function shouldSendArgument(arg) {
+function shouldSendArgument (arg) {
   if (!arg.name) {
     return false
   }
@@ -371,7 +431,7 @@ function shouldSendArgument(arg) {
   return arg.type !== 'html'
 }
 
-function formatArgumentValueForApi(arg, rawValue) {
+function formatArgumentValueForApi (arg, rawValue) {
   if (arg.type === 'checkbox' || arg.type === 'confirmation') {
     return rawValue === '1' || rawValue === true || rawValue === 'true' ? '1' : '0'
   }
@@ -387,7 +447,7 @@ function formatArgumentValueForApi(arg, rawValue) {
   return rawValue ?? ''
 }
 
-function getSelectedArgumentEntries() {
+function getSelectedArgumentEntries () {
   const entries = []
 
   for (const arg of actionArguments.value) {
@@ -404,17 +464,17 @@ function getSelectedArgumentEntries() {
   return entries
 }
 
-function getArgumentValues() {
+function getArgumentValues () {
   return getSelectedArgumentEntries().map(({ name, value }) => ({ name, value }))
 }
 
-function getArgumentMapForTemplate() {
+function getArgumentMapForTemplate () {
   return Object.fromEntries(
     getSelectedArgumentEntries().map(({ name, value }) => [name, value])
   )
 }
 
-function updateJustificationFromTemplate() {
+function updateJustificationFromTemplate () {
   if (!justificationTemplate.value || justificationEditedManually.value) {
     return
   }
@@ -422,7 +482,7 @@ function updateJustificationFromTemplate() {
   justificationValue.value = applyArgumentTemplate(justificationTemplate.value, getArgumentMapForTemplate())
 }
 
-function getUniqueId() {
+function getUniqueId () {
   if (window.isSecureContext) {
     return window.crypto.randomUUID()
   } else {
@@ -430,7 +490,7 @@ function getUniqueId() {
   }
 }
 
-function getBrowserSuggestions(arg) {
+function getBrowserSuggestions (arg) {
   if (!arg.suggestionsBrowserKey) {
     return []
   }
@@ -448,7 +508,7 @@ function getBrowserSuggestions(arg) {
   return []
 }
 
-function saveBrowserSuggestions() {
+function saveBrowserSuggestions () {
   for (const arg of actionArguments.value) {
     if (arg.suggestionsBrowserKey) {
       const value = argValues.value[arg.name]
@@ -484,7 +544,7 @@ function saveBrowserSuggestions() {
   }
 }
 
-async function startAction(actionArgs) {
+async function startAction (actionArgs) {
   const startActionArgs = {
     bindingId: props.bindingId,
     arguments: actionArgs,
@@ -506,7 +566,7 @@ async function startAction(actionArgs) {
   }
 }
 
-async function handleSubmit(event) {
+async function handleSubmit (event) {
   event.preventDefault()
 
   if (!formReady.value) {
@@ -568,24 +628,24 @@ async function handleSubmit(event) {
   }
 }
 
-function handleCancel() {
+function handleCancel () {
   router.back()
   clearBookmark()
 }
 
-function clearBookmark() {
+function clearBookmark () {
   window.history.replaceState({
     path: window.location.pathname
   }, '', window.location.pathname)
 }
 
-function show() {
+function show () {
   if (dialog.value) {
     dialog.value.showModal()
   }
 }
 
-function close() {
+function close () {
   if (dialog.value) {
     dialog.value.close()
   }
@@ -638,7 +698,6 @@ form {
   grid-template-columns: max-content auto auto;
 }
 
-
 .argument-description {
   font-size: 0.875rem;
   color: #666;

+ 68 - 36
frontend/resources/vue/views/DiagnosticsView.vue

@@ -1,10 +1,14 @@
 <template>
   <Section :title="t('diagnostics.get-support')">
-    <p>{{ t('diagnostics.get-support-description') }}
+    <p>
+      {{ t('diagnostics.get-support-description') }}
     </p>
     <ul>
       <li>
-        <a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">{{ t('diagnostics.where-to-find-help') }}</a>
+        <a
+          href="https://docs.olivetin.app/troubleshooting/wheretofindhelp.html"
+          target="_blank"
+        >{{ t('diagnostics.where-to-find-help') }}</a>
       </li>
     </ul>
   </Section>
@@ -21,26 +25,61 @@
   <Section :title="t('diagnostics.server-diagnostics')">
     <p>{{ t('diagnostics.server-diagnostics-description') }}</p>
     <p>
-      <a href="https://docs.olivetin.app/troubleshooting/server-diagnostics.html" target="_blank">{{ t('diagnostics.server-diagnostics-docs') }}</a>
+      <a
+        href="https://docs.olivetin.app/troubleshooting/server-diagnostics.html"
+        target="_blank"
+      >{{ t('diagnostics.server-diagnostics-docs') }}</a>
     </p>
 
     <div role="toolbar">
-      <button @click="generateServerDiagnostics" :disabled="loading" class = "good">{{ t('diagnostics.generate-server-diagnostics') }}</button>
-      <button @click="copyServerDiagnostics" :disabled="!serverDiagnostics || loading" :class="serverDiagnosticsCopied ? 'good' : ''">{{ serverDiagnosticsCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
+      <button
+        :disabled="loading"
+        class="good"
+        @click="generateServerDiagnostics"
+      >
+        {{ t('diagnostics.generate-server-diagnostics') }}
+      </button>
+      <button
+        :disabled="!serverDiagnostics || loading"
+        :class="serverDiagnosticsCopied ? 'good' : ''"
+        @click="copyServerDiagnostics"
+      >
+        {{ serverDiagnosticsCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}
+      </button>
     </div>
 
-    <textarea v-model="serverDiagnostics" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
+    <textarea
+      v-model="serverDiagnostics"
+      readonly
+      style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"
+    />
   </Section>
 
   <Section :title="t('diagnostics.browser-info')">
     <p>{{ t('diagnostics.browser-info-description') }}</p>
 
     <div role="toolbar">
-      <button @click="generateBrowserInfo" :disabled="loading" class = "good">{{ t('diagnostics.generate-browser-info') }}</button>
-      <button @click="copyBrowserInfo" :disabled="!browserInfo || loading" :class="browserInfoCopied ? 'good' : ''">{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
+      <button
+        :disabled="loading"
+        class="good"
+        @click="generateBrowserInfo"
+      >
+        {{ t('diagnostics.generate-browser-info') }}
+      </button>
+      <button
+        :disabled="!browserInfo || loading"
+        :class="browserInfoCopied ? 'good' : ''"
+        @click="copyBrowserInfo"
+      >
+        {{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}
+      </button>
     </div>
 
-    <textarea v-model="browserInfo" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
+    <textarea
+      v-model="browserInfo"
+      readonly
+      style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"
+    />
   </Section>
 </template>
 
@@ -58,17 +97,17 @@ const browserInfo = ref('')
 const serverDiagnosticsCopied = ref(false)
 const browserInfoCopied = ref(false)
 
-async function fetchDiagnostics() {
+async function fetchDiagnostics () {
   loading.value = true
 
   try {
-    const response = await window.client.getDiagnostics();
+    const response = await window.client.getDiagnostics()
     diagnostics.value = {
       sshFoundKey: response.SshFoundKey,
       sshFoundConfig: response.SshFoundConfig
-    };
+    }
   } catch (err) {
-    console.error('Failed to fetch diagnostics:', err);
+    console.error('Failed to fetch diagnostics:', err)
     diagnostics.value = {
       sshFoundKey: t('diagnostics.unknown'),
       sshFoundConfig: t('diagnostics.unknown')
@@ -77,20 +116,13 @@ async function fetchDiagnostics() {
   loading.value = false
 }
 
-function formatKey(key) {
-  return key
-    .replace(/([A-Z])/g, ' $1')
-    .replace(/^./, str => str.toUpperCase())
-    .trim()
-}
-
-async function generateServerDiagnostics() {
+async function generateServerDiagnostics () {
   const response = await window.client.serverDiagnostics()
-  console.log("response", response)
+  console.log('response', response)
   serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
 }
 
-async function copyServerDiagnostics() {
+async function copyServerDiagnostics () {
   try {
     await navigator.clipboard.writeText(serverDiagnostics.value)
     serverDiagnosticsCopied.value = true
@@ -102,7 +134,7 @@ async function copyServerDiagnostics() {
   }
 }
 
-async function generateBrowserInfo() {
+async function generateBrowserInfo () {
   loading.value = true
   try {
     let userAgentData = 'N/A'
@@ -159,7 +191,7 @@ async function generateBrowserInfo() {
       })(),
       hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
       maxTouchPoints: navigator.maxTouchPoints || 'N/A',
-      userAgentData: userAgentData
+      userAgentData
     }
 
     const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
@@ -168,20 +200,20 @@ async function generateBrowserInfo() {
       : '[hidden]'
     const currentLanguage = locale.value || t('diagnostics.unknown')
 
-    let output = '';
-    output += `\`\`\`\n`
+    let output = ''
+    output += '```\n'
     output += '### BROWSER INFO START (copy all text to BROWSER INFO END)\n'
-    output += `# OliveTin Information\n`
+    output += '# OliveTin Information\n'
     output += `olivetinVersion: ${olivetinVersion}\n`
     output += `currentLanguage: ${currentLanguage}\n`
-    output += `\n# Browser Information\n`
+    output += '\n# Browser Information\n'
     output += `userAgent: ${info.userAgent}\n`
     output += `platform: ${info.platform}\n`
     output += `language: ${info.language}\n`
     output += `languages: ${info.languages}\n`
-    output += `\n# User Agent Data\n`
+    output += '\n# User Agent Data\n'
     output += `userAgentData:\n${info.userAgentData}\n`
-    output += `\n# Display Information\n`
+    output += '\n# Display Information\n'
     output += `screenWidth: ${info.screenWidth}\n`
     output += `screenHeight: ${info.screenHeight}\n`
     output += `screenColorDepth: ${info.screenColorDepth}\n`
@@ -189,18 +221,18 @@ async function generateBrowserInfo() {
     output += `viewportWidth: ${info.viewportWidth}\n`
     output += `viewportHeight: ${info.viewportHeight}\n`
     output += `devicePixelRatio: ${info.devicePixelRatio}\n`
-    output += `\n# Feature Support\n`
+    output += '\n# Feature Support\n'
     output += `cookieEnabled: ${info.cookieEnabled}\n`
     output += `localStorageEnabled: ${info.localStorageEnabled}\n`
     output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
     output += `onLine: ${info.onLine}\n`
     output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
     output += `maxTouchPoints: ${info.maxTouchPoints}\n`
-    output += `\n# Location & Time\n`
+    output += '\n# Location & Time\n'
     output += `timezone: ${info.timezone}\n`
     output += `timezoneOffset: ${info.timezoneOffset}\n`
-    output += `\n### BROWSER INFO END (copy all text from BROWSER INFO START)`
-    output += `\n\`\`\`\n`
+    output += '\n### BROWSER INFO END (copy all text from BROWSER INFO START)'
+    output += '\n```\n'
 
     browserInfo.value = output
   } finally {
@@ -208,7 +240,7 @@ async function generateBrowserInfo() {
   }
 }
 
-async function copyBrowserInfo() {
+async function copyBrowserInfo () {
   try {
     await navigator.clipboard.writeText(browserInfo.value)
     browserInfoCopied.value = true

+ 44 - 41
frontend/resources/vue/views/EntitiesView.vue

@@ -1,51 +1,54 @@
 <template>
-	<Section v-if="!definitionsLoaded" title="Loading entity definitions..." />
-	<Section
-		v-else-if="totalInstances === 0"
-		title="There are no entities to show yet."
-	>
-		<p>
-			When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
-		</p>
-	</Section>
-	<template v-else>
-		<EntityDefinitionSection
-			v-for="def in entityDefinitions"
-			:key="def.title"
-			:definition="def"
-		/>
-	</template>
+  <Section
+    v-if="!definitionsLoaded"
+    title="Loading entity definitions..."
+  />
+  <Section
+    v-else-if="totalInstances === 0"
+    title="There are no entities to show yet."
+  >
+    <p>
+      When OliveTin has registered entity instances (for example from entity files or your setup), they will be listed here.
+    </p>
+  </Section>
+  <template v-else>
+    <EntityDefinitionSection
+      v-for="def in entityDefinitions"
+      :key="def.title"
+      :definition="def"
+    />
+  </template>
 </template>
 
 <script setup>
-	import { ref, computed, onMounted } from 'vue'
-	import Section from 'picocrank/vue/components/Section.vue'
-	import EntityDefinitionSection from '../components/EntityDefinitionSection.vue'
+import { ref, computed, onMounted } from 'vue'
+import Section from 'picocrank/vue/components/Section.vue'
+import EntityDefinitionSection from '../components/EntityDefinitionSection.vue'
 
-	const definitionsLoaded = ref(false)
-	const entityDefinitions = ref([])
+const definitionsLoaded = ref(false)
+const entityDefinitions = ref([])
 
-	const totalInstances = computed(() =>
-		entityDefinitions.value.reduce(
-			(sum, def) => sum + (def.instances?.length ?? 0),
-			0,
-		),
-	)
+const totalInstances = computed(() =>
+  entityDefinitions.value.reduce(
+    (sum, def) => sum + (def.instances?.length ?? 0),
+    0
+  )
+)
 
-	async function fetchEntities() {
-		try {
-			const ret = await window.client.getEntities()
-			entityDefinitions.value = ret.entityDefinitions ?? []
-		} catch (err) {
-			console.error('Failed to fetch entities:', err)
-			window.showBigError('fetch-entities', 'getting entities', err, false)
-			entityDefinitions.value = []
-		} finally {
-			definitionsLoaded.value = true
-		}
-	}
+async function fetchEntities () {
+  try {
+    const ret = await window.client.getEntities()
+    entityDefinitions.value = ret.entityDefinitions ?? []
+  } catch (err) {
+    console.error('Failed to fetch entities:', err)
+    window.showBigError('fetch-entities', 'getting entities', err, false)
+    entityDefinitions.value = []
+  } finally {
+    definitionsLoaded.value = true
+  }
+}
 
-	onMounted(() => {
+onMounted(() => {
 	    fetchEntities()
-	})
+})
 </script>

+ 168 - 122
frontend/resources/vue/views/EntityDetailsView.vue

@@ -1,130 +1,176 @@
 <template>
-	<Section>
-		<template #title>
-			<span class="section-title-with-icon">
-				Entity Details:
-				<ActionIconGlyph v-if="entityIcon" class="entity-title-icon" :glyph="entityIcon" />
-				<span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
-			</span>
-		</template>
-		<template #toolbar>
-			<button @click="goBack" class="back-button">
-				<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
-				<span>Back</span>
-			</button>
-		</template>
-		<div v-if="!entityDetails">
-			<p>Loading entity details...</p>
-		</div>
-		<template v-else>
-			<dl>
-				<dt>Type</dt>
-				<dd>
-					<router-link :to="{ name: 'Entities' }" class="entity-type-link">
-						{{ entityType }}
-					</router-link>
-				</dd>
-				<dt v-if="entityDetails.title">Title</dt>
-				<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd>
-				<template v-if="entityDetails.fields">
-					<template v-for="(value, key) in entityDetails.fields" :key="key">
-						<dt>{{ key }}</dt>
-						<dd>{{ value }}</dd>
-					</template>
-				</template>
-			</dl>
-			<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
-		</template>
-	</Section>
-
-	<Section v-if="entityDetails" title="Dashboard Entity Directories">
-		<div v-if="filteredDirectories.length > 0" class="directories-section">
-			<ul class="directory-list">
-				<li v-for="(directory, idx) in filteredDirectories" :key="idx">
-					<router-link
-						:to="{
-							name: 'Dashboard',
-							params: {
-								title: directory,
-								entityType: entityType,
-								entityKey: entityKey
-							}
-						}">
-						{{ directory }}
-					</router-link>
-				</li>
-			</ul>
-		</div>
-		<p v-else>No directories found for this entity.
-			<a href="https://docs.olivetin.app/dashboards/entity-directories.html" target="_blank" rel="noopener noreferrer">Learn more</a>
-		</p>
-	</Section>
-
-	<section v-if="entityDetails && relatedActions.length > 0" class="transparent">
-		<div class="dashboard-row">
-			<fieldset>
-				<legend class="visually-hidden">Related actions</legend>
-				<template v-for="(related, idx) in relatedActions" :key="related.action?.bindingId || idx">
-					<ActionButton
-						v-if="related.action"
-						:action-data="related.action"
-						:prefilled-arguments="related.prefilledArguments"
-					/>
-				</template>
-			</fieldset>
-		</div>
-	</section>
+  <Section>
+    <template #title>
+      <span class="section-title-with-icon">
+        Entity Details:
+        <ActionIconGlyph
+          v-if="entityIcon"
+          class="entity-title-icon"
+          :glyph="entityIcon"
+        />
+        <span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
+      </span>
+    </template>
+    <template #toolbar>
+      <button
+        class="back-button"
+        @click="goBack"
+      >
+        <HugeiconsIcon
+          :icon="ArrowLeftIcon"
+          width="1.2em"
+          height="1.2em"
+        />
+        <span>Back</span>
+      </button>
+    </template>
+    <div v-if="!entityDetails">
+      <p>Loading entity details...</p>
+    </div>
+    <template v-else>
+      <dl>
+        <dt>Type</dt>
+        <dd>
+          <router-link
+            :to="{ name: 'Entities' }"
+            class="entity-type-link"
+          >
+            {{ entityType }}
+          </router-link>
+        </dd>
+        <dt v-if="entityDetails.title">
+          Title
+        </dt>
+        <dd v-if="entityDetails.title">
+          {{ entityDetails.title }}
+        </dd>
+        <template v-if="entityDetails.fields">
+          <template
+            v-for="(value, key) in entityDetails.fields"
+            :key="key"
+          >
+            <dt>{{ key }}</dt>
+            <dd>{{ value }}</dd>
+          </template>
+        </template>
+      </dl>
+      <p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">
+        No details available for this entity.
+      </p>
+    </template>
+  </Section>
+
+  <Section
+    v-if="entityDetails"
+    title="Dashboard Entity Directories"
+  >
+    <div
+      v-if="filteredDirectories.length > 0"
+      class="directories-section"
+    >
+      <ul class="directory-list">
+        <li
+          v-for="(directory, idx) in filteredDirectories"
+          :key="idx"
+        >
+          <router-link
+            :to="{
+              name: 'Dashboard',
+              params: {
+                title: directory,
+                entityType: entityType,
+                entityKey: entityKey
+              }
+            }"
+          >
+            {{ directory }}
+          </router-link>
+        </li>
+      </ul>
+    </div>
+    <p v-else>
+      No directories found for this entity.
+      <a
+        href="https://docs.olivetin.app/dashboards/entity-directories.html"
+        target="_blank"
+        rel="noopener noreferrer"
+      >Learn more</a>
+    </p>
+  </Section>
+
+  <section
+    v-if="entityDetails && relatedActions.length > 0"
+    class="transparent"
+  >
+    <div class="dashboard-row">
+      <fieldset>
+        <legend class="visually-hidden">
+          Related actions
+        </legend>
+        <template
+          v-for="(related, idx) in relatedActions"
+          :key="related.action?.bindingId || idx"
+        >
+          <ActionButton
+            v-if="related.action"
+            :action-data="related.action"
+            :prefilled-arguments="related.prefilledArguments"
+          />
+        </template>
+      </fieldset>
+    </div>
+  </section>
 </template>
 
 <script setup>
-	import { ref, computed, onMounted } from 'vue'
-	import { useRouter } from 'vue-router'
-	import { HugeiconsIcon } from '@hugeicons/vue'
-	import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
-	import Section from 'picocrank/vue/components/Section.vue'
-	import ActionButton from '../ActionButton.vue'
-	import ActionIconGlyph from '../components/ActionIconGlyph.vue'
-
-	const router = useRouter()
-	const entityDetails = ref(null)
-
-	const props = defineProps({
-		entityType: String,
-		entityKey: String
-	})
-
-	const filteredDirectories = computed(() => {
-		if (!entityDetails.value?.directories) {
-			return []
-		}
-		return entityDetails.value.directories.filter(d => d)
-	})
-
-	const relatedActions = computed(() => entityDetails.value?.relatedActions ?? [])
-
-	const entityIcon = computed(() => entityDetails.value?.icon ?? '')
-
-	function goBack() {
-		router.push({ name: 'Entities' })
-	}
-
-	async function fetchEntityDetails() {
-		try {
-			const response = await window.client.getEntity({
-				type: props.entityType,
-				uniqueKey: props.entityKey
-			})
-
-			entityDetails.value = response
-		} catch (err) {
-			console.error('Failed to fetch entity details:', err)
-			window.showBigError('fetch-entity-details', 'getting entity details', err, false)
-		}
-	}
-
-	onMounted(() => {
+import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { HugeiconsIcon } from '@hugeicons/vue'
+import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
+import Section from 'picocrank/vue/components/Section.vue'
+import ActionButton from '../ActionButton.vue'
+import ActionIconGlyph from '../components/ActionIconGlyph.vue'
+
+const router = useRouter()
+const entityDetails = ref(null)
+
+const props = defineProps({
+  entityType: String,
+  entityKey: String
+})
+
+const filteredDirectories = computed(() => {
+  if (!entityDetails.value?.directories) {
+    return []
+  }
+  return entityDetails.value.directories.filter(d => d)
+})
+
+const relatedActions = computed(() => entityDetails.value?.relatedActions ?? [])
+
+const entityIcon = computed(() => entityDetails.value?.icon ?? '')
+
+function goBack () {
+  router.push({ name: 'Entities' })
+}
+
+async function fetchEntityDetails () {
+  try {
+    const response = await window.client.getEntity({
+      type: props.entityType,
+      uniqueKey: props.entityKey
+    })
+
+    entityDetails.value = response
+  } catch (err) {
+    console.error('Failed to fetch entity details:', err)
+    window.showBigError('fetch-entity-details', 'getting entity details', err, false)
+  }
+}
+
+onMounted(() => {
 	    fetchEntityDetails()
-	})
+})
 
 </script>
 

+ 170 - 118
frontend/resources/vue/views/ExecutionView.vue

@@ -9,87 +9,141 @@
           class="action-details-title-link"
           :title="titleTooltip"
         >
-          <ActionIconGlyph class="action-title-icon" :glyph="icon" />
-          <LogActionTitle v-if="logEntry" :action-title="title" :justification="logEntry.justification" />
+          <ActionIconGlyph
+            class="action-title-icon"
+            :glyph="icon"
+          />
+          <LogActionTitle
+            v-if="logEntry"
+            :action-title="title"
+            :justification="logEntry.justification"
+          />
           <span v-else>{{ title }}</span>
         </router-link>
         <template v-else>
-          <LogActionTitle v-if="logEntry" :action-title="title" :justification="logEntry.justification" />
+          <LogActionTitle
+            v-if="logEntry"
+            :action-title="title"
+            :justification="logEntry.justification"
+          />
           <span v-else>{{ title }}</span>
         </template>
       </span>
     </template>
     <template #toolbar>
-			<button
-				v-for="dashboard in backToDashboards"
-				:key="dashboard.path"
-				@click="goToDashboard(dashboard.path)"
-				:title="'Back to ' + dashboard.title"
-				class="button neutral"
-			>
-				<HugeiconsIcon :icon="DashboardSquare01Icon" />
-				{{ dashboard.title }}
-			</button>
-			<button v-if="backToDashboards.length === 0" @click="goBack" title="Go back" class="button neutral">
-				<HugeiconsIcon :icon="ArrowLeftIcon" />
-				Back
-			</button>
+      <button
+        v-for="dashboard in backToDashboards"
+        :key="dashboard.path"
+        :title="'Back to ' + dashboard.title"
+        class="button neutral"
+        @click="goToDashboard(dashboard.path)"
+      >
+        <HugeiconsIcon :icon="DashboardSquare01Icon" />
+        {{ dashboard.title }}
+      </button>
+      <button
+        v-if="backToDashboards.length === 0"
+        title="Go back"
+        class="button neutral"
+        @click="goBack"
+      >
+        <HugeiconsIcon :icon="ArrowLeftIcon" />
+        Back
+      </button>
     </template>
 
-		<div v-if="logEntry" class = "flex-row">
-				<dl class = "fg1">
-					<dt>Duration</dt>
-					<dd><span v-html="duration"></span></dd>
-
-					<dt>Status</dt>
-					<dd class="execution-dialog-status">
-						<ActionStatusDisplay :log-entry="logEntry" :link-queued-status="true" />
-					</dd>
-				</dl>
+    <div
+      v-if="logEntry"
+      class="flex-row"
+    >
+      <dl class="fg1">
+        <dt>Duration</dt>
+        <dd><span v-html="duration" /></dd>
+
+        <dt>Status</dt>
+        <dd class="execution-dialog-status">
+          <ActionStatusDisplay
+            :log-entry="logEntry"
+            :link-queued-status="true"
+          />
+        </dd>
+      </dl>
     </div>
 
-		<div v-if="notFound" class="error-message padded-content">
-			<h3>Execution Not Found</h3>
-			<p>{{ errorMessage }}</p>
-			<p>The execution with ID <code>{{ executionTrackingId }}</code> could not be found.</p>
-			<router-link to="/logs">View all logs</router-link> or <router-link to="/">return to home</router-link>.
-		</div>
+    <div
+      v-if="notFound"
+      class="error-message padded-content"
+    >
+      <h3>Execution Not Found</h3>
+      <p>{{ errorMessage }}</p>
+      <p>The execution with ID <code>{{ executionTrackingId }}</code> could not be found.</p>
+      <router-link to="/logs">
+        View all logs
+      </router-link> or <router-link to="/">
+        return to home
+      </router-link>.
+    </div>
 
     <div class="xterm-output-container">
       <div class="xterm-overlay-toolbar">
-        <button type="button" class="xterm-overlay-button" @click="copyOutput" title="Copy to clipboard">
+        <button
+          type="button"
+          class="xterm-overlay-button"
+          title="Copy to clipboard"
+          @click="copyOutput"
+        >
           <HugeiconsIcon :icon="Copy01Icon" />
         </button>
-        <button type="button" class="xterm-overlay-button" @click="toggleSize" title="Toggle fullscreen">
-          <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-            <path fill="currentColor"
-                  d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z" />
+        <button
+          type="button"
+          class="xterm-overlay-button"
+          title="Toggle fullscreen"
+          @click="toggleSize"
+        >
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="1em"
+            height="1em"
+            viewBox="0 0 24 24"
+          >
+            <path
+              fill="currentColor"
+              d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z"
+            />
           </svg>
         </button>
       </div>
-      <div ref="xtermOutput"></div>
+      <div ref="xtermOutput" />
     </div>
 
-			<br />
-
-			<div class="flex-row g1 buttons padded-content">
-				<div class = "fg1" />
-
-					<button :disabled="!canRerun" @click="rerunAction" title="Rerun">
-						<HugeiconsIcon :icon="WorkoutRunIcon" />
-						Rerun
-					</button>
-					<button :disabled="!canKill" @click="killAction" title="Kill" id = "execution-dialog-kill-action">
-						<HugeiconsIcon :icon="Cancel02Icon" />
-						Kill
-					</button>
-				</div>
-
-	</Section>
+    <br>
+
+    <div class="flex-row g1 buttons padded-content">
+      <div class="fg1" />
+
+      <button
+        :disabled="!canRerun"
+        title="Rerun"
+        @click="rerunAction"
+      >
+        <HugeiconsIcon :icon="WorkoutRunIcon" />
+        Rerun
+      </button>
+      <button
+        id="execution-dialog-kill-action"
+        :disabled="!canKill"
+        title="Kill"
+        @click="killAction"
+      >
+        <HugeiconsIcon :icon="Cancel02Icon" />
+        Kill
+      </button>
+    </div>
+  </Section>
 </template>
 
 <script setup>
-	import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
 import ActionIconGlyph from '../components/ActionIconGlyph.vue'
 import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
 import LogActionTitle from '../components/LogActionTitle.vue'
@@ -113,8 +167,8 @@ const xtermOutput = ref(null)
 
 const props = defineProps({
   executionTrackingId: {
-	type: String,
-	required: true
+    type: String,
+    required: true
   }
 })
 
@@ -138,7 +192,7 @@ const errorMessage = ref('')
 let executionTicker = null
 let terminal = null
 
-function initializeTerminal() {
+function initializeTerminal () {
   terminal = new OutputTerminal(executionTrackingId.value)
   terminal.open(xtermOutput.value)
   terminal.resize(80, 40)
@@ -146,7 +200,7 @@ function initializeTerminal() {
   window.terminal = terminal
 }
 
-function toggleSize() {
+function toggleSize () {
   if (!xtermOutput.value) {
     return
   }
@@ -162,7 +216,7 @@ function toggleSize() {
   }
 }
 
-async function copyOutput() {
+async function copyOutput () {
   const text = terminal?.getBufferAsString?.() || logEntry.value?.output || ''
   if (!text) {
     return
@@ -175,7 +229,7 @@ async function copyOutput() {
   }
 }
 
-async function reset() {
+async function reset () {
   executionSeconds.value = 0
   executionTrackingId.value = 'notset'
   hideBasics.value = false
@@ -195,31 +249,31 @@ async function reset() {
   errorMessage.value = ''
 
   if (terminal) {
-	await terminal.reset()
-	terminal.fit()
+    await terminal.reset()
+    terminal.fit()
   }
 }
 
-function show(actionButton) {
+function show (actionButton) {
   if (actionButton) {
-	icon.value = actionButton.glyph ?? ''
+    icon.value = actionButton.glyph ?? ''
   }
 
   canKill.value = true
 
   // Clear existing ticker
   if (executionTicker) {
-	clearInterval(executionTicker)
+    clearInterval(executionTicker)
   }
 
   executionSeconds.value = 0
   executionTick()
   executionTicker = setInterval(() => {
-	executionTick()
+    executionTick()
   }, 1000)
 }
 
-async function rerunAction() {
+async function rerunAction () {
   const bindingId = logEntry.value?.bindingId
   if (!logEntry.value || !bindingId) {
     console.error('Cannot rerun: no action ID available')
@@ -247,36 +301,36 @@ async function rerunAction() {
   }
 }
 
-async function killAction() {
+async function killAction () {
   if (!executionTrackingId.value || executionTrackingId.value === 'notset') {
-	return
+    return
   }
 
   const killActionArgs = {
-	executionTrackingId: executionTrackingId.value
+    executionTrackingId: executionTrackingId.value
   }
 
   try {
-	await window.client.killAction(killActionArgs)
+    await window.client.killAction(killActionArgs)
   } catch (err) {
-	console.error('Failed to kill action:', err)
+    console.error('Failed to kill action:', err)
   }
 }
 
-function executionTick() {
+function executionTick () {
   executionSeconds.value++
   updateDuration(null)
 }
 
-function hideEverythingApartFromOutput() {
+function hideEverythingApartFromOutput () {
   hideDetailsOnResult.value = true
   hideBasics.value = true
   hideDetailsOnResult.value = true
   hideBasics.value = true
 }
 
-async function fetchExecutionResult(executionTrackingIdParam) {
-  console.log("fetchExecutionResult", executionTrackingIdParam)
+async function fetchExecutionResult (executionTrackingIdParam) {
+  console.log('fetchExecutionResult', executionTrackingIdParam)
 
   executionTrackingId.value = executionTrackingIdParam
   notFound.value = false
@@ -284,50 +338,49 @@ async function fetchExecutionResult(executionTrackingIdParam) {
   backToDashboards.value = []
 
   const executionStatusArgs = {
-	executionTrackingId: executionTrackingId.value
+    executionTrackingId: executionTrackingId.value
   }
 
   try {
-	const executionStatusResult = await window.client.executionStatus(executionStatusArgs)
+    const executionStatusResult = await window.client.executionStatus(executionStatusArgs)
 
-	await renderExecutionResult(executionStatusResult)
+    await renderExecutionResult(executionStatusResult)
   } catch (err) {
-	// Check if it's a "not found" error (404 or similar)
-	if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
+    // Check if it's a "not found" error (404 or similar)
+    if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
 	  notFound.value = true
 	  errorMessage.value = err.message || 'The execution could not be found in the system.'
-	} else {
+    } else {
 	  renderError(err)
-	}
-	throw err
+    }
+    throw err
   }
 }
 
-function updateDuration(logEntryParam) {
+function updateDuration (logEntryParam) {
   logEntry.value = logEntryParam
   if (logEntry.value == null) {
-	duration.value = executionSeconds.value + ' seconds'
-	duration.value = duration.value
+    duration.value = executionSeconds.value + ' seconds'
   } else if (!logEntry.value.executionStarted) {
-	duration.value = logEntry.value.datetimeStarted + ' (request time). Not executed.'
+    duration.value = logEntry.value.datetimeStarted + ' (request time). Not executed.'
   } else if (logEntry.value.executionStarted && !logEntry.value.executionFinished) {
-	duration.value = logEntry.value.datetimeStarted
+    duration.value = logEntry.value.datetimeStarted
   } else {
-	let delta = ''
-	try {
+    let delta = ''
+    try {
 		  delta = (new Date(logEntry.value.datetimeFinished) - new Date(logEntry.value.datetimeStarted)) / 1000
 	  delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
-	} catch (e) {
+    } catch (e) {
 	  console.warn('Failed to calculate delta', e)
-	}
-	duration.value = logEntry.value.datetimeStarted + ' &rarr; ' + logEntry.value.datetimeFinished
-	if (delta !== '') {
+    }
+    duration.value = logEntry.value.datetimeStarted + ' &rarr; ' + logEntry.value.datetimeFinished
+    if (delta !== '') {
 	  duration.value += ' (' + delta + ')'
-	}
+    }
   }
 }
 
-async function renderExecutionResult(res) {
+async function renderExecutionResult (res) {
   logEntry.value = res.logEntry
   if (res.backToDashboards) {
     backToDashboards.value = res.backToDashboards.slice(0, 3)
@@ -335,12 +388,12 @@ async function renderExecutionResult(res) {
 
   // Clear ticker
   if (executionTicker) {
-	clearInterval(executionTicker)
+    clearInterval(executionTicker)
   }
   executionTicker = null
 
   if (hideDetailsOnResult.value) {
-	hideDetails.value = true
+    hideDetails.value = true
   }
 
   executionTrackingId.value = res.logEntry.executionTrackingId
@@ -355,41 +408,41 @@ async function renderExecutionResult(res) {
   updateDuration(res.logEntry)
 
   if (terminal) {
-	await terminal.reset()
-	await terminal.write(res.logEntry.output, () => {
+    await terminal.reset()
+    await terminal.write(res.logEntry.output, () => {
 	  terminal.fit()
-	})
+    })
   }
 }
 
-function renderError(err) {
+function renderError (err) {
   window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
 }
 
-function handleClose() {
+function handleClose () {
   if (executionTicker) {
-	clearInterval(executionTicker)
+    clearInterval(executionTicker)
   }
 
   executionTicker = null
 }
 
-function cleanup() {
+function cleanup () {
   if (executionTicker) {
-	clearInterval(executionTicker)
+    clearInterval(executionTicker)
   }
   executionTicker = null
   if (terminal != null) {
-	terminal.close()
+    terminal.close()
   }
   terminal = null
 }
 
-function goBack() {
+function goBack () {
   router.back()
 }
 
-function goToDashboard(path) {
+function goToDashboard (path) {
   router.push(path)
 }
 
@@ -409,16 +462,15 @@ onMounted(() => {
   fetchExecutionResult(props.executionTrackingId)
 
   watch(
-	() => buttonResults[props.executionTrackingId],
-	(newResult, oldResult) => {
+    () => buttonResults[props.executionTrackingId],
+    (newResult, oldResult) => {
 	  if (newResult) {
-		renderExecutionResult({
+        renderExecutionResult({
 		  logEntry: newResult
-		})
+        })
 	  }
-	}
+    }
   )
-
 })
 
 onBeforeUnmount(() => {

+ 67 - 22
frontend/resources/vue/views/LoginView.vue

@@ -1,33 +1,78 @@
 <template>
-  <Section title="Login to OliveTin" class="small">
+  <Section
+    title="Login to OliveTin"
+    class="small"
+  >
     <div class="login-form">
-      <div v-if="!hasOAuth && !hasLocalLogin" class="login-disabled">
+      <div
+        v-if="!hasOAuth && !hasLocalLogin"
+        class="login-disabled"
+      >
         <span>This server is not configured with either OAuth, or local users, so you cannot login.</span>
       </div>
 
-      <div v-if="hasOAuth" class="login-oauth2">
+      <div
+        v-if="hasOAuth"
+        class="login-oauth2"
+      >
         <h3>OAuth Login</h3>
         <div class="oauth-providers">
-          <button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button"
-            @click="loginWithOAuth(provider)">
-            <span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
+          <button
+            v-for="provider in oauthProviders"
+            :key="provider.key"
+            class="oauth-button"
+            @click="loginWithOAuth(provider)"
+          >
+            <span
+              v-if="provider.icon"
+              class="provider-icon"
+              v-html="provider.icon"
+            />
             <span class="provider-name">Login with {{ provider.title }}</span>
           </button>
         </div>
       </div>
 
-      <div v-if="hasLocalLogin" class="login-local">
+      <div
+        v-if="hasLocalLogin"
+        class="login-local"
+      >
         <h3>Local Login</h3>
-        <form @submit.prevent="handleLocalLogin" class="local-login-form">
-          <div v-if="loginError" class="bad">
+        <form
+          class="local-login-form"
+          @submit.prevent="handleLocalLogin"
+        >
+          <div
+            v-if="loginError"
+            class="bad"
+          >
             {{ loginError }}
           </div>
 
-          <input id="username" v-model="username" type="text" name="username" autocomplete="username" required placeholder="Username" />
-          <input id="password" v-model="password" type="password" name="password" autocomplete="current-password" placeholder="Password"
-            required />
-
-          <button type="submit" :disabled="loading" class="login-button">
+          <input
+            id="username"
+            v-model="username"
+            type="text"
+            name="username"
+            autocomplete="username"
+            required
+            placeholder="Username"
+          >
+          <input
+            id="password"
+            v-model="password"
+            type="password"
+            name="password"
+            autocomplete="current-password"
+            placeholder="Password"
+            required
+          >
+
+          <button
+            type="submit"
+            :disabled="loading"
+            class="login-button"
+          >
             {{ loading ? 'Logging in...' : 'Login' }}
           </button>
         </form>
@@ -51,7 +96,7 @@ const hasOAuth = ref(false)
 const hasLocalLogin = ref(false)
 const oauthProviders = ref([])
 
-function loadLoginOptions() {
+function loadLoginOptions () {
   // Use the init response data that was loaded in App.vue
   if (window.initResponse) {
     hasOAuth.value = window.initResponse.oAuth2Providers && window.initResponse.oAuth2Providers.length > 0
@@ -65,7 +110,7 @@ function loadLoginOptions() {
   }
 }
 
-async function handleLocalLogin() {
+async function handleLocalLogin () {
   loading.value = true
   loginError.value = ''
 
@@ -83,7 +128,7 @@ async function handleLocalLogin() {
         window.initError = false
         window.initErrorMessage = ''
         window.initCompleted = true
-        
+
         // Update the header with new user info
         if (window.updateHeaderFromInit) {
           window.updateHeaderFromInit()
@@ -91,7 +136,7 @@ async function handleLocalLogin() {
       } catch (initErr) {
         console.error('Failed to reinitialize after login:', initErr)
       }
-      
+
       // Redirect to home page on successful login
       router.push('/')
     } else {
@@ -105,21 +150,21 @@ async function handleLocalLogin() {
   }
 }
 
-function loginWithOAuth(provider) {
+function loginWithOAuth (provider) {
   if (!provider.key) {
     console.error('OAuth provider missing key:', provider)
     return
   }
-  
+
   const providerKey = encodeURIComponent(provider.key)
   window.location.href = `/oauth/login?provider=${providerKey}`
 }
 
 onMounted(() => {
   loadLoginOptions()
-  
+
   // Also watch for when init response becomes available
-  const stopWatcher = watch(() => window.initResponse, () => {
+  watch(() => window.initResponse, () => {
     loadLoginOptions()
   }, { immediate: true })
 })

+ 33 - 20
frontend/resources/vue/views/LogsCalendarView.vue

@@ -1,9 +1,23 @@
 <template>
-  <Section :title="t('logs.calendar-title')" :padding="false">
+  <Section
+    :title="t('logs.calendar-title')"
+    :padding="false"
+  >
     <template #toolbar>
-      <router-link to="/logs" class="button neutral">
-        <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-          <path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
+      <router-link
+        to="/logs"
+        class="button neutral"
+      >
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="1em"
+          height="1em"
+          viewBox="0 0 24 24"
+        >
+          <path
+            fill="currentColor"
+            d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"
+          />
         </svg>
         {{ t('logs.back-to-list') }}
       </router-link>
@@ -52,7 +66,7 @@ const calendarEvents = computed(() => {
     .map(log => {
       const startDate = new Date(log.datetimeStarted)
       let endDate = log.datetimeFinished ? new Date(log.datetimeFinished) : null
-      
+
       // Validate end date
       if (endDate && isNaN(endDate.getTime())) {
         endDate = null
@@ -62,8 +76,8 @@ const calendarEvents = computed(() => {
         id: log.executionTrackingId,
         title: log.actionTitle || 'Untitled Action',
         date: startDate,
-        startDate: startDate,
-        endDate: endDate,
+        startDate,
+        endDate,
         actionIcon: log.actionIcon,
         user: log.user,
         tags: log.tags,
@@ -72,44 +86,43 @@ const calendarEvents = computed(() => {
     })
 })
 
-async function fetchLogs() {
+async function fetchLogs () {
   loading.value = true
   error.value = null
-  
+
   try {
     // Currently fetches only the default page (startOffset: 0)
     // Multi-page fetching: loop through pages until no more logs or limit reached
     const allLogs = []
     let startOffset = BigInt(0)
     const maxLogs = 10000 // Reasonable limit to prevent excessive API calls
-    const pageSize = 100 // Typical page size, will be updated from API response
-    
+
     while (allLogs.length < maxLogs) {
       const args = {
-        "startOffset": startOffset,
+        startOffset
       }
 
       const response = await window.client.getLogs(args)
       const pageLogs = response.logs || []
-      
+
       // If no logs returned, we've reached the end
       if (pageLogs.length === 0) {
         break
       }
-      
+
       // Append logs from this page
       allLogs.push(...pageLogs)
-      
+
       // Update offset for next page
       const currentPageSize = Number(response.pageSize) || pageLogs.length
       startOffset += BigInt(currentPageSize)
-      
+
       // If we got fewer logs than the page size, we've reached the end
       if (pageLogs.length < currentPageSize) {
         break
       }
     }
-    
+
     logs.value = allLogs
   } catch (err) {
     console.error('Failed to fetch logs:', err)
@@ -120,14 +133,14 @@ async function fetchLogs() {
   }
 }
 
-function handleEventClick(event) {
+function handleEventClick (event) {
   // Navigate to the execution view when clicking on a calendar event
   if (event.id) {
     router.push(`/logs/${event.id}`)
   }
 }
 
-function handleDayClick(date) {
+function handleDayClick (date) {
   // Navigate to logs list filtered by the selected date
   // Format date as YYYY-MM-DD using local date components to avoid timezone issues
   const year = date.getFullYear()
@@ -137,7 +150,7 @@ function handleDayClick(date) {
   router.push({ path: '/logs', query: { date: dateString } })
 }
 
-function handleMonthChange(month, year) {
+function handleMonthChange (month, year) {
   currentMonthIndex.value = month
   currentYear.value = year
   // Optionally fetch logs for the new month if needed

+ 216 - 120
frontend/resources/vue/views/LogsListView.vue

@@ -1,116 +1,212 @@
 <template>
-  <Section :title="t('logs.title')" :padding="false">
-      <template #toolbar>
-        <router-link to="/logs/queue" class="button neutral">
-          {{ t('logs.queue') }}
-        </router-link>
-        <router-link to="/logs/calendar" class="button neutral">
-          {{ t('logs.calendar') }}
-        </router-link>
-        <label class="input-with-icons">
-          <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-            <path fill="currentColor"
-              d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
-          </svg>
-          <input
-            :placeholder="t('logs.filter-placeholder')"
-            v-model="searchText"
-            list="logs-filter-suggestions"
-            :aria-invalid="filterError ? 'true' : 'false'"
+  <Section
+    :title="t('logs.title')"
+    :padding="false"
+  >
+    <template #toolbar>
+      <router-link
+        to="/logs/queue"
+        class="button neutral"
+      >
+        {{ t('logs.queue') }}
+      </router-link>
+      <router-link
+        to="/logs/calendar"
+        class="button neutral"
+      >
+        {{ t('logs.calendar') }}
+      </router-link>
+      <label class="input-with-icons">
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="1em"
+          height="1em"
+          viewBox="0 0 24 24"
+        >
+          <path
+            fill="currentColor"
+            d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
           />
-          <datalist id="logs-filter-suggestions">
-            <option v-for="suggestion in filterSuggestions" :key="suggestion" :value="suggestion" />
-          </datalist>
-          <button :title="t('logs.clear-filter')" :disabled="!searchText" @click="clearSearch">
-            <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-              <path fill="currentColor"
-                d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
-            </svg>
-          </button>
-        </label>
-      </template>
-
-      <div class="padding logs-intro">
-        <p>{{ t('logs.page-description') }}</p>
-        <details class="filter-help">
-          <summary>{{ t('logs.filter-help-title') }}</summary>
-          <p>{{ t('logs.filter-help-intro') }}</p>
-          <p>{{ t('logs.filter-help-fields') }}</p>
-          <p><code>{{ t('logs.filter-help-examples') }}</code></p>
-        </details>
-        <p v-if="filterError" class="filter-error" role="alert">{{ filterError }}</p>
-      </div>
-
-      <div v-show="logs.length > 0">
-        <table class="logs-table row-hover">
-          <thead>
-            <tr>
-              <th>
-                <div class="timestamp-header">
-                  <span>{{ t('logs.timestamp') }}</span>
-                  <span v-if="selectedDate" class="date-filter-indicator">
-                    <span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
-                    <button :title="t('logs.clear-date-filter')" @click="clearDateFilter" class="clear-date-button">
-                      <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-                        <path fill="currentColor"
-                          d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
-                      </svg>
-                    </button>
-                  </span>
-                </div>
-              </th>
-              <th>{{ t('logs.action') }}</th>
-              <th>{{ t('logs.metadata') }}</th>
-              <th>{{ t('logs.status') }}</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="log in logs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
-              <td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
-              <td>
-                <ActionIconGlyph class="icon" :glyph="log.actionIcon" />
-                <router-link :to="`/logs/${log.executionTrackingId}`">
-                  <LogActionTitle :action-title="log.actionTitle" :justification="log.justification" />
-                </router-link>
-              </td>
-              <td class="tags">
-                <span class="annotation">
-                  <span class="annotation-key">User:</span>
-                  <span class="annotation-val">{{ log.user }}</span>
-                </span>
-                <span v-if="log.tags && log.tags.length > 0" class="tag-list">
-                  <span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
-                </span>
-              </td>
-              <td class="exit-code">
-                <ActionStatusDisplay :logEntry="log" />
-              </td>
-            </tr>
-          </tbody>
-        </table>
-
-        <Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class = "padding"
-          @page-size-change="handlePageSizeChange" itemTitle="execution logs" />
-      </div>
-
-      <div v-show="logs.length === 0 && !loading && searchText && !filterError" class="empty-state padding">
-        <p>{{ t('logs.no-logs-for-filter') }}</p>
-        <button @click="clearSearch" class="button neutral">
-          {{ t('logs.clear-filter') }}
-        </button>
-      </div>
-
-      <div v-show="selectedDate && logs.length === 0 && !loading && !searchText" class="empty-state padding">
-        <p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
-        <button @click="clearDateFilter" class="button neutral">
-          {{ t('logs.clear-date-filter') }}
+        </svg>
+        <input
+          v-model="searchText"
+          :placeholder="t('logs.filter-placeholder')"
+          list="logs-filter-suggestions"
+          :aria-invalid="filterError ? 'true' : 'false'"
+        >
+        <datalist id="logs-filter-suggestions">
+          <option
+            v-for="suggestion in filterSuggestions"
+            :key="suggestion"
+            :value="suggestion"
+          />
+        </datalist>
+        <button
+          :title="t('logs.clear-filter')"
+          :disabled="!searchText"
+          @click="clearSearch"
+        >
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="1em"
+            height="1em"
+            viewBox="0 0 24 24"
+          >
+            <path
+              fill="currentColor"
+              d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
+            />
+          </svg>
         </button>
-      </div>
-
-      <div v-show="logs.length === 0 && !loading && !selectedDate && !searchText" class="empty-state padding">
-        <p>{{ t('logs.no-logs-to-display') }}</p>
-        <router-link to="/">{{ t('return-to-index') }}</router-link>
-      </div>
+      </label>
+    </template>
+
+    <div class="padding logs-intro">
+      <p>{{ t('logs.page-description') }}</p>
+      <details class="filter-help">
+        <summary>{{ t('logs.filter-help-title') }}</summary>
+        <p>{{ t('logs.filter-help-intro') }}</p>
+        <p>{{ t('logs.filter-help-fields') }}</p>
+        <p><code>{{ t('logs.filter-help-examples') }}</code></p>
+      </details>
+      <p
+        v-if="filterError"
+        class="filter-error"
+        role="alert"
+      >
+        {{ filterError }}
+      </p>
+    </div>
+
+    <div v-show="logs.length > 0">
+      <table class="logs-table row-hover">
+        <thead>
+          <tr>
+            <th>
+              <div class="timestamp-header">
+                <span>{{ t('logs.timestamp') }}</span>
+                <span
+                  v-if="selectedDate"
+                  class="date-filter-indicator"
+                >
+                  <span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
+                  <button
+                    :title="t('logs.clear-date-filter')"
+                    class="clear-date-button"
+                    @click="clearDateFilter"
+                  >
+                    <svg
+                      xmlns="http://www.w3.org/2000/svg"
+                      width="1em"
+                      height="1em"
+                      viewBox="0 0 24 24"
+                    >
+                      <path
+                        fill="currentColor"
+                        d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
+                      />
+                    </svg>
+                  </button>
+                </span>
+              </div>
+            </th>
+            <th>{{ t('logs.action') }}</th>
+            <th>{{ t('logs.metadata') }}</th>
+            <th>{{ t('logs.status') }}</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr
+            v-for="log in logs"
+            :key="log.executionTrackingId"
+            class="log-row"
+            :title="log.actionTitle"
+          >
+            <td class="timestamp">
+              {{ formatTimestamp(log.datetimeStarted) }}
+            </td>
+            <td>
+              <ActionIconGlyph
+                class="icon"
+                :glyph="log.actionIcon"
+              />
+              <router-link :to="`/logs/${log.executionTrackingId}`">
+                <LogActionTitle
+                  :action-title="log.actionTitle"
+                  :justification="log.justification"
+                />
+              </router-link>
+            </td>
+            <td class="tags">
+              <span class="annotation">
+                <span class="annotation-key">User:</span>
+                <span class="annotation-val">{{ log.user }}</span>
+              </span>
+              <span
+                v-if="log.tags && log.tags.length > 0"
+                class="tag-list"
+              >
+                <span
+                  v-for="tag in log.tags"
+                  :key="tag"
+                  class="tag"
+                >{{ tag }}</span>
+              </span>
+            </td>
+            <td class="exit-code">
+              <ActionStatusDisplay :log-entry="log" />
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <Pagination
+        :page-size="pageSize"
+        :total="totalCount"
+        :current-page="currentPage"
+        :page="currentPage"
+        class="padding"
+        item-title="execution logs"
+        @page-change="handlePageChange"
+        @page-size-change="handlePageSizeChange"
+      />
+    </div>
+
+    <div
+      v-show="logs.length === 0 && !loading && searchText && !filterError"
+      class="empty-state padding"
+    >
+      <p>{{ t('logs.no-logs-for-filter') }}</p>
+      <button
+        class="button neutral"
+        @click="clearSearch"
+      >
+        {{ t('logs.clear-filter') }}
+      </button>
+    </div>
+
+    <div
+      v-show="selectedDate && logs.length === 0 && !loading && !searchText"
+      class="empty-state padding"
+    >
+      <p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
+      <button
+        class="button neutral"
+        @click="clearDateFilter"
+      >
+        {{ t('logs.clear-date-filter') }}
+      </button>
+    </div>
+
+    <div
+      v-show="logs.length === 0 && !loading && !selectedDate && !searchText"
+      class="empty-state padding"
+    >
+      <p>{{ t('logs.no-logs-to-display') }}</p>
+      <router-link to="/">
+        {{ t('return-to-index') }}
+      </router-link>
+    </div>
   </Section>
 </template>
 
@@ -150,7 +246,7 @@ const filterSuggestions = [
 
 const { t } = useI18n()
 
-function updateDateFromRoute() {
+function updateDateFromRoute () {
   const dateParam = route.query.date
   if (dateParam) {
     selectedDate.value = dateParam
@@ -169,7 +265,7 @@ watch(searchText, () => {
   scheduleFetchLogs()
 })
 
-async function fetchLogs() {
+async function fetchLogs () {
   loading.value = true
   filterError.value = ''
   try {
@@ -206,7 +302,7 @@ async function fetchLogs() {
   }
 }
 
-function scheduleFetchLogs() {
+function scheduleFetchLogs () {
   if (fetchTimer) {
     clearTimeout(fetchTimer)
   }
@@ -215,18 +311,18 @@ function scheduleFetchLogs() {
   }, 400)
 }
 
-function clearSearch() {
+function clearSearch () {
   searchText.value = ''
 }
 
-function clearDateFilter() {
+function clearDateFilter () {
   selectedDate.value = null
   const query = { ...route.query }
   delete query.date
   router.push({ path: route.path, query })
 }
 
-function formatDateFilter(dateString) {
+function formatDateFilter (dateString) {
   try {
     const date = new Date(dateString + 'T00:00:00')
     return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
@@ -235,7 +331,7 @@ function formatDateFilter(dateString) {
   }
 }
 
-function formatTimestamp(timestamp) {
+function formatTimestamp (timestamp) {
   if (!timestamp) return 'Unknown'
   try {
     const date = new Date(timestamp)
@@ -245,18 +341,18 @@ function formatTimestamp(timestamp) {
   }
 }
 
-function handlePageChange(page) {
+function handlePageChange (page) {
   currentPage.value = page
   fetchLogs()
 }
 
-function handlePageSizeChange(newPageSize) {
+function handlePageSizeChange (newPageSize) {
   pageSize.value = newPageSize
   currentPage.value = 1
   fetchLogs()
 }
 
-function onExecutionEvent(evt) {
+function onExecutionEvent (evt) {
   const logEntry = getExecutionLogEntry(evt)
   if (!logEntry) {
     return

+ 64 - 16
frontend/resources/vue/views/LogsQueueView.vue

@@ -1,19 +1,40 @@
 <template>
-  <Section :title="t('logs.queue-title')" :padding="false">
+  <Section
+    :title="t('logs.queue-title')"
+    :padding="false"
+  >
     <template #toolbar>
-      <router-link to="/logs" class="button neutral">
-        <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
-          <path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
+      <router-link
+        to="/logs"
+        class="button neutral"
+      >
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="1em"
+          height="1em"
+          viewBox="0 0 24 24"
+        >
+          <path
+            fill="currentColor"
+            d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"
+          />
         </svg>
         {{ t('logs.back-to-list') }}
       </router-link>
     </template>
 
-    <p class="padding">{{ t('logs.queue-page-description') }}</p>
+    <p class="padding">
+      {{ t('logs.queue-page-description') }}
+    </p>
 
-    <div v-if="groups.length === 0 && !loading" class="empty-state padding">
+    <div
+      v-if="groups.length === 0 && !loading"
+      class="empty-state padding"
+    >
       <p>{{ t('logs.queue-empty') }}</p>
-      <router-link to="/logs">{{ t('logs.back-to-list') }}</router-link>
+      <router-link to="/logs">
+        {{ t('logs.back-to-list') }}
+      </router-link>
     </div>
   </Section>
 
@@ -24,7 +45,10 @@
   >
     <div class="section-header flex-row">
       <div class="fg1 queue-action-group-heading">
-        <ActionIconGlyph class="icon" :glyph="actionGroup.icon" />
+        <ActionIconGlyph
+          class="icon"
+          :glyph="actionGroup.icon"
+        />
         <h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
       </div>
       <ActionGroupLimitsLabel
@@ -44,23 +68,34 @@
           </tr>
         </thead>
         <tbody>
-          <template v-for="action in actionGroup.actions" :key="`${actionGroup.name}:${action.bindingId}`">
+          <template
+            v-for="action in actionGroup.actions"
+            :key="`${actionGroup.name}:${action.bindingId}`"
+          >
             <tr
               v-for="(entry, index) in action.entries"
               :key="entry.executionTrackingId"
               class="log-row"
               :title="action.actionTitle"
             >
-              <td class="timestamp">{{ formatTimestamp(entry.datetimeStarted) }}</td>
+              <td class="timestamp">
+                {{ formatTimestamp(entry.datetimeStarted) }}
+              </td>
               <td>
-                <ActionIconGlyph class="icon" :glyph="action.actionIcon" />
+                <ActionIconGlyph
+                  class="icon"
+                  :glyph="action.actionIcon"
+                />
                 <router-link :to="`/logs/${entry.executionTrackingId}`">
                   <LogActionTitle
                     :action-title="action.actionTitle"
                     :justification="entry.justification"
                   />
                 </router-link>
-                <span v-if="action.entityPrefix" class="queue-entity annotation">
+                <span
+                  v-if="action.entityPrefix"
+                  class="queue-entity annotation"
+                >
                   {{ action.entityPrefix }}
                 </span>
               </td>
@@ -69,15 +104,28 @@
                   <span class="annotation-key">User:</span>
                   <span class="annotation-val">{{ entry.user }}</span>
                 </span>
-                <span v-if="entry.tags && entry.tags.length > 0" class="tag-list">
-                  <span v-for="tag in entry.tags" :key="tag" class="tag">{{ tag }}</span>
+                <span
+                  v-if="entry.tags && entry.tags.length > 0"
+                  class="tag-list"
+                >
+                  <span
+                    v-for="tag in entry.tags"
+                    :key="tag"
+                    class="tag"
+                  >{{ tag }}</span>
                 </span>
               </td>
               <td class="exit-code">
-                <span v-if="!entry.executionFinished" class="queue-position">
+                <span
+                  v-if="!entry.executionFinished"
+                  class="queue-position"
+                >
                   {{ t('logs.queue-position', { position: index + 1 }) }}
                 </span>
-                <ActionStatusDisplay :logEntry="entry" link-queued-status />
+                <ActionStatusDisplay
+                  :log-entry="entry"
+                  link-queued-status
+                />
               </td>
             </tr>
           </template>

+ 12 - 6
frontend/resources/vue/views/NotFoundView.vue

@@ -4,12 +4,18 @@
       <div class="not-found-content">
         <h1>404</h1>
         <h2>Page Not Found</h2>
-        
+
         <div class="actions">
-          <button class = "button good" @click="goToHome">
+          <button
+            class="button good"
+            @click="goToHome"
+          >
             Go to Home
           </button>
-          <button class="button neutral" @click="goBack">
+          <button
+            class="button neutral"
+            @click="goBack"
+          >
             Go Back
           </button>
         </div>
@@ -22,10 +28,10 @@
 export default {
   name: 'NotFoundView',
   methods: {
-    goBack() {
+    goBack () {
       this.$router.go(-1)
     },
-    goToHome() {
+    goToHome () {
       this.$router.push('/')
     }
   }
@@ -57,4 +63,4 @@ export default {
   color: #666;
   margin-bottom: 2rem;
 }
-</style> 
+</style>

+ 52 - 22
frontend/resources/vue/views/UserControlPanel.vue

@@ -1,27 +1,58 @@
 <template>
-  <Section title="User Information" class="small">
-    <div v-if="!isLoggedIn" class="user-not-logged-in">
+  <Section
+    title="User Information"
+    class="small"
+  >
+    <div
+      v-if="!isLoggedIn"
+      class="user-not-logged-in"
+    >
       <p>You are not currently logged in.</p>
-      <p>To access user settings and logout, please <router-link to="/login">log in</router-link>.</p>
+      <p>
+        To access user settings and logout, please <router-link to="/login">
+          log in
+        </router-link>.
+      </p>
     </div>
 
-    <div v-else class="user-control-panel">
+    <div
+      v-else
+      class="user-control-panel"
+    >
       <dl class="user-info">
         <dt>Username</dt>
         <dd>{{ username }}</dd>
-        <dt v-if="userProvider !== 'system'">Provider</dt>
-        <dd v-if="userProvider !== 'system'">{{ userProvider }}</dd>
-        <dt v-if="usergroup">Group</dt>
-        <dd v-if="usergroup">{{ usergroup }}</dd>
-        <dt v-if="acls && acls.length > 0">Matched ACLs</dt>
+        <dt v-if="userProvider !== 'system'">
+          Provider
+        </dt>
+        <dd v-if="userProvider !== 'system'">
+          {{ userProvider }}
+        </dd>
+        <dt v-if="usergroup">
+          Group
+        </dt>
+        <dd v-if="usergroup">
+          {{ usergroup }}
+        </dd>
+        <dt v-if="acls && acls.length > 0">
+          Matched ACLs
+        </dt>
         <dd v-if="acls && acls.length > 0">
-          <span class="acl-tag" v-for="(acl, idx) in acls" :key="`acl-${idx}`">{{ acl }}</span>
+          <span
+            v-for="(acl, idx) in acls"
+            :key="`acl-${idx}`"
+            class="acl-tag"
+          >{{ acl }}</span>
         </dd>
       </dl>
 
       <div class="user-actions">
         <div class="action-buttons">
-          <button @click="handleLogout" class="button bad" :disabled="loggingOut">
+          <button
+            class="button bad"
+            :disabled="loggingOut"
+            @click="handleLogout"
+          >
             {{ loggingOut ? 'Logging out...' : 'Logout' }}
           </button>
         </div>
@@ -31,7 +62,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted, watch, onUnmounted } from 'vue'
+import { ref, onMounted, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
 import Section from 'picocrank/vue/components/Section.vue'
 
@@ -44,7 +75,7 @@ const usergroup = ref('')
 const loggingOut = ref(false)
 const acls = ref([])
 
-function updateUserInfo() {
+function updateUserInfo () {
   if (window.initResponse) {
     isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
     username.value = window.initResponse.authenticatedUser
@@ -53,7 +84,7 @@ function updateUserInfo() {
   }
 }
 
-async function fetchWhoAmI() {
+async function fetchWhoAmI () {
   try {
     const res = await window.client.whoAmI({})
     acls.value = res.acls || []
@@ -67,12 +98,12 @@ async function fetchWhoAmI() {
   }
 }
 
-async function handleLogout() {
+async function handleLogout () {
   loggingOut.value = true
-  
+
   try {
     await window.client.logout({})
-    
+
     // Re-initialize to get updated user context (should be guest)
     try {
       const initResponse = await window.client.init({})
@@ -80,7 +111,7 @@ async function handleLogout() {
       window.initError = false
       window.initErrorMessage = ''
       window.initCompleted = true
-      
+
       // Update the header with new user info
       if (window.updateHeaderFromInit) {
         window.updateHeaderFromInit()
@@ -88,7 +119,7 @@ async function handleLogout() {
     } catch (initErr) {
       console.error('Failed to reinitialize after logout:', initErr)
     }
-    
+
     // Redirect based on init response: if login is required, go to login page
     if (window.initResponse && window.initResponse.loginRequired) {
       router.push('/login')
@@ -102,13 +133,12 @@ async function handleLogout() {
   }
 }
 
-let watchInterval = null
+const watchInterval = null
 
 onMounted(() => {
   updateUserInfo()
   fetchWhoAmI()
-  
- })
+})
 
 onUnmounted(() => {
   if (watchInterval) {

+ 4 - 4
integration-tests/package-lock.json

@@ -13,7 +13,7 @@
       },
       "devDependencies": {
         "chai": "^6.2.2",
-        "eslint": "^10.5.0",
+        "eslint": "^10.6.0",
         "mocha": "^11.7.6",
         "selenium-webdriver": "^4.45.0"
       }
@@ -783,9 +783,9 @@
       }
     },
     "node_modules/eslint": {
-      "version": "10.5.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
-      "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
+      "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
       "dev": true,
       "license": "MIT",
       "workspaces": [

+ 1 - 1
integration-tests/package.json

@@ -12,7 +12,7 @@
   "license": "AGPL-3.0-only",
   "devDependencies": {
     "chai": "^6.2.2",
-    "eslint": "^10.5.0",
+    "eslint": "^10.6.0",
     "mocha": "^11.7.6",
     "selenium-webdriver": "^4.45.0"
   },

+ 3 - 3
lang/go.mod

@@ -1,11 +1,11 @@
 module github.com/OliveTin/OliveTin/langtool
 
-go 1.24.0
+go 1.25.0
 
 require (
-	github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac
+	github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
 	github.com/sirupsen/logrus v1.9.4
 	gopkg.in/yaml.v3 v3.0.1
 )
 
-require golang.org/x/sys v0.40.0 // indirect
+require golang.org/x/sys v0.47.0 // indirect

+ 4 - 16
lang/go.sum

@@ -1,28 +1,16 @@
-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/jamesread/golure v0.0.0-20250919212919-976d085a100c h1:v8gN2xXFQjkF0PsoGSqDviRNmPHcBsvl6rMSbvXz1sM=
-github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
-github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac h1:JQ6AC9tf2xhwTxMY9nuIeOPM7Cj0BDeCNgKPrNgQvtQ=
-github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
+github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp9swe2d6Y6ij2ecd16E+i2oHAH/xaA=
+github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
 github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
-golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-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=

+ 1 - 1
scripts/devcheck.py

@@ -46,7 +46,7 @@ TOOL_GROUPS: list[tuple[str, frozenset[str]]] = [
     ),
     (
         "Go codestyle (install via: make go-tools)",
-        frozenset({"gocyclo", "gocritic"}),
+        frozenset({"golangci-lint"}),
     ),
     (
         "Protocol buffers (install via: make -C service go-tools-all)",

+ 25 - 0
service/.golangci.yml

@@ -0,0 +1,25 @@
+version: "2"
+
+run:
+  timeout: 5m
+  tests: true
+
+linters:
+  default: none
+  enable:
+    - errcheck
+    - gocritic
+    - gocyclo
+    - ineffassign
+    - misspell
+    - staticcheck
+    - unconvert
+    - unused
+
+linters-settings:
+  gocyclo:
+    min-complexity: 5
+
+issues:
+  exclude-dirs:
+    - gen

+ 3 - 6
service/Makefile

@@ -30,8 +30,7 @@ compile: compile-armhf compile-x64-lin compile-x64-win
 codestyle: go-tools
 	go fmt ./...
 	go vet ./...
-	gocyclo -over 4 internal
-	gocritic check ./...
+	golangci-lint run ./...
 
 test: unittests
 
@@ -54,13 +53,11 @@ find-flakey-tests-inf:
 	go run ./scripts/find-flakey-tests-inf
 
 go-tools:
-	go install "github.com/fzipp/gocyclo/cmd/gocyclo"
-	go install "github.com/go-critic/go-critic/cmd/gocritic"
+	go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest"
 
 .PHONY: unittests unittests-fast find-flakey-tests find-flakey-tests-inf
 
 go-tools-all:
 	go install "github.com/bufbuild/buf/cmd/buf"
-	go install "github.com/fzipp/gocyclo/cmd/gocyclo"
-	go install "github.com/go-critic/go-critic/cmd/gocritic"
+	go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest"
 	go install "google.golang.org/protobuf/cmd/protoc-gen-go"

+ 210 - 62
service/go.mod

@@ -1,6 +1,6 @@
 module github.com/OliveTin/OliveTin
 
-go 1.25.10
+go 1.26
 
 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
 
@@ -10,36 +10,39 @@ require (
 	github.com/MicahParks/keyfunc/v3 v3.8.0
 	github.com/PaesslerAG/jsonpath v0.1.1
 	github.com/alexedwards/argon2id v1.0.0
-	github.com/bufbuild/buf v1.70.0
+	github.com/bufbuild/buf v1.71.0
+	github.com/expr-lang/expr v1.17.8
 	github.com/fsnotify/fsnotify v1.10.1
-	github.com/fzipp/gocyclo v0.6.0
-	github.com/go-critic/go-critic v0.14.3
+	github.com/go-viper/mapstructure/v2 v2.5.0
 	github.com/golang-jwt/jwt/v5 v5.3.1
+	github.com/golangci/golangci-lint/v2 v2.12.2
 	github.com/google/uuid v1.6.0
 	github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
 	github.com/knadh/koanf/parsers/yaml v1.1.0
 	github.com/knadh/koanf/providers/env v1.1.0
 	github.com/knadh/koanf/providers/file v1.2.1
 	github.com/knadh/koanf/providers/rawbytes v1.0.0
-	github.com/knadh/koanf/v2 v2.3.4
+	github.com/knadh/koanf/v2 v2.3.5
 	github.com/prometheus/client_golang v1.23.2
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/sirupsen/logrus v1.9.4
 	github.com/stretchr/testify v1.11.1
 	go.akshayshah.org/connectproto v0.6.0
-	golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
+	golang.org/x/exp v0.0.0-20260611194520-c48552f49976
 	golang.org/x/oauth2 v0.36.0
-	golang.org/x/sys v0.45.0
+	golang.org/x/sys v0.47.0
 	google.golang.org/protobuf v1.36.11
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
-	buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
+	4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
+	4d63.com/gochecknoglobals v0.2.2 // indirect
+	buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 // indirect
 	buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
 	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
-	buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 // indirect
-	buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 // indirect
+	buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 // indirect
+	buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 // indirect
 	buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect
 	buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda // indirect
 	buf.build/go/bufplugin v0.10.0 // indirect
@@ -50,33 +53,86 @@ require (
 	buf.build/go/spdx v0.2.0 // indirect
 	buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
 	cel.dev/expr v0.25.2 // indirect
+	charm.land/lipgloss/v2 v2.0.3 // indirect
+	codeberg.org/chavacava/garif v0.2.0 // indirect
+	codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
 	connectrpc.com/otelconnect v0.9.0 // indirect
-	github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+	dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
+	dev.gaijin.team/go/golib v0.6.0 // indirect
+	github.com/4meepo/tagalign v1.4.3 // indirect
+	github.com/Abirdcfly/dupword v0.1.7 // indirect
+	github.com/AdminBenni/iota-mixing v1.0.0 // indirect
+	github.com/AlwxSin/noinlineerr v1.0.5 // indirect
+	github.com/Antonboom/errname v1.1.1 // indirect
+	github.com/Antonboom/nilnil v1.1.1 // indirect
+	github.com/Antonboom/testifylint v1.6.4 // indirect
+	github.com/BurntSushi/toml v1.6.0 // indirect
+	github.com/ClickHouse/clickhouse-go-linter v1.2.0 // indirect
+	github.com/Djarvur/go-err113 v0.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.5.0 // indirect
 	github.com/MicahParks/jwkset v0.11.0 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
+	github.com/MirrexOne/unqueryvet v1.5.4 // indirect
+	github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
 	github.com/PaesslerAG/gval v1.2.4 // indirect
+	github.com/alecthomas/chroma/v2 v2.24.1 // indirect
+	github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
+	github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
+	github.com/alexkohler/prealloc v1.1.0 // indirect
+	github.com/alfatraining/structtag v1.0.0 // indirect
+	github.com/alingse/asasalint v0.0.11 // indirect
+	github.com/alingse/nilnesserr v0.2.0 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+	github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
+	github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 // indirect
+	github.com/bkielbasa/cyclop v1.2.3 // indirect
+	github.com/blizzy78/varnamelen v0.8.0 // indirect
+	github.com/bombsimon/wsl/v4 v4.7.0 // indirect
+	github.com/bombsimon/wsl/v5 v5.8.0 // indirect
+	github.com/breml/bidichk v0.3.3 // indirect
+	github.com/breml/errchkjson v0.4.1 // indirect
+	github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 // indirect
 	github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
+	github.com/butuzov/ireturn v0.4.1 // indirect
+	github.com/butuzov/mirror v1.3.0 // indirect
+	github.com/catenacyber/perfsprint v0.10.1 // indirect
+	github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/charithe/durationcheck v0.0.11 // indirect
+	github.com/charmbracelet/colorprofile v0.4.3 // indirect
+	github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
+	github.com/charmbracelet/x/ansi v0.11.7 // indirect
+	github.com/charmbracelet/x/term v0.2.2 // indirect
+	github.com/charmbracelet/x/termios v0.1.1 // indirect
+	github.com/charmbracelet/x/windows v0.2.2 // indirect
+	github.com/ckaznocha/intrange v0.3.1 // indirect
 	github.com/cli/browser v1.3.0 // indirect
+	github.com/clipperhouse/displaywidth v0.11.0 // indirect
+	github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
 	github.com/containerd/errdefs v1.0.0 // indirect
 	github.com/containerd/errdefs/pkg v0.3.0 // indirect
-	github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
-	github.com/cristalhq/acmd v0.12.0 // indirect
+	github.com/curioswitch/go-reassign v0.3.0 // indirect
+	github.com/daixiang0/gci v0.13.7 // indirect
+	github.com/dave/dst v0.27.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/denis-tingaikin/go-header v0.5.0 // indirect
 	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/cli v29.5.2+incompatible // indirect
-	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v28.5.2+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.9.7 // indirect
+	github.com/dlclark/regexp2 v1.12.0 // indirect
+	github.com/docker/cli v29.6.1+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.9.8 // indirect
 	github.com/docker/go-connections v0.7.0 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
-	github.com/expr-lang/expr v1.17.8 // indirect
-	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/go-chi/chi/v5 v5.2.5 // indirect
+	github.com/ettle/strcase v0.2.0 // indirect
+	github.com/fatih/color v1.19.0 // indirect
+	github.com/fatih/structtag v1.2.0 // indirect
+	github.com/felixge/httpsnoop v1.1.0 // indirect
+	github.com/firefart/nonamedreturns v1.0.6 // indirect
+	github.com/fzipp/gocyclo v0.6.0 // indirect
+	github.com/ghostiam/protogetter v0.3.20 // indirect
+	github.com/go-critic/go-critic v0.14.4 // indirect
+	github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-toolsmith/astcast v1.1.0 // indirect
@@ -84,83 +140,175 @@ require (
 	github.com/go-toolsmith/astequal v1.2.0 // indirect
 	github.com/go-toolsmith/astfmt v1.1.0 // indirect
 	github.com/go-toolsmith/astp v1.1.0 // indirect
-	github.com/go-toolsmith/pkgload v1.2.2 // indirect
 	github.com/go-toolsmith/strparse v1.1.0 // indirect
 	github.com/go-toolsmith/typep v1.1.0 // indirect
-	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
+	github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
+	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/godoc-lint/godoc-lint v0.11.2 // indirect
 	github.com/gofrs/flock v0.13.0 // indirect
-	github.com/google/cel-go v0.28.1 // indirect
+	github.com/golangci/asciicheck v0.5.0 // indirect
+	github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
+	github.com/golangci/go-printf-func-name v0.1.1 // indirect
+	github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
+	github.com/golangci/golines v0.15.0 // indirect
+	github.com/golangci/misspell v0.8.0 // indirect
+	github.com/golangci/plugin-module-register v0.1.2 // indirect
+	github.com/golangci/revgrep v0.8.0 // indirect
+	github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect
+	github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
+	github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
+	github.com/google/cel-go v0.29.2 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
-	github.com/google/go-containerregistry v0.21.6 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
+	github.com/google/go-containerregistry v0.21.7 // indirect
+	github.com/gordonklaus/ineffassign v0.2.0 // indirect
+	github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
+	github.com/gostaticanalysis/comment v1.5.0 // indirect
+	github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
+	github.com/gostaticanalysis/nilerr v0.1.2 // indirect
+	github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
+	github.com/hashicorp/go-version v1.9.0 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/hexops/gotextdiff v1.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jdx/go-netrc v1.0.0 // indirect
-	github.com/klauspost/compress v1.18.6 // indirect
+	github.com/jgautheron/goconst v1.10.0 // indirect
+	github.com/jjti/go-spancheck v0.6.5 // indirect
+	github.com/julz/importas v0.2.0 // indirect
+	github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
+	github.com/kisielk/errcheck v1.10.0 // indirect
+	github.com/kkHAIKE/contextcheck v1.1.6 // indirect
+	github.com/klauspost/compress v1.19.0 // indirect
 	github.com/klauspost/pgzip v1.2.6 // indirect
 	github.com/knadh/koanf/maps v0.1.2 // indirect
-	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/kulti/thelper v0.7.1 // indirect
+	github.com/kunwardeep/paralleltest v1.0.15 // indirect
+	github.com/lasiar/canonicalheader v1.1.2 // indirect
+	github.com/ldez/exptostd v0.4.5 // indirect
+	github.com/ldez/gomoddirectives v0.8.0 // indirect
+	github.com/ldez/grignotin v0.10.1 // indirect
+	github.com/ldez/structtags v0.6.1 // indirect
+	github.com/ldez/tagliatelle v0.7.2 // indirect
+	github.com/ldez/usetesting v0.5.0 // indirect
+	github.com/leonklingele/grouper v1.1.2 // indirect
+	github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
+	github.com/macabu/inamedparam v0.2.0 // indirect
+	github.com/magiconair/properties v1.8.6 // indirect
+	github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
+	github.com/manuelarte/funcorder v0.6.0 // indirect
+	github.com/maratori/testableexamples v1.0.1 // indirect
+	github.com/maratori/testpackage v1.1.2 // indirect
+	github.com/matoous/godox v1.1.0 // indirect
+	github.com/mattn/go-colorable v0.1.15 // indirect
 	github.com/mattn/go-isatty v0.0.22 // indirect
+	github.com/mattn/go-runewidth v0.0.23 // indirect
+	github.com/mgechev/revive v1.15.0 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/docker-image-spec v1.3.1 // indirect
-	github.com/moby/moby/api v1.54.2 // indirect
-	github.com/moby/moby/client v0.4.1 // indirect
-	github.com/moby/term v0.5.2 // indirect
-	github.com/morikuni/aec v1.1.0 // indirect
+	github.com/moby/moby/api v1.55.0 // indirect
+	github.com/moby/moby/client v0.5.0 // indirect
+	github.com/moricho/tparallel v0.3.2 // indirect
+	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/nakabonne/nestif v0.3.1 // indirect
+	github.com/nishanths/exhaustive v0.12.0 // indirect
+	github.com/nishanths/predeclared v0.2.2 // indirect
+	github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.1 // indirect
-	github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pelletier/go-toml v1.9.5 // indirect
+	github.com/pelletier/go-toml/v2 v2.3.1 // indirect
+	github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.67.5 // indirect
-	github.com/prometheus/procfs v0.20.1 // indirect
+	github.com/prometheus/common v0.69.0 // indirect
+	github.com/prometheus/procfs v0.21.1 // indirect
 	github.com/quasilyte/go-ruleguard v0.4.5 // indirect
+	github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
 	github.com/quasilyte/gogrep v0.5.0 // indirect
 	github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
 	github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
 	github.com/quic-go/qpack v0.6.0 // indirect
-	github.com/quic-go/quic-go v0.59.1 // indirect
+	github.com/quic-go/quic-go v0.60.0 // indirect
+	github.com/raeperd/recvcheck v0.2.0 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/rs/cors v1.11.1 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-	github.com/segmentio/asm v1.2.1 // indirect
-	github.com/segmentio/encoding v0.5.4 // indirect
+	github.com/ryancurrah/gomodguard v1.4.1 // indirect
+	github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
+	github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
+	github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
+	github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
+	github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
+	github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
+	github.com/securego/gosec/v2 v2.26.1 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/sivchari/containedctx v1.0.3 // indirect
+	github.com/sonatard/noctx v0.5.1 // indirect
+	github.com/sourcegraph/go-diff v0.8.0 // indirect
+	github.com/spf13/afero v1.15.0 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/cobra v1.10.2 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
-	github.com/stoewer/go-strcase v1.3.1 // indirect
-	github.com/tetratelabs/wazero v1.11.0 // indirect
+	github.com/spf13/viper v1.12.0 // indirect
+	github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
+	github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
+	github.com/stretchr/objx v0.5.2 // indirect
+	github.com/subosito/gotenv v1.4.1 // indirect
+	github.com/tetafro/godot v1.5.6 // indirect
+	github.com/tetratelabs/wazero v1.12.0 // indirect
 	github.com/tidwall/btree v1.8.1 // indirect
-	github.com/vbatts/tar-split v0.12.3 // indirect
-	go.lsp.dev/jsonrpc2 v0.10.0 // indirect
-	go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
-	go.lsp.dev/protocol v0.12.0 // indirect
-	go.lsp.dev/uri v0.3.0 // indirect
+	github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect
+	github.com/timonwong/loggercheck v0.11.0 // indirect
+	github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
+	github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
+	github.com/ultraware/funlen v0.2.0 // indirect
+	github.com/ultraware/whitespace v0.2.0 // indirect
+	github.com/uudashr/gocognit v1.2.1 // indirect
+	github.com/uudashr/iface v1.4.2 // indirect
+	github.com/xen0n/gosmopolitan v1.3.0 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+	github.com/yagipy/maintidx v1.0.0 // indirect
+	github.com/yeya24/promlinter v0.3.0 // indirect
+	github.com/ykadowak/zerologlint v0.1.5 // indirect
+	gitlab.com/bosi/decorder v0.4.2 // indirect
+	go-simpler.org/musttag v0.14.0 // indirect
+	go-simpler.org/sloglint v0.12.0 // indirect
+	go.augendre.info/arangolint v0.4.0 // indirect
+	go.augendre.info/fatcontext v0.9.0 // indirect
+	go.lsp.dev/jsonrpc2 v1.0.1 // indirect
+	go.lsp.dev/protocol v1.0.1 // indirect
+	go.lsp.dev/uri v1.0.1 // indirect
 	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
-	go.opentelemetry.io/otel v1.43.0 // indirect
-	go.opentelemetry.io/otel/metric v1.43.0 // indirect
-	go.opentelemetry.io/otel/trace v1.43.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
+	go.opentelemetry.io/otel v1.44.0 // indirect
+	go.opentelemetry.io/otel/metric v1.44.0 // indirect
+	go.opentelemetry.io/otel/trace v1.44.0 // indirect
 	go.uber.org/mock v0.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.28.0 // indirect
-	go.yaml.in/yaml/v2 v2.4.4 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
-	golang.org/x/crypto v0.52.0 // indirect
-	golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect
-	golang.org/x/mod v0.36.0 // indirect
-	golang.org/x/net v0.55.0 // indirect
-	golang.org/x/sync v0.20.0 // indirect
-	golang.org/x/term v0.43.0 // indirect
-	golang.org/x/text v0.37.0 // indirect
+	golang.org/x/crypto v0.53.0 // indirect
+	golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect
+	golang.org/x/mod v0.37.0 // indirect
+	golang.org/x/net v0.56.0 // indirect
+	golang.org/x/sync v0.22.0 // indirect
+	golang.org/x/term v0.44.0 // indirect
+	golang.org/x/text v0.39.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
-	golang.org/x/tools v0.45.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
-	google.golang.org/grpc v1.79.3 // indirect
+	golang.org/x/tools v0.47.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	honnef.co/go/tools v0.7.0 // indirect
+	mvdan.cc/gofumpt v0.10.0 // indirect
+	mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
 	mvdan.cc/xurls/v2 v2.6.0 // indirect
 	pluginrpc.com/pluginrpc v0.5.0 // indirect
 )

+ 512 - 467
service/go.sum

@@ -1,226 +1,240 @@
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 h1:zQ9C3e6FtwSZUFuKAQfpIKGFk5ZuRoGt5g35Bix55sI=
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ=
+4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=
+4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
+4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
+4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 h1:bHi5/cwz7IPRhFcrRA4iytTjDI3WwFkv1m5K7y+YOTc=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ=
 buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY=
 buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 h1:eQ6XRVUaYYZFOZvBsyrOYLWbw6464s5dVnHscxa0b8w=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2/go.mod h1:omxVRch3jEPMINnUipLsuRWoEhND6LPXELKBG7xzyDw=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2 h1:cMzWbIukJ5uk1M58CtqmBE7Ojacg/t2nAg4AbS78uX8=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2/go.mod h1:GL3rFhQQsaI3PCBa0y5X71UHs6q5E/Xf9Q8WXBxE7a8=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 h1:XPrWCd9ydEo5Ofv1aNJVJaxndMXLQjRO9vVzsJG3jL8=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1 h1:DcwtSdaY9CwXwPSOneDxJ/B0OCAgNPQQaQxAr/pTHvc=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1/go.mod h1:WjOwVG7wzFSwEkjCjHVRWEOdGYyON/TQYPabl7N2VGI=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 h1:f8pa4iy1Bs+hQ16f3jg22rV/StDKIRj1rNNWX5rLwZ8=
-buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1/go.mod h1:7MNigA51XJjPKrLnbcE61BmgW+pAp3mLW61gUqLBBRY=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 h1:Yreby6Ypa58wdQUEm9Fnc5g8n/jP487Dq3aK5yBYwfk=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 h1:QK2GkcPxqh2oG5mTMAHejculun8nxto+p7mlgh8fPTM=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 h1:szEsAEWp6MqAX3NHUFq3ZH3oGKyajcte99KrCz0MpQ0=
+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1/go.mod h1:VKtlgFOV7s6j5/wPr+d6c0Q80c8MF4OfZev4e0d9pNo=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 h1:JYYze3qnJZxo4PWTSZGKH+NJs0Vi2DYFo/NzUpa8Em4=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1/go.mod h1:5QgvXU4BhJqHgkO27sFJrRQHABWN4X6QnGvL8hWCJKk=
 buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q=
 buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts=
-buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8=
-buf.build/go/app v0.2.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo=
 buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda h1:eysSyjrJtkxU1A/9+Kv+1Mwq9K6BYBw+STIOVsZ256Y=
 buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ=
-buf.build/go/bufplugin v0.9.0 h1:ktZJNP3If7ldcWVqh46XKeiYJVPxHQxCfjzVQDzZ/lo=
-buf.build/go/bufplugin v0.9.0/go.mod h1:Z0CxA3sKQ6EPz/Os4kJJneeRO6CjPeidtP1ABh5jPPY=
 buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU=
 buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I=
 buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=
 buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w=
 buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=
 buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
-buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
-buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
-buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
-buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
-buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE=
-buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE=
 buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
 buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
-buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
-buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
 buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38=
 buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
 buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
-buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U=
-buf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg=
 buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2bPPU2Yzp7iAU0Wk9KJ2OoAhNiU=
 buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU=
-cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
-cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
 cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
 cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
-connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
-connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
-connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
-connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
+charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
+charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
+codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY=
+codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ=
+codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
+codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
 connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
 connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
-connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
-connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
+connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
+connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
 connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
 connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
-github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
-github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
+dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
+dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=
+dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=
+github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=
+github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
+github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ=
+github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4=
+github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
+github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
+github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
+github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
+github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=
+github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=
+github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=
+github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=
+github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
+github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/ClickHouse/clickhouse-go-linter v1.2.0 h1:zbm174up3hTKjp0wKZVnTzRiG7tSF5XZF0FJG/MuCBI=
+github.com/ClickHouse/clickhouse-go-linter v1.2.0/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
+github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
+github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
 github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
-github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
-github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
 github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=
 github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
+github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
+github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
+github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
+github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
 github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
 github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
 github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
 github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
 github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
 github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
+github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
+github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
+github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
+github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
+github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
 github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
+github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
+github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
+github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
+github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
+github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
+github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
+github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
+github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
+github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
+github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
 github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
+github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
+github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
+github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
-github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
+github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
+github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
+github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
 github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
+github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
+github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
+github.com/bombsimon/wsl/v5 v5.8.0 h1:JTkyfs4yl8SPejrCF2GdABXE+mO1WvM7iUYzRWlsxDs=
+github.com/bombsimon/wsl/v5 v5.8.0/go.mod h1:AbOLsulgkqP4ZnitHf9gwPtCOGlrzkk0jb0uNxRSY0o=
+github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
+github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
+github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
+github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
 github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
 github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
-github.com/bufbuild/buf v1.61.0 h1:JPaK/RM2eoheyzznW+1LxaFgN6xjBCi8s25q2kUbH9A=
-github.com/bufbuild/buf v1.61.0/go.mod h1:Xs3leBmxjL5tTnSVYfNwNXHXD1k5et3fR/tJyIyQl4s=
-github.com/bufbuild/buf v1.63.0 h1:vMIRozWqYcOU992FqcGgAp8LjoWVTzr52qEolUT+xi4=
-github.com/bufbuild/buf v1.63.0/go.mod h1:IWF+TIxwmk4DeyDmguN8WhxKFKQitcX2WnP+RlJlDiY=
-github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs=
-github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
-github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
-github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
-github.com/bufbuild/buf v1.66.0 h1:6kksYJpu6r45bvPJSTwNSwRqiAjrwB9YyU7skjNzFVo=
-github.com/bufbuild/buf v1.66.0/go.mod h1:tWVlwtIPZ7kzlCB9D0hbbfrroT0GNCybPdPQXq1i1Ac=
-github.com/bufbuild/buf v1.66.1 h1:wqmmU+6uoxB/eYDOmXq2To4qEXvOJN7gR6L9AxrPL1E=
-github.com/bufbuild/buf v1.66.1/go.mod h1:Vd3ELm8IePWaDJaS9FLy94FFOnLrjLi4mDxmXtw9Xio=
-github.com/bufbuild/buf v1.69.0 h1:q1YTnHJISHuoeUdmsuC9u+nb9rV8glM/TOsPNEteEzg=
-github.com/bufbuild/buf v1.69.0/go.mod h1:Q3KRCXSanDCMFs2zL/MqUwUQV0OUqs23P2sy58CW0nc=
-github.com/bufbuild/buf v1.70.0 h1:rGL4TGoy8F1DbQa4BSlMOVBBR7lWblfnKxUdOxmeFns=
-github.com/bufbuild/buf v1.70.0/go.mod h1:5gCCIpDmBzhiSJwqmxmbdN5aRZQYGmFSGnOBE7seP8c=
-github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
-github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
-github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
-github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
-github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREBMDRbAajlaYTtD4uC9f2UYbqqyhaveDqJ35G/w=
-github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
-github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
-github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
-github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 h1:nxt1QhP9rMQNFhHTdcNFwJ9wKCSdBjd28gz+qGDv4kM=
-github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
-github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156 h1:XOfIInPVufMjifwy3fli8qQVsGHWVCDVY/zp6elAOsY=
-github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
-github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91 h1:RPIMBLTMx/CRy0NVyb6yJDlGx2Vo84FsU+kAh46zqIA=
-github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91/go.mod h1:DhgqsRznX/F0sGkUYtTQJRP+q8xMReQRQ3qr+n1opWU=
-github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 h1:f4T4k/41jHHhp2Otl6ZShDedr4wF9b+NdqIfLezx4R4=
-github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
-github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
-github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
+github.com/bufbuild/buf v1.71.0 h1:/XLs8mFKjlM/LVbBBBQ3uSNcyEClyAV8lJiO7QR04d8=
+github.com/bufbuild/buf v1.71.0/go.mod h1:fbHMfu0nEbIPQT3IG7rAfPZXoFYEfZJuqFbAXu1wnRU=
+github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 h1:N0ZOKMwHpKsFhMGSxnB6JiVkM7SbH8IAKlGGO0SpQBA=
+github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
 github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
 github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
-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/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
+github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
+github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
+github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
+github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
+github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
+github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
+github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
 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/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
+github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
+github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
+github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
+github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
+github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
+github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
+github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
+github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
+github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
+github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
+github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
 github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
+github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
+github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
+github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
+github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
 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/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
-github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
-github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
-github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
-github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
-github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
-github.com/cristalhq/acmd v0.12.0 h1:RdlKnxjN+txbQosg8p/TRNZ+J1Rdne43MVQZ1zDhGWk=
-github.com/cristalhq/acmd v0.12.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
+github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
+github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
+github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
+github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
+github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
+github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
+github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
+github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
 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/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
+github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
-github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
-github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
-github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=
-github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU=
-github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
-github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
-github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
-github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
-github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
-github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
-github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
-github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
-github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA=
-github.com/docker/docker-credential-helpers v0.9.7/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
-github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
-github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
+github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8=
+github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
+github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
 github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
 github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
+github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
 github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
 github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-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/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
+github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
+github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
+github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
+github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
 github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
 github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
-github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
-github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
-github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
-github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
-github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
-github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
-github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo=
-github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
-github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
-github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
+github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
+github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
+github.com/go-critic/go-critic v0.14.4 h1:dSX4C3pWSeuMVxvQh6yG8U0ReSf3YOmKi4nwX5q7n/8=
+github.com/go-critic/go-critic v0.14.4/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
+github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
+github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
+github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
 github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
 github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
@@ -240,64 +254,104 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
 github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
 github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
 github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
-github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
-github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
 github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
+github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM=
+github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo=
 github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
 github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
-github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
-github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
 github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
-github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
-github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
-github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
-github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
-github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
-github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
-github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
-github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
+github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
+github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
+github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
+github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
+github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
+github.com/golangci/golangci-lint/v2 v2.12.2 h1:7+d1uY0bq1MU2UV3R5pW5Q7QWdcoq4naMRXM+gsJKrs=
+github.com/golangci/golangci-lint/v2 v2.12.2/go.mod h1:opqHHuIcTG2R+4akzWMd4o1BnD9/1LcjICWOujr91U8=
+github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
+github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
+github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
+github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
+github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
+github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
+github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
+github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
+github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg=
+github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk=
+github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
+github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
+github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
+github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
+github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k=
+github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
-github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
-github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI=
-github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
-github.com/google/go-containerregistry v0.21.3 h1:Xr+yt3VvwOOn/5nJzd7UoOhwPGiPkYW0zWDLLUXqAi4=
-github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc=
-github.com/google/go-containerregistry v0.21.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM=
-github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU=
-github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM=
-github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo=
+github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU=
+github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
+github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
+github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
+github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
+github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
+github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
+github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
+github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
+github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
+github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=
+github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=
+github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
+github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
+github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
+github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
+github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
+github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c h1:v8gN2xXFQjkF0PsoGSqDviRNmPHcBsvl6rMSbvXz1sM=
-github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
-github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac h1:JQ6AC9tf2xhwTxMY9nuIeOPM7Cj0BDeCNgKPrNgQvtQ=
-github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
 github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp9swe2d6Y6ij2ecd16E+i2oHAH/xaA=
 github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
 github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
 github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
+github.com/jgautheron/goconst v1.10.0 h1:Ptt+OoE4NaEWKhLrWrrN3IpZdGLiqaf7WLnEX/iv4Jw=
+github.com/jgautheron/goconst v1.10.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
 github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
 github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
-github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
-github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
-github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
-github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
-github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
-github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
-github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
-github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
-github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
-github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
+github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
+github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
+github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
+github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
+github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
+github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=
+github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
+github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
+github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
+github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
+github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
@@ -310,92 +364,139 @@ github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP
 github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
 github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/zsJyc/8qaSnzcdNI=
 github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo=
-github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
-github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
-github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
-github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
-github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc=
-github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
+github.com/knadh/koanf/v2 v2.3.5 h1:2dXJUYaKGm4SGYeoAtBviq9+02JZo/pxQ2ssOd60rJg=
+github.com/knadh/koanf/v2 v2.3.5/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
 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=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
+github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
+github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=
+github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
+github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
+github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
+github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
+github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=
+github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=
+github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
+github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
+github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
+github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
+github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk=
+github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI=
+github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
+github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
+github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
+github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
+github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
+github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
+github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
+github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
+github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
+github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
+github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
+github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
+github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
+github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
+github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
+github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=
+github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
+github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
 github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
+github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=
+github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 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/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
-github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
-github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
-github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
-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.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
-github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
-github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
-github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
+github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
+github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
+github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
+github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
+github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
+github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
+github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
+github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
+github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
+github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
+github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
+github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=
+github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
+github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps=
+github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
+github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
 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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
 github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
-github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
-github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
-github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
-github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
-github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
-github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
-github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
-github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
+github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
+github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
+github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
+github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 h1:YCvN6o1QTbVkeHMlTjnzfP02SvsmOZLlJdFzw/f9BTk=
+github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
-github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
-github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
-github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
-github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
-github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
-github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk=
+github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
+github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
+github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
 github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=
 github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4=
 github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
 github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
+github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
+github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
 github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
 github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
 github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
 github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
 github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
 github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
+github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
+github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
 github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
 github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
-github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
-github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
-github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
-github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
-github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
-github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
+github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
+github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
+github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=
+github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=
 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=
@@ -406,336 +507,280 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
 github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
-github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
-github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
-github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
-github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
-github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
-github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
-github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
+github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
+github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
+github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
+github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
+github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
+github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
+github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
+github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
+github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
+github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
+github.com/securego/gosec/v2 v2.26.1 h1:gdkttGhQFVehqRJ8grKH4DrpqM/QlPKNHBnl8QgcEC4=
+github.com/securego/gosec/v2 v2.26.1/go.mod h1:57UW4p0uoP3kxoTkhoo3axLdVAi+OWrLg/Ax/kdqtPE=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
 github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
+github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
+github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
+github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
+github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY=
+github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
 github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
 github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
-github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
+github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
+github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
+github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
+github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
+github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
 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.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=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+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=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
-github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
+github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
+github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
+github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
+github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
+github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
+github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
+github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
 github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
 github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
-github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
-github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
-github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw=
-github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0=
+github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 h1:SiHe5XLTn9sFWJ5pBwJ5FN/4j34q9ZlOAD//kMoMYp0=
+github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
+github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
+github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
+github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
+github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
+github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
+github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
+github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
+github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
+github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
+github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
+github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
+github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
+github.com/uudashr/iface v1.4.2 h1:06Vq5RKVYThBsj0Bnw4oasMjD1r+7CE/bcKOA8dVSvg=
+github.com/uudashr/iface v1.4.2/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
+github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
+github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
+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/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
+github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
+github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
+github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
+github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
+github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.akshayshah.org/attest v1.0.0 h1:RVGitcLbAO5i4PIJJDztZ/E9qQ8VSp1PS5PnR4Btg0c=
+gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
+gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
+go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
+go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
+go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
+go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
+go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
+go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
+go.akshayshah.org/attest v1.0.0 h1:f66BDlh/xo2KjIfmtqOFlj5cpn6mvGrP1LXY3Tex4L0=
 go.akshayshah.org/attest v1.0.0/go.mod h1:PnWzcW5j9dkyGwTlBmUsYpPnHG0AUPrs1RQ+HrldWO0=
 go.akshayshah.org/connectproto v0.6.0 h1:tqmysQF2AfvUeYS03mRAAZTFpiQeXqhGIDnH1GO2D2U=
 go.akshayshah.org/connectproto v0.6.0/go.mod h1:uA9TR/6MhBlLn0fh8VXRyL26EKTJlimWao4jbz7JHbA=
-go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
-go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
-go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=
-go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=
-go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=
-go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=
-go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
-go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
+go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
+go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
+go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=
+go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
+go.lsp.dev/jsonrpc2 v1.0.1 h1:JZAchpm6RcOuwEOnZ7kF3kA1EpEjdJZ/6rMSfOF3+F4=
+go.lsp.dev/jsonrpc2 v1.0.1/go.mod h1:WN+TesqrPumBmUdsuWyn9DHq3zl4i85L/qG0NLdj8qg=
+go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU=
+go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk=
+go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30=
+go.lsp.dev/uri v1.0.1/go.mod h1:06a0ghafs4PuqkLsJGUmlgPHw6+kH79edRXjOT/rA8s=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
-go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
-go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
-go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
-go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
-go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
-go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
-go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
-go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
-go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
-go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
-go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
-go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
-go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
-go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
-go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
-go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
-go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
-go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
-go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
-go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
-go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
-go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
-go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
-go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
-go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
-go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
-go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
-go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
-go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
-go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
-go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
-go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
-go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
-go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
-go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
-go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
-go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
+go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
+go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
+go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
+go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
+go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
+go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
+go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
+go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
+go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
 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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
 go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
-go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
 go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
-go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
-go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
 go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
 go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
-golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
-golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
-golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
-golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
-golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
-golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
-golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
-golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
-golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
-golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
-golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
-golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
-golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
-golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
-golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
-golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
-golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
-golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
+golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
-golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
-golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 h1:RMc8anw0hCPcg5CZYN2PEQ8nMwosk461R6vFwPrCFVg=
-golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
-golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a h1:n3SZDk8iNpMasCwQD7/0dIaCVf3gJiGZ9Rqa094jUN0=
-golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
-golang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90 h1:cfW8UCYSVdPblxA7qQe3o5Iad55Vsx4BFmuGS9RNOmc=
-golang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
-golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a h1:H06+n8uULVXJdhbdJ9+40jLzRcAPQP2h1UXcs01jzsk=
-golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
+golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 h1:GTD/WuaexTazIG/SxLOz4rEKZPDVilmVVC2nz4xhwfE=
+golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
-golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
-golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
-golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
-golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
-golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
-golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
-golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
-golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
-golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
+golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
-golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
-golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
-golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
-golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
-golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
-golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
-golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
-golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
-golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
-golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
-golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
 golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
 golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
 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-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 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-20200323222414-85ca7c5b95cd/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-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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/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=
 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.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
-golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
-golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
-golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
-golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
-golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
-golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
-golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
-golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
-golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
-golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
-golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
-golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
-golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
-golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
-golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
-golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
-golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
-golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
-golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
-golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
-golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
+golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
 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-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
+golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
-golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
-golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
-golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
-golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
-golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
-golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
-golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
-golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
-golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
+golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
+golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
+golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
+golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
 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=
-google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
-google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
-google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
-google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
-google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4=
-google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
-google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
-google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
-google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
-google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
-google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
-google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA=
-google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8=
-google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
-google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
-google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw=
-google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
-google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
-google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
-google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
 google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
-gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
+honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
+mvdan.cc/gofumpt v0.10.0 h1:yGGpRS2pBN2OQIi7b21IXknJna7faPkFaVfHLrN6Euo=
+mvdan.cc/gofumpt v0.10.0/go.mod h1:sU2ElXHzOEmvoPqfutYG7uunlueR4K2T1JFml40SzP4=
+mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI=
+mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU=
 mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
 mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
+pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
+pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
 pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
 pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=

+ 2 - 2
service/internal/acl/acl.go

@@ -134,7 +134,7 @@ func IsAllowedKill(cfg *config.Config, user *authpublic.AuthenticatedUser, actio
 	return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
 }
 
-func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser) bool {
+func isACLRelevantToAction(actionAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser) bool {
 	if !slices.Contains(user.Acls, acl.Name) {
 		// If the user does not have this ACL, then it is not relevant
 
@@ -156,7 +156,7 @@ func getRelevantAcls(cfg *config.Config, actionAcls []string, user *authpublic.A
 	var ret []*config.AccessControlList
 
 	for _, acl := range cfg.AccessControlLists {
-		if isACLRelevantToAction(cfg, actionAcls, acl, user) {
+		if isACLRelevantToAction(actionAcls, acl, user) {
 			ret = append(ret, acl)
 		}
 	}

+ 5 - 9
service/internal/auth/otjwt/jwt_test.go

@@ -46,7 +46,7 @@ func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
 	if err != nil {
 		t.Fatalf("failed to create temp file: %v", err)
 	}
-	defer tmpFile.Close()
+	defer func() { _ = tmpFile.Close() }()
 
 	t.Logf("Created File: %s", tmpFile.Name())
 
@@ -65,10 +65,6 @@ func newMux() *http.ServeMux {
 	return mux
 }
 
-func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string {
-	return createJWTTokenWithExpirationAndAudience(t, privateKey, expire, "")
-}
-
 func createJWTTokenWithExpirationAndAudience(t *testing.T, privateKey *rsa.PrivateKey, expire int64, audience string) string {
 	token := jwt.New(jwt.SigningMethodRS256)
 	claims := token.Claims.(jwt.MapClaims)
@@ -108,7 +104,7 @@ func setupJWTTestHandler(t *testing.T, cfg *config.Config) http.Handler {
 }
 
 func verifyJWTResponse(t *testing.T, res *http.Response, expectCode int) {
-	defer res.Body.Close()
+	defer func() { _ = res.Body.Close() }()
 	assert.Equal(t, expectCode, res.StatusCode)
 	body, _ := io.ReadAll(res.Body)
 	t.Logf("Response body: %s", string(body))
@@ -120,7 +116,7 @@ func testJwkValidation(t *testing.T, expire int64, expectCode int) {
 
 func testJwkValidationWithAudience(t *testing.T, expire int64, expectCode int, configAudience, tokenAudience string) {
 	privateKey, publicKeyPath := createKeys(t)
-	defer os.Remove(publicKeyPath)
+	defer func() { _ = os.Remove(publicKeyPath) }()
 
 	cfg := config.DefaultConfig()
 	cfg.AuthJwtPubKeyPath = publicKeyPath
@@ -186,7 +182,7 @@ func makeJWTRequest(t *testing.T, srv *httptest.Server, tokenStr string) *http.R
 
 func TestJWTHeader(t *testing.T) {
 	privateKey, publicKeyPath := createKeys(t)
-	defer os.Remove(publicKeyPath)
+	defer func() { _ = os.Remove(publicKeyPath) }()
 
 	cfg := config.DefaultConfig()
 	cfg.AuthJwtPubKeyPath = publicKeyPath
@@ -217,7 +213,7 @@ func TestJWTHeader(t *testing.T) {
 	defer srv.Close()
 
 	res := makeJWTRequest(t, srv, tokenStr)
-	defer res.Body.Close()
+	defer func() { _ = res.Body.Close() }()
 
 	assert.Equal(t, 200, res.StatusCode)
 	body, _ := io.ReadAll(res.Body)

+ 2 - 2
service/internal/auth/otoauth2/restapi_auth_oauth2.go

@@ -354,7 +354,7 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
 		return ret
 	}
 
-	defer res.Body.Close()
+	defer func() { _ = res.Body.Close() }()
 
 	contents, err := io.ReadAll(res.Body)
 
@@ -369,7 +369,7 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
 		log.Debugf("OAuth2 User Data: %v+", string(contents))
 	}
 
-	err = json.Unmarshal([]byte(contents), &userData)
+	err = json.Unmarshal(contents, &userData)
 
 	if err != nil {
 		log.Errorf("Failed to unmarshal user data: %v", err)

+ 8 - 8
service/internal/config/config_reloader_user_test.go

@@ -31,12 +31,12 @@ actions:
 	// Create temporary file
 	tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
 	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	// Write test config to file
 	_, err = tmpFile.WriteString(testConfig)
 	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	assert.NoError(t, tmpFile.Close())
 
 	// Load config using koanf
 	k := koanf.New(".")
@@ -82,11 +82,11 @@ actions:
 
 	tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml")
 	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	_, err = tmpFile.WriteString(testConfig)
 	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	assert.NoError(t, tmpFile.Close())
 
 	k := koanf.New(".")
 	err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
@@ -117,11 +117,11 @@ actions:
 
 	tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml")
 	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	_, err = tmpFile.WriteString(testConfig)
 	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	assert.NoError(t, tmpFile.Close())
 
 	k := koanf.New(".")
 	err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
@@ -148,11 +148,11 @@ actions:
 
 	tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml")
 	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	_, err = tmpFile.WriteString(testConfig)
 	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	assert.NoError(t, tmpFile.Close())
 
 	k := koanf.New(".")
 	err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())

+ 1 - 1
service/internal/executor/executor.go

@@ -982,7 +982,7 @@ func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) {
 }
 
 func hasBindingAndAction(req *ExecutionRequest) bool {
-	return !(req.Binding == nil || req.Binding.Action == nil)
+	return req.Binding != nil && req.Binding.Action != nil
 }
 
 func hasExec(req *ExecutionRequest) bool {

+ 0 - 10
service/internal/executor/group_concurrency.go

@@ -186,16 +186,6 @@ func (e *Executor) groupsHaveCapacityForQueued(req *ExecutionRequest) bool {
 	return true
 }
 
-func firstFullGroupName(e *Executor, req *ExecutionRequest) string {
-	for _, limit := range actionGroupLimits(req) {
-		if e.countActiveInGroup(limit.name) >= (limit.maxConcurrent + 1) {
-			return limit.name
-		}
-	}
-
-	return ""
-}
-
 func firstFullGroupNameLocked(e *Executor, req *ExecutionRequest) string {
 	for _, limit := range actionGroupLimits(req) {
 		if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) {

+ 1 - 1
service/internal/executor/timeout_context.go

@@ -55,7 +55,7 @@ func (tc *timeoutContext) setProcess(process *os.Process) {
 	tc.processMu.Unlock()
 
 	// If deadline already expired before process was set, kill now
-	if tc.Context.Err() == context.DeadlineExceeded && process != nil {
+	if tc.Err() == context.DeadlineExceeded && process != nil {
 		logEntry := &InternalLogEntry{Process: process}
 		if err := tc.executor.Kill(logEntry); err != nil {
 			log.WithFields(log.Fields{

+ 1 - 1
service/internal/filehelper/file_change_notify.go

@@ -74,7 +74,7 @@ func watchPath(ctx *watchContext) {
 		return
 	}
 
-	defer watcher.Close()
+	defer func() { _ = watcher.Close() }()
 
 	done := make(chan bool)
 

+ 1 - 2
service/internal/filehelper/file_write.go

@@ -15,8 +15,6 @@ func WriteFile(filename string, out []byte) {
 
 	if _, err := os.Stat(filename); os.IsNotExist(err) {
 		handle, err := os.Create(filename)
-		handle.Close()
-
 		if err != nil {
 			log.WithFields(log.Fields{
 				"error": err,
@@ -25,6 +23,7 @@ func WriteFile(filename string, out []byte) {
 			return
 		}
 
+		_ = handle.Close()
 	}
 
 	err := os.WriteFile(filename, out, 0600)

+ 1 - 1
service/internal/installationinfo/runtimeinfo.go

@@ -96,7 +96,7 @@ func getOsReleasePrettyName() string {
 		}
 	}
 
-	handle.Close()
+	_ = handle.Close()
 
 	return "notfound"
 }

+ 0 - 23
service/internal/servicehost/log_directory.go

@@ -1,7 +1,6 @@
 package servicehost
 
 import (
-	"os"
 	"path/filepath"
 )
 
@@ -20,25 +19,3 @@ func resolveLogDirectory(dir string, baseDir string) string {
 
 	return filepath.Join(baseDir, dir)
 }
-
-func executableDirectory() (string, error) {
-	ex, err := os.Executable()
-	if err != nil {
-		return "", err
-	}
-
-	return filepath.Dir(ex), nil
-}
-
-func configuredServiceLogDirectory(dir string) (string, error) {
-	if dir == "" {
-		return "", nil
-	}
-
-	exeDir, err := executableDirectory()
-	if err != nil {
-		return "", err
-	}
-
-	return resolveLogDirectory(dir, exeDir), nil
-}

+ 30 - 0
service/internal/servicehost/log_directory_windows.go

@@ -0,0 +1,30 @@
+//go:build windows
+
+package servicehost
+
+import (
+	"os"
+	"path/filepath"
+)
+
+func executableDirectory() (string, error) {
+	ex, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Dir(ex), nil
+}
+
+func configuredServiceLogDirectory(dir string) (string, error) {
+	if dir == "" {
+		return "", nil
+	}
+
+	exeDir, err := executableDirectory()
+	if err != nil {
+		return "", err
+	}
+
+	return resolveLogDirectory(dir, exeDir), nil
+}

+ 1 - 1
service/scripts/find-flakey-tests-inf/main.go

@@ -202,7 +202,7 @@ func appendFile(path, content string) error {
 	if err != nil {
 		return err
 	}
-	defer file.Close()
+	defer func() { _ = file.Close() }()
 
 	_, err = file.WriteString(content)
 	return err

+ 1 - 2
service/tools.go

@@ -5,7 +5,6 @@ package tools
 
 import (
 	_ "github.com/bufbuild/buf/cmd/buf"
-	_ "github.com/fzipp/gocyclo/cmd/gocyclo"
-	_ "github.com/go-critic/go-critic/cmd/gocritic"
+	_ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
 	_ "google.golang.org/protobuf/cmd/protoc-gen-go"
 )