James Read 1 mesiac pred
rodič
commit
c56b4f3855
77 zmenil súbory, kde vykonal 4680 pridanie a 2931 odobranie
  1. 1 0
      .github/PULL_REQUEST_TEMPLATE.md
  2. 9 4
      .github/workflows/build-and-release.yml
  3. 46 18
      .goreleaser.yml
  4. 6 5
      .pre-commit-config.yaml
  5. 1 0
      CONTRIBUTING.adoc
  6. 43 2
      docs/modules/ROOT/pages/security/acl.adoc
  7. 7 7
      docs/modules/dev/pages/signing.adoc
  8. 12 2
      frontend/.eslintrc.json
  9. 2 2
      frontend/Makefile
  10. 372 188
      frontend/package-lock.json
  11. 8 7
      frontend/package.json
  12. 242 184
      frontend/resources/vue/ActionButton.vue
  13. 418 326
      frontend/resources/vue/App.vue
  14. 253 186
      frontend/resources/vue/Dashboard.vue
  15. 30 24
      frontend/resources/vue/ExecutionButton.vue
  16. 4 1
      frontend/resources/vue/components/ActionGroupLimitsLabel.vue
  17. 36 30
      frontend/resources/vue/components/ActionIconGlyph.vue
  18. 60 56
      frontend/resources/vue/components/ActionStatusDisplay.vue
  19. 44 39
      frontend/resources/vue/components/Breadcrumbs.vue
  20. 24 11
      frontend/resources/vue/components/ChoiceChecklist.vue
  21. 25 19
      frontend/resources/vue/components/ChoiceCombobox.vue
  22. 19 7
      frontend/resources/vue/components/ConnectionBanner.vue
  23. 39 23
      frontend/resources/vue/components/DashboardComponent.vue
  24. 29 22
      frontend/resources/vue/components/DashboardComponentDirectory.vue
  25. 11 8
      frontend/resources/vue/components/DashboardComponentDisplay.vue
  26. 14 8
      frontend/resources/vue/components/DashboardComponentMostRecentExecution.vue
  27. 187 160
      frontend/resources/vue/components/EntityDefinitionSection.vue
  28. 75 72
      frontend/resources/vue/components/EntityInstancesTable.vue
  29. 45 25
      frontend/resources/vue/components/EntityListFilter.vue
  30. 4 1
      frontend/resources/vue/components/LogActionTitle.vue
  31. 15 18
      frontend/resources/vue/router.js
  32. 6 6
      frontend/resources/vue/utils/choiceChecklistHelpers.js
  33. 3 3
      frontend/resources/vue/utils/justificationTemplate.js
  34. 12 12
      frontend/resources/vue/utils/prefilledArguments.js
  35. 221 140
      frontend/resources/vue/views/ActionDetailsView.vue
  36. 92 22
      frontend/resources/vue/views/ActionExecConditionsView.vue
  37. 154 93
      frontend/resources/vue/views/ArgumentForm.vue
  38. 78 37
      frontend/resources/vue/views/DiagnosticsView.vue
  39. 44 41
      frontend/resources/vue/views/EntitiesView.vue
  40. 168 122
      frontend/resources/vue/views/EntityDetailsView.vue
  41. 170 118
      frontend/resources/vue/views/ExecutionView.vue
  42. 104 22
      frontend/resources/vue/views/LoginView.vue
  43. 33 20
      frontend/resources/vue/views/LogsCalendarView.vue
  44. 216 120
      frontend/resources/vue/views/LogsListView.vue
  45. 64 16
      frontend/resources/vue/views/LogsQueueView.vue
  46. 12 6
      frontend/resources/vue/views/NotFoundView.vue
  47. 52 22
      frontend/resources/vue/views/UserControlPanel.vue
  48. 4 4
      integration-tests/package-lock.json
  49. 1 1
      integration-tests/package.json
  50. 33 0
      integration-tests/tests/dashboardAcls/config.yaml
  51. 38 0
      integration-tests/tests/dashboardAcls/dashboardAcls.mjs
  52. 3 3
      lang/go.mod
  53. 4 16
      lang/go.sum
  54. 1 1
      scripts/devcheck.py
  55. 23 0
      service/.golangci.yml
  56. 3 6
      service/Makefile
  57. 210 62
      service/go.mod
  58. 512 467
      service/go.sum
  59. 40 38
      service/internal/acl/acl.go
  60. 86 0
      service/internal/acl/acl_dashboard_test.go
  61. 110 0
      service/internal/api/dashboard_acl_test.go
  62. 13 1
      service/internal/api/dashboards.go
  63. 5 9
      service/internal/auth/otjwt/jwt_test.go
  64. 3 3
      service/internal/auth/otoauth2/restapi_auth_oauth2.go
  65. 1 0
      service/internal/config/config.go
  66. 17 16
      service/internal/config/config_reloader_user_test.go
  67. 1 1
      service/internal/executor/executor.go
  68. 0 10
      service/internal/executor/group_concurrency.go
  69. 1 1
      service/internal/executor/timeout_context.go
  70. 5 1
      service/internal/filehelper/file_change_notify.go
  71. 6 2
      service/internal/filehelper/file_write.go
  72. 10 2
      service/internal/installationinfo/runtimeinfo.go
  73. 0 23
      service/internal/servicehost/log_directory.go
  74. 30 0
      service/internal/servicehost/log_directory_windows.go
  75. 6 3
      service/scripts/find-flakey-tests-inf/main.go
  76. 1 2
      service/tools.go
  77. 3 4
      var/windows/signpath-publish-signed.sh

+ 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.

+ 9 - 4
.github/workflows/build-and-release.yml

@@ -156,8 +156,10 @@ jobs:
         if: steps.release.outputs.new_release_published == 'true'
         uses: actions/upload-artifact@v7
         with:
-          name: unsigned-windows-zip
+          # Upload as-is so SignPath receives OliveTin-windows-amd64.zip, not a wrapper zip.
+          # With archive: false, the artifact name is the filename (`name` is ignored).
           path: dist/OliveTin-windows-amd64.zip
+          archive: false
           if-no-files-found: error
 
       - name: Upload unsigned Windows MSI for SignPath
@@ -165,8 +167,8 @@ jobs:
         if: steps.release.outputs.new_release_published == 'true'
         uses: actions/upload-artifact@v7
         with:
