This commit is contained in:
James Read 2026-07-21 19:43:12 +00:00 committed by GitHub
commit c56b4f3855
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 4615 additions and 2866 deletions

View File

@ -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 followed the "1 logical change" rule.
- [ ] I have forked the project, and raised this PR on a feature branch. - [ ] I have forked the project, and raised this PR on a feature branch.
- [ ] I ran the `pre-commit` hooks, and my commit message was validated. - [ ] 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 compile` runs without any issues.
- [ ] `make -wC service codestyle` runs without any issues. - [ ] `make -wC service codestyle` runs without any issues.
- [ ] `make -wC service unittests` runs without any issues. - [ ] `make -wC service unittests` runs without any issues.

View File

@ -156,8 +156,10 @@ jobs:
if: steps.release.outputs.new_release_published == 'true' if: steps.release.outputs.new_release_published == 'true'
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: 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 path: dist/OliveTin-windows-amd64.zip
archive: false
if-no-files-found: error if-no-files-found: error
- name: Upload unsigned Windows MSI for SignPath - name: Upload unsigned Windows MSI for SignPath
@ -165,8 +167,8 @@ jobs:
if: steps.release.outputs.new_release_published == 'true' if: steps.release.outputs.new_release_published == 'true'
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: unsigned-windows-msi
path: dist/OliveTin-windows-amd64.msi path: dist/OliveTin-windows-amd64.msi
archive: false
if-no-files-found: error if-no-files-found: error
- name: Archive binaries - name: Archive binaries
@ -205,7 +207,7 @@ jobs:
fi fi
done done
if [[ "${missing}" -ne 0 ]]; then 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 exit 1
fi fi
@ -219,6 +221,8 @@ jobs:
artifact-configuration-slug: windows-zip artifact-configuration-slug: windows-zip
github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }} github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }}
wait-for-completion: true 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 output-artifact-directory: signed-windows-zip
- name: Sign Windows MSI - name: Sign Windows MSI
@ -231,9 +235,10 @@ jobs:
artifact-configuration-slug: windows-msi artifact-configuration-slug: windows-msi
github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }} github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }}
wait-for-completion: true wait-for-completion: true
skip-decompress: true
output-artifact-directory: signed-windows-msi output-artifact-directory: signed-windows-msi
- name: Publish signed Windows assets and undraft release - name: Upload signed Windows assets
env: env:
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }} GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }} GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}

View File

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

View File

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

View File

@ -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 git clone https://github.com/OliveTin/OliveTin.git
cd OliveTin cd OliveTin
pre-commit install pre-commit install
pre-commit install --hook-type pre-push
# Step3: compile binary for current dev env (OS, ARCH) # Step3: compile binary for current dev env (OS, ARCH)
# `make proto` will also run `make go-tools`, which installs "buf". This binary # `make proto` will also run `make go-tools`, which installs "buf". This binary

View File

@ -116,6 +116,48 @@ accessControlLists:
addToEveryAction: true 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. == 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. 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/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/oauth2.adoc[Configure OAuth2] - Set up OAuth2 for user groups
* xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for ACL design * xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for ACL design

View File

@ -3,7 +3,7 @@
OliveTin signs release binaries on two platforms: 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). * **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 ## 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. 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: Signed artifacts:
* `OliveTin.exe` inside `OliveTin-windows-amd64.zip` * `OliveTin.exe` inside `OliveTin-windows-amd64.zip`
* nested `OliveTin.exe` and the `OliveTin-windows-amd64.msi` installer (deep signing) * 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 ### Prerequisites
@ -168,9 +168,9 @@ In **Settings → Secrets and variables → Actions**:
On a new semantic-release from `main`: On a new semantic-release from `main`:
1. GoReleaser publishes container images and creates a **draft** GitHub release (including unsigned Windows zip/MSI). 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 those Windows files as GitHub Actions artifacts. 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 clobber-upload signed files, refresh `checksums.txt`, and undraft the release. 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). 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 ### 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`] - 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`] - 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`] - 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`]

View File

@ -4,12 +4,22 @@
"es2021": true "es2021": true
}, },
"extends": [ "extends": [
"plugin:vue/recommended",
"standard" "standard"
], ],
"parser": "vue-eslint-parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": 12, "ecmaVersion": 12,
"sourceType": "module" "sourceType": "module",
"parser": {
"js": "espree"
}
}, },
"rules": { "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"
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,34 +1,71 @@
<template> <template>
<div :id="`actionButton-${bindingId}`" role="none" class="action-button" @contextmenu.prevent="openActionDetails"> <div
:id="`actionButton-${bindingId}`"
role="none"
class="action-button"
@contextmenu.prevent="openActionDetails"
>
<span <span
v-if="showExecutionIndicator" v-if="showExecutionIndicator"
class="execution-indicator" class="execution-indicator"
:class="executionIndicatorClass" :class="executionIndicatorClass"
:title="executionIndicatorTitle" :title="executionIndicatorTitle"
aria-hidden="true" aria-hidden="true"
></span> />
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled" <button
:class="combinedClasses" @click="handleClick"> :id="`actionButtonInner-${bindingId}`"
:title="title"
<div v-if="showNavigateOnStartIcons" class="navigate-on-start-container"> :disabled="!canExec || isDisabled"
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start"> :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" /> <HugeiconsIcon :icon="ComputerTerminal01Icon" />
</div> </div>
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start"> <div
v-if="navigateOnStart == 'arg'"
class="navigate-on-start"
title="Opens an argument form on start"
>
<HugeiconsIcon :icon="TypeCursorIcon" /> <HugeiconsIcon :icon="TypeCursorIcon" />
</div> </div>
<div v-if="navigateOnStart == 'hist'" class="navigate-on-start" title="Opens action execution history on start"> <div
v-if="navigateOnStart == 'hist'"
class="navigate-on-start"
title="Opens action execution history on start"
>
<HugeiconsIcon :icon="WorkHistoryIcon" /> <HugeiconsIcon :icon="WorkHistoryIcon" />
</div> </div>
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background"> <div
v-if="navigateOnStart == ''"
class="navigate-on-start"
title="Run in the background"
>
<HugeiconsIcon :icon="WorkoutRunIcon" /> <HugeiconsIcon :icon="WorkoutRunIcon" />
</div> </div>
</div> </div>
<ActionIconGlyph class="icon" :glyph="actionGlyph" /> <ActionIconGlyph
<span class="title" aria-live="polite">{{ displayTitle }} class="icon"
:glyph="actionGlyph"
/>
<span
class="title"
aria-live="polite"
>{{ displayTitle }}
</span> </span>
<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span> <span
v-if="rateLimitMessage"
class="rate-limit-message"
>{{ rateLimitMessage }}</span>
</button> </button>
</div> </div>
</template> </template>
@ -84,7 +121,6 @@ const displayTitle = ref('')
// State // State
const isDisabled = ref(false) const isDisabled = ref(false)
const showArgumentForm = ref(false)
// Rate limiting // Rate limiting
const rateLimitExpires = ref(0) const rateLimitExpires = ref(0)
@ -211,10 +247,16 @@ function updateFromJson(json) {
if (json.datetimeRateLimitExpires) { if (json.datetimeRateLimitExpires) {
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
rateLimitExpires.value = date.getTime() / 1000 rateLimitExpires.value = date.getTime() / 1000
if (bindingId.value) {
rateLimits[bindingId.value] = rateLimitExpires.value
}
updateRateLimitStatus() updateRateLimitStatus()
} else if (json.datetimeRateLimitExpires === '') { } else if (json.datetimeRateLimitExpires === '') {
// Explicitly clear if empty string // Explicitly clear if empty string
rateLimitExpires.value = 0 rateLimitExpires.value = 0
if (bindingId.value) {
rateLimits[bindingId.value] = 0
}
updateRateLimitStatus() updateRateLimitStatus()
} }
} }
@ -323,6 +365,15 @@ async function pollExecutionUntilDone (trackingId) {
} }
} }
let stopButtonResultWatch = null
function stopWatchingButtonResult () {
if (stopButtonResultWatch) {
stopButtonResultWatch()
stopButtonResultWatch = null
}
}
async function startAction (actionArgs) { async function startAction (actionArgs) {
buttonClasses.value = [] // Removes old animation classes buttonClasses.value = [] // Removes old animation classes
@ -340,7 +391,8 @@ async function startAction(actionArgs) {
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId) console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
watch( stopWatchingButtonResult()
stopButtonResultWatch = watch(
() => buttonResults[startActionArgs.uniqueTrackingId], () => buttonResults[startActionArgs.uniqueTrackingId],
(newResult, oldResult) => { (newResult, oldResult) => {
onLogEntryChanged(newResult) onLogEntryChanged(newResult)
@ -361,12 +413,18 @@ async function startAction(actionArgs) {
await pollExecutionUntilDone(trackingId) await pollExecutionUntilDone(trackingId)
} }
} catch (err) { } catch (err) {
stopWatchingButtonResult()
console.error('Failed to start action:', err) console.error('Failed to start action:', err)
} }
} }
function onLogEntryChanged (logEntry) { function onLogEntryChanged (logEntry) {
if (!logEntry) {
return
}
if (logEntry.executionFinished) { if (logEntry.executionFinished) {
stopWatchingButtonResult()
onExecutionFinished(logEntry) onExecutionFinished(logEntry)
} else if (logEntry.queued && !logEntry.executionStarted) { } else if (logEntry.queued && !logEntry.executionStarted) {
onExecutionQueued(logEntry) onExecutionQueued(logEntry)

View File

@ -1,68 +1,141 @@
<template> <template>
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation"> <Header
:title="pageTitle"
:logo-url="logoUrl"
:sidebar-enabled="sidebarEnabled"
:top-bar-enabled="topbarEnabled"
:navigation="navigation"
@toggle-sidebar="toggleSidebar"
>
<template #toolbar> <template #toolbar>
<div id="banner" v-if="bannerMessage" :style="bannerCss"> <div
v-if="bannerMessage"
id="banner"
:style="bannerCss"
>
<p>{{ bannerMessage }}</p> <p>{{ bannerMessage }}</p>
</div> </div>
</template> </template>
<template #user-info> <template #user-info>
<ConnectionBanner /> <ConnectionBanner />
<div class="flex-row user-info" style="gap: .5em;"> <div
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span> class="flex-row user-info"
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn"> 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> <span id="username-text">{{ username }}</span>
</router-link> </router-link>
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" /> <HugeiconsIcon
v-if="isLoggedIn"
:icon="UserCircle02Icon"
width="1.5em"
height="1.5em"
/>
</div> </div>
</template> </template>
</Header> </Header>
<div id="layout"> <div id="layout">
<Navigation ref="navigation"> <Navigation ref="navigation">
<Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/> <Sidebar
v-if="sidebarEnabled && showNavigation"
id="mainnav"
ref="sidebar"
/>
</Navigation> </Navigation>
<div id="content" initial-martial-complete="{{ hasLoaded }}"> <div
id="content"
:initial-martial-complete="hasLoaded"
>
<main title="Main content"> <main title="Main content">
<router-view :key="$route.fullPath" /> <router-view :key="$route.fullPath" />
</main> </main>
<footer title="footer" v-if="showFooter"> <footer
v-if="showFooter"
title="footer"
>
<p> <p>
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" /> <img
title="application icon"
:src="logoUrl"
alt="OliveTin logo"
style="height: 1em;"
class="logo"
>
OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span> OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
</p> </p>
<p> <p>
<span> <span>
<a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a> <a
href="https://docs.olivetin.app"
target="_new"
>{{ t('docs') }}</a>
</span> </span>
<span> <span>
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a> <a
href="https://github.com/OliveTin/OliveTin/issues/new/choose"
target="_new"
>{{ t('raise-issue') }}</a>
</span> </span>
<span> <span>
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a> <a
href="#"
@click.prevent="openLanguageDialog"
>{{ currentLanguageName }}</a>
</span> </span>
<span v-if="availableThemes.length > 1"> <span v-if="availableThemes.length > 1">
<a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a> <a
href="#"
@click.prevent="openThemeDialog"
>{{ currentThemeName }}</a>
</span> </span>
</p> </p>
<p v-if="showVersionNumber"> <p v-if="showVersionNumber">
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a> <a
id="available-version"
href="http://olivetin.app"
target="_blank"
hidden
>?</a>
</p> </p>
</footer> </footer>
</div> </div>
</div> </div>
<dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick"> <dialog
<div class="dialog-content" @click.stop> ref="languageDialog"
class="language-dialog"
@click="handleLanguageDialogClick"
>
<div
class="dialog-content"
@click.stop
>
<h2>{{ t('language-dialog.title') }}</h2> <h2>{{ t('language-dialog.title') }}</h2>
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select"> <select
<option v-for="(name, code) in availableLanguages" :key="code" :value="code"> v-model="selectedLanguage"
class="language-select"
@change="changeLanguage"
>
<option
v-for="(name, code) in availableLanguages"
:key="code"
:value="code"
>
{{ code === 'auto' ? name : `${name} (${code})` }} {{ code === 'auto' ? name : `${name} (${code})` }}
</option> </option>
</select> </select>
@ -72,55 +145,74 @@
<span v-else>{{ t('language-dialog.not-available') }}</span> <span v-else>{{ t('language-dialog.not-available') }}</span>
</p> </p>
<div class="dialog-buttons"> <div class="dialog-buttons">
<button @click="closeLanguageDialog">{{ t('language-dialog.close') }}</button> <button @click="closeLanguageDialog">
{{ t('language-dialog.close') }}
</button>
</div> </div>
</div> </div>
</dialog> </dialog>
<dialog ref="themeDialog" class="theme-dialog" @click="handleThemeDialogClick"> <dialog
<div class="dialog-content" @click.stop> ref="themeDialog"
class="theme-dialog"
@click="handleThemeDialogClick"
>
<div
class="dialog-content"
@click.stop
>
<h2>{{ t('theme-dialog.title') }}</h2> <h2>{{ t('theme-dialog.title') }}</h2>
<select v-model="selectedTheme" @change="changeTheme" class="language-select"> <select
<option value="">{{ t('theme-dialog.default') }}</option> v-model="selectedTheme"
<option v-for="theme in availableThemes" :key="theme" :value="theme"> class="language-select"
@change="changeTheme"
>
<option value="">
{{ t('theme-dialog.default') }}
</option>
<option
v-for="theme in availableThemes"
:key="theme"
:value="theme"
>
{{ theme }} {{ theme }}
</option> </option>
</select> </select>
<div class="dialog-buttons"> <div class="dialog-buttons">
<button @click="closeThemeDialog">{{ t('theme-dialog.close') }}</button> <button @click="closeThemeDialog">
{{ t('theme-dialog.close') }}
</button>
</div> </div>
</div> </div>
</dialog> </dialog>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router'
import Sidebar from 'picocrank/vue/components/Sidebar.vue'; import Sidebar from 'picocrank/vue/components/Sidebar.vue'
import Navigation from 'picocrank/vue/components/Navigation.vue'; import Navigation from 'picocrank/vue/components/Navigation.vue'
import Header from 'picocrank/vue/components/Header.vue'; import Header from 'picocrank/vue/components/Header.vue'
import ConnectionBanner from './components/ConnectionBanner.vue'; import ConnectionBanner from './components/ConnectionBanner.vue'
import { connectEventStreamIfNeeded } from '../../js/websocket.js'; import { connectEventStreamIfNeeded } from '../../js/websocket.js'
import { HugeiconsIcon } from '@hugeicons/vue' import { HugeiconsIcon } from '@hugeicons/vue'
import { Menu01Icon } from '@hugeicons/core-free-icons' import { UserCircle02Icon, DashboardSquare01Icon } from '@hugeicons/core-free-icons'
import { UserCircle02Icon } from '@hugeicons/core-free-icons' import logoUrl from '../../OliveTinLogo.png'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons' import { useI18n } from 'vue-i18n'
import logoUrl from '../../OliveTinLogo.png'; import combinedTranslations from '../../../lang/combined_output.json'
import { useI18n } from 'vue-i18n'; const { t } = useI18n()
import combinedTranslations from '../../../lang/combined_output.json';
const { t, locale } = useI18n();
const router = useRouter(); const router = useRouter()
const sidebar = ref(null); const sidebar = ref(null)
const navigation = ref(null); const navigation = ref(null)
const username = ref('notset'); const username = ref('notset')
const isLoggedIn = ref(false); const isLoggedIn = ref(false)
const currentVersion = ref('?'); const currentVersion = ref('?')
const pageTitle = ref('OliveTin'); const pageTitle = ref('OliveTin')
const bannerMessage = ref(''); const bannerMessage = ref('')
const bannerCss = ref(''); const bannerCss = ref('')
const hasLoaded = ref(false); const hasLoaded = ref(false)
const showFooter = ref(true) const showFooter = ref(true)
const showNavigation = ref(true) const showNavigation = ref(true)
const showLogs = ref(true) const showLogs = ref(true)
@ -144,8 +236,8 @@ const selectedTheme = ref(themePreference.value)
// Available languages with display names // Available languages with display names
const availableLanguages = { const availableLanguages = {
'auto': 'Browser Language', auto: 'Browser Language',
'en': 'English', en: 'English',
'de-DE': 'Deutsch', 'de-DE': 'Deutsch',
'es-ES': 'Español', 'es-ES': 'Español',
'it-IT': 'Italiano', 'it-IT': 'Italiano',
@ -155,7 +247,7 @@ const availableLanguages = {
// Computed property to get current language display name // Computed property to get current language display name
const currentLanguageName = computed(() => { const currentLanguageName = computed(() => {
if (languagePreference.value === 'auto') { if (languagePreference.value === 'auto') {
return availableLanguages['auto'] return availableLanguages.auto
} }
return availableLanguages[languagePreference.value] || languagePreference.value return availableLanguages[languagePreference.value] || languagePreference.value
@ -263,7 +355,7 @@ function renderNavigation() {
name: rootDashboard, name: rootDashboard,
title: rootDashboard, title: rootDashboard,
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`, path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
icon: DashboardSquare01Icon, icon: DashboardSquare01Icon
}) })
} }
@ -370,7 +462,7 @@ function applyTheme() {
if (themePreference.value && themePreference.value !== '') { if (themePreference.value && themePreference.value !== '') {
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);` themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
} else { } else {
themeStyle.textContent = `@import url('/theme.css') layer(theme);` themeStyle.textContent = '@import url(\'/theme.css\') layer(theme);'
} }
} }

View File

@ -1,34 +1,97 @@
<template> <template>
<section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;"> <section
<HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" /> 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>Loading dashboard...</p>
<p style="color: var(--fg2);">{{ loadingTime }}s</p> <p style="color: var(--fg2);">
{{ loadingTime }}s
</p>
</section> </section>
<section v-if="initError" style="text-align: center; padding: 2em;" class = "bad"> <section
<h2 style="color: var(--error);">Initialization Failed</h2> v-if="initError"
style="text-align: center; padding: 2em;"
class="bad"
>
<h2 style="color: var(--error);">
Initialization Failed
</h2>
<p>{{ initError }}</p> <p>{{ initError }}</p>
<p style="color: var(--fg2);">Please check your configuration and try again.</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> </section>
<template v-else-if="dashboard"> <template v-else-if="dashboard">
<section v-if="dashboard.contents.length == 0"> <section v-if="dashboard.contents.length == 0">
<div class="back-button-container" v-if="isDirectory"> <div
<button @click="goBack" class="back-button"> v-if="isDirectory"
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" /> class="back-button-container"
>
<button
class="back-button"
@click="goBack"
>
<HugeiconsIcon
:icon="ArrowLeftIcon"
width="1.2em"
height="1.2em"
/>
<span>Back</span> <span>Back</span>
</button> </button>
</div> </div>
<h2>{{ dashboard.title }}</h2> <h2>{{ dashboard.title }}</h2>
<p style = "text-align: center" class = "padding">This dashboard is empty.</p> <p
style="text-align: center"
class="padding"
>
This dashboard is empty.
</p>
</section> </section>
<section class="transparent" v-else> <section
<div class="back-button-container" v-if="isDirectory"> v-else
<button @click="goBack" class="back-button"> class="transparent"
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" /> >
<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> <span>Back</span>
</button> </button>
</div> </div>
<div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title"> <div
v-for="component in dashboard.contents"
:key="component.title"
class="dashboard-row"
>
<h2 v-if="dashboard.title != 'Default'"> <h2 v-if="dashboard.title != 'Default'">
<router-link <router-link
v-if="component.entityType && component.entityKey" v-if="component.entityType && component.entityKey"
@ -39,14 +102,18 @@
entityKey: component.entityKey entityKey: component.entityKey
} }
}" }"
class="entity-link"> class="entity-link"
>
{{ component.title }} {{ component.title }}
</router-link> </router-link>
<span v-else>{{ component.title }}</span> <span v-else>{{ component.title }}</span>
</h2> </h2>
<fieldset :class="component.cssClass"> <fieldset :class="component.cssClass">
<template v-for="subcomponent in component.contents"> <template
v-for="subcomponent in component.contents"
:key="subcomponent.title || subcomponent.type"
>
<DashboardComponent :component="subcomponent" /> <DashboardComponent :component="subcomponent" />
</template> </template>
</fieldset> </fieldset>
@ -81,6 +148,7 @@ const router = useRouter()
const dashboard = ref(null) const dashboard = ref(null)
const loadingTime = ref(0) const loadingTime = ref(0)
const initError = ref(null) const initError = ref(null)
const loadError = ref(null)
let loadingTimer = null let loadingTimer = null
let checkInitInterval = null let checkInitInterval = null
let dashboardRequestId = 0 let dashboardRequestId = 0
@ -118,7 +186,7 @@ async function getDashboard() {
try { try {
const request = { const request = {
title: title, title
} }
if (props.entityType && props.entityKey) { if (props.entityType && props.entityKey) {
@ -140,8 +208,9 @@ async function getDashboard() {
const pageTitle = window.initResponse?.pageTitle || 'OliveTin' const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = ret.dashboard.title + ' - ' + pageTitle document.title = ret.dashboard.title + ' - ' + pageTitle
// Clear any previous init error since we successfully loaded // Clear any previous errors since we successfully loaded
initError.value = null initError.value = null
loadError.value = null
// Stop the loading timer once dashboard is loaded // Stop the loading timer once dashboard is loaded
if (loadingTimer) { if (loadingTimer) {
@ -156,9 +225,9 @@ async function getDashboard() {
return return
} }
// On error, provide a safe fallback state
console.error('Failed to load dashboard', e) console.error('Failed to load dashboard', e)
dashboard.value = { title: title || 'Default', contents: [] } dashboard.value = null
loadError.value = e.message || 'Failed to load dashboard'
const pageTitle = window.initResponse?.pageTitle || 'OliveTin' const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = 'Error - ' + pageTitle document.title = 'Error - ' + pageTitle
@ -167,14 +236,13 @@ async function getDashboard() {
clearInterval(loadingTimer) clearInterval(loadingTimer)
loadingTimer = null loadingTimer = null
} }
// Set attribute even on error so tests can proceed
document.body.setAttribute('loaded-dashboard', title || 'error')
} }
} }
function waitForInitAndLoadDashboard () { function waitForInitAndLoadDashboard () {
document.body.removeAttribute('loaded-dashboard') document.body.removeAttribute('loaded-dashboard')
loadError.value = null
dashboard.value = null
if (loadingTimer) { if (loadingTimer) {
clearInterval(loadingTimer) clearInterval(loadingTimer)
@ -279,7 +347,6 @@ fieldset {
place-items: stretch; place-items: stretch;
} }
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
:id="`execution-${executionTrackingId}`" :id="`execution-${trackingId}`"
class="execution-button" class="execution-button"
> >
<button <button
@ -24,8 +24,10 @@ export default {
required: true required: true
} }
}, },
emits: ['show'],
data () { data () {
return { return {
trackingId: '',
ellapsed: 0, ellapsed: 0,
isWaiting: true isWaiting: true
} }
@ -40,22 +42,23 @@ export default {
} }
}, },
mounted () { mounted () {
this.trackingId = this.executionTrackingId
this.constructFromJson(this.executionTrackingId) this.constructFromJson(this.executionTrackingId)
}, },
methods: { methods: {
constructFromJson (json) { constructFromJson (json) {
this.executionTrackingId = json this.trackingId = json
this.ellapsed = 0 this.ellapsed = 0
this.isWaiting = true this.isWaiting = true
}, },
show() { async show () {
this.$emit('show') this.$emit('show')
if (window.executionDialog) { if (window.executionDialog) {
window.executionDialog.reset() await window.executionDialog.reset()
window.executionDialog.show() window.executionDialog.show()
window.executionDialog.fetchExecutionResult(this.executionTrackingId) window.executionDialog.fetchExecutionResult(this.trackingId)
} }
}, },
@ -87,7 +90,10 @@ export default {
// For execution button, we don't need to update classes as much // For execution button, we don't need to update classes as much
// since it's a simpler component // since it's a simpler component
if (resultCssClass) { if (resultCssClass) {
this.$el.classList.add(resultCssClass) const button = this.$el.querySelector('button')
if (button) {
button.classList.add(resultCssClass)
}
} }
} }
} }

View File

@ -1,5 +1,8 @@
<template> <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 }) }} {{ t('logs.action-group-limits', { concurrent: maxConcurrent, queueSize }) }}
</span> </span>
</template> </template>

View File

@ -7,8 +7,14 @@
height="1em" height="1em"
class="action-icon-glyph-svg" class="action-icon-glyph-svg"
/> />
<span v-else-if="decodedTextGlyphIsHtml" v-html="decodedTextGlyph"></span> <span
<span v-else v-text="decodedTextGlyph"></span> v-else-if="decodedTextGlyphIsHtml"
v-html="decodedTextGlyph"
/>
<span
v-else
v-text="decodedTextGlyph"
/>
</span> </span>
</template> </template>
@ -22,15 +28,15 @@ const hugeiconsPrefix = 'hugeicons:'
/** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */ /** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
const hugeiconsRegistry = { const hugeiconsRegistry = {
CommandLineIcon, CommandLineIcon
} }
const props = defineProps({ const props = defineProps({
glyph: { glyph: {
type: String, type: String,
required: false, required: false,
default: '', default: ''
}, }
}) })
const hugeiconsModel = computed(() => { const hugeiconsModel = computed(() => {

View File

@ -1,9 +1,14 @@
<template> <template>
<div :class="statusClass + ' annotation'"> <div :class="statusClass + ' annotation'">
<router-link v-if="showQueueLink" to="/logs/queue" class="queue-status-link">{{ statusText }}</router-link> <router-link
v-if="showQueueLink"
to="/logs/queue"
class="queue-status-link"
>
{{ statusText }}
</router-link>
<span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span> <span v-else>{{ statusText }}</span><span>{{ exitCodeText }}</span>
</div> </div>
</template> </template>
<script setup> <script setup>
@ -110,5 +115,4 @@ const statusClass = computed(() => {
text-decoration: underline; text-decoration: underline;
} }
</style> </style>

View File

@ -1,14 +1,50 @@
<template> <template>
<div id="breadcrumbs"> <div id="breadcrumbs">
<template v-for="(link, index) in links" :key="link.name"> <template
<router-link :to="link.href">{{ link.name }}</router-link> v-for="(link, index) in links"
<span v-if="index < links.length - 1" class="separator"> :key="link.name"
>
<router-link :to="link.href">
{{ link.name }}
</router-link>
<span
v-if="index < links.length - 1"
class="separator"
>
&raquo; &raquo;
</span> </span>
</template> </template>
</div> </div>
</template> </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> <style scoped>
span { span {
color: #bbb; color: #bbb;
@ -26,34 +62,3 @@ a:hover {
} }
</style> </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>

View File

@ -1,15 +1,28 @@
<template> <template>
<div class="choice-checklist" :id="wrapperId"> <div
:id="wrapperId"
class="choice-checklist"
>
<div class="choice-checklist-controls"> <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 Select all
</button> </button>
<button type="button" class="choice-checklist-control" @click="selectNone"> <button
type="button"
class="choice-checklist-control"
@click="selectNone"
>
Select none Select none
</button> </button>
</div> </div>
<fieldset class="choice-checklist-fieldset"> <fieldset class="choice-checklist-fieldset">
<legend class="visually-hidden">{{ label || name }}</legend> <legend class="visually-hidden">
{{ label || name }}
</legend>
<label <label
v-for="(choice, index) in choices" v-for="(choice, index) in choices"
:key="choice.value" :key="choice.value"
@ -21,7 +34,7 @@
type="checkbox" type="checkbox"
:checked="isSelected(choice.value)" :checked="isSelected(choice.value)"
@change="handleToggle(choice.value)" @change="handleToggle(choice.value)"
/> >
<span>{{ choiceLabel(choice) }}</span> <span>{{ choiceLabel(choice) }}</span>
</label> </label>
</fieldset> </fieldset>
@ -34,7 +47,7 @@
:required="required" :required="required"
tabindex="-1" tabindex="-1"
aria-hidden="true" aria-hidden="true"
/> >
</div> </div>
</template> </template>

View File

@ -1,8 +1,11 @@
<template> <template>
<div class="choice-combobox" ref="rootRef"> <div
ref="rootRef"
class="choice-combobox"
>
<input <input
ref="searchInputRef"
:id="id" :id="id"
ref="searchInputRef"
type="text" type="text"
class="choice-combobox-input" class="choice-combobox-input"
role="combobox" role="combobox"
@ -17,12 +20,12 @@
@input="handleSearchInput" @input="handleSearchInput"
@keydown="handleKeydown" @keydown="handleKeydown"
@blur="handleBlur" @blur="handleBlur"
/> >
<input <input
:name="name" :name="name"
type="hidden" type="hidden"
:value="modelValue" :value="modelValue"
/> >
<ul <ul
v-if="isOpen && filteredChoices.length > 0" v-if="isOpen && filteredChoices.length > 0"
:id="listboxId" :id="listboxId"
@ -44,7 +47,10 @@
{{ choiceLabel(choice) }} {{ choiceLabel(choice) }}
</li> </li>
</ul> </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 No matching options
</div> </div>
</div> </div>

View File

@ -1,8 +1,20 @@
<template> <template>
<span id="connection-banner" v-if="connectionState.showDisconnectedBanner" class="inline-notification critical user-info-connection"> <span
<span class="connection-banner-sr-only" role="status">{{ staticAnnouncement }}</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"> <span aria-hidden="true">
<a :href="websocketDocsUrl" target="_blank" rel="noopener noreferrer" class="connection-banner-link">{{ linkText }}</a>{{ bannerSuffix }} <a
:href="websocketDocsUrl"
target="_blank"
rel="noopener noreferrer"
class="connection-banner-link"
>{{ linkText }}</a>{{ bannerSuffix }}
</span> </span>
</span> </span>
</template> </template>

View File

@ -1,14 +1,31 @@
<template> <template>
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :cssClass="component.cssClass" :key="component.title" /> <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" /> <DashboardComponentDirectory
v-else-if="component.type == 'directory'"
:component="component"
/>
<DashboardComponentDisplay v-else-if="component.type == 'display'" :component="component" /> <DashboardComponentDisplay
v-else-if="component.type == 'display'"
:component="component"
/>
<DashboardComponentMostRecentExecution v-else-if="component.type == 'stdout-most-recent-execution'" :component="component" /> <DashboardComponentMostRecentExecution
v-else-if="component.type == 'stdout-most-recent-execution'"
:component="component"
/>
<template v-else-if="component.type == 'fieldset'"> <template v-else-if="component.type == 'fieldset'">
<template v-for="subcomponent in component.contents" :key="subcomponent.title"> <template
v-for="subcomponent in component.contents"
:key="subcomponent.title"
>
<DashboardComponent :component="subcomponent" /> <DashboardComponent :component="subcomponent" />
</template> </template>
</template> </template>
@ -17,7 +34,6 @@
OTHER: {{ component.type }} OTHER: {{ component.type }}
{{ component }} {{ component }}
</div> </div>
</template> </template>
<script setup> <script setup>
@ -26,7 +42,7 @@ import DashboardComponentMostRecentExecution from './DashboardComponentMostRecen
import DashboardComponentDirectory from './DashboardComponentDirectory.vue' import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
import DashboardComponentDisplay from './DashboardComponentDisplay.vue' import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
const props = defineProps({ defineProps({
component: { component: {
type: Object, type: Object,
required: true required: true

View File

@ -1,6 +1,13 @@
<template> <template>
<button class="directory-button" :class="component.cssClass" @click="navigateToDirectory"> <button
<span class="icon" v-html="unicodeIcon"></span> class="directory-button"
:class="component.cssClass"
@click="navigateToDirectory"
>
<span
class="icon"
v-html="unicodeIcon"
/>
<span class="title">{{ component.title }}</span> <span class="title">{{ component.title }}</span>
</button> </button>
</template> </template>

View File

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

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="mre-container" :class="component.cssClass"> <div
class="mre-container"
:class="component.cssClass"
>
<router-link <router-link
v-if="executionTrackingId" v-if="executionTrackingId"
:to="`/logs/${executionTrackingId}`" :to="`/logs/${executionTrackingId}`"
@ -7,7 +10,10 @@
> >
<pre class="mre-output">{{ output }}</pre> <pre class="mre-output">{{ output }}</pre>
</router-link> </router-link>
<pre v-else class="mre-output fg-important">{{ output }}</pre> <pre
v-else
class="mre-output fg-important"
>{{ output }}</pre>
</div> </div>
</template> </template>

View File

@ -3,44 +3,71 @@
<template #title> <template #title>
<span class="section-title-with-icon"> <span class="section-title-with-icon">
Entity: Entity:
<ActionIconGlyph v-if="definition.icon" class="entity-title-icon" :glyph="definition.icon" /> <ActionIconGlyph
v-if="definition.icon"
class="entity-title-icon"
:glyph="definition.icon"
/>
{{ definition.title }} {{ definition.title }}
</span> </span>
</template> </template>
<template v-if="hasTable" #toolbar> <template
v-if="hasTable"
#toolbar
>
<EntityListFilter v-model="searchText" /> <EntityListFilter v-model="searchText" />
</template> </template>
<p v-if="!hasTable">{{ definition.instances.length }} instances.</p> <p v-if="!hasTable">
{{ definition.instances.length }} instances.
</p>
<template v-if="hasTable"> <template v-if="hasTable">
<p v-if="tableError" class="table-error padding" role="alert">{{ tableError }}</p> <p
v-if="tableError"
class="table-error padding"
role="alert"
>
{{ tableError }}
</p>
<EntityInstancesTable <EntityInstancesTable
v-else v-else
v-model:page="currentPage"
v-model:page-size="pageSize"
:instances="tableInstances" :instances="tableInstances"
:properties="definition.properties" :properties="definition.properties"
:total-instances="totalInstances" :total-instances="totalInstances"
v-model:page="currentPage"
v-model:page-size="pageSize"
/> />
</template> </template>
<ul v-else> <ul v-else>
<li v-for="inst in definition.instances" :key="inst.uniqueKey"> <li
v-for="inst in definition.instances"
:key="inst.uniqueKey"
>
<router-link :to="entityDetailsRoute(inst)"> <router-link :to="entityDetailsRoute(inst)">
{{ inst.title }} {{ inst.title }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
<div v-if="usedDashboards.length > 0" :class="{ padding: hasTable }"> <div
v-if="usedDashboards.length > 0"
:class="{ padding: hasTable }"
>
<h3>Used on Dashboards:</h3> <h3>Used on Dashboards:</h3>
<ul> <ul>
<li v-for="dash in usedDashboards" :key="dash"> <li
v-for="dash in usedDashboards"
:key="dash"
>
<template v-if="isEntityDirectory(dash)"> <template v-if="isEntityDirectory(dash)">
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span> {{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
</template> </template>
<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }"> <router-link
v-else-if="!dash.includes('entity:')"
:to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }"
>
{{ getDashboardTitle(dash) }} {{ getDashboardTitle(dash) }}
</router-link> </router-link>
<span v-else>{{ dash }}</span> <span v-else>{{ dash }}</span>

View File

@ -11,11 +11,14 @@
</template> </template>
</Table> </Table>
<div v-if="totalInstances > 0" class="padding"> <div
v-if="totalInstances > 0"
class="padding"
>
<Pagination <Pagination
:total="totalInstances"
v-model:page="currentPageModel" v-model:page="currentPageModel"
v-model:page-size="pageSizeModel" v-model:page-size="pageSizeModel"
:total="totalInstances"
item-title="entities" item-title="entities"
/> />
</div> </div>

View File

@ -1,19 +1,39 @@
<template> <template>
<label class="input-with-icons"> <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"> <svg
<path fill="currentColor" xmlns="http://www.w3.org/2000/svg"
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" /> 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> </svg>
<input <input
:value="modelValue" :value="modelValue"
aria-label="Filter entities" aria-label="Filter entities"
placeholder="Filter entities..." placeholder="Filter entities..."
@input="$emit('update:modelValue', $event.target.value)" @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"
/> />
<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> </svg>
</button> </button>
</label> </label>

View File

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

View File

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

View File

@ -3,7 +3,11 @@
<template #title> <template #title>
<span class="section-title-with-icon"> <span class="section-title-with-icon">
Action Details: Action Details:
<ActionIconGlyph v-if="action" class="action-title-icon" :glyph="action.icon" /> <ActionIconGlyph
v-if="action"
class="action-title-icon"
:glyph="action.icon"
/>
{{ actionTitle }} {{ actionTitle }}
</span> </span>
</template> </template>
@ -12,14 +16,19 @@
<button <button
v-for="dashboard in backToDashboards" v-for="dashboard in backToDashboards"
:key="dashboard.path" :key="dashboard.path"
@click="goToDashboard(dashboard.path)"
:title="'Back to ' + dashboard.title" :title="'Back to ' + dashboard.title"
class="button neutral" class="button neutral"
@click="goToDashboard(dashboard.path)"
> >
<HugeiconsIcon :icon="DashboardSquare01Icon" /> <HugeiconsIcon :icon="DashboardSquare01Icon" />
{{ dashboard.title }} {{ dashboard.title }}
</button> </button>
<button v-if="action" @click="startAction" title="Run this action" class="button neutral"> <button
v-if="action"
title="Run this action"
class="button neutral"
@click="startAction"
>
<HugeiconsIcon :icon="WorkoutRunIcon" /> <HugeiconsIcon :icon="WorkoutRunIcon" />
Run Run
</button> </button>
@ -34,7 +43,10 @@
</div> </div>
</template> </template>
<div class = "flex-row padding" v-if="action"> <div
v-if="action"
class="flex-row padding"
>
<div class="fg1"> <div class="fg1">
<dl> <dl>
<dt>Timeout</dt> <dt>Timeout</dt>
@ -42,12 +54,28 @@
<template v-if="actionGroups.length > 0"> <template v-if="actionGroups.length > 0">
<dt> <dt>
<router-link :to="{ name: 'LogsQueue' }" class="action-groups-link">Action groups</router-link> <router-link
:to="{ name: 'LogsQueue' }"
class="action-groups-link"
>
Action groups
</router-link>
</dt> </dt>
<dd> <dd>
<ul class="action-group-list"> <ul class="action-group-list">
<li v-for="group in actionGroups" :key="group.name" class="action-group-row"> <li
<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 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" :max-concurrent="group.maxConcurrent"
:queue-size="group.queueSize" :queue-size="group.queueSize"
/> />
@ -63,15 +91,36 @@
<div style="align-self: start; text-align: right;"> <div style="align-self: start; text-align: right;">
<div class="filter-container"> <div class="filter-container">
<label class="input-with-icons"> <label class="input-with-icons">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <svg
<path fill="currentColor" xmlns="http://www.w3.org/2000/svg"
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" /> 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> </svg>
<input placeholder="Filter current page" v-model="searchText" /> <input
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch"> v-model="searchText"
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> placeholder="Filter current page"
<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" /> <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> </svg>
</button> </button>
</label> </label>
@ -91,9 +140,18 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle"> <tr
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td> v-for="log in filteredLogs"
<td class="duration">{{ formatExecutionDuration(log) }}</td> :key="log.executionTrackingId"
class="log-row"
:title="log.actionTitle"
>
<td class="timestamp">
{{ formatTimestamp(log.datetimeStarted) }}
</td>
<td class="duration">
{{ formatExecutionDuration(log) }}
</td>
<td> <td>
<router-link :to="`/logs/${log.executionTrackingId}`"> <router-link :to="`/logs/${log.executionTrackingId}`">
<LogActionTitle :justification="log.justification"> <LogActionTitle :justification="log.justification">
@ -106,24 +164,47 @@
<span class="annotation-key">User:</span> <span class="annotation-key">User:</span>
<span class="annotation-val">{{ log.user }}</span> <span class="annotation-val">{{ log.user }}</span>
</span> </span>
<span v-if="log.tags && log.tags.length > 0" class="tag-list"> <span
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</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> </span>
</td> </td>
<td class="exit-code"> <td class="exit-code">
<ActionStatusDisplay :logEntry="log" :link-queued-status="true" /> <ActionStatusDisplay
:log-entry="log"
:link-queued-status="true"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class="padding" <Pagination
@page-size-change="handlePageSizeChange" itemTitle="execution logs" /> :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>
<div v-show="logs.length === 0 && !loading" class="empty-state"> <div
v-show="logs.length === 0 && !loading"
class="empty-state"
>
<p>This action has no execution history.</p> <p>This action has no execution history.</p>
<router-link to="/">Return to index</router-link> <router-link to="/">
Return to index
</router-link>
</div> </div>
</Section> </Section>
</template> </template>
@ -181,9 +262,9 @@ async function fetchActionLogs() {
const startOffset = (currentPage.value - 1) * pageSize.value const startOffset = (currentPage.value - 1) * pageSize.value
const args = { const args = {
"actionId": actionId, actionId,
"startOffset": BigInt(startOffset), startOffset: BigInt(startOffset),
"pageSize": BigInt(Number(pageSize.value)), pageSize: BigInt(Number(pageSize.value))
} }
const response = await window.client.getActionLogs(args) const response = await window.client.getActionLogs(args)
@ -207,7 +288,7 @@ async function fetchAction() {
try { try {
const actionId = route.params.actionId const actionId = route.params.actionId
const args = { const args = {
"bindingId": actionId bindingId: actionId
} }
const response = await window.client.getActionBinding(args) const response = await window.client.getActionBinding(args)
action.value = response.action action.value = response.action

View File

@ -1,12 +1,21 @@
<template> <template>
<Section :title="'Execution conditions: ' + actionTitle" :padding="false"> <Section
:title="'Execution conditions: ' + actionTitle"
:padding="false"
>
<template #toolbar> <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 Back to action details
</router-link> </router-link>
</template> </template>
<div v-if="action" class="padding content"> <div
v-if="action"
class="padding content"
>
<p> <p>
These entries mirror the automatic triggers from your OliveTin configuration for this action. These entries mirror the automatic triggers from your OliveTin configuration for this action.
You can always run the action manually as well. You can always run the action manually as well.
@ -14,7 +23,12 @@
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
On click 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> </h3>
<p> <p>
Manual execution from the web UI (dashboard or action details), or via the API (for example StartAction), 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"> <template v-if="action.execOnStartup">
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
<code>execOnStartup</code> <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> </h3>
<p>Runs once when OliveTin starts.</p> <p>Runs once when OliveTin starts.</p>
</template> </template>
@ -32,37 +51,72 @@
<template v-if="nonEmptyList(action.execOnCron)"> <template v-if="nonEmptyList(action.execOnCron)">
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
<code>execOnCron</code> <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> </h3>
<ul> <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> </ul>
</template> </template>
<template v-if="nonEmptyList(action.execOnFileCreatedInDir)"> <template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
<code>execOnFileCreatedInDir</code> <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> </h3>
<ul> <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> </ul>
</template> </template>
<template v-if="nonEmptyList(action.execOnFileChangedInDir)"> <template v-if="nonEmptyList(action.execOnFileChangedInDir)">
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
<code>execOnFileChangedInDir</code> <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> </h3>
<ul> <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> </ul>
</template> </template>
<template v-if="action.execOnCalendarFile"> <template v-if="action.execOnCalendarFile">
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
<code>execOnCalendarFile</code> <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> </h3>
<p><code>{{ action.execOnCalendarFile }}</code></p> <p><code>{{ action.execOnCalendarFile }}</code></p>
</template> </template>
@ -70,10 +124,18 @@
<template v-if="nonEmptyList(action.execOnWebhooks)"> <template v-if="nonEmptyList(action.execOnWebhooks)">
<h3 class="exec-type-heading"> <h3 class="exec-type-heading">
<code>execOnWebhook</code> <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> </h3>
<ul class="webhook-list"> <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.template">template: <code>{{ wh.template }}</code></span>
<span v-if="wh.matchPath"> · matchPath: <code>{{ wh.matchPath }}</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> <span v-if="nonEmptyObject(wh.matchHeaders)"> · matchHeaders: <code>{{ wh.matchHeaders }}</code></span>
@ -83,14 +145,22 @@
</ul> </ul>
</template> </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. This action has no automatic triggers in configuration besides on-demand execution.
</p> </p>
</div> </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> <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> </div>
</Section> </Section>
</template> </template>
@ -112,7 +182,7 @@ const execConditionDocs = {
fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html', fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html', fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
calendar: 'https://docs.olivetin.app/action_execution/oncalendar.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) {

View File

@ -8,7 +8,11 @@
:to="`/action/${bindingId}`" :to="`/action/${bindingId}`"
class="action-details-title-link" 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 }} {{ title }}
</router-link> </router-link>
</span> </span>
@ -17,48 +21,96 @@
<div class="section-content padding"> <div class="section-content padding">
<form @submit="handleSubmit"> <form @submit="handleSubmit">
<template v-if="actionArguments.length > 0"> <template v-if="actionArguments.length > 0">
<template
<template v-for="arg in actionArguments" :key="arg.name"> v-for="arg in actionArguments"
<label v-if="arg.type !== 'checklist'" :for="argumentFieldId(arg.name)"> :key="arg.name"
>
<label
v-if="arg.type !== 'checklist'"
:for="argumentFieldId(arg.name)"
>
{{ formatLabel(arg.title) }} {{ formatLabel(arg.title) }}
</label> </label>
<div v-else class="argument-label"> <div
v-else
class="argument-label"
>
{{ formatLabel(arg.title) }} {{ formatLabel(arg.title) }}
</div> </div>
<datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="argumentFieldChoicesId(arg.name)"> <datalist
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key"> 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 }} {{ suggestion }}
</option> </option>
<option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion"> <option
v-for="(suggestion, index) in getBrowserSuggestions(arg)"
:key="`browser-${index}`"
:value="suggestion"
>
{{ suggestion }} {{ suggestion }}
</option> </option>
</datalist> </datalist>
<ChoiceCombobox v-if="getInputComponent(arg) === 'select'" :id="argumentFieldId(arg.name)" :name="arg.name" <ChoiceCombobox
:choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required" v-if="getInputComponent(arg) === 'select'"
@update:model-value="handleChoiceUpdate(arg, $event)" /> :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" <ChoiceChecklist
:label="arg.title" :choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required" v-else-if="arg.type === 'checklist'"
@update:model-value="handleChoiceUpdate(arg, $event)" /> :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" <component
:is="getInputComponent(arg)"
v-else
:id="argumentFieldId(arg.name)"
:name="arg.name"
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)" :value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined" :checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
:list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined" :list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined"
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined" :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined" :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)" :step="arg.type === 'datetime' ? 1 : undefined"
@input="handleInput(arg, $event)" @change="handleChange(arg, $event)" /> :pattern="getPattern(arg)"
@input="handleInput(arg, $event)"
@change="handleChange(arg, $event)"
/>
<span class="argument-description" v-html="arg.description"></span> <span
class="argument-description"
v-html="arg.description"
/>
</template> </template>
</template> </template>
<template v-if="justificationRequired"> <template v-if="justificationRequired">
<label for="justification">Justification:</label> <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> </template>
<div v-if="actionArguments.length === 0 && !justificationRequired"> <div v-if="actionArguments.length === 0 && !justificationRequired">
@ -66,10 +118,18 @@
</div> </div>
<div class="buttons"> <div class="buttons">
<button name="start" type="submit" :disabled="!formReady || (hasConfirmation && !confirmationChecked)"> <button
name="start"
type="submit"
:disabled="!formReady || (hasConfirmation && !confirmationChecked)"
>
Start Start
</button> </button>
<button name="cancel" type="button" @click="handleCancel"> <button
name="cancel"
type="button"
@click="handleCancel"
>
Cancel Cancel
</button> </button>
</div> </div>
@ -259,11 +319,13 @@ function getArgumentValue(arg) {
function handleJustificationInput (event) { function handleJustificationInput (event) {
justificationValue.value = event.target.value justificationValue.value = event.target.value
justificationEditedManually.value = true 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 const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
argValues.value[arg.name] = value argValues.value[arg.name] = value
event.target.setCustomValidity('')
updateUrlWithArg(arg.name, value) updateUrlWithArg(arg.name, value)
updateJustificationFromTemplate() updateJustificationFromTemplate()
} }
@ -316,7 +378,7 @@ async function validateArgument(arg, value) {
try { try {
const validateArgumentTypeArgs = { const validateArgumentTypeArgs = {
value: value, value,
type: arg.type, type: arg.type,
bindingId: props.bindingId, bindingId: props.bindingId,
argumentName: arg.name argumentName: arg.name
@ -638,7 +700,6 @@ form {
grid-template-columns: max-content auto auto; grid-template-columns: max-content auto auto;
} }
.argument-description { .argument-description {
font-size: 0.875rem; font-size: 0.875rem;
color: #666; color: #666;

View File

@ -1,10 +1,14 @@
<template> <template>
<Section :title="t('diagnostics.get-support')"> <Section :title="t('diagnostics.get-support')">
<p>{{ t('diagnostics.get-support-description') }} <p>
{{ t('diagnostics.get-support-description') }}
</p> </p>
<ul> <ul>
<li> <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> </li>
</ul> </ul>
</Section> </Section>
@ -21,26 +25,61 @@
<Section :title="t('diagnostics.server-diagnostics')"> <Section :title="t('diagnostics.server-diagnostics')">
<p>{{ t('diagnostics.server-diagnostics-description') }}</p> <p>{{ t('diagnostics.server-diagnostics-description') }}</p>
<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> </p>
<div role="toolbar"> <div role="toolbar">
<button @click="generateServerDiagnostics" :disabled="loading" class = "good">{{ t('diagnostics.generate-server-diagnostics') }}</button> <button
<button @click="copyServerDiagnostics" :disabled="!serverDiagnostics || loading" :class="serverDiagnosticsCopied ? 'good' : ''">{{ serverDiagnosticsCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</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> </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>
<Section :title="t('diagnostics.browser-info')"> <Section :title="t('diagnostics.browser-info')">
<p>{{ t('diagnostics.browser-info-description') }}</p> <p>{{ t('diagnostics.browser-info-description') }}</p>
<div role="toolbar"> <div role="toolbar">
<button @click="generateBrowserInfo" :disabled="loading" class = "good">{{ t('diagnostics.generate-browser-info') }}</button> <button
<button @click="copyBrowserInfo" :disabled="!browserInfo || loading" :class="browserInfoCopied ? 'good' : ''">{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</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> </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> </Section>
</template> </template>
@ -62,13 +101,13 @@ async function fetchDiagnostics() {
loading.value = true loading.value = true
try { try {
const response = await window.client.getDiagnostics(); const response = await window.client.getDiagnostics()
diagnostics.value = { diagnostics.value = {
sshFoundKey: response.SshFoundKey, sshFoundKey: response.SshFoundKey,
sshFoundConfig: response.SshFoundConfig sshFoundConfig: response.SshFoundConfig
}; }
} catch (err) { } catch (err) {
console.error('Failed to fetch diagnostics:', err); console.error('Failed to fetch diagnostics:', err)
diagnostics.value = { diagnostics.value = {
sshFoundKey: t('diagnostics.unknown'), sshFoundKey: t('diagnostics.unknown'),
sshFoundConfig: t('diagnostics.unknown') sshFoundConfig: t('diagnostics.unknown')
@ -77,17 +116,19 @@ async function fetchDiagnostics() {
loading.value = false loading.value = false
} }
function formatKey(key) {
return key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim()
}
async function generateServerDiagnostics () { async function generateServerDiagnostics () {
loading.value = true
try {
const response = await window.client.serverDiagnostics() const response = await window.client.serverDiagnostics()
console.log("response", response) console.log('response', response)
serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n` 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 () {
@ -159,7 +200,7 @@ async function generateBrowserInfo() {
})(), })(),
hardwareConcurrency: navigator.hardwareConcurrency || 'N/A', hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
maxTouchPoints: navigator.maxTouchPoints || 'N/A', maxTouchPoints: navigator.maxTouchPoints || 'N/A',
userAgentData: userAgentData userAgentData
} }
const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
@ -168,20 +209,20 @@ async function generateBrowserInfo() {
: '[hidden]' : '[hidden]'
const currentLanguage = locale.value || t('diagnostics.unknown') const currentLanguage = locale.value || t('diagnostics.unknown')
let output = ''; let output = ''
output += `\`\`\`\n` output += '```\n'
output += '### BROWSER INFO START (copy all text to BROWSER INFO END)\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 += `olivetinVersion: ${olivetinVersion}\n`
output += `currentLanguage: ${currentLanguage}\n` output += `currentLanguage: ${currentLanguage}\n`
output += `\n# Browser Information\n` output += '\n# Browser Information\n'
output += `userAgent: ${info.userAgent}\n` output += `userAgent: ${info.userAgent}\n`
output += `platform: ${info.platform}\n` output += `platform: ${info.platform}\n`
output += `language: ${info.language}\n` output += `language: ${info.language}\n`
output += `languages: ${info.languages}\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 += `userAgentData:\n${info.userAgentData}\n`
output += `\n# Display Information\n` output += '\n# Display Information\n'
output += `screenWidth: ${info.screenWidth}\n` output += `screenWidth: ${info.screenWidth}\n`
output += `screenHeight: ${info.screenHeight}\n` output += `screenHeight: ${info.screenHeight}\n`
output += `screenColorDepth: ${info.screenColorDepth}\n` output += `screenColorDepth: ${info.screenColorDepth}\n`
@ -189,18 +230,18 @@ async function generateBrowserInfo() {
output += `viewportWidth: ${info.viewportWidth}\n` output += `viewportWidth: ${info.viewportWidth}\n`
output += `viewportHeight: ${info.viewportHeight}\n` output += `viewportHeight: ${info.viewportHeight}\n`
output += `devicePixelRatio: ${info.devicePixelRatio}\n` output += `devicePixelRatio: ${info.devicePixelRatio}\n`
output += `\n# Feature Support\n` output += '\n# Feature Support\n'
output += `cookieEnabled: ${info.cookieEnabled}\n` output += `cookieEnabled: ${info.cookieEnabled}\n`
output += `localStorageEnabled: ${info.localStorageEnabled}\n` output += `localStorageEnabled: ${info.localStorageEnabled}\n`
output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n` output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
output += `onLine: ${info.onLine}\n` output += `onLine: ${info.onLine}\n`
output += `hardwareConcurrency: ${info.hardwareConcurrency}\n` output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
output += `maxTouchPoints: ${info.maxTouchPoints}\n` output += `maxTouchPoints: ${info.maxTouchPoints}\n`
output += `\n# Location & Time\n` output += '\n# Location & Time\n'
output += `timezone: ${info.timezone}\n` output += `timezone: ${info.timezone}\n`
output += `timezoneOffset: ${info.timezoneOffset}\n` output += `timezoneOffset: ${info.timezoneOffset}\n`
output += `\n### BROWSER INFO END (copy all text from BROWSER INFO START)` output += '\n### BROWSER INFO END (copy all text from BROWSER INFO START)'
output += `\n\`\`\`\n` output += '\n```\n'
browserInfo.value = output browserInfo.value = output
} finally { } finally {

View File

@ -1,5 +1,8 @@
<template> <template>
<Section v-if="!definitionsLoaded" title="Loading entity definitions..." /> <Section
v-if="!definitionsLoaded"
title="Loading entity definitions..."
/>
<Section <Section
v-else-if="totalInstances === 0" v-else-if="totalInstances === 0"
title="There are no entities to show yet." title="There are no entities to show yet."
@ -28,8 +31,8 @@
const totalInstances = computed(() => const totalInstances = computed(() =>
entityDefinitions.value.reduce( entityDefinitions.value.reduce(
(sum, def) => sum + (def.instances?.length ?? 0), (sum, def) => sum + (def.instances?.length ?? 0),
0, 0
), )
) )
async function fetchEntities () { async function fetchEntities () {

View File

@ -3,13 +3,24 @@
<template #title> <template #title>
<span class="section-title-with-icon"> <span class="section-title-with-icon">
Entity Details: Entity Details:
<ActionIconGlyph v-if="entityIcon" class="entity-title-icon" :glyph="entityIcon" /> <ActionIconGlyph
v-if="entityIcon"
class="entity-title-icon"
:glyph="entityIcon"
/>
<span v-if="entityDetails?.title">{{ entityDetails.title }}</span> <span v-if="entityDetails?.title">{{ entityDetails.title }}</span>
</span> </span>
</template> </template>
<template #toolbar> <template #toolbar>
<button @click="goBack" class="back-button"> <button
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" /> class="back-button"
@click="goBack"
>
<HugeiconsIcon
:icon="ArrowLeftIcon"
width="1.2em"
height="1.2em"
/>
<span>Back</span> <span>Back</span>
</button> </button>
</template> </template>
@ -20,27 +31,48 @@
<dl> <dl>
<dt>Type</dt> <dt>Type</dt>
<dd> <dd>
<router-link :to="{ name: 'Entities' }" class="entity-type-link"> <router-link
:to="{ name: 'Entities' }"
class="entity-type-link"
>
{{ entityType }} {{ entityType }}
</router-link> </router-link>
</dd> </dd>
<dt v-if="entityDetails.title">Title</dt> <dt v-if="entityDetails.title">
<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd> Title
</dt>
<dd v-if="entityDetails.title">
{{ entityDetails.title }}
</dd>
<template v-if="entityDetails.fields"> <template v-if="entityDetails.fields">
<template v-for="(value, key) in entityDetails.fields" :key="key"> <template
v-for="(value, key) in entityDetails.fields"
:key="key"
>
<dt>{{ key }}</dt> <dt>{{ key }}</dt>
<dd>{{ value }}</dd> <dd>{{ value }}</dd>
</template> </template>
</template> </template>
</dl> </dl>
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p> <p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">
No details available for this entity.
</p>
</template> </template>
</Section> </Section>
<Section v-if="entityDetails" title="Dashboard Entity Directories"> <Section
<div v-if="filteredDirectories.length > 0" class="directories-section"> v-if="entityDetails"
title="Dashboard Entity Directories"
>
<div
v-if="filteredDirectories.length > 0"
class="directories-section"
>
<ul class="directory-list"> <ul class="directory-list">
<li v-for="(directory, idx) in filteredDirectories" :key="idx"> <li
v-for="(directory, idx) in filteredDirectories"
:key="idx"
>
<router-link <router-link
:to="{ :to="{
name: 'Dashboard', name: 'Dashboard',
@ -49,22 +81,36 @@
entityType: entityType, entityType: entityType,
entityKey: entityKey entityKey: entityKey
} }
}"> }"
>
{{ directory }} {{ directory }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
</div> </div>
<p v-else>No directories found for this entity. <p v-else>
<a href="https://docs.olivetin.app/dashboards/entity-directories.html" target="_blank" rel="noopener noreferrer">Learn more</a> 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> </p>
</Section> </Section>
<section v-if="entityDetails && relatedActions.length > 0" class="transparent"> <section
v-if="entityDetails && relatedActions.length > 0"
class="transparent"
>
<div class="dashboard-row"> <div class="dashboard-row">
<fieldset> <fieldset>
<legend class="visually-hidden">Related actions</legend> <legend class="visually-hidden">
<template v-for="(related, idx) in relatedActions" :key="related.action?.bindingId || idx"> Related actions
</legend>
<template
v-for="(related, idx) in relatedActions"
:key="related.action?.bindingId || idx"
>
<ActionButton <ActionButton
v-if="related.action" v-if="related.action"
:action-data="related.action" :action-data="related.action"

View File

@ -9,12 +9,23 @@
class="action-details-title-link" class="action-details-title-link"
:title="titleTooltip" :title="titleTooltip"
> >
<ActionIconGlyph class="action-title-icon" :glyph="icon" /> <ActionIconGlyph
<LogActionTitle v-if="logEntry" :action-title="title" :justification="logEntry.justification" /> class="action-title-icon"
:glyph="icon"
/>
<LogActionTitle
v-if="logEntry"
:action-title="title"
:justification="logEntry.justification"
/>
<span v-else>{{ title }}</span> <span v-else>{{ title }}</span>
</router-link> </router-link>
<template v-else> <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> <span v-else>{{ title }}</span>
</template> </template>
</span> </span>
@ -23,68 +34,111 @@
<button <button
v-for="dashboard in backToDashboards" v-for="dashboard in backToDashboards"
:key="dashboard.path" :key="dashboard.path"
@click="goToDashboard(dashboard.path)"
:title="'Back to ' + dashboard.title" :title="'Back to ' + dashboard.title"
class="button neutral" class="button neutral"
@click="goToDashboard(dashboard.path)"
> >
<HugeiconsIcon :icon="DashboardSquare01Icon" /> <HugeiconsIcon :icon="DashboardSquare01Icon" />
{{ dashboard.title }} {{ dashboard.title }}
</button> </button>
<button v-if="backToDashboards.length === 0" @click="goBack" title="Go back" class="button neutral"> <button
v-if="backToDashboards.length === 0"
title="Go back"
class="button neutral"
@click="goBack"
>
<HugeiconsIcon :icon="ArrowLeftIcon" /> <HugeiconsIcon :icon="ArrowLeftIcon" />
Back Back
</button> </button>
</template> </template>
<div v-if="logEntry" class = "flex-row"> <div
v-if="logEntry"
class="flex-row"
>
<dl class="fg1"> <dl class="fg1">
<dt>Duration</dt> <dt>Duration</dt>
<dd><span v-html="duration"></span></dd> <dd><span v-html="duration" /></dd>
<dt>Status</dt> <dt>Status</dt>
<dd class="execution-dialog-status"> <dd class="execution-dialog-status">
<ActionStatusDisplay :log-entry="logEntry" :link-queued-status="true" /> <ActionStatusDisplay
:log-entry="logEntry"
:link-queued-status="true"
/>
</dd> </dd>
</dl> </dl>
</div> </div>
<div v-if="notFound" class="error-message padded-content"> <div
v-if="notFound"
class="error-message padded-content"
>
<h3>Execution Not Found</h3> <h3>Execution Not Found</h3>
<p>{{ errorMessage }}</p> <p>{{ errorMessage }}</p>
<p>The execution with ID <code>{{ executionTrackingId }}</code> could not be found.</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>. <router-link to="/logs">
View all logs
</router-link> or <router-link to="/">
return to home
</router-link>.
</div> </div>
<div class="xterm-output-container"> <div class="xterm-output-container">
<div class="xterm-overlay-toolbar"> <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" /> <HugeiconsIcon :icon="Copy01Icon" />
</button> </button>
<button type="button" class="xterm-overlay-button" @click="toggleSize" title="Toggle fullscreen"> <button
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> type="button"
<path fill="currentColor" class="xterm-overlay-button"
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" /> 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> </svg>
</button> </button>
</div> </div>
<div ref="xtermOutput"></div> <div ref="xtermOutput" />
</div> </div>
<br /> <br>
<div class="flex-row g1 buttons padded-content"> <div class="flex-row g1 buttons padded-content">
<div class="fg1" /> <div class="fg1" />
<button :disabled="!canRerun" @click="rerunAction" title="Rerun"> <button
:disabled="!canRerun"
title="Rerun"
@click="rerunAction"
>
<HugeiconsIcon :icon="WorkoutRunIcon" /> <HugeiconsIcon :icon="WorkoutRunIcon" />
Rerun Rerun
</button> </button>
<button :disabled="!canKill" @click="killAction" title="Kill" id = "execution-dialog-kill-action"> <button
id="execution-dialog-kill-action"
:disabled="!canKill"
title="Kill"
@click="killAction"
>
<HugeiconsIcon :icon="Cancel02Icon" /> <HugeiconsIcon :icon="Cancel02Icon" />
Kill Kill
</button> </button>
</div> </div>
</Section> </Section>
</template> </template>
@ -276,7 +330,7 @@ function hideEverythingApartFromOutput() {
} }
async function fetchExecutionResult (executionTrackingIdParam) { async function fetchExecutionResult (executionTrackingIdParam) {
console.log("fetchExecutionResult", executionTrackingIdParam) console.log('fetchExecutionResult', executionTrackingIdParam)
executionTrackingId.value = executionTrackingIdParam executionTrackingId.value = executionTrackingIdParam
notFound.value = false notFound.value = false
@ -307,7 +361,6 @@ function updateDuration(logEntryParam) {
logEntry.value = logEntryParam logEntry.value = logEntryParam
if (logEntry.value == null) { if (logEntry.value == null) {
duration.value = executionSeconds.value + ' seconds' duration.value = executionSeconds.value + ' seconds'
duration.value = duration.value
} else if (!logEntry.value.executionStarted) { } 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) { } else if (logEntry.value.executionStarted && !logEntry.value.executionFinished) {
@ -418,7 +471,6 @@ onMounted(() => {
} }
} }
) )
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -1,33 +1,83 @@
<template> <template>
<Section title="Login to OliveTin" class="small"> <Section
title="Login to OliveTin"
class="small"
>
<div class="login-form"> <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> <span>This server is not configured with either OAuth, or local users, so you cannot login.</span>
</div> </div>
<div v-if="hasOAuth" class="login-oauth2"> <div
v-if="hasOAuth"
class="login-oauth2"
>
<h3>OAuth Login</h3> <h3>OAuth Login</h3>
<div class="oauth-providers"> <div class="oauth-providers">
<button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button" <button
@click="loginWithOAuth(provider)"> v-for="provider in oauthProviders"
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span> :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> <span class="provider-name">Login with {{ provider.title }}</span>
</button> </button>
</div> </div>
</div> </div>
<div v-if="hasLocalLogin" class="login-local"> <div
v-if="hasLocalLogin"
class="login-local"
>
<h3>Local Login</h3> <h3>Local Login</h3>
<form @submit.prevent="handleLocalLogin" class="local-login-form"> <form
<div v-if="loginError" class="bad"> class="local-login-form"
@submit.prevent="handleLocalLogin"
>
<div
v-if="loginError"
class="bad"
>
{{ loginError }} {{ loginError }}
</div> </div>
<input id="username" v-model="username" type="text" name="username" autocomplete="username" required placeholder="Username" /> <input
<input id="password" v-model="password" type="password" name="password" autocomplete="current-password" placeholder="Password" id="username"
required /> 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"> <button
type="submit"
:disabled="loading"
class="login-button"
>
{{ loading ? 'Logging in...' : 'Login' }} {{ loading ? 'Logging in...' : 'Login' }}
</button> </button>
</form> </form>
@ -51,6 +101,38 @@ const hasOAuth = ref(false)
const hasLocalLogin = ref(false) const hasLocalLogin = ref(false)
const oauthProviders = ref([]) const oauthProviders = ref([])
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 () { function loadLoginOptions () {
// Use the init response data that was loaded in App.vue // Use the init response data that was loaded in App.vue
if (window.initResponse) { if (window.initResponse) {
@ -119,7 +201,7 @@ onMounted(() => {
loadLoginOptions() loadLoginOptions()
// Also watch for when init response becomes available // Also watch for when init response becomes available
const stopWatcher = watch(() => window.initResponse, () => { watch(() => window.initResponse, () => {
loadLoginOptions() loadLoginOptions()
}, { immediate: true }) }, { immediate: true })
}) })

View File

@ -1,9 +1,23 @@
<template> <template>
<Section :title="t('logs.calendar-title')" :padding="false"> <Section
:title="t('logs.calendar-title')"
:padding="false"
>
<template #toolbar> <template #toolbar>
<router-link to="/logs" class="button neutral"> <router-link
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> to="/logs"
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/> 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> </svg>
{{ t('logs.back-to-list') }} {{ t('logs.back-to-list') }}
</router-link> </router-link>
@ -62,8 +76,8 @@ const calendarEvents = computed(() => {
id: log.executionTrackingId, id: log.executionTrackingId,
title: log.actionTitle || 'Untitled Action', title: log.actionTitle || 'Untitled Action',
date: startDate, date: startDate,
startDate: startDate, startDate,
endDate: endDate, endDate,
actionIcon: log.actionIcon, actionIcon: log.actionIcon,
user: log.user, user: log.user,
tags: log.tags, tags: log.tags,
@ -82,11 +96,10 @@ async function fetchLogs() {
const allLogs = [] const allLogs = []
let startOffset = BigInt(0) let startOffset = BigInt(0)
const maxLogs = 10000 // Reasonable limit to prevent excessive API calls 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) { while (allLogs.length < maxLogs) {
const args = { const args = {
"startOffset": startOffset, startOffset
} }
const response = await window.client.getLogs(args) const response = await window.client.getLogs(args)

View File

@ -1,30 +1,61 @@
<template> <template>
<Section :title="t('logs.title')" :padding="false"> <Section
:title="t('logs.title')"
:padding="false"
>
<template #toolbar> <template #toolbar>
<router-link to="/logs/queue" class="button neutral"> <router-link
to="/logs/queue"
class="button neutral"
>
{{ t('logs.queue') }} {{ t('logs.queue') }}
</router-link> </router-link>
<router-link to="/logs/calendar" class="button neutral"> <router-link
to="/logs/calendar"
class="button neutral"
>
{{ t('logs.calendar') }} {{ t('logs.calendar') }}
</router-link> </router-link>
<label class="input-with-icons"> <label class="input-with-icons">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <svg
<path fill="currentColor" xmlns="http://www.w3.org/2000/svg"
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" /> 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> </svg>
<input <input
:placeholder="t('logs.filter-placeholder')"
v-model="searchText" v-model="searchText"
:placeholder="t('logs.filter-placeholder')"
list="logs-filter-suggestions" list="logs-filter-suggestions"
:aria-invalid="filterError ? 'true' : 'false'" :aria-invalid="filterError ? 'true' : 'false'"
/> >
<datalist id="logs-filter-suggestions"> <datalist id="logs-filter-suggestions">
<option v-for="suggestion in filterSuggestions" :key="suggestion" :value="suggestion" /> <option
v-for="suggestion in filterSuggestions"
:key="suggestion"
:value="suggestion"
/>
</datalist> </datalist>
<button :title="t('logs.clear-filter')" :disabled="!searchText" @click="clearSearch"> <button
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> :title="t('logs.clear-filter')"
<path fill="currentColor" :disabled="!searchText"
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" /> @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> </svg>
</button> </button>
</label> </label>
@ -38,7 +69,13 @@
<p>{{ t('logs.filter-help-fields') }}</p> <p>{{ t('logs.filter-help-fields') }}</p>
<p><code>{{ t('logs.filter-help-examples') }}</code></p> <p><code>{{ t('logs.filter-help-examples') }}</code></p>
</details> </details>
<p v-if="filterError" class="filter-error" role="alert">{{ filterError }}</p> <p
v-if="filterError"
class="filter-error"
role="alert"
>
{{ filterError }}
</p>
</div> </div>
<div v-show="logs.length > 0"> <div v-show="logs.length > 0">
@ -48,12 +85,26 @@
<th> <th>
<div class="timestamp-header"> <div class="timestamp-header">
<span>{{ t('logs.timestamp') }}</span> <span>{{ t('logs.timestamp') }}</span>
<span v-if="selectedDate" class="date-filter-indicator"> <span
v-if="selectedDate"
class="date-filter-indicator"
>
<span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span> <span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
<button :title="t('logs.clear-date-filter')" @click="clearDateFilter" class="clear-date-button"> <button
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> :title="t('logs.clear-date-filter')"
<path fill="currentColor" class="clear-date-button"
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" /> @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> </svg>
</button> </button>
</span> </span>
@ -65,12 +116,25 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="log in logs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle"> <tr
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td> v-for="log in logs"
:key="log.executionTrackingId"
class="log-row"
:title="log.actionTitle"
>
<td class="timestamp">
{{ formatTimestamp(log.datetimeStarted) }}
</td>
<td> <td>
<ActionIconGlyph class="icon" :glyph="log.actionIcon" /> <ActionIconGlyph
class="icon"
:glyph="log.actionIcon"
/>
<router-link :to="`/logs/${log.executionTrackingId}`"> <router-link :to="`/logs/${log.executionTrackingId}`">
<LogActionTitle :action-title="log.actionTitle" :justification="log.justification" /> <LogActionTitle
:action-title="log.actionTitle"
:justification="log.justification"
/>
</router-link> </router-link>
</td> </td>
<td class="tags"> <td class="tags">
@ -78,38 +142,70 @@
<span class="annotation-key">User:</span> <span class="annotation-key">User:</span>
<span class="annotation-val">{{ log.user }}</span> <span class="annotation-val">{{ log.user }}</span>
</span> </span>
<span v-if="log.tags && log.tags.length > 0" class="tag-list"> <span
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</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> </span>
</td> </td>
<td class="exit-code"> <td class="exit-code">
<ActionStatusDisplay :logEntry="log" /> <ActionStatusDisplay :log-entry="log" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class = "padding" <Pagination
@page-size-change="handlePageSizeChange" itemTitle="execution logs" /> :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>
<div v-show="logs.length === 0 && !loading && searchText && !filterError" class="empty-state padding"> <div
v-show="logs.length === 0 && !loading && searchText && !filterError"
class="empty-state padding"
>
<p>{{ t('logs.no-logs-for-filter') }}</p> <p>{{ t('logs.no-logs-for-filter') }}</p>
<button @click="clearSearch" class="button neutral"> <button
class="button neutral"
@click="clearSearch"
>
{{ t('logs.clear-filter') }} {{ t('logs.clear-filter') }}
</button> </button>
</div> </div>
<div v-show="selectedDate && logs.length === 0 && !loading && !searchText" class="empty-state padding"> <div
v-show="selectedDate && logs.length === 0 && !loading && !searchText"
class="empty-state padding"
>
<p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p> <p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
<button @click="clearDateFilter" class="button neutral"> <button
class="button neutral"
@click="clearDateFilter"
>
{{ t('logs.clear-date-filter') }} {{ t('logs.clear-date-filter') }}
</button> </button>
</div> </div>
<div v-show="logs.length === 0 && !loading && !selectedDate && !searchText" class="empty-state padding"> <div
v-show="logs.length === 0 && !loading && !selectedDate && !searchText"
class="empty-state padding"
>
<p>{{ t('logs.no-logs-to-display') }}</p> <p>{{ t('logs.no-logs-to-display') }}</p>
<router-link to="/">{{ t('return-to-index') }}</router-link> <router-link to="/">
{{ t('return-to-index') }}
</router-link>
</div> </div>
</Section> </Section>
</template> </template>

View File

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

View File

@ -6,10 +6,16 @@
<h2>Page Not Found</h2> <h2>Page Not Found</h2>
<div class="actions"> <div class="actions">
<button class = "button good" @click="goToHome"> <button
class="button good"
@click="goToHome"
>
Go to Home Go to Home
</button> </button>
<button class="button neutral" @click="goBack"> <button
class="button neutral"
@click="goBack"
>
Go Back Go Back
</button> </button>
</div> </div>

View File

@ -1,27 +1,58 @@
<template> <template>
<Section title="User Information" class="small"> <Section
<div v-if="!isLoggedIn" class="user-not-logged-in"> title="User Information"
class="small"
>
<div
v-if="!isLoggedIn"
class="user-not-logged-in"
>
<p>You are not currently logged in.</p> <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>
<div v-else class="user-control-panel"> <div
v-else
class="user-control-panel"
>
<dl class="user-info"> <dl class="user-info">
<dt>Username</dt> <dt>Username</dt>
<dd>{{ username }}</dd> <dd>{{ username }}</dd>
<dt v-if="userProvider !== 'system'">Provider</dt> <dt v-if="userProvider !== 'system'">
<dd v-if="userProvider !== 'system'">{{ userProvider }}</dd> Provider
<dt v-if="usergroup">Group</dt> </dt>
<dd v-if="usergroup">{{ usergroup }}</dd> <dd v-if="userProvider !== 'system'">
<dt v-if="acls && acls.length > 0">Matched ACLs</dt> {{ 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"> <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> </dd>
</dl> </dl>
<div class="user-actions"> <div class="user-actions">
<div class="action-buttons"> <div class="action-buttons">
<button @click="handleLogout" class="button bad" :disabled="loggingOut"> <button
class="button bad"
:disabled="loggingOut"
@click="handleLogout"
>
{{ loggingOut ? 'Logging out...' : 'Logout' }} {{ loggingOut ? 'Logging out...' : 'Logout' }}
</button> </button>
</div> </div>
@ -31,7 +62,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Section from 'picocrank/vue/components/Section.vue' import Section from 'picocrank/vue/components/Section.vue'
@ -102,12 +133,11 @@ async function handleLogout() {
} }
} }
let watchInterval = null const watchInterval = null
onMounted(() => { onMounted(() => {
updateUserInfo() updateUserInfo()
fetchWhoAmI() fetchWhoAmI()
}) })
onUnmounted(() => { onUnmounted(() => {

View File

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

View File

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

View File

@ -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

View File

@ -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')
})
})

View File

@ -1,11 +1,11 @@
module github.com/OliveTin/OliveTin/langtool module github.com/OliveTin/OliveTin/langtool
go 1.24.0 go 1.25.0
require ( 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 github.com/sirupsen/logrus v1.9.4
gopkg.in/yaml.v3 v3.0.1 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

View File

@ -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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp9swe2d6Y6ij2ecd16E+i2oHAH/xaA=
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE= github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= 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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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

23
service/.golangci.yml Normal file
View File

@ -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

View File

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

View File

@ -1,6 +1,6 @@
module github.com/OliveTin/OliveTin module github.com/OliveTin/OliveTin
go 1.25.10 go 1.26
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 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/MicahParks/keyfunc/v3 v3.8.0
github.com/PaesslerAG/jsonpath v0.1.1 github.com/PaesslerAG/jsonpath v0.1.1
github.com/alexedwards/argon2id v1.0.0 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/fsnotify/fsnotify v1.10.1
github.com/fzipp/gocyclo v0.6.0 github.com/go-viper/mapstructure/v2 v2.5.0
github.com/go-critic/go-critic v0.14.3
github.com/golang-jwt/jwt/v5 v5.3.1 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/google/uuid v1.6.0
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da
github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/parsers/yaml v1.1.0
github.com/knadh/koanf/providers/env 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/file v1.2.1
github.com/knadh/koanf/providers/rawbytes v1.0.0 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/prometheus/client_golang v1.23.2
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.4 github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
go.akshayshah.org/connectproto v0.6.0 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/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 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( 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/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/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/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 // indirect
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.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/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/app v0.2.1-0.20260407195847-833f8f978cda // indirect
buf.build/go/bufplugin v0.10.0 // 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/spdx v0.2.0 // indirect
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
cel.dev/expr v0.25.2 // 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 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/MicahParks/jwkset v0.11.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // 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/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/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/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/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/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/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 v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.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/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/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/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.5.2+incompatible // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/cli v29.6.1+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.8 // indirect
github.com/docker/docker-credential-helpers v0.9.7 // indirect
github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/expr-lang/expr v1.17.8 // indirect github.com/ettle/strcase v0.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fatih/color v1.19.0 // indirect
github.com/go-chi/chi/v5 v5.2.5 // 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/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // 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/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp 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/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep 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/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-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.21.6 // indirect github.com/google/go-containerregistry v0.21.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdx/go-netrc v1.0.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/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.2 // 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-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/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.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/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect github.com/moby/moby/api v1.55.0 // indirect
github.com/moby/moby/client v0.4.1 // indirect github.com/moby/moby/client v0.5.0 // indirect
github.com/moby/term v0.5.2 // indirect github.com/moricho/tparallel v0.3.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pkg/errors v0.9.1 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.69.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // 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/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/quic-go/qpack v0.6.0 // 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/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/rs/cors v1.11.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/segmentio/encoding v0.5.4 // 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/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/cobra v1.10.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect github.com/spf13/viper v1.12.0 // indirect
github.com/tetratelabs/wazero v1.11.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/tidwall/btree v1.8.1 // indirect
github.com/vbatts/tar-split v0.12.3 // indirect github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect
go.lsp.dev/jsonrpc2 v0.10.0 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
go.lsp.dev/protocol v0.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
go.lsp.dev/uri v0.3.0 // 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/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.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 go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.52.0 // indirect golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/mod v0.36.0 // indirect golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.55.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.22.0 // indirect
golang.org/x/term v0.43.0 // indirect golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect golang.org/x/text v0.39.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/grpc v1.79.3 // 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 mvdan.cc/xurls/v2 v2.6.0 // indirect
pluginrpc.com/pluginrpc v0.5.0 // indirect pluginrpc.com/pluginrpc v0.5.0 // indirect
) )

File diff suppressed because it is too large Load Diff

View File

@ -21,37 +21,35 @@ func (p PermissionBits) Has(permission PermissionBits) bool {
return p&permission != 0 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 { if cfg.LogDebugOptions.AclNotMatched {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"User": user.Username, "User": user.Username,
"Action": action.Title, "Resource": resourceTitle,
"ACL": acl.Name, "ACL": acl.Name,
}).Debugf("%v - ACL Not Matched", aclFunction) }).Debugf("%v - ACL Not Matched", aclFunction)
} }
} }
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) { func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, acl *config.AccessControlList) {
actionTitle := "N/A" if resourceTitle == "" {
resourceTitle = "N/A"
if action != nil {
actionTitle = action.Title
} }
if cfg.LogDebugOptions.AclMatched { if cfg.LogDebugOptions.AclMatched {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"User": user.Username, "User": user.Username,
"Action": actionTitle, "Resource": resourceTitle,
"ACL": acl.Name, "ACL": acl.Name,
}).Debugf("%v - Matched ACL", aclFunction) }).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 { if cfg.LogDebugOptions.AclNoneMatched {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"User": user.Username, "User": user.Username,
"Action": action.Title, "Resource": resourceTitle,
"Default": defaultPermission, "Default": defaultPermission,
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction) }).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
} }
@ -81,12 +79,12 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
return ret return ret
} }
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action) bool { func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, resourceTitle string, resourceAcls []string, includeAddToEvery bool) bool {
relevantAcls := getRelevantAcls(cfg, action.Acls, user) relevantAcls := getRelevantAcls(cfg, resourceAcls, user, includeAddToEvery)
if cfg.LogDebugOptions.AclCheckStarted { if cfg.LogDebugOptions.AclCheckStarted {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"actionTitle": action.Title, "resourceTitle": resourceTitle,
"username": user.Username, "username": user.Username,
"usergroupLine": user.UsergroupLine, "usergroupLine": user.UsergroupLine,
"relevantAcls": len(relevantAcls), "relevantAcls": len(relevantAcls),
@ -98,27 +96,27 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
permissionBits := permissionsConfigToBits(acl.Permissions) permissionBits := permissionsConfigToBits(acl.Permissions)
if permissionBits.Has(requiredPermission) { if permissionBits.Has(requiredPermission) {
logAclMatched(cfg, aclFunction, user, action, acl) logAclMatched(cfg, aclFunction, user, resourceTitle, acl)
return true return true
} else {
logAclNotMatched(cfg, aclFunction, user, action, acl)
}
} }
logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs) logAclNotMatched(cfg, aclFunction, user, resourceTitle, acl)
}
logAclNoneMatched(cfg, aclFunction, user, resourceTitle, defaultValue)
return defaultValue return defaultValue
} }
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs // 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 { 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 // IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool { 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 // 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 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 { 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 { // 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
}
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) { if !slices.Contains(user.Acls, acl.Name) {
// If the user does not have this ACL, then it is not relevant
return false return false
} }
if acl.AddToEveryAction { if includeAddToEvery && acl.AddToEveryAction {
return true return true
} }
if slices.Contains(actionAcls, acl.Name) { return slices.Contains(resourceAcls, acl.Name)
return true
} }
return false func getRelevantAcls(cfg *config.Config, resourceAcls []string, user *authpublic.AuthenticatedUser, includeAddToEvery bool) []*config.AccessControlList {
}
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *authpublic.AuthenticatedUser) []*config.AccessControlList {
var ret []*config.AccessControlList var ret []*config.AccessControlList
for _, acl := range cfg.AccessControlLists { for _, acl := range cfg.AccessControlLists {
if isACLRelevantToAction(cfg, actionAcls, acl, user) { if isACLRelevant(resourceAcls, acl, user, includeAddToEvery) {
ret = append(ret, acl) ret = append(ret, acl)
} }
} }

View File

@ -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")
}

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -982,7 +982,7 @@ func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) {
} }
func hasBindingAndAction(req *ExecutionRequest) bool { 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 { func hasExec(req *ExecutionRequest) bool {

View File

@ -186,16 +186,6 @@ func (e *Executor) groupsHaveCapacityForQueued(req *ExecutionRequest) bool {
return true 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 { func firstFullGroupNameLocked(e *Executor, req *ExecutionRequest) string {
for _, limit := range actionGroupLimits(req) { for _, limit := range actionGroupLimits(req) {
if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) { if e.countActiveInGroupLocked(limit.name) >= (limit.maxConcurrent + 1) {

View File

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

View File

@ -74,7 +74,11 @@ func watchPath(ctx *watchContext) {
return 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) done := make(chan bool)

View File

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

View File

@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
log "github.com/sirupsen/logrus"
) )
type RuntimeInfo struct { type RuntimeInfo struct {
@ -85,6 +87,14 @@ func getOsReleasePrettyName() string {
return "" 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 := bufio.NewScanner(handle)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@ -96,7 +106,5 @@ func getOsReleasePrettyName() string {
} }
} }
handle.Close()
return "notfound" return "notfound"
} }

View File

@ -1,7 +1,6 @@
package servicehost package servicehost
import ( import (
"os"
"path/filepath" "path/filepath"
) )
@ -20,25 +19,3 @@ func resolveLogDirectory(dir string, baseDir string) string {
return filepath.Join(baseDir, dir) 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
}

View File

@ -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
}

View File

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

View File

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

View File

@ -72,7 +72,8 @@ update_checksum() {
update_checksum "${ZIP_NAME}" update_checksum "${ZIP_NAME}"
update_checksum "${MSI_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}" \ gh release upload "${TAG}" \
"${DIST_DIR}/${ZIP_NAME}" \ "${DIST_DIR}/${ZIP_NAME}" \
"${DIST_DIR}/${MSI_NAME}" \ "${DIST_DIR}/${MSI_NAME}" \
@ -87,6 +88,4 @@ if ! gh release upload "${TAG}" "${checksums_path}" --clobber; then
exit 1 exit 1
fi fi
gh release edit "${TAG}" --draft=false echo "Uploaded signed ${ZIP_NAME} and ${MSI_NAME} to release ${TAG}"
echo "Published signed ${ZIP_NAME} and ${MSI_NAME} on release ${TAG}"