-          name: unsigned-windows-msi
           path: dist/OliveTin-windows-amd64.msi
+          archive: false
           if-no-files-found: error
 
       - name: Archive binaries
@@ -205,7 +207,7 @@ jobs:
             fi
           done
           if [[ "${missing}" -ne 0 ]]; then
-            echo "Windows signing is required before a draft release can be published. Configure SignPath secrets/vars (see docs/modules/dev/pages/signing.adoc)." >&2
+            echo "SignPath secrets/vars are required to upload signed Windows assets. Configure them (see docs/modules/dev/pages/signing.adoc)." >&2
             exit 1
           fi
 
@@ -219,6 +221,8 @@ jobs:
           artifact-configuration-slug: windows-zip
           github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }}
           wait-for-completion: true
+          # Preserve the signed .zip/.msi files; default decompress would unpack the zip.
+          skip-decompress: true
           output-artifact-directory: signed-windows-zip
 
       - name: Sign Windows MSI
@@ -231,9 +235,10 @@ jobs:
           artifact-configuration-slug: windows-msi
           github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }}
           wait-for-completion: true
+          skip-decompress: true
           output-artifact-directory: signed-windows-msi
 
-      - name: Publish signed Windows assets and undraft release
+      - name: Upload signed Windows assets
         env:
           GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}

+ 46 - 18
.goreleaser.yml

@@ -6,6 +6,7 @@ before:
     - make windows-resources VERSION={{ .Version }}
 
 builds:
+  # id defaults to project_name (OliveTin) — required for notarize.macos defaults.
   - skip: '{{ isEnvSet "GORELEASER_SKIP_BUILD" }}'
     env:
       - CGO_ENABLED=0
@@ -14,7 +15,6 @@ builds:
     dir: service
     goos:
       - linux
-      - windows
       - darwin
       - freebsd
 
@@ -33,19 +33,31 @@ builds:
       - goos: darwin
         goarch: arm # Mac does not work on [32bit] arm
 
-      - goos: windows
-        goarch: arm
-
-      - goos: windows # Does anyone use Windows on arm64?
-        goarch: arm64
+    ldflags:
+      - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
 
+  # Built and archived for SignPath, but excluded from the GitHub release upload.
+  - id: windows
+    skip: '{{ isEnvSet "GORELEASER_SKIP_BUILD" }}'
+    env:
+      - CGO_ENABLED=0
+    binary: OliveTin
+    main: .
+    dir: service
+    goos:
+      - windows
+    goarch:
+      - amd64
     ldflags:
       - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
 
 checksum:
   name_template: 'checksums.txt'
-  extra_files:
-    - glob: ./dist/OliveTin-windows-amd64.msi
+  # Exclude the windows archive; signed zip/MSI checksums are added after SignPath.
+  ids:
+    - default
+    - openrc
+
 snapshot:
   version_template: "{{ .Branch }}-{{ .ShortCommit }}"
 changelog:
@@ -73,7 +85,26 @@ changelog:
       - '^Merge branch'
 
 archives:
-  - formats: tar.gz
+  - id: default
+    ids:
+      - OliveTin
+    formats: tar.gz
+    files:
+      - config.yaml
+      - LICENSE
+      - README.md
+      - src: Dockerfile.singlearch
+        dst: Dockerfile
+      - examples/backupScript.sh
+      - webui
+      - var
+    name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
+    wrap_in_directory: true
+
+  - id: windows
+    ids:
+      - windows
+    formats: zip
     files:
       - config.yaml
       - LICENSE
@@ -85,9 +116,6 @@ archives:
       - var
     name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
     wrap_in_directory: true
-    format_overrides:
-      - goos: windows
-        formats: zip
 
 # dockers_v2: single multi-platform build with buildx; keeps default provenance + SBOM.
 # Replaces legacy dockers + docker_manifests (avoids "is a manifest list" when attestations are on).
@@ -187,8 +215,7 @@ nfpms:
 notarize:
   macos:
     - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
-      ids:
-        - default
+      # Defaults to project_name when omitted; must match builds[].id (also project_name).
       sign:
         certificate: "{{.Env.MACOS_SIGN_P12}}"
         password: "{{.Env.MACOS_SIGN_PASSWORD}}"
@@ -200,10 +227,11 @@ notarize:
         timeout: 30m
 
 release:
-  # Stay draft until the sign-windows job replaces unsigned Windows assets and undrafts.
-  draft: true
-  extra_files:
-    - glob: ./dist/OliveTin-windows-amd64.msi
+  # Publish immediately without Windows assets; sign-windows uploads signed zip/MSI later.
+  draft: false
+  ids:
+    - default
+    - openrc
   footer: |
     ## Container images (from GitHub)
 

+ 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/|Makefile)
 
       - id: frontend-codestyle
         name: frontend-codestyle
         entry: make frontend-codestyle
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^(frontend/|Makefile)
 
       - id: service-unittests
         name: service-unittests
         entry: make service-unittests
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^(service/|proto/|lang/|Makefile)
 
       - id: service-build
         name: service-build
         entry: make service
         language: system
         pass_filenames: false
-        always_run: true
+        files: ^(service/|proto/|lang/|Makefile)
 
       - 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

+ 43 - 2
docs/modules/ROOT/pages/security/acl.adoc

@@ -74,7 +74,7 @@ defaultPermissions:
   logs: true
 ----
 
-In the example above, all users will start off with the permissions to only see action logs - but will not be able to view or execute actions. 
+In the example above, all users will start off with the permissions to only see action logs - but will not be able to view or execute actions.
 
 It is then possible to add an "admins" ACL on top of every action. In the example below, we define one extra ACL called "admins", which matches any users with the usergroup also called "admins". This ACL will then be applied to all actions, and will allow users in the "admins" usergroup to view and execute the action.
 
@@ -116,6 +116,48 @@ accessControlLists:
     addToEveryAction: true
 ```
 
+== ACLs and Dashboards
+
+Root dashboards can also list `acls`. This controls whether the **whole dashboard page** is visible (including `display` widgets and entity fieldsets), not just action buttons.
+
+* If a dashboard has **no** `acls` (or an empty list), it is unrestricted — anyone can see it in the side menu (subject to the usual “empty dashboard” hiding).
+* If a dashboard lists one or more `acls`, access uses the same allow-list rules as actions: a matching ACL that grants `view`, otherwise `defaultPermissions.view`.
+* `addToEveryAction` does **not** apply to dashboards. List the ACL on the dashboard explicitly when you want to restrict it.
+* Nested fieldsets and directories do not have their own `acls`; the root dashboard decision covers the whole page.
+
+Action `acls` still control individual buttons. Use dashboard `acls` when you need to hide a page that contains status or other non-action content from some users.
+
+[source,yaml]
+.`config.yaml`
+----
+defaultPermissions:
+  view: false
+  exec: false
+
+accessControlLists:
+  - name: admins
+    matchUsergroups:
+      - admins
+    permissions:
+      view: true
+      exec: true
+
+dashboards:
+  - title: Public tools
+    contents:
+      - title: Welcome
+        type: display
+
+  - title: Services
+    acls:
+      - admins
+    contents:
+      - title: 'Status: running'
+        type: display
+----
+
+In the example above, guests can open **Public tools**, but **Services** is hidden from the side menu and cannot be loaded by deep link.
+
 == ACL Matching - usernames and usergroups.
 
 You can match users based on their usergroup which is the most common, but it is also possible to match based on the user's username.
@@ -149,4 +191,3 @@ Now that you understand ACLs, here's how to implement them:
 * xref:security/local.adoc[Set up local users] - Create users for ACL matching
 * xref:security/oauth2.adoc[Configure OAuth2] - Set up OAuth2 for user groups
 * xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for ACL design
-

+ 7 - 7
docs/modules/dev/pages/signing.adoc

@@ -3,7 +3,7 @@
 OliveTin signs release binaries on two platforms:
 
 * **macOS** — Developer ID + notarization via [quill](https://github.com/anchore/quill) inside GoReleaser (optional only when `MACOS_SIGN_P12` is unset).
-* **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (required before a draft release is published).
+* **Windows** — Authenticode via [SignPath Foundation](https://signpath.org/) in a separate GitHub Actions job (signed zip/MSI are uploaded after the release is published).
 
 ## macOS release signing
 
@@ -117,14 +117,14 @@ A signed and notarized binary should report `accepted` with `source=Notarized De
 
 Windows Authenticode signing uses [SignPath Foundation](https://signpath.org/) (free for qualifying open-source projects). It does **not** use GoReleaser Pro.
 
-GoReleaser creates a **draft** GitHub release with unsigned Windows assets. A separate `sign-windows` job submits those assets to SignPath, replaces them on the draft (including updated `checksums.txt`), then publishes the release.
+GoReleaser publishes a GitHub release **without** Windows zip/MSI assets. A separate `sign-windows` job submits the unsigned Windows files (as workflow artifacts) to SignPath, then uploads the signed zip/MSI and updates `checksums.txt` on the already-published release.
 
 Signed artifacts:
 
 * `OliveTin.exe` inside `OliveTin-windows-amd64.zip`
 * nested `OliveTin.exe` and the `OliveTin-windows-amd64.msi` installer (deep signing)
 
-Signing is **required** to publish. If SignPath secrets/vars are missing, `sign-windows` fails and the draft stays unpublished.
+If SignPath secrets/vars are missing or signing fails, the release still publishes; Windows assets are simply missing until a successful `sign-windows` run. Install URLs for the Windows zip may 404 until signing finishes.
 
 ### Prerequisites
 
@@ -168,9 +168,9 @@ In **Settings → Secrets and variables → Actions**:
 
 On a new semantic-release from `main`:
 
-1. GoReleaser publishes container images and creates a **draft** GitHub release (including unsigned Windows zip/MSI).
-2. The build job uploads those Windows files as GitHub Actions artifacts.
-3. The `sign-windows` job submits each artifact to SignPath, waits for completion, then runs `var/windows/signpath-publish-signed.sh` to clobber-upload signed files, refresh `checksums.txt`, and undraft the release.
+1. GoReleaser publishes container images and a GitHub release **without** Windows zip/MSI assets (those are built locally for SignPath only).
+2. The build job uploads the unsigned Windows files as GitHub Actions artifacts.
+3. The `sign-windows` job submits each artifact to SignPath, waits for completion, then runs `var/windows/signpath-publish-signed.sh` to upload the signed files and refresh `checksums.txt`.
 
 All jobs in this chain use GitHub-hosted runners (required by SignPath for OSS projects).
 
@@ -188,7 +188,7 @@ signtool verify /pa OliveTin-windows-amd64.msi
 
 ### Configuration reference
 
-- Draft release: `release.draft: true` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
+- Release publish (no Windows assets initially): `release.draft: false` and `release.ids` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
 - CI job: `sign-windows` in link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`]
 - Publish helper: link:https://github.com/OliveTin/OliveTin/blob/main/var/windows/signpath-publish-signed.sh[`var/windows/signpath-publish-signed.sh`]
 - SignPath artifact configs (reference only): link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-zip.xml[`signpath/windows-zip.xml`], link:https://github.com/OliveTin/OliveTin/blob/main/docs/modules/dev/signpath/windows-msi.xml[`signpath/windows-msi.xml`]

+ 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

+ 372 - 188
frontend/package-lock.json

@@ -11,38 +11,39 @@
 			"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": {
-				"node": ">=22.0.0"
+				"node": "^20.19.0 || >=22.12.0"
 			}
 		},
 		"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",

+ 8 - 7
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,22 +25,22 @@
 	"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"
 	},
 	"engines": {
-		"node": ">=22.0.0"
+		"node": "^20.19.0 || >=22.12.0"
 	}
 }

+ 242 - 184
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,117 @@ 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
+    if (bindingId.value) {
+      rateLimits[bindingId.value] = rateLimitExpires.value
+    }
+    updateRateLimitStatus()
   } else if (json.datetimeRateLimitExpires === '') {
-	// Explicitly clear if empty string
-	rateLimitExpires.value = 0
-	updateRateLimitStatus()
+    // Explicitly clear if empty string
+    rateLimitExpires.value = 0
+    if (bindingId.value) {
+      rateLimits[bindingId.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 +365,138 @@ async function pollExecutionUntilDone (trackingId) {
   }
 }
 
-async function startAction(actionArgs) {
+let stopButtonResultWatch = null
+
+function stopWatchingButtonResult () {
+  if (stopButtonResultWatch) {
+    stopButtonResultWatch()
+    stopButtonResultWatch = null
+  }
+}
+
+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) => {
+  stopWatchingButtonResult()
+  stopButtonResultWatch = watch(
+    () => 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)
+    stopWatchingButtonResult()
+    console.error('Failed to start action:', err)
   }
 }
 
-function onLogEntryChanged(logEntry) {
+function onLogEntryChanged (logEntry) {
+  if (!logEntry) {
+    return
+  }
+
   if (logEntry.executionFinished) {
-	onExecutionFinished(logEntry)
+    stopWatchingButtonResult()
+    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 +506,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>
 

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

@@ -1,58 +1,125 @@
 <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 && !loadError"
+    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>
+  <section
+    v-else-if="loadError"
+    style="text-align: center; padding: 2em;"
+    class="bad"
+  >
+    <h2 style="color: var(--error);">
+      Failed to Load Dashboard
+    </h2>
+    <p>{{ loadError }}</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,189 +130,190 @@ 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()
 const dashboard = ref(null)
 const loadingTime = ref(0)
 const initError = ref(null)
+const loadError = ref(null)
 let loadingTimer = null
 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,
-        }
+  try {
+    const request = {
+      title
+    }
 
-        if (props.entityType && props.entityKey) {
-            request.entityType = props.entityType
-            request.entityKey = props.entityKey
-        }
+    if (props.entityType && props.entityKey) {
+      request.entityType = props.entityType
+      request.entityKey = props.entityKey
+    }
 
-        const ret = await window.client.getDashboard(request)
+    const ret = await window.client.getDashboard(request)
 
-        if (requestId !== dashboardRequestId) {
-            return
-        }
+    if (requestId !== dashboardRequestId) {
+      return
+    }
 
-        if (!ret || !ret.dashboard) {
-            throw new Error('No dashboard found')
-        }
+    if (!ret || !ret.dashboard) {
+      throw new Error('No dashboard found')
+    }
 
-        dashboard.value = ret.dashboard
-        const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
-        document.title = ret.dashboard.title + ' - ' + pageTitle
+    dashboard.value = ret.dashboard
+    const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
+    document.title = ret.dashboard.title + ' - ' + pageTitle
 
-        // Clear any previous init error since we successfully loaded
-        initError.value = null
+    // Clear any previous errors since we successfully loaded
+    initError.value = null
+    loadError.value = null
 
-        // Stop the loading timer once dashboard is loaded
-        if (loadingTimer) {
-            clearInterval(loadingTimer)
-            loadingTimer = null
-        }
+    // Stop the loading timer once dashboard is loaded
+    if (loadingTimer) {
+      clearInterval(loadingTimer)
+      loadingTimer = null
+    }
 
-        // Set attribute to indicate dashboard is loaded successfully
-        document.body.setAttribute('loaded-dashboard', title || 'default')
-    } catch (e) {
-        if (requestId !== dashboardRequestId) {
-            return
-        }
+    // Set attribute to indicate dashboard is loaded successfully
+    document.body.setAttribute('loaded-dashboard', title || 'default')
+  } catch (e) {
+    if (requestId !== dashboardRequestId) {
+      return
+    }
 
-        // 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
+    console.error('Failed to load dashboard', e)
+    dashboard.value = null
+    loadError.value = e.message || 'Failed to load dashboard'
+    const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
+    document.title = 'Error - ' + pageTitle
 
-        // Stop the loading timer on error
-        if (loadingTimer) {
-            clearInterval(loadingTimer)
-            loadingTimer = null
-        }
-
-        // 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')
-
+function waitForInitAndLoadDashboard () {
+  document.body.removeAttribute('loaded-dashboard')
+  loadError.value = null
+  dashboard.value = null
+
+  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 +347,6 @@ fieldset {
 	place-items: stretch;
 }
 
-
 @keyframes spin {
     from {
         transform: rotate(0deg);

+ 30 - 24
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,27 @@
 </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() {
+  emits: ['show'],
+  data () {
     return {
+      trackingId: '',
       ellapsed: 0,
       isWaiting: true
     }
   },
   computed: {
-    buttonText() {
+    buttonText () {
       if (this.isWaiting) {
         return 'Executing...'
       } else {
@@ -39,33 +41,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() {
+
+    async show () {
       this.$emit('show')
-      
+
       if (window.executionDialog) {
-        window.executionDialog.reset()
+        await 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,17 +80,20 @@ 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) {
-        this.$el.classList.add(resultCssClass)
+        const button = this.$el.querySelector('button')
+        if (button) {
+          button.classList.add(resultCssClass)
+        }
       }
     }
   }
@@ -138,4 +144,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

+ 154 - 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,35 @@ 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
+  event.target.setCustomValidity('')
 }
 
-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
+  event.target.setCustomValidity('')
   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 +340,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 +378,7 @@ async function validateArgument(arg, value) {
 
   try {
     const validateArgumentTypeArgs = {
-      value: value,
+      value,
       type: arg.type,
       bindingId: props.bindingId,
       argumentName: arg.name
@@ -348,7 +410,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 +425,7 @@ function updateUrlWithArg(name, value) {
   }
 }
 
-function shouldSendArgument(arg) {
+function shouldSendArgument (arg) {
   if (!arg.name) {
     return false
   }
@@ -371,7 +433,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 +449,7 @@ function formatArgumentValueForApi(arg, rawValue) {
   return rawValue ?? ''
 }
 
-function getSelectedArgumentEntries() {
+function getSelectedArgumentEntries () {
   const entries = []
 
   for (const arg of actionArguments.value) {
@@ -404,17 +466,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 +484,7 @@ function updateJustificationFromTemplate() {
   justificationValue.value = applyArgumentTemplate(justificationTemplate.value, getArgumentMapForTemplate())
 }
 
-function getUniqueId() {
+function getUniqueId () {
   if (window.isSecureContext) {
     return window.crypto.randomUUID()
   } else {
@@ -430,7 +492,7 @@ function getUniqueId() {
   }
 }
 
-function getBrowserSuggestions(arg) {
+function getBrowserSuggestions (arg) {
   if (!arg.suggestionsBrowserKey) {
     return []
   }
@@ -448,7 +510,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 +546,7 @@ function saveBrowserSuggestions() {
   }
 }
 
-async function startAction(actionArgs) {
+async function startAction (actionArgs) {
   const startActionArgs = {
     bindingId: props.bindingId,
     arguments: actionArgs,
@@ -506,7 +568,7 @@ async function startAction(actionArgs) {
   }
 }
 
-async function handleSubmit(event) {
+async function handleSubmit (event) {
   event.preventDefault()
 
   if (!formReady.value) {
@@ -568,24 +630,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 +700,6 @@ form {
   grid-template-columns: max-content auto auto;
 }
 
-
 .argument-description {
   font-size: 0.875rem;
   color: #666;

+ 78 - 37
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,22 @@ async function fetchDiagnostics() {
   loading.value = false
 }
 
-function formatKey(key) {
-  return key
-    .replace(/([A-Z])/g, ' $1')
-    .replace(/^./, str => str.toUpperCase())
-    .trim()
-}
+async function generateServerDiagnostics () {
+  loading.value = true
 
-async function generateServerDiagnostics() {
-  const response = await window.client.serverDiagnostics()
-  console.log("response", response)
-  serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
+  try {
+    const response = await window.client.serverDiagnostics()
+    console.log('response', response)
+    serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
+  } catch (err) {
+    console.error('Failed to generate server diagnostics:', err)
+    serverDiagnostics.value = ''
+  } finally {
+    loading.value = false
+  }
 }
 
-async function copyServerDiagnostics() {
+async function copyServerDiagnostics () {
   try {
     await navigator.clipboard.writeText(serverDiagnostics.value)
     serverDiagnosticsCopied.value = true
@@ -102,7 +143,7 @@ async function copyServerDiagnostics() {
   }
 }
 
-async function generateBrowserInfo() {
+async function generateBrowserInfo () {
   loading.value = true
   try {
     let userAgentData = 'N/A'
@@ -159,7 +200,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 +209,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 +230,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 +249,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(() => {

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

@@ -1,33 +1,83 @@
 <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="providerIcon(provider)"
+              class="provider-icon"
+            >
+              <iconify-icon
+                v-if="providerIcon(provider).kind === 'iconify'"
+                :icon="providerIcon(provider).id"
+              />
+              <span v-else>{{ providerIcon(provider).text }}</span>
+            </span>
             <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 +101,39 @@ const hasOAuth = ref(false)
 const hasLocalLogin = ref(false)
 const oauthProviders = ref([])
 
-function loadLoginOptions() {
+const trustedProviderIconifyIds = {
+  github: 'simple-icons:github',
+  google: 'simple-icons:google'
+}
+
+function providerIcon (provider) {
+  const raw = (provider?.icon || '').trim()
+  if (!raw) {
+    return null
+  }
+
+  const iconifyTagMatch = raw.match(/<iconify-icon\b[^>]*\bicon=["']([^"']+)["'][^>]*>/i)
+  if (iconifyTagMatch) {
+    return { kind: 'iconify', id: iconifyTagMatch[1] }
+  }
+
+  if (/^[a-z0-9-]+:[a-z0-9-]+$/i.test(raw)) {
+    return { kind: 'iconify', id: raw }
+  }
+
+  const trustedId = trustedProviderIconifyIds[provider.key]
+  if (trustedId && (raw.includes('<') || raw === provider.key)) {
+    return { kind: 'iconify', id: trustedId }
+  }
+
+  if (!raw.includes('<')) {
+    return { kind: 'text', text: raw }
+  }
+
+  return trustedId ? { kind: 'iconify', id: trustedId } : null
+}
+
+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 +147,7 @@ function loadLoginOptions() {
   }
 }
 
-async function handleLocalLogin() {
+async function handleLocalLogin () {
   loading.value = true
   loginError.value = ''
 
@@ -83,7 +165,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 +173,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 +187,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"
   },

+ 33 - 0
integration-tests/tests/dashboardAcls/config.yaml

@@ -0,0 +1,33 @@
+#
+# Integration Test Config: dashboardAcls
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+defaultPermissions:
+  view: false
+  exec: false
+
+accessControlLists:
+  - name: admins
+    matchUsergroups:
+      - admins
+    permissions:
+      view: true
+      exec: true
+
+dashboards:
+  - title: Public tools
+    contents:
+      - title: Welcome guest
+        type: display
+
+  - title: Services
+    acls:
+      - admins
+    contents:
+      - title: 'Status: running'
+        type: display

+ 38 - 0
integration-tests/tests/dashboardAcls/dashboardAcls.mjs

@@ -0,0 +1,38 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import {
+  getRootAndWait,
+  openSidebar,
+  getNavigationLinks,
+  takeScreenshotOnFailure,
+} from '../../lib/elements.js'
+
+describe('config: dashboardAcls', function () {
+  before(async function () {
+    await runner.start('dashboardAcls')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('hides ACL-restricted dashboards from guests in the side menu', async function () {
+    await getRootAndWait()
+    await openSidebar()
+
+    const navigationLinks = await getNavigationLinks()
+    expect(navigationLinks).to.not.be.empty
+
+    const linkTexts = []
+    for (const link of navigationLinks) {
+      linkTexts.push(await link.getText())
+    }
+
+    expect(linkTexts).to.include('Public tools')
+    expect(linkTexts).to.not.include('Services')
+  })
+})

+ 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)",

+ 23 - 0
service/.golangci.yml

@@ -0,0 +1,23 @@
+version: "2"
+
+run:
+  timeout: 5m
+  tests: true
+
+linters:
+  default: none
+  enable:
+    - errcheck
+    - gocritic
+    - gocyclo
+    - ineffassign
+    - misspell
+    - staticcheck
+    - unconvert
+    - unused
+  settings:
+    gocyclo:
+      min-complexity: 5
+  exclusions:
+    paths:
+      - 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@v2.12.2"
 
 .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@v2.12.2"
 	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=

+ 40 - 38
service/internal/acl/acl.go

@@ -21,38 +21,36 @@ func (p PermissionBits) Has(permission PermissionBits) bool {
 	return p&permission != 0
 }
 
-func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
+func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
 	if cfg.LogDebugOptions.AclNotMatched {
 		log.WithFields(log.Fields{
-			"User":   user.Username,
-			"Action": action.Title,
-			"ACL":    acl.Name,
+			"User":     user.Username,
+			"Resource": resourceTitle,
+			"ACL":      acl.Name,
 		}).Debugf("%v - ACL Not Matched", aclFunction)
 	}
 }
 
-func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
-	actionTitle := "N/A"
-
-	if action != nil {
-		actionTitle = action.Title
+func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
+	if resourceTitle == "" {
+		resourceTitle = "N/A"
 	}
 
 	if cfg.LogDebugOptions.AclMatched {
 		log.WithFields(log.Fields{
-			"User":   user.Username,
-			"Action": actionTitle,
-			"ACL":    acl.Name,
+			"User":     user.Username,
+			"Resource": resourceTitle,
+			"ACL":      acl.Name,
 		}).Debugf("%v - Matched ACL", aclFunction)
 	}
 }
 
-func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, defaultPermission bool) {
+func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, defaultPermission bool) {
 	if cfg.LogDebugOptions.AclNoneMatched {
 		log.WithFields(log.Fields{
-			"User":    user.Username,
-			"Action":  action.Title,
-			"Default": defaultPermission,
+			"User":     user.Username,
+			"Resource": resourceTitle,
+			"Default":  defaultPermission,
 		}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
 	}
 }
@@ -81,12 +79,12 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
 	return ret
 }
 
-func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action) bool {
-	relevantAcls := getRelevantAcls(cfg, action.Acls, user)
+func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, resourceAcls []string, includeAddToEvery bool) bool {
+	relevantAcls := getRelevantAcls(cfg, resourceAcls, user, includeAddToEvery)
 
 	if cfg.LogDebugOptions.AclCheckStarted {
 		log.WithFields(log.Fields{
-			"actionTitle":        action.Title,
+			"resourceTitle":      resourceTitle,
 			"username":           user.Username,
 			"usergroupLine":      user.UsergroupLine,
 			"relevantAcls":       len(relevantAcls),
@@ -98,27 +96,27 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
 		permissionBits := permissionsConfigToBits(acl.Permissions)
 
 		if permissionBits.Has(requiredPermission) {
-			logAclMatched(cfg, aclFunction, user, action, acl)
+			logAclMatched(cfg, aclFunction, user, resourceTitle, acl)
 
 			return true
-		} else {
-			logAclNotMatched(cfg, aclFunction, user, action, acl)
 		}
+
+		logAclNotMatched(cfg, aclFunction, user, resourceTitle, acl)
 	}
 
-	logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)
+	logAclNoneMatched(cfg, aclFunction, user, resourceTitle, defaultValue)
 
 	return defaultValue
 }
 
 // IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
 func IsAllowedLogs(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
-	return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
+	return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action.Title, action.Acls, true)
 }
 
 // IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
 func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
-	return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
+	return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action.Title, action.Acls, true)
 }
 
 // IsAllowedView checks if a User is allowed to view an Action
@@ -127,36 +125,40 @@ func IsAllowedView(cfg *config.Config, user *authpublic.AuthenticatedUser, actio
 		return false
 	}
 
-	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
+	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action.Title, action.Acls, true)
 }
 
 func IsAllowedKill(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
-	return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
+	return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action.Title, action.Acls, true)
 }
 
-func isACLRelevantToAction(cfg *config.Config, 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
-
-		return false
+// IsAllowedViewDashboard checks if a user may see a root dashboard.
+// Dashboards with no acls are unrestricted. AddToEveryAction does not apply.
+func IsAllowedViewDashboard(cfg *config.Config, user *authpublic.AuthenticatedUser, dashboard *config.DashboardComponent) bool {
+	if dashboard == nil || len(dashboard.Acls) == 0 {
+		return true
 	}
 
-	if acl.AddToEveryAction {
-		return true
+	return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedViewDashboard", user, dashboard.Title, dashboard.Acls, false)
+}
+
+func isACLRelevant(resourceAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser, includeAddToEvery bool) bool {
+	if !slices.Contains(user.Acls, acl.Name) {
+		return false
 	}
 
-	if slices.Contains(actionAcls, acl.Name) {
+	if includeAddToEvery && acl.AddToEveryAction {
 		return true
 	}
 
-	return false
+	return slices.Contains(resourceAcls, acl.Name)
 }
 
-func getRelevantAcls(cfg *config.Config, actionAcls []string, user *authpublic.AuthenticatedUser) []*config.AccessControlList {
+func getRelevantAcls(cfg *config.Config, resourceAcls []string, user *authpublic.AuthenticatedUser, includeAddToEvery bool) []*config.AccessControlList {
 	var ret []*config.AccessControlList
 
 	for _, acl := range cfg.AccessControlLists {
-		if isACLRelevantToAction(cfg, actionAcls, acl, user) {
+		if isACLRelevant(resourceAcls, acl, user, includeAddToEvery) {
 			ret = append(ret, acl)
 		}
 	}

+ 86 - 0
service/internal/acl/acl_dashboard_test.go

@@ -0,0 +1,86 @@
+package acl
+
+import (
+	"testing"
+
+	authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
+	config "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAllowedViewDashboardAbsentAclsUnrestricted(t *testing.T) {
+	cfg := config.DefaultConfig()
+	cfg.DefaultPermissions.View = false
+
+	dashboard := &config.DashboardComponent{
+		Title: "Public",
+		Contents: []*config.DashboardComponent{
+			{Title: "Status", Type: "display"},
+		},
+	}
+
+	guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
+	guest.BuildUserAcls(cfg)
+
+	assert.True(t, IsAllowedViewDashboard(cfg, guest, dashboard))
+	assert.True(t, IsAllowedViewDashboard(cfg, guest, nil))
+}
+
+func TestIsAllowedViewDashboardAllowDenyAndDefaultFallback(t *testing.T) {
+	cfg := config.DefaultConfig()
+	cfg.DefaultPermissions.View = false
+	cfg.AccessControlLists = []*config.AccessControlList{
+		{
+			Name:           "admins",
+			MatchUsernames: []string{"admin"},
+			Permissions:    config.PermissionsList{View: true, Exec: true},
+		},
+	}
+
+	dashboard := &config.DashboardComponent{
+		Title: "Services",
+		Acls:  []string{"admins"},
+		Contents: []*config.DashboardComponent{
+			{Title: "Status: running", Type: "display"},
+		},
+	}
+
+	guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
+	guest.BuildUserAcls(cfg)
+	admin := &authpublic.AuthenticatedUser{Username: "admin"}
+	admin.BuildUserAcls(cfg)
+
+	assert.False(t, IsAllowedViewDashboard(cfg, guest, dashboard))
+	assert.True(t, IsAllowedViewDashboard(cfg, admin, dashboard))
+
+	cfg.DefaultPermissions.View = true
+	assert.True(t, IsAllowedViewDashboard(cfg, guest, dashboard),
+		"when no relevant ACL matches, fall back to defaultPermissions.view")
+}
+
+func TestIsAllowedViewDashboardIgnoresAddToEveryAction(t *testing.T) {
+	cfg := config.DefaultConfig()
+	cfg.DefaultPermissions.View = false
+	cfg.AccessControlLists = []*config.AccessControlList{
+		{
+			Name:             "admins",
+			MatchUsernames:   []string{"admin"},
+			AddToEveryAction: true,
+			Permissions:      config.PermissionsList{View: true, Exec: true},
+		},
+	}
+
+	dashboard := &config.DashboardComponent{
+		Title: "Secret",
+		Acls:  []string{"other"},
+		Contents: []*config.DashboardComponent{
+			{Title: "Hidden status", Type: "display"},
+		},
+	}
+
+	admin := &authpublic.AuthenticatedUser{Username: "admin"}
+	admin.BuildUserAcls(cfg)
+
+	assert.False(t, IsAllowedViewDashboard(cfg, admin, dashboard),
+		"AddToEveryAction must not grant dashboard view without listing the ACL on the dashboard")
+}

+ 110 - 0
service/internal/api/dashboard_acl_test.go

@@ -0,0 +1,110 @@
+package api
+
+import (
+	"testing"
+
+	authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
+	config "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/executor"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func buildDashboardAclTestConfig() *config.Config {
+	cfg := config.DefaultConfig()
+	cfg.DefaultPermissions.View = false
+	cfg.DefaultPermissions.Exec = false
+	cfg.AccessControlLists = []*config.AccessControlList{
+		{
+			Name:           "admins",
+			MatchUsernames: []string{"admin"},
+			Permissions:    config.PermissionsList{View: true, Exec: true},
+		},
+	}
+	cfg.Dashboards = []*config.DashboardComponent{
+		{
+			Title: "Public tools",
+			Contents: []*config.DashboardComponent{
+				{Title: "Welcome", Type: "display"},
+			},
+		},
+		{
+			Title: "Services",
+			Acls:  []string{"admins"},
+			Contents: []*config.DashboardComponent{
+				{Title: "Status: running", Type: "display"},
+			},
+		},
+	}
+
+	return cfg
+}
+
+func TestDashboardAclsRootNavAndGetDashboard(t *testing.T) {
+	cfg := buildDashboardAclTestConfig()
+	ex := executor.DefaultExecutor(cfg)
+	api := newServer(ex)
+
+	guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
+	guest.BuildUserAcls(cfg)
+	admin := &authpublic.AuthenticatedUser{Username: "admin"}
+	admin.BuildUserAcls(cfg)
+
+	guestRoots := api.buildRootDashboards(guest, cfg.Dashboards)
+	assert.Contains(t, guestRoots, "Public tools")
+	assert.NotContains(t, guestRoots, "Services")
+
+	adminRoots := api.buildRootDashboards(admin, cfg.Dashboards)
+	assert.Contains(t, adminRoots, "Public tools")
+	assert.Contains(t, adminRoots, "Services")
+
+	guestRR := api.createDashboardRenderRequest(guest, "", "")
+	assert.Nil(t, renderDashboard(guestRR, "Services"),
+		"GetDashboard must not leak ACL-restricted dashboard content via deep link")
+
+	adminRR := api.createDashboardRenderRequest(admin, "", "")
+	db := renderDashboard(adminRR, "Services")
+	require.NotNil(t, db)
+	assert.Equal(t, "Services", db.Title)
+}
+
+func TestDashboardAclsNestedDirectoryDeepLink(t *testing.T) {
+	cfg := buildDashboardAclTestConfig()
+	cfg.Dashboards = []*config.DashboardComponent{
+		{
+			Title: "Public tools",
+			Contents: []*config.DashboardComponent{
+				{Title: "Welcome", Type: "display"},
+			},
+		},
+		{
+			Title: "Services",
+			Acls:  []string{"admins"},
+			Contents: []*config.DashboardComponent{
+				{
+					Title: "Infrastructure",
+					Contents: []*config.DashboardComponent{
+						{Title: "Status: running", Type: "display"},
+					},
+				},
+			},
+		},
+	}
+
+	ex := executor.DefaultExecutor(cfg)
+	api := newServer(ex)
+
+	guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
+	guest.BuildUserAcls(cfg)
+	admin := &authpublic.AuthenticatedUser{Username: "admin"}
+	admin.BuildUserAcls(cfg)
+
+	guestRR := api.createDashboardRenderRequest(guest, "", "")
+	assert.Nil(t, renderDashboard(guestRR, "Infrastructure"),
+		"nested directory under ACL-restricted root must not leak via deep link")
+
+	adminRR := api.createDashboardRenderRequest(admin, "", "")
+	db := renderDashboard(adminRR, "Infrastructure")
+	require.NotNil(t, db)
+	assert.Equal(t, "Infrastructure", db.Title)
+}

+ 13 - 1
service/internal/api/dashboards.go

@@ -52,6 +52,10 @@ func findDashboardByTitle(rr *DashboardRenderRequest, dashboardTitle string) *co
 }
 
 func renderDashboardIfValid(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
+	if !acl.IsAllowedViewDashboard(rr.cfg, rr.AuthenticatedUser, dashboard) {
+		return nil
+	}
+
 	if len(dashboard.Contents) == 0 {
 		logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
 		return nil
@@ -71,13 +75,21 @@ func renderDirectoryDashboard(rr *DashboardRenderRequest, dashboardTitle string)
 
 func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent {
 	for _, dashboard := range rr.cfg.Dashboards {
-		if component := searchDirectoryInComponent(dashboard, title); component != nil {
+		if component := findDirectoryInRootIfAllowed(rr, dashboard, title); component != nil {
 			return component
 		}
 	}
 	return nil
 }
 
+func findDirectoryInRootIfAllowed(rr *DashboardRenderRequest, root *config.DashboardComponent, title string) *config.DashboardComponent {
+	if !acl.IsAllowedViewDashboard(rr.cfg, rr.AuthenticatedUser, root) {
+		return nil
+	}
+
+	return searchDirectoryInComponent(root, title)
+}
+
 func searchDirectoryInComponent(component *config.DashboardComponent, title string) *config.DashboardComponent {
 	if isMatchingDirectory(component, title) {
 		return component

+ 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)

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

@@ -349,13 +349,13 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
 		return ret
 	}
 
+	defer func() { _ = res.Body.Close() }()
+
 	if res.StatusCode != http.StatusOK {
 		log.Errorf("Failed to get user data: %v", res.StatusCode)
 		return ret
 	}
 
-	defer res.Body.Close()
-
 	contents, err := io.ReadAll(res.Body)
 
 	if err != nil {
@@ -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)

+ 1 - 0
service/internal/config/config.go

@@ -289,6 +289,7 @@ type DashboardComponent struct {
 	Entity       string                `koanf:"entity"`
 	Icon         string                `koanf:"icon"`
 	CssClass     string                `koanf:"cssClass"`
+	Acls         []string              `koanf:"acls"`
 	InlineAction *Action               `koanf:"inlineAction"`
 	Contents     []*DashboardComponent `koanf:"contents"`
 }

+ 17 - 16
service/internal/config/config_reloader_user_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/knadh/koanf/providers/file"
 	"github.com/knadh/koanf/v2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestUserLoadingFromConfig(t *testing.T) {
@@ -30,13 +31,13 @@ actions:
 
 	// Create temporary file
 	tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
-	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	require.NoError(t, err, "Should create temporary file")
+	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()
+	require.NoError(t, err, "Should write test config to file")
+	require.NoError(t, tmpFile.Close())
 
 	// Load config using koanf
 	k := koanf.New(".")
@@ -81,12 +82,12 @@ actions:
 `
 
 	tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml")
-	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	require.NoError(t, err, "Should create temporary file")
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	_, err = tmpFile.WriteString(testConfig)
-	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	require.NoError(t, err, "Should write test config to file")
+	require.NoError(t, tmpFile.Close())
 
 	k := koanf.New(".")
 	err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
@@ -116,12 +117,12 @@ actions:
 `
 
 	tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml")
-	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	require.NoError(t, err, "Should create temporary file")
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	_, err = tmpFile.WriteString(testConfig)
-	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	require.NoError(t, err, "Should write test config to file")
+	require.NoError(t, tmpFile.Close())
 
 	k := koanf.New(".")
 	err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
@@ -147,12 +148,12 @@ actions:
 `
 
 	tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml")
-	assert.NoError(t, err, "Should create temporary file")
-	defer os.Remove(tmpFile.Name())
+	require.NoError(t, err, "Should create temporary file")
+	defer func() { _ = os.Remove(tmpFile.Name()) }()
 
 	_, err = tmpFile.WriteString(testConfig)
-	assert.NoError(t, err, "Should write test config to file")
-	tmpFile.Close()
+	require.NoError(t, err, "Should write test config to file")
+	require.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{

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

@@ -74,7 +74,11 @@ func watchPath(ctx *watchContext) {
 		return
 	}
 
-	defer watcher.Close()
+	defer func() {
+		if err := watcher.Close(); err != nil {
+			log.Errorf("Failed to close file watcher: %v", err)
+		}
+	}()
 
 	done := make(chan bool)
 

+ 6 - 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,12 @@ func WriteFile(filename string, out []byte) {
 			return
 		}
 
+		if err := handle.Close(); err != nil {
+			log.WithFields(log.Fields{
+				"error":    err,
+				"filename": filename,
+			}).Errorf("Failed to close %v", filename)
+		}
 	}
 
 	err := os.WriteFile(filename, out, 0600)

+ 10 - 2
service/internal/installationinfo/runtimeinfo.go

@@ -8,6 +8,8 @@ import (
 	"path/filepath"
 	"runtime"
 	"strings"
+
+	log "github.com/sirupsen/logrus"
 )
 
 type RuntimeInfo struct {
@@ -85,6 +87,14 @@ func getOsReleasePrettyName() string {
 		return ""
 	}
 
+	defer func() {
+		if closeErr := handle.Close(); closeErr != nil {
+			log.WithFields(log.Fields{
+				"error": closeErr,
+			}).Warn("Failed to close /etc/os-release")
+		}
+	}()
+
 	scanner := bufio.NewScanner(handle)
 	scanner.Split(bufio.ScanLines)
 
@@ -96,7 +106,5 @@ func getOsReleasePrettyName() string {
 		}
 	}
 
-	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
+}

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

@@ -202,10 +202,13 @@ func appendFile(path, content string) error {
 	if err != nil {
 		return err
 	}
-	defer file.Close()
 
-	_, err = file.WriteString(content)
-	return err
+	_, writeErr := file.WriteString(content)
+	closeErr := file.Close()
+	if writeErr != nil {
+		return writeErr
+	}
+	return closeErr
 }
 
 func newTestRunState() *testRunState {

+ 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"
 )

+ 3 - 4
var/windows/signpath-publish-signed.sh

@@ -72,7 +72,8 @@ update_checksum() {
 update_checksum "${ZIP_NAME}"
 update_checksum "${MSI_NAME}"
 
-# Replace binaries first so a failed checksums upload leaves the draft recoverable.
+# Upload binaries first so a failed checksums upload can restore the previous file.
+# --clobber overwrites same-named assets (needed if the sign job is re-run).
 gh release upload "${TAG}" \
   "${DIST_DIR}/${ZIP_NAME}" \
   "${DIST_DIR}/${MSI_NAME}" \
@@ -87,6 +88,4 @@ if ! gh release upload "${TAG}" "${checksums_path}" --clobber; then
   exit 1
 fi
 
-gh release edit "${TAG}" --draft=false
-
-echo "Published signed ${ZIP_NAME} and ${MSI_NAME} on release ${TAG}"
+echo "Uploaded signed ${ZIP_NAME} and ${MSI_NAME} to release ${TAG}"