This commit is contained in:
James Read 2026-07-28 04:46:27 +00:00 committed by GitHub
commit 42c50dfa79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 3625 additions and 937 deletions

6
AI.md
View File

@ -5,6 +5,10 @@
- [x] The project does not include any AI functionality at runtime.
- [x] No data, usage, or similar is sent to, or analyized by AI.
## Development - Autonomy level
OliveTin development uses up to **Level 2 of 5 (pair programmer)** on the [AI Levels of Autonomy in Software Engineering](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/) scale: AI may generate suggestions and non-trivial changes, but humans review and decide what lands in the project.
## Development - Contributions
- [x] The project **does accept** contributions that were written with AI help. **However**:
@ -12,7 +16,7 @@
- AI often generates very unmaintainable code as it gets longer - loads of duplication, very little function re-use amd very poor at following style guides / idiomatic design. All code contributions (AI or not) are scrutinized hard for **maintainability** and **clean merging**. Please follow the CONTRIBUTORS guide.
- AI that helps with short tab completion is generally fine.
- AI that writes lots of new code across lots of files, or makes lots of superfluous changes is generally less likely to be accepted.
- Vibe coding is not a suitable way to contribute to this project.
- Vibe coding is generally not a suitable way to contribute to this project.
- [x] Contributors should declare when AI has been used to help write contributions in the pull request body message.
- [x] The project uses AI as an **optional** part of the PR process (coderabbitai). Please raise any concerns about usage within the PR.
- [x] Suggestions from coderabbitai can be accepted verbaitem, but ideally it should be the PR author that uses coderabbitai as a guide, who then re-writes the contribution.

View File

@ -10,7 +10,7 @@
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
[![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%201%20of%205%20(assistance--only)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/)
[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%202%20of%205%20(pair--programmer)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/)
[OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
</div>
@ -87,4 +87,10 @@ OliveTin follows these principles:
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **MacOS** and other platforms, too!
You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **macOS** and other platforms, too!
## Donations & Sponsorship
OliveTin does not accept monetary donations or sponsorship. See [Donations & Sponsorship](https://docs.olivetin.app/reference/donations_and_sponsorship.html) for the full explanation.
* Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).

View File

@ -1,11 +1,29 @@
= Diagnostics
OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. It's mainly designed for checking SSH configuration at the moment.
OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues.
This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar:
image::advanced_configuration/diagnostics/diagnostics.png[]
== Configuration issues
The Diagnostics page includes a **Configuration issues** section. OliveTin checks the loaded configuration for problems such as:
* Filesystem watch paths that cannot be created (for example missing directories used by `execOnFileCreatedInDir`)
* Argument templates that fail to parse
* Missing entity files or empty entity data for entity-bound actions
* Invalid cron schedules
* Entity-bound actions that also use `execOnCron` (cron cannot bind an entity)
* Unset environment variables referenced from configuration
* Other sanitize-time warnings (for example unknown action groups)
When one or more issues exist, the Diagnostics navigation link shows a count badge with the number of issues so operators notice without opening the page first. The list and badge refresh after configuration reload and after entity file changes.
Issues that refer to a specific action are only shown to users who are allowed to view that action. The badge count uses the same per-user filtering. Issues that are not tied to an action (for example unset environment variables) remain visible to anyone who can open Diagnostics.
Where possible, each issue includes the configuration source file that defined the related action or entity (for example a file under an `include` directory).
== Disabling Diagnostics
The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.

View File

@ -3,6 +3,8 @@
The `confirmation` type argument is a special argument type, which simply disables the "Start" button until a checkbox is ticked. This can be useful if you have an action with no other arguments, but you want to prevent accidental button-clicks starting the action.
Confirmation arguments are usually left unnamed, so nothing is substituted into the command. If you do give the argument a `name`, the only allowed values are `0` (unchecked) and `1` (checked), so confirmation is safe to use with `shell:` actions.
[source,yaml]
----
actions:

View File

@ -10,6 +10,7 @@ A full list of argument types are below;
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
| dnsname | xref:args/input.adoc[Textbox] | A DNS hostname (RFC 1123). Short names (e.g. `webserver`) and FQDNs (e.g. `webserver.example.com`). Letters/digits/hyphens only, no underscores. Optional trailing dot allowed.
| shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers.
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
@ -19,7 +20,7 @@ A full list of argument types are below;
| regex:... | xref:args/input.adoc[Textbox] | Version 2024.03.081 and above support custom regex patterns. See xref:args/regex.adoc[Custom regex arguments].
| int | xref:args/input.adoc[Textbox] | Any number, made up of the characters 0 to 9. Negative numbers are not supported.
| url | xref:args/input.adoc[Textbox] | A URL (e.g. https://example.com). Accepts any scheme, including `file://` and `ftp://`. See warning below.
| confirmation | xref:args/input_confirmation.adoc[Confirmation] | A "hidden" argument that makes the action require a confirmation before launching.
| confirmation | xref:args/input_confirmation.adoc[Confirmation] | A UI gate that requires a checkbox before starting. Usually unnamed (nothing is substituted). If named, the value is only `0` or `1`.
| checklist | xref:args/input_checklist.adoc[Checklist] | Multiple checkboxes from predefined choices. Selected values are passed as a comma-separated string.
| n/a, but `choices` used | xref:args/input_dropdown.adoc[Dropdown] | A "hidden" argument that makes the action require a confirmation before launching.
| raw_string_multiline | xref:args/input_textarea.adoc[Textarea] | Anything. This is **dangerous**, as effectively people can type anything they like

View File

@ -6,10 +6,6 @@ You will need approximately **10 minutes** to install OliveTin on almost every p
You probably will need about **10-20 minutes** to understand how OliveTin configuration works, and to be able to write your first action to make it do something useful.
== Where to find help
include::partial$support.adoc[]
== First step: where are you installing?
* Linux
@ -27,9 +23,17 @@ include::partial$support.adoc[]
*** xref:install/docker_compose.adoc[Docker Compose]
*** xref:install/helm.adoc[Kubernetes with Helm]
*** xref:install/k8s.adoc[Kubernetes with Manifests]
* xref:install/bsd.adoc[FreeBSD]
* xref:install/windows.adoc[Windows]
* xref:install/macos.adoc[MacOS]
* xref:install/bsd.adoc[BSD]
* Windows
** xref:install/windows.adoc[Windows]
** xref:install/windows.adoc[Windows Service]
* macOS
** xref:install/macos.adoc[macOS Desktop]
** xref:install/macos.adoc[macOS Service]
== Where to find help
include::partial$support.adoc[]
== What's Next?

View File

@ -34,11 +34,11 @@ tar -xzf OliveTin-darwin-arm64.tar.gz
cd OliveTin-darwin-arm64
----
== Remove the Gatekeeper quarantine
== Gatekeeper and notarization
The binary is downloaded from the internet and is not notarized by Apple, so on first run Gatekeeper blocks it with a message like _"OliveTin can't be opened because Apple cannot check it for malicious software."_
Current release binaries are **Developer ID signed and notarized** by Apple. After extract, you should be able to run `./OliveTin` normally.
Clear the quarantine attribute so it will run:
If Gatekeeper still blocks an older (unsigned) build, or you see a prompt that Apple cannot check the binary for malicious software, clear the quarantine attribute:
[source,shell]
----

View File

@ -2,7 +2,7 @@
This option installs OliveTin as a launchd service, so it runs in the background and starts automatically. This is the macOS equivalent of running OliveTin as a Linux systemd service or a xref:install/windows_service.adoc[Windows service]. If you just want to run OliveTin as a regular application, follow the xref:install/macos.adoc[macOS install] instructions instead.
Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and clear the Gatekeeper quarantine) and confirm OliveTin starts correctly by running `./OliveTin`.
Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and confirm Gatekeeper allows the binary) and confirm OliveTin starts correctly by running `./OliveTin`.
== Choose LaunchAgent or LaunchDaemon

View File

@ -29,6 +29,8 @@ actions:
From the above example, you can see there There are two types of logs - **results (.yaml)** and **output (.log)**
Filenames are built from the action title, a unix timestamp, and the execution tracking ID. Characters that are unsafe in filenames (for example `/` and `\`) are replaced with `_` so titles like `Create/update Monthly Report` still write a single file. The original title is unchanged inside the results YAML.
* **Results (.yaml)** - this captures almost everything that OliveTin knows about the action and looks like this.
+
[source,yaml]

View File

@ -3,9 +3,8 @@
Sometimes I (James Read) get asked if people can donate money, or sponsor me for OliveTin. If you are reading this page, maybe you are thinking the same.
**I do not accept donations or sponsorship** for OliveTin, but I want to **thank you very much indeed** for thinking about the potential.
**I do not accept monetary donations or sponsorship** for OliveTin, but I want to **thank you very much indeed** for thinking about the potential.
I have a job that pays me, where I don't write code as part of my day job (very often) - I enjoy coding as a hobby in my spare time. If I get money for that, it somehow takes the pleasure or fun out of it, or makes it feel like a job. There are concerns that I might "prefer" to work on one user's feature request if I am sponsored, or even feel compelled that I have to work on something because someone gave me money. I don't want OliveTin development to go that way. There are also other little considerations, like being paid on the side out of my job then affects my job, and tax, and so on and so on. This is why I don't take any form of money, donations or sponsorship.
Another way that you can show your appreciation for OliveTin, that actually means a lot more than money, is to xref:reference/contribute.adoc[contribute to OliveTin]. There are little ways and big ways to contribute - depends on if you have 2 minutes or 2 weeks to give!

View File

@ -199,6 +199,11 @@ async function handleConfigChangedEvent (j) {
window.dispatchEvent(j)
}
async function handleEntityChangedEvent (j) {
await refreshInitAfterConfigChange()
window.dispatchEvent(j)
}
const eventCaseToTypeName = {
entityChanged: 'EventEntityChanged',
configChanged: 'EventConfigChanged',
@ -230,9 +235,13 @@ function handleEvent (msg) {
case 'EventHeartbeat':
break
case 'EventOutputChunk':
case 'EventEntityChanged':
window.dispatchEvent(j)
break
case 'EventEntityChanged':
handleEntityChangedEvent(j).catch((err) => {
console.error('EventEntityChanged handler failed:', err)
})
break
case 'EventExecutionFinished':
case 'EventExecutionStarted':
window.dispatchEvent(j)

View File

@ -11,25 +11,25 @@
"dependencies": {
"@connectrpc/connect": "^2.1.2",
"@connectrpc/connect-web": "^2.1.2",
"@hugeicons/core-free-icons": "^4.2.2",
"@hugeicons/core-free-icons": "^4.2.3",
"@hugeicons/vue": "^1.0.7",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue": "^6.0.8",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"iconify-icon": "^3.0.2",
"picocrank": "^1.18.2",
"picocrank": "^1.21.1",
"standard": "^17.1.2",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.1.3",
"vue": "^3.5.39",
"vue-i18n": "^11.4.6",
"vue-router": "^5.1.0"
"vite": "^8.1.5",
"vue": "^3.5.40",
"vue-i18n": "^11.4.7",
"vue-router": "^5.2.0"
},
"devDependencies": {
"eslint-plugin-vue": "^10.9.2",
"eslint-plugin-vue": "^10.10.0",
"process": "^0.11.10",
"stylelint": "^17.14.0",
"stylelint": "^17.14.1",
"stylelint-config-standard": "^40.0.0"
},
"engines": {
@ -52,13 +52,13 @@
}
},
"node_modules/@babel/generator": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz",
"integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
"integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^8.0.0-rc.5",
"@babel/types": "^8.0.0-rc.5",
"@babel/parser": "^8.0.0",
"@babel/types": "^8.0.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"@types/jsesc": "^2.5.0",
@ -69,30 +69,30 @@
}
},
"node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.5.tgz",
"integrity": "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
"integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.5.tgz",
"integrity": "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==",
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
"integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/generator/node_modules/@babel/parser": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.5.tgz",
"integrity": "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==",
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
"integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
"license": "MIT",
"dependencies": {
"@babel/types": "^8.0.0-rc.5"
"@babel/types": "^8.0.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -102,13 +102,13 @@
}
},
"node_modules/@babel/generator/node_modules/@babel/types": {
"version": "8.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.5.tgz",
"integrity": "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==",
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
"integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^8.0.0-rc.5",
"@babel/helper-validator-identifier": "^8.0.0-rc.5"
"@babel/helper-string-parser": "^8.0.0",
"@babel/helper-validator-identifier": "^8.0.4"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
@ -168,13 +168,13 @@
"peer": true
},
"node_modules/@cacheable/memory": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
"integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/utils": "^2.4.1",
"@cacheable/utils": "^2.5.0",
"@keyv/bigmap": "^1.3.1",
"hookified": "^1.15.1",
"keyv": "^5.6.0"
@ -208,9 +208,9 @@
}
},
"node_modules/@cacheable/utils": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
"integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -295,9 +295,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
"integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
"dev": true,
"funding": [
{
@ -883,9 +883,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
@ -942,9 +942,9 @@
}
},
"node_modules/@hugeicons/core-free-icons": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz",
"integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.3.tgz",
"integrity": "sha512-fpiU0qFZkv4ABi23xJ2m4qNr0BBfuIaYjPboN8WDA6aj9D+OzXa7eykxKOpACZRuGbDz1U9S5DsNx1d5KNnsgA==",
"license": "MIT"
},
"node_modules/@hugeicons/vue": {
@ -998,14 +998,14 @@
"license": "MIT"
},
"node_modules/@intlify/core-base": {
"version": "11.4.6",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz",
"integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==",
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz",
"integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==",
"license": "MIT",
"dependencies": {
"@intlify/devtools-types": "11.4.6",
"@intlify/message-compiler": "11.4.6",
"@intlify/shared": "11.4.6"
"@intlify/devtools-types": "11.4.7",
"@intlify/message-compiler": "11.4.7",
"@intlify/shared": "11.4.7"
},
"engines": {
"node": ">= 22"
@ -1015,13 +1015,13 @@
}
},
"node_modules/@intlify/devtools-types": {
"version": "11.4.6",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz",
"integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==",
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz",
"integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.4.6",
"@intlify/shared": "11.4.6"
"@intlify/core-base": "11.4.7",
"@intlify/shared": "11.4.7"
},
"engines": {
"node": ">= 22"
@ -1031,12 +1031,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.4.6",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz",
"integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==",
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz",
"integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.4.6",
"@intlify/shared": "11.4.7",
"source-map-js": "^1.0.2"
},
"engines": {
@ -1047,9 +1047,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "11.4.6",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz",
"integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==",
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz",
"integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==",
"license": "MIT",
"engines": {
"node": ">= 22"
@ -1481,9 +1481,9 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
"integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
"integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "^1.0.1"
@ -1497,9 +1497,9 @@
}
},
"node_modules/@vue-macros/common": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
"integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz",
"integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-sfc": "^3.5.22",
@ -1524,53 +1524,53 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
"integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.7",
"@vue/shared": "3.5.39",
"@vue/shared": "3.5.40",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz",
"integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.39",
"@vue/shared": "3.5.39"
"@vue/compiler-core": "3.5.40",
"@vue/shared": "3.5.40"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz",
"integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.7",
"@vue/compiler-core": "3.5.39",
"@vue/compiler-dom": "3.5.39",
"@vue/compiler-ssr": "3.5.39",
"@vue/shared": "3.5.39",
"@vue/compiler-core": "3.5.40",
"@vue/compiler-dom": "3.5.40",
"@vue/compiler-ssr": "3.5.40",
"@vue/shared": "3.5.40",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.15",
"postcss": "^8.5.19",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz",
"integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.39",
"@vue/shared": "3.5.39"
"@vue/compiler-dom": "3.5.40",
"@vue/shared": "3.5.40"
}
},
"node_modules/@vue/devtools-api": {
@ -1580,71 +1580,69 @@
"license": "MIT"
},
"node_modules/@vue/devtools-kit": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz",
"integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz",
"integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^8.1.2",
"@vue/devtools-shared": "^8.1.5",
"birpc": "^2.6.1",
"hookable": "^5.5.3",
"perfect-debounce": "^2.0.0"
}
},
"node_modules/@vue/devtools-shared": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz",
"integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz",
"integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==",
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
"integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.39"
"@vue/shared": "3.5.40"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz",
"integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.39",
"@vue/shared": "3.5.39"
"@vue/reactivity": "3.5.40",
"@vue/shared": "3.5.40"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz",
"integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.39",
"@vue/runtime-core": "3.5.39",
"@vue/shared": "3.5.39",
"@vue/reactivity": "3.5.40",
"@vue/runtime-core": "3.5.40",
"@vue/shared": "3.5.40",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz",
"integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.39",
"@vue/shared": "3.5.39"
},
"peerDependencies": {
"vue": "3.5.39"
"@vue/compiler-ssr": "3.5.40",
"@vue/runtime-dom": "3.5.40",
"@vue/shared": "3.5.40"
}
},
"node_modules/@vue/shared": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz",
"integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==",
"license": "MIT"
},
"node_modules/@xterm/addon-fit": {
@ -1974,9 +1972,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -2018,14 +2016,14 @@
}
},
"node_modules/cacheable": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
"integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/memory": "^2.0.8",
"@cacheable/utils": "^2.4.1",
"@cacheable/memory": "^2.2.0",
"@cacheable/utils": "^2.5.0",
"hookified": "^1.15.0",
"keyv": "^5.6.0",
"qified": "^0.10.1"
@ -3015,18 +3013,18 @@
}
},
"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==",
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz",
"integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@eslint-community/eslint-utils": "^4.9.1",
"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"
"postcss-selector-parser": "^7.1.4",
"semver": "^7.8.5",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3232,9 +3230,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"dev": true,
"funding": [
{
@ -3597,9 +3595,9 @@
}
},
"node_modules/globby": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz",
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==",
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz",
"integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3618,9 +3616,9 @@
}
},
"node_modules/globby/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true,
"license": "MIT",
"engines": {
@ -4311,9 +4309,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"funding": [
{
"type": "github",
@ -4934,9 +4932,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"funding": [
{
"type": "github",
@ -4966,6 +4964,12 @@
"node": ">=0.10.0"
}
},
"node_modules/nostics": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
"integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==",
"license": "MIT"
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@ -5268,9 +5272,9 @@
"license": "ISC"
},
"node_modules/picocrank": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz",
"integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==",
"version": "1.21.1",
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.1.tgz",
"integrity": "sha512-VnfNFok7BkpLKymAIbD2zUA0bf/AiNBS63Xk2mfi8uXOkdAmf06pQaI0ssGBv2/S9p1jbCATWj9OFz1uqa39NQ==",
"license": "ISC",
"dependencies": {
"@hugeicons/core-free-icons": "^4.2.2",
@ -5400,9 +5404,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"version": "8.5.22",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
"funding": [
{
"type": "opencollective",
@ -5419,7 +5423,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -6233,9 +6237,9 @@
}
},
"node_modules/stylelint": {
"version": "17.14.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
"version": "17.14.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz",
"integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==",
"dev": true,
"funding": [
{
@ -6251,7 +6255,7 @@
"dependencies": {
"@csstools/css-calc": "^3.2.1",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-syntax-patches-for-csstree": "^1.1.5",
"@csstools/css-syntax-patches-for-csstree": "^1.1.6",
"@csstools/css-tokenizer": "^4.0.0",
"@csstools/media-query-list-parser": "^5.0.0",
"@csstools/selector-resolve-nested": "^4.0.0",
@ -6263,9 +6267,9 @@
"debug": "^4.4.3",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^11.1.3",
"file-entry-cache": "^11.1.5",
"global-modules": "^2.0.0",
"globby": "^16.2.0",
"globby": "^16.2.1",
"globjoin": "^0.1.4",
"html-tags": "^5.1.0",
"ignore": "^7.0.5",
@ -6275,12 +6279,12 @@
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.5.15",
"postcss": "^8.5.16",
"postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.1.4",
"postcss-value-parser": "^4.2.0",
"string-width": "^8.2.1",
"supports-hyperlinks": "^4.4.0",
"supports-hyperlinks": "^4.5.0",
"svg-tags": "^1.0.0",
"table": "^6.9.0",
"write-file-atomic": "^7.0.1"
@ -6355,23 +6359,23 @@
}
},
"node_modules/stylelint/node_modules/file-entry-cache": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
"integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.22"
"flat-cache": "^6.1.23"
}
},
"node_modules/stylelint/node_modules/flat-cache": {
"version": "6.1.22",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
"integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
"version": "6.1.23",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
"integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cacheable": "^2.3.4",
"cacheable": "^2.5.0",
"flatted": "^3.4.2",
"hookified": "^1.15.0"
}
@ -6431,9 +6435,9 @@
}
},
"node_modules/supports-hyperlinks": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz",
"integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz",
"integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6750,27 +6754,67 @@
}
},
"node_modules/unplugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
"integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
"integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"picomatch": "^4.0.3",
"picomatch": "^4.0.4",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@farmfe/core": "*",
"@rspack/core": "*",
"bun-types-no-globals": "*",
"esbuild": "*",
"rolldown": "*",
"rollup": "*",
"unloader": "*",
"vite": "*",
"webpack": "*"
},
"peerDependenciesMeta": {
"@farmfe/core": {
"optional": true
},
"@rspack/core": {
"optional": true
},
"bun-types-no-globals": {
"optional": true
},
"esbuild": {
"optional": true
},
"rolldown": {
"optional": true
},
"rollup": {
"optional": true
},
"unloader": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/unplugin-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz",
"integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==",
"license": "MIT",
"dependencies": {
"pathe": "^2.0.3",
"picomatch": "^4.0.3"
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=20.19.0"
@ -6872,15 +6916,15 @@
}
},
"node_modules/vite": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.16",
"rolldown": "~1.1.3",
"picomatch": "^4.0.5",
"postcss": "^8.5.17",
"rolldown": "~1.1.5",
"tinyglobby": "^0.2.17"
},
"bin": {
@ -6949,9 +6993,9 @@
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
"engines": {
"node": ">=12"
@ -6961,16 +7005,16 @@
}
},
"node_modules/vue": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
"version": "3.5.40",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
"integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.39",
"@vue/compiler-sfc": "3.5.39",
"@vue/runtime-dom": "3.5.39",
"@vue/server-renderer": "3.5.39",
"@vue/shared": "3.5.39"
"@vue/compiler-dom": "3.5.40",
"@vue/compiler-sfc": "3.5.40",
"@vue/runtime-dom": "3.5.40",
"@vue/server-renderer": "3.5.40",
"@vue/shared": "3.5.40"
},
"peerDependencies": {
"typescript": "*"
@ -7074,14 +7118,14 @@
}
},
"node_modules/vue-i18n": {
"version": "11.4.6",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
"integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==",
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz",
"integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.4.6",
"@intlify/devtools-types": "11.4.6",
"@intlify/shared": "11.4.6",
"@intlify/core-base": "11.4.7",
"@intlify/devtools-types": "11.4.7",
"@intlify/shared": "11.4.7",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
@ -7095,27 +7139,28 @@
}
},
"node_modules/vue-router": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
"integrity": "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz",
"integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^8.0.0-rc.4",
"@vue-macros/common": "^3.1.1",
"@vue/devtools-api": "^8.1.2",
"@babel/generator": "^8.0.0",
"@vue-macros/common": "^3.1.3",
"@vue/devtools-api": "^8.1.5",
"ast-walker-scope": "^0.9.0",
"chokidar": "^5.0.0",
"json5": "^2.2.3",
"local-pkg": "^1.1.2",
"local-pkg": "^1.2.1",
"magic-string": "^0.30.21",
"mlly": "^1.8.2",
"muggle-string": "^0.4.1",
"nostics": "^1.1.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"picomatch": "^4.0.5",
"scule": "^1.3.0",
"tinyglobby": "^0.2.16",
"unplugin": "^3.0.0",
"unplugin-utils": "^0.3.1",
"tinyglobby": "^0.2.17",
"unplugin": "^3.3.0",
"unplugin-utils": "^0.3.2",
"yaml": "^2.9.0"
},
"funding": {
@ -7123,10 +7168,10 @@
},
"peerDependencies": {
"@pinia/colada": ">=0.21.2",
"@vue/compiler-sfc": "^3.5.34",
"pinia": "^3.0.4",
"vite": "^7.0.0 || ^8.0.0",
"vue": "^3.5.34"
"@vue/compiler-sfc": "^3.5.34 || ^4.0.0",
"pinia": "^3.0.4 || ^4.0.2",
"vite": "^7.3.0 || ^8.0.0",
"vue": "^3.5.34 || ^4.0.0"
},
"peerDependenciesMeta": {
"@pinia/colada": {
@ -7144,12 +7189,12 @@
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz",
"integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz",
"integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^8.1.2"
"@vue/devtools-kit": "^8.1.5"
}
},
"node_modules/vue-router/node_modules/json5": {
@ -7165,9 +7210,9 @@
}
},
"node_modules/vue-router/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
"engines": {
"node": ">=12"
@ -7320,13 +7365,13 @@
}
},
"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==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/yaml": {

View File

@ -5,9 +5,9 @@
"repository": "https://github.com/OliveTin/OliveTin",
"source": "index.html",
"devDependencies": {
"eslint-plugin-vue": "^10.9.2",
"eslint-plugin-vue": "^10.10.0",
"process": "^0.11.10",
"stylelint": "^17.14.0",
"stylelint": "^17.14.1",
"stylelint-config-standard": "^40.0.0"
},
"scripts": {
@ -25,20 +25,20 @@
"dependencies": {
"@connectrpc/connect": "^2.1.2",
"@connectrpc/connect-web": "^2.1.2",
"@hugeicons/core-free-icons": "^4.2.2",
"@hugeicons/core-free-icons": "^4.2.3",
"@hugeicons/vue": "^1.0.7",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue": "^6.0.8",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"iconify-icon": "^3.0.2",
"picocrank": "^1.18.2",
"picocrank": "^1.21.1",
"standard": "^17.1.2",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.1.3",
"vue": "^3.5.39",
"vue-i18n": "^11.4.6",
"vue-router": "^5.1.0"
"vite": "^8.1.5",
"vue": "^3.5.40",
"vue-i18n": "^11.4.7",
"vue-router": "^5.2.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.12.1
// @generated by protoc-gen-es v2.13.0
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
/* eslint-disable */
@ -1666,6 +1666,57 @@ export declare type GetDiagnosticsRequest = Message<"olivetin.api.v1.GetDiagnost
*/
export declare const GetDiagnosticsRequestSchema: GenMessage<GetDiagnosticsRequest>;
/**
* @generated from message olivetin.api.v1.ConfigIssue
*/
export declare type ConfigIssue = Message<"olivetin.api.v1.ConfigIssue"> & {
/**
* @generated from field: string severity = 1;
*/
severity: string;
/**
* @generated from field: string code = 2;
*/
code: string;
/**
* @generated from field: string message = 3;
*/
message: string;
/**
* @generated from field: string action_id = 4;
*/
actionId: string;
/**
* @generated from field: string action_title = 5;
*/
actionTitle: string;
/**
* @generated from field: string argument_name = 6;
*/
argumentName: string;
/**
* @generated from field: string source = 7;
*/
source: string;
/**
* @generated from field: string config_file = 8;
*/
configFile: string;
};
/**
* Describes the message olivetin.api.v1.ConfigIssue.
* Use `create(ConfigIssueSchema)` to create a new message.
*/
export declare const ConfigIssueSchema: GenMessage<ConfigIssue>;
/**
* @generated from message olivetin.api.v1.GetDiagnosticsResponse
*/
@ -1679,6 +1730,11 @@ export declare type GetDiagnosticsResponse = Message<"olivetin.api.v1.GetDiagnos
* @generated from field: string SshFoundConfig = 2;
*/
SshFoundConfig: string;
/**
* @generated from field: repeated olivetin.api.v1.ConfigIssue config_issues = 3;
*/
configIssues: ConfigIssue[];
};
/**
@ -1829,6 +1885,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
* @generated from field: bool show_navigate_on_start_icons = 25;
*/
showNavigateOnStartIcons: boolean;
/**
* @generated from field: int32 config_issue_count = 26;
*/
configIssueCount: number;
};
/**

File diff suppressed because one or more lines are too long

View File

@ -345,8 +345,8 @@ function renderNavigation () {
const rootDashboards = window.initResponse?.rootDashboards || []
if (typeof navigation.value.clear === 'function') {
navigation.value.clear()
if (typeof navigation.value.clearNavigationLinks === 'function') {
navigation.value.clearNavigationLinks()
}
for (const rootDashboard of rootDashboards) {
@ -367,7 +367,10 @@ function renderNavigation () {
}
if (showDiagnostics.value) {
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
const issueCount = window.initResponse?.configIssueCount || 0
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'), {
count: issueCount
})
}
}

View File

@ -291,7 +291,7 @@ function getInputType (arg) {
return 'checkbox'
}
if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
if (arg.type === 'ascii_identifier' || arg.type === 'dnsname' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
return 'text'
}

View File

@ -1,4 +1,28 @@
<template>
<Section :title="t('diagnostics.config-issues')">
<p>{{ t('diagnostics.config-issues-description') }}</p>
<p v-if="!loading && configIssues.length === 0">
{{ t('diagnostics.config-issues-none') }}
</p>
<Table
v-else
:data="configIssueRows"
:headers="configIssueHeaders"
:show-pagination="false"
>
<template #cell-severity="{ value }">
<div
class="tag"
:class="value === 'error' ? 'fg-bad' : 'fg-warning'"
>
{{ value }}
</div>
</template>
</Table>
</Section>
<Section :title="t('diagnostics.get-support')">
<p>
{{ t('diagnostics.get-support-description') }}
@ -84,19 +108,41 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue'
import Table from 'picocrank/vue/components/Table.vue'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const diagnostics = ref({})
const configIssues = ref([])
const loading = ref(false)
const serverDiagnostics = ref('')
const browserInfo = ref('')
const serverDiagnosticsCopied = ref(false)
const browserInfoCopied = ref(false)
const configIssueHeaders = computed(() => [
{ key: 'severity', label: t('diagnostics.config-issue-severity'), sortable: true, width: '7rem' },
{ key: 'code', label: t('diagnostics.config-issue-code'), sortable: true, width: '12rem' },
{ key: 'message', label: t('diagnostics.config-issue-message'), sortable: false },
{ key: 'actionTitle', label: t('diagnostics.config-issue-action'), sortable: true, width: '10rem' },
{ key: 'argumentName', label: t('diagnostics.config-issue-argument'), sortable: true, width: '8rem' },
{ key: 'configFile', label: t('diagnostics.config-issue-config-file'), sortable: true, width: '14rem' },
{ key: 'source', label: t('diagnostics.config-issue-source'), sortable: false, width: '12rem' }
])
const configIssueRows = computed(() => configIssues.value.map((issue) => ({
severity: issue.severity || '',
code: issue.code || '',
message: issue.message || '',
actionTitle: issue.actionTitle || '',
argumentName: issue.argumentName || '',
configFile: issue.configFile || '',
source: issue.source || ''
})))
async function fetchDiagnostics () {
loading.value = true
@ -106,12 +152,14 @@ async function fetchDiagnostics () {
sshFoundKey: response.SshFoundKey,
sshFoundConfig: response.SshFoundConfig
}
configIssues.value = response.configIssues || []
} catch (err) {
console.error('Failed to fetch diagnostics:', err)
diagnostics.value = {
sshFoundKey: t('diagnostics.unknown'),
sshFoundConfig: t('diagnostics.unknown')
}
configIssues.value = []
}
loading.value = false
}
@ -263,85 +311,12 @@ async function copyBrowserInfo () {
onMounted(() => {
fetchDiagnostics()
window.addEventListener('EventConfigChanged', fetchDiagnostics)
window.addEventListener('EventEntityChanged', fetchDiagnostics)
})
onUnmounted(() => {
window.removeEventListener('EventConfigChanged', fetchDiagnostics)
window.removeEventListener('EventEntityChanged', fetchDiagnostics)
})
</script>
<style scoped>
.diagnostics-view {
padding: 1rem;
}
.diagnostics-content {
max-width: 800px;
margin: 0 auto;
}
.note {
background: #f8f9fa;
border-left: 4px solid #007bff;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0 4px 4px 0;
font-size: 0.875rem;
color: #495057;
}
.note a {
color: #007bff;
text-decoration: none;
}
.note a:hover {
text-decoration: underline;
}
.diagnostics-table {
width: 100%;
border-collapse: collapse;
}
.diagnostics-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f1f3f4;
}
.diagnostics-table td:first-child {
font-weight: 500;
color: #495057;
background: #f8f9fa;
}
.diagnostics-table tr:last-child td {
border-bottom: none;
}
.error-list {
padding: 1rem;
}
.error-item {
background: #f8d7da;
color: #721c24;
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 4px;
border-left: 4px solid #dc3545;
font-family: monospace;
font-size: 0.875rem;
}
.error-item:last-child {
margin-bottom: 0;
}
.flex-col {
display: flex;
flex-direction: column;
}
.section-content {
display: flex;
flex-direction: column;
gap: 1em;
}
</style>

View File

@ -9,13 +9,13 @@
"version": "1.0.0",
"license": "AGPL-3.0-only",
"dependencies": {
"wait-on": "^9.0.10"
"wait-on": "^9.1.0"
},
"devDependencies": {
"chai": "^6.2.2",
"eslint": "^10.6.0",
"eslint": "^10.8.0",
"mocha": "^11.7.6",
"selenium-webdriver": "^4.45.0"
"selenium-webdriver": "^4.46.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -92,9 +92,9 @@
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
"integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
"integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -172,9 +172,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
"integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.7.tgz",
"integrity": "sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
@ -333,6 +333,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@ -391,13 +403,14 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
@ -408,16 +421,16 @@
"dev": true
},
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/brace-expansion/node_modules/balanced-match": {
@ -636,7 +649,6 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -734,9 +746,9 @@
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -783,9 +795,9 @@
}
},
"node_modules/eslint": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
"dev": true,
"license": "MIT",
"workspaces": [
@ -795,7 +807,7 @@
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.5",
"@eslint/config-helpers": "^0.6.0",
"@eslint/config-helpers": "^0.7.0",
"@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.2",
"@humanfs/node": "^0.16.6",
@ -819,7 +831,7 @@
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"minimatch": "^10.2.4",
"minimatch": "^10.2.5",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@ -1159,9 +1171,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1253,6 +1265,19 @@
"he": "bin/he"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -1375,9 +1400,9 @@
}
},
"node_modules/joi": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz",
"integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==",
"version": "18.2.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz",
"integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/address": "^5.1.1",
@ -1393,9 +1418,9 @@
}
},
"node_modules/js-yaml": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
"funding": [
{
@ -1625,9 +1650,9 @@
}
},
"node_modules/mocha/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1669,7 +1694,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
@ -1879,9 +1903,9 @@
"dev": true
},
"node_modules/selenium-webdriver": {
"version": "4.45.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.45.0.tgz",
"integrity": "sha512-Cb2nqvJiwXVOtRTCYHX9D1FJR5+Ls7aL3Nev0t6n4CpXsQ//YGiiUmSCbvTDDeLtbV85SZ46qmLab4SIYKXWRw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.46.0.tgz",
"integrity": "sha512-UlTkgnx9y+bf3QxFsrgUyMqC2oy1Yf+qcOs7xIC/XfsUPv/ow7Sicx6SJ7AeOn2/Z+xF1M8SLqyn983wipI82w==",
"dev": true,
"funding": [
{
@ -2136,13 +2160,13 @@
"dev": true
},
"node_modules/wait-on": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz",
"integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==",
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz",
"integrity": "sha512-PymrLXHLBM1Ju/Xspb2ADUhbPSMvbnuNvy/mN2hWtpbJ3da0h3Ky1LqwKPG5QSVR57liyO0iUpfipYl/s5qNvA==",
"license": "MIT",
"dependencies": {
"axios": "^1.16.0",
"joi": "^18.2.1",
"axios": "^1.18.1",
"joi": "^18.2.3",
"lodash": "^4.18.1",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"

View File

@ -12,11 +12,11 @@
"license": "AGPL-3.0-only",
"devDependencies": {
"chai": "^6.2.2",
"eslint": "^10.6.0",
"eslint": "^10.8.0",
"mocha": "^11.7.6",
"selenium-webdriver": "^4.45.0"
"selenium-webdriver": "^4.46.0"
},
"dependencies": {
"wait-on": "^9.0.10"
"wait-on": "^9.1.0"
}
}

View File

@ -32,7 +32,7 @@ describe('config: dashboards with basic fieldsets', function () {
await openSidebar()
const navigationLinks = await getNavigationLinks()
assert.equal(navigationLinks.length, 5, 'Expected the nav to only have 5 links') // test dashboard + logs + diagnostics + entities + separator
assert.equal(navigationLinks.length, 4, 'Expected the nav to only have 4 links') // test dashboard + entities + logs + diagnostics
const firstLink = await navigationLinks[0]

View File

@ -32,6 +32,6 @@ describe('config: empty dashboards are hidden', function () {
const navigationLinks = await getNavigationLinks()
expect(navigationLinks).to.not.be.empty
expect(navigationLinks.length).to.be.equal(4, 'Expected the nav to only have 4 links')
expect(navigationLinks.length).to.be.equal(3, 'Expected the nav to only have 3 links') // entities + logs + diagnostics
})
})

View File

@ -5,6 +5,17 @@
"connected": "Verbunden",
"diagnostics.browser-info": "Browser-Informationen",
"diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.",
"diagnostics.config-issue-action": "Aktion",
"diagnostics.config-issue-argument": "Argument",
"diagnostics.config-issue-code": "Code",
"diagnostics.config-issue-config-file": "Konfigurationsdatei",
"diagnostics.config-issue-detail": "Detail",
"diagnostics.config-issue-message": "Meldung",
"diagnostics.config-issue-severity": "Schweregrad",
"diagnostics.config-issue-source": "Quelle",
"diagnostics.config-issues": "Konfigurationsprobleme",
"diagnostics.config-issues-description": "In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.",
"diagnostics.config-issues-none": "Keine Konfigurationsprobleme erkannt.",
"diagnostics.copied": "Kopiert!",
"diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
"diagnostics.found-config": "Konfiguration gefunden",
@ -41,10 +52,18 @@
"logs.clear-filter": "Suchfilter löschen",
"logs.completed": "Abgeschlossen",
"logs.exit-code": "Ausführungscode",
"logs.filter-error": "Filterausdruck konnte nicht angewendet werden.",
"logs.filter-help-examples": "Beispiele: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
"logs.filter-help-fields": "Felder: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatoren: ==, !=, contains. Ein !-Präfix vor einem einzelnen Wort schließt passende Einträge aus.",
"logs.filter-help-intro": "Filter laufen auf dem Server über Einträge, die Sie anzeigen dürfen. Kombinieren Sie Begriffe mit and / or.",
"logs.filter-help-title": "Filtersyntax",
"logs.filter-placeholder": "Protokolle filtern (z. B. !Update oder Status != Completed)",
"logs.metadata": "Metadaten",
"logs.no-logs-for-filter": "Keine Protokolle entsprechen dem aktuellen Filter.",
"logs.no-logs-to-display": "Es gibt keine Protokolle zu anzeigen.",
"logs.page-description": "Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.",
"logs.queue": "Warteschlange",
"logs.queue-action-details": "Aktionsdetails",
"logs.queue-default-group": "Standard",
"logs.queue-empty": "Derzeit gibt es keine aktiven oder wartenden Ausführungen.",
"logs.queue-entity": "Entität",
@ -76,6 +95,17 @@
"connected": "Connected",
"diagnostics.browser-info": "Browser Info",
"diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.",
"diagnostics.config-issue-action": "Action",
"diagnostics.config-issue-argument": "Argument",
"diagnostics.config-issue-code": "Code",
"diagnostics.config-issue-config-file": "Config file",
"diagnostics.config-issue-detail": "Detail",
"diagnostics.config-issue-message": "Message",
"diagnostics.config-issue-severity": "Severity",
"diagnostics.config-issue-source": "Source",
"diagnostics.config-issues": "Configuration issues",
"diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.",
"diagnostics.config-issues-none": "No configuration issues detected.",
"diagnostics.copied": "Copied!",
"diagnostics.copy-to-clipboard": "Copy to Clipboard",
"diagnostics.found-config": "Found Config",
@ -155,6 +185,17 @@
"connected": "Conectado",
"diagnostics.browser-info": "Información del navegador",
"diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.",
"diagnostics.config-issue-action": "Acción",
"diagnostics.config-issue-argument": "Argumento",
"diagnostics.config-issue-code": "Código",
"diagnostics.config-issue-config-file": "Archivo de configuración",
"diagnostics.config-issue-detail": "Detalle",
"diagnostics.config-issue-message": "Mensaje",
"diagnostics.config-issue-severity": "Severidad",
"diagnostics.config-issue-source": "Origen",
"diagnostics.config-issues": "Problemas de configuración",
"diagnostics.config-issues-description": "Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.",
"diagnostics.config-issues-none": "No se detectaron problemas de configuración.",
"diagnostics.copied": "¡Copiado!",
"diagnostics.copy-to-clipboard": "Copiar al portapapeles",
"diagnostics.found-config": "Configuración encontrada",
@ -191,10 +232,18 @@
"logs.clear-filter": "Limpiar filtro de búsqueda",
"logs.completed": "Completado",
"logs.exit-code": "Código de salida",
"logs.filter-error": "No se pudo aplicar la expresión del filtro.",
"logs.filter-help-examples": "Ejemplos: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
"logs.filter-help-fields": "Campos: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operadores: ==, !=, contains. Un prefijo ! en una sola palabra excluye las entradas coincidentes.",
"logs.filter-help-intro": "Los filtros se ejecutan en el servidor sobre las entradas que puede ver. Combine términos con and / or.",
"logs.filter-help-title": "Sintaxis del filtro",
"logs.filter-placeholder": "Filtrar registros (p. ej. !Update o Status != Completed)",
"logs.metadata": "Metadatos",
"logs.no-logs-for-filter": "Ningún registro coincide con el filtro actual.",
"logs.no-logs-to-display": "No hay registros para mostrar.",
"logs.page-description": "Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.",
"logs.queue": "Cola",
"logs.queue-action-details": "Detalles de la acción",
"logs.queue-default-group": "Predeterminado",
"logs.queue-empty": "No hay ejecuciones activas o en espera en este momento.",
"logs.queue-entity": "Entidad",
@ -226,6 +275,17 @@
"connected": "Connesso",
"diagnostics.browser-info": "Informazioni del browser",
"diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.",
"diagnostics.config-issue-action": "Azione",
"diagnostics.config-issue-argument": "Argomento",
"diagnostics.config-issue-code": "Codice",
"diagnostics.config-issue-config-file": "File di configurazione",
"diagnostics.config-issue-detail": "Dettaglio",
"diagnostics.config-issue-message": "Messaggio",
"diagnostics.config-issue-severity": "Gravità",
"diagnostics.config-issue-source": "Origine",
"diagnostics.config-issues": "Problemi di configurazione",
"diagnostics.config-issues-description": "Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.",
"diagnostics.config-issues-none": "Nessun problema di configurazione rilevato.",
"diagnostics.copied": "Copiato!",
"diagnostics.copy-to-clipboard": "Copia negli appunti",
"diagnostics.found-config": "Configurazione trovata",
@ -262,10 +322,18 @@
"logs.clear-filter": "Cancella filtro di ricerca",
"logs.completed": "Completato",
"logs.exit-code": "Codice di uscita",
"logs.filter-error": "Impossibile applicare l'espressione di filtro.",
"logs.filter-help-examples": "Esempi: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
"logs.filter-help-fields": "Campi: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatori: ==, !=, contains. Un prefisso ! su una singola parola esclude le voci corrispondenti.",
"logs.filter-help-intro": "I filtri vengono eseguiti sul server sulle voci che sei autorizzato a visualizzare. Combina i termini con and / or.",
"logs.filter-help-title": "Sintassi del filtro",
"logs.filter-placeholder": "Filtra i registri (es. !Update o Status != Completed)",
"logs.metadata": "Metadati",
"logs.no-logs-for-filter": "Nessun registro corrisponde al filtro corrente.",
"logs.no-logs-to-display": "Non ci sono registri da mostrare.",
"logs.page-description": "Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.",
"logs.queue": "Coda",
"logs.queue-action-details": "Dettagli azione",
"logs.queue-default-group": "Predefinito",
"logs.queue-empty": "Non ci sono esecuzioni attive o in attesa al momento.",
"logs.queue-entity": "Entità",
@ -297,6 +365,17 @@
"connected": "已连接",
"diagnostics.browser-info": "浏览器信息",
"diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。",
"diagnostics.config-issue-action": "操作",
"diagnostics.config-issue-argument": "参数",
"diagnostics.config-issue-code": "代码",
"diagnostics.config-issue-config-file": "配置文件",
"diagnostics.config-issue-detail": "详情",
"diagnostics.config-issue-message": "消息",
"diagnostics.config-issue-severity": "严重程度",
"diagnostics.config-issue-source": "来源",
"diagnostics.config-issues": "配置问题",
"diagnostics.config-issues-description": "当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。",
"diagnostics.config-issues-none": "未检测到配置问题。",
"diagnostics.copied": "已复制!",
"diagnostics.copy-to-clipboard": "复制到剪贴板",
"diagnostics.found-config": "找到配置",
@ -333,10 +412,18 @@
"logs.clear-filter": "清除搜索筛选器",
"logs.completed": "完成",
"logs.exit-code": "退出代码",
"logs.filter-error": "无法应用过滤表达式。",
"logs.filter-help-examples": "示例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
"logs.filter-help-fields": "字段:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。运算符:==, !=, contains。在单个单词前加上 ! 前缀可排除匹配的条目。",
"logs.filter-help-intro": "过滤器会在服务器端针对您有权限查看的条目进行筛选。您可以使用 and / or 来组合搜索条件。",
"logs.filter-help-title": "过滤语法",
"logs.filter-placeholder": "过滤日志(例如 !Update 或 Status != Completed)",
"logs.metadata": "元数据",
"logs.no-logs-for-filter": "没有符合当前过滤条件的日志。",
"logs.no-logs-to-display": "没有日志可显示。",
"logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。",
"logs.queue": "队列",
"logs.queue-action-details": "动作详细信息",
"logs.queue-default-group": "默认",
"logs.queue-empty": "当前没有正在运行或等待中的执行。",
"logs.queue-entity": "实体",
@ -363,6 +450,96 @@
"theme-dialog.default": "默认主题",
"theme-dialog.title": "选择主题",
"welcome": "欢迎使用 OliveTin"
},
"zh-Hant-TW": {
"connected": "已連線",
"diagnostics.browser-info": "瀏覽器資訊",
"diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。",
"diagnostics.config-issue-action": "動作",
"diagnostics.config-issue-argument": "參數",
"diagnostics.config-issue-code": "代碼",
"diagnostics.config-issue-config-file": "設定檔",
"diagnostics.config-issue-detail": "詳細資料",
"diagnostics.config-issue-message": "訊息",
"diagnostics.config-issue-severity": "嚴重程度",
"diagnostics.config-issue-source": "來源",
"diagnostics.config-issues": "設定問題",
"diagnostics.config-issues-description": "目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。",
"diagnostics.config-issues-none": "未偵測到設定問題。",
"diagnostics.copied": "已複製!",
"diagnostics.copy-to-clipboard": "複製到剪貼簿",
"diagnostics.found-config": "找到設定檔 (Config)",
"diagnostics.found-key": "找到金鑰 (Key)",
"diagnostics.generate-browser-info": "產生瀏覽器資訊",
"diagnostics.generate-server-diagnostics": "產生伺服器診斷資訊",
"diagnostics.get-support": "尋求技術支援",
"diagnostics.get-support-description": "如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。",
"diagnostics.server-diagnostics": "伺服器診斷",
"diagnostics.server-diagnostics-description": "此區塊可讓您產生詳細的設定與環境報告。建議在提出支援請求時附上此報告。",
"diagnostics.server-diagnostics-docs": "伺服器診斷官方文件",
"diagnostics.ssh": "SSH",
"diagnostics.unknown": "未知",
"diagnostics.useragent-data-error": "取得 userAgentData 時發生錯誤",
"diagnostics.where-to-find-help": "哪裡可以尋求協助",
"disconnected": "已斷線",
"disconnected-banner-announcement": "系統事件 WebSocket 已斷線。",
"disconnected-banner-link-text": "系統事件 WebSocket 已斷線",
"disconnected-banner-suffix": " 自 {disconnectedSince} 起。將在 {reconnectIn} 後嘗試重新連線。",
"disconnected-banner-suffix-reconnecting": " 自 {disconnectedSince} 起。嘗試重新連線中…",
"docs": "官方文件",
"language-dialog.browser-languages": "瀏覽器語言",
"language-dialog.close": "關閉",
"language-dialog.not-available": "無法使用",
"language-dialog.title": "選擇語言",
"login-button": "登入",
"logs.action": "動作",
"logs.action-group-limits": "並行數:{concurrent},佇列大小:{queueSize}",
"logs.back-to-list": "返回清單",
"logs.blocked": "已阻擋",
"logs.calendar": "日曆",
"logs.calendar-title": "日誌日曆",
"logs.clear-date-filter": "清除日期過濾條件",
"logs.clear-filter": "清除搜尋過濾條件",
"logs.completed": "已完成",
"logs.exit-code": "結束代碼",
"logs.filter-error": "無法套用過濾運算式。",
"logs.filter-help-examples": "範例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed",
"logs.filter-help-fields": "欄位:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。運算子:==, !=, contains。在單一單字前加上 ! 前綴可排除相符的項目。",
"logs.filter-help-intro": "過濾器會在伺服器端針對您有權限檢視的項目進行篩選。您可以使用 and / or 來組合搜尋條件。",
"logs.filter-help-title": "過濾語法",
"logs.filter-placeholder": "過濾日誌 (例如 !Update 或 Status != Completed)",
"logs.metadata": "中繼資料",
"logs.no-logs-for-filter": "沒有符合目前過濾條件的日誌。",
"logs.no-logs-to-display": "目前沒有可顯示的日誌。",
"logs.page-description": "這是已執行動作的日誌清單。請使用過濾條件欄位來搜尋關鍵字與運算式。",
"logs.queue": "佇列",
"logs.queue-action-details": "動作詳細資訊",
"logs.queue-default-group": "預設",
"logs.queue-empty": "目前沒有使用中或等待中的執行項目。",
"logs.queue-entity": "實體",
"logs.queue-group-active": "{active} 個使用中 (最大值 {max})",
"logs.queue-group-active-unlimited": "{active} 個使用中",
"logs.queue-page-description": "依動作群組分類的使用中與等待中執行項目。您無權檢視的項目將會被隱藏。",
"logs.queue-position": "#{position}",
"logs.queue-running": "執行中",
"logs.queue-title": "執行佇列",
"logs.queue-waiting": "等待中",
"logs.status": "狀態",
"logs.timed-out": "執行逾時",
"logs.timestamp": "時間戳記",
"logs.title": "日誌",
"nav.actions": "動作",
"nav.diagnostics": "診斷",
"nav.entities": "實體",
"nav.logs": "日誌",
"raise-issue": "在 GitHub 上建立 Issue",
"reconnecting": "重新連線中…",
"return-to-index": "返回首頁",
"search-filter": "過濾目前頁面",
"theme-dialog.close": "關閉",
"theme-dialog.default": "預設佈景主題",
"theme-dialog.title": "選擇佈景主題",
"welcome": "歡迎使用 OliveTin"
}
}
}

View File

@ -17,6 +17,13 @@ translations:
docs: Dokumentation
logs.title: Protokolle
logs.page-description: Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.
logs.filter-placeholder: Protokolle filtern (z. B. !Update oder Status != Completed)
logs.filter-help-title: Filtersyntax
logs.filter-help-intro: Filter laufen auf dem Server über Einträge, die Sie anzeigen dürfen. Kombinieren Sie Begriffe mit and / or.
logs.filter-help-fields: "Felder: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatoren: ==, !=, contains. Ein !-Präfix vor einem einzelnen Wort schließt passende Einträge aus."
logs.filter-help-examples: "Beispiele: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed"
logs.filter-error: Filterausdruck konnte nicht angewendet werden.
logs.no-logs-for-filter: Keine Protokolle entsprechen dem aktuellen Filter.
logs.timestamp: Zeitstempel
logs.action: Aktion
logs.metadata: Metadaten
@ -43,9 +50,21 @@ translations:
logs.queue-running: Läuft
logs.queue-position: "#{position}"
logs.queue-entity: Entität
logs.queue-action-details: Aktionsdetails
diagnostics.get-support: Unterstützung erhalten
diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.
diagnostics.where-to-find-help: Wo Sie Hilfe finden
diagnostics.config-issues: Konfigurationsprobleme
diagnostics.config-issues-description: In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.
diagnostics.config-issues-none: Keine Konfigurationsprobleme erkannt.
diagnostics.config-issue-severity: Schweregrad
diagnostics.config-issue-code: Code
diagnostics.config-issue-message: Meldung
diagnostics.config-issue-action: Aktion
diagnostics.config-issue-argument: Argument
diagnostics.config-issue-config-file: Konfigurationsdatei
diagnostics.config-issue-detail: Detail
diagnostics.config-issue-source: Quelle
diagnostics.ssh: SSH
diagnostics.found-key: Schlüssel gefunden
diagnostics.found-config: Konfiguration gefunden

View File

@ -54,6 +54,17 @@ translations:
diagnostics.get-support: Get support
diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page.
diagnostics.where-to-find-help: Where to find help
diagnostics.config-issues: Configuration issues
diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.
diagnostics.config-issues-none: No configuration issues detected.
diagnostics.config-issue-severity: Severity
diagnostics.config-issue-code: Code
diagnostics.config-issue-message: Message
diagnostics.config-issue-action: Action
diagnostics.config-issue-argument: Argument
diagnostics.config-issue-config-file: Config file
diagnostics.config-issue-detail: Detail
diagnostics.config-issue-source: Source
diagnostics.ssh: SSH
diagnostics.found-key: Found Key
diagnostics.found-config: Found Config

View File

@ -17,6 +17,13 @@ translations:
docs: Documentación
logs.title: Registros
logs.page-description: Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.
logs.filter-placeholder: Filtrar registros (p. ej. !Update o Status != Completed)
logs.filter-help-title: Sintaxis del filtro
logs.filter-help-intro: Los filtros se ejecutan en el servidor sobre las entradas que puede ver. Combine términos con and / or.
logs.filter-help-fields: "Campos: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operadores: ==, !=, contains. Un prefijo ! en una sola palabra excluye las entradas coincidentes."
logs.filter-help-examples: "Ejemplos: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed"
logs.filter-error: No se pudo aplicar la expresión del filtro.
logs.no-logs-for-filter: Ningún registro coincide con el filtro actual.
logs.timestamp: Marca de tiempo
logs.action: Acción
logs.metadata: Metadatos
@ -43,9 +50,21 @@ translations:
logs.queue-running: En ejecución
logs.queue-position: "#{position}"
logs.queue-entity: Entidad
logs.queue-action-details: Detalles de la acción
diagnostics.get-support: Obtener soporte
diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.
diagnostics.where-to-find-help: Dónde encontrar ayuda
diagnostics.config-issues: Problemas de configuración
diagnostics.config-issues-description: Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.
diagnostics.config-issues-none: No se detectaron problemas de configuración.
diagnostics.config-issue-severity: Severidad
diagnostics.config-issue-code: Código
diagnostics.config-issue-message: Mensaje
diagnostics.config-issue-action: Acción
diagnostics.config-issue-argument: Argumento
diagnostics.config-issue-config-file: Archivo de configuración
diagnostics.config-issue-detail: Detalle
diagnostics.config-issue-source: Origen
diagnostics.ssh: SSH
diagnostics.found-key: Clave encontrada
diagnostics.found-config: Configuración encontrada

View File

@ -17,6 +17,13 @@ translations:
raise-issue: Segnala un problema su GitHub
logs.title: Registri
logs.page-description: Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.
logs.filter-placeholder: Filtra i registri (es. !Update o Status != Completed)
logs.filter-help-title: Sintassi del filtro
logs.filter-help-intro: I filtri vengono eseguiti sul server sulle voci che sei autorizzato a visualizzare. Combina i termini con and / or.
logs.filter-help-fields: "Campi: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatori: ==, !=, contains. Un prefisso ! su una singola parola esclude le voci corrispondenti."
logs.filter-help-examples: "Esempi: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed"
logs.filter-error: Impossibile applicare l'espressione di filtro.
logs.no-logs-for-filter: Nessun registro corrisponde al filtro corrente.
logs.timestamp: Date e ora
logs.action: Azione
logs.metadata: Metadati
@ -43,9 +50,21 @@ translations:
logs.queue-running: In esecuzione
logs.queue-position: "#{position}"
logs.queue-entity: Entità
logs.queue-action-details: Dettagli azione
diagnostics.get-support: Ottenere supporto
diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.
diagnostics.where-to-find-help: Dove trovare aiuto
diagnostics.config-issues: Problemi di configurazione
diagnostics.config-issues-description: Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.
diagnostics.config-issues-none: Nessun problema di configurazione rilevato.
diagnostics.config-issue-severity: Gravità
diagnostics.config-issue-code: Codice
diagnostics.config-issue-message: Messaggio
diagnostics.config-issue-action: Azione
diagnostics.config-issue-argument: Argomento
diagnostics.config-issue-config-file: File di configurazione
diagnostics.config-issue-detail: Dettaglio
diagnostics.config-issue-source: Origine
diagnostics.ssh: SSH
diagnostics.found-key: Chiave trovata
diagnostics.found-config: Configurazione trovata

View File

@ -17,6 +17,13 @@ translations:
docs: 文档
logs.title: 日志
logs.page-description: 这是一个动作执行日志列表。您可以按动作标题过滤列表。
logs.filter-placeholder: 过滤日志(例如 !Update 或 Status != Completed)
logs.filter-help-title: 过滤语法
logs.filter-help-intro: 过滤器会在服务器端针对您有权限查看的条目进行筛选。您可以使用 and / or 来组合搜索条件。
logs.filter-help-fields: "字段:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。运算符:==, !=, contains。在单个单词前加上 ! 前缀可排除匹配的条目。"
logs.filter-help-examples: "示例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed"
logs.filter-error: 无法应用过滤表达式。
logs.no-logs-for-filter: 没有符合当前过滤条件的日志。
logs.timestamp: 时间戳
logs.action: 动作
logs.metadata: 元数据
@ -52,9 +59,21 @@ translations:
logs.queue-running: 运行中
logs.queue-position: "第 {position} 位"
logs.queue-entity: 实体
logs.queue-action-details: 动作详细信息
diagnostics.get-support: 获取支持
diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。
diagnostics.where-to-find-help: 在哪里找到帮助
diagnostics.config-issues: 配置问题
diagnostics.config-issues-description: 当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。
diagnostics.config-issues-none: 未检测到配置问题。
diagnostics.config-issue-severity: 严重程度
diagnostics.config-issue-code: 代码
diagnostics.config-issue-message: 消息
diagnostics.config-issue-action: 操作
diagnostics.config-issue-argument: 参数
diagnostics.config-issue-config-file: 配置文件
diagnostics.config-issue-detail: 详情
diagnostics.config-issue-source: 来源
diagnostics.ssh: SSH
diagnostics.found-key: 找到密钥
diagnostics.found-config: 找到配置

90
lang/zh-Hant-TW.yaml Normal file
View File

@ -0,0 +1,90 @@
schemaVersion: 1
translations:
welcome: 歡迎使用 OliveTin
docs: 官方文件
raise-issue: 在 GitHub 上建立 Issue
nav.actions: 動作
nav.logs: 日誌
nav.entities: 實體
nav.diagnostics: 診斷
connected: 已連線
disconnected: 已斷線
reconnecting: 重新連線中…
disconnected-banner-announcement: 系統事件 WebSocket 已斷線。
disconnected-banner-link-text: "系統事件 WebSocket 已斷線"
disconnected-banner-suffix: " 自 {disconnectedSince} 起。將在 {reconnectIn} 後嘗試重新連線。"
disconnected-banner-suffix-reconnecting: " 自 {disconnectedSince} 起。嘗試重新連線中…"
login-button: 登入
logs.title: 日誌
logs.page-description: 這是已執行動作的日誌清單。請使用過濾條件欄位來搜尋關鍵字與運算式。
logs.filter-placeholder: 過濾日誌 (例如 !Update 或 Status != Completed)
logs.filter-help-title: 過濾語法
logs.filter-help-intro: 過濾器會在伺服器端針對您有權限檢視的項目進行篩選。您可以使用 and / or 來組合搜尋條件。
logs.filter-help-fields: "欄位:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。運算子:==, !=, contains。在單一單字前加上 ! 前綴可排除相符的項目。"
logs.filter-help-examples: "範例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed"
logs.filter-error: 無法套用過濾運算式。
logs.no-logs-for-filter: 沒有符合目前過濾條件的日誌。
logs.timestamp: 時間戳記
logs.action: 動作
logs.metadata: 中繼資料
logs.status: 狀態
logs.no-logs-to-display: 目前沒有可顯示的日誌。
logs.timed-out: 執行逾時
logs.blocked: 已阻擋
logs.exit-code: 結束代碼
logs.completed: 已完成
logs.clear-filter: 清除搜尋過濾條件
logs.clear-date-filter: 清除日期過濾條件
logs.calendar: 日曆
logs.calendar-title: 日誌日曆
logs.back-to-list: 返回清單
logs.queue: 佇列
logs.queue-title: 執行佇列
logs.queue-page-description: 依動作群組分類的使用中與等待中執行項目。您無權檢視的項目將會被隱藏。
logs.queue-empty: 目前沒有使用中或等待中的執行項目。
logs.queue-default-group: 預設
logs.action-group-limits: "並行數:{concurrent},佇列大小:{queueSize}"
logs.queue-group-active: "{active} 個使用中 (最大值 {max})"
logs.queue-group-active-unlimited: "{active} 個使用中"
logs.queue-waiting: 等待中
logs.queue-running: 執行中
logs.queue-position: "#{position}"
logs.queue-entity: 實體
logs.queue-action-details: 動作詳細資訊
diagnostics.get-support: 尋求技術支援
diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。
diagnostics.where-to-find-help: 哪裡可以尋求協助
diagnostics.config-issues: 設定問題
diagnostics.config-issues-description: 目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。
diagnostics.config-issues-none: 未偵測到設定問題。
diagnostics.config-issue-severity: 嚴重程度
diagnostics.config-issue-code: 代碼
diagnostics.config-issue-message: 訊息
diagnostics.config-issue-action: 動作
diagnostics.config-issue-argument: 參數
diagnostics.config-issue-config-file: 設定檔
diagnostics.config-issue-detail: 詳細資料
diagnostics.config-issue-source: 來源
diagnostics.ssh: SSH
diagnostics.found-key: 找到金鑰 (Key)
diagnostics.found-config: 找到設定檔 (Config)
diagnostics.server-diagnostics: 伺服器診斷
diagnostics.server-diagnostics-description: 此區塊可讓您產生詳細的設定與環境報告。建議在提出支援請求時附上此報告。
diagnostics.server-diagnostics-docs: 伺服器診斷官方文件
diagnostics.generate-server-diagnostics: 產生伺服器診斷資訊
diagnostics.browser-info: 瀏覽器資訊
diagnostics.browser-info-description: 此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。
diagnostics.generate-browser-info: 產生瀏覽器資訊
diagnostics.copy-to-clipboard: 複製到剪貼簿
diagnostics.copied: 已複製!
diagnostics.unknown: 未知
diagnostics.useragent-data-error: 取得 userAgentData 時發生錯誤
return-to-index: 返回首頁
search-filter: 過濾目前頁面
language-dialog.title: 選擇語言
language-dialog.browser-languages: 瀏覽器語言
language-dialog.not-available: 無法使用
language-dialog.close: 關閉
theme-dialog.title: 選擇佈景主題
theme-dialog.default: 預設佈景主題
theme-dialog.close: 關閉

View File

@ -379,9 +379,21 @@ message LogoutResponse {
message GetDiagnosticsRequest {
}
message ConfigIssue {
string severity = 1;
string code = 2;
string message = 3;
string action_id = 4;
string action_title = 5;
string argument_name = 6;
string source = 7;
string config_file = 8;
}
message GetDiagnosticsResponse {
string SshFoundKey = 1;
string SshFoundConfig = 2;
repeated ConfigIssue config_issues = 3;
}
message InitRequest {}
@ -412,6 +424,7 @@ message InitResponse {
bool login_required = 23;
repeated string available_themes = 24; // List of available theme names
bool show_navigate_on_start_icons = 25;
int32 config_issue_count = 26;
}
message AdditionalLink {

View File

@ -3871,17 +3871,118 @@ func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64}
}
type ConfigIssue struct {
state protoimpl.MessageState `protogen:"open.v1"`
Severity string `protobuf:"bytes,1,opt,name=severity,proto3" json:"severity,omitempty"`
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
ActionId string `protobuf:"bytes,4,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
ActionTitle string `protobuf:"bytes,5,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
ArgumentName string `protobuf:"bytes,6,opt,name=argument_name,json=argumentName,proto3" json:"argument_name,omitempty"`
Source string `protobuf:"bytes,7,opt,name=source,proto3" json:"source,omitempty"`
ConfigFile string `protobuf:"bytes,8,opt,name=config_file,json=configFile,proto3" json:"config_file,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConfigIssue) Reset() {
*x = ConfigIssue{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConfigIssue) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConfigIssue) ProtoMessage() {}
func (x *ConfigIssue) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConfigIssue.ProtoReflect.Descriptor instead.
func (*ConfigIssue) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65}
}
func (x *ConfigIssue) GetSeverity() string {
if x != nil {
return x.Severity
}
return ""
}
func (x *ConfigIssue) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *ConfigIssue) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ConfigIssue) GetActionId() string {
if x != nil {
return x.ActionId
}
return ""
}
func (x *ConfigIssue) GetActionTitle() string {
if x != nil {
return x.ActionTitle
}
return ""
}
func (x *ConfigIssue) GetArgumentName() string {
if x != nil {
return x.ArgumentName
}
return ""
}
func (x *ConfigIssue) GetSource() string {
if x != nil {
return x.Source
}
return ""
}
func (x *ConfigIssue) GetConfigFile() string {
if x != nil {
return x.ConfigFile
}
return ""
}
type GetDiagnosticsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"`
SshFoundConfig string `protobuf:"bytes,2,opt,name=SshFoundConfig,proto3" json:"SshFoundConfig,omitempty"`
ConfigIssues []*ConfigIssue `protobuf:"bytes,3,rep,name=config_issues,json=configIssues,proto3" json:"config_issues,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDiagnosticsResponse) Reset() {
*x = GetDiagnosticsResponse{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3893,7 +3994,7 @@ func (x *GetDiagnosticsResponse) String() string {
func (*GetDiagnosticsResponse) ProtoMessage() {}
func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3906,7 +4007,7 @@ func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead.
func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66}
}
func (x *GetDiagnosticsResponse) GetSshFoundKey() string {
@ -3923,6 +4024,13 @@ func (x *GetDiagnosticsResponse) GetSshFoundConfig() string {
return ""
}
func (x *GetDiagnosticsResponse) GetConfigIssues() []*ConfigIssue {
if x != nil {
return x.ConfigIssues
}
return nil
}
type InitRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@ -3931,7 +4039,7 @@ type InitRequest struct {
func (x *InitRequest) Reset() {
*x = InitRequest{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3943,7 +4051,7 @@ func (x *InitRequest) String() string {
func (*InitRequest) ProtoMessage() {}
func (x *InitRequest) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3956,7 +4064,7 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead.
func (*InitRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67}
}
type InitResponse struct {
@ -3986,13 +4094,14 @@ type InitResponse struct {
LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"`
AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names
ShowNavigateOnStartIcons bool `protobuf:"varint,25,opt,name=show_navigate_on_start_icons,json=showNavigateOnStartIcons,proto3" json:"show_navigate_on_start_icons,omitempty"`
ConfigIssueCount int32 `protobuf:"varint,26,opt,name=config_issue_count,json=configIssueCount,proto3" json:"config_issue_count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InitResponse) Reset() {
*x = InitResponse{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4004,7 +4113,7 @@ func (x *InitResponse) String() string {
func (*InitResponse) ProtoMessage() {}
func (x *InitResponse) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4017,7 +4126,7 @@ func (x *InitResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead.
func (*InitResponse) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68}
}
func (x *InitResponse) GetShowFooter() bool {
@ -4195,6 +4304,13 @@ func (x *InitResponse) GetShowNavigateOnStartIcons() bool {
return false
}
func (x *InitResponse) GetConfigIssueCount() int32 {
if x != nil {
return x.ConfigIssueCount
}
return 0
}
type AdditionalLink struct {
state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@ -4205,7 +4321,7 @@ type AdditionalLink struct {
func (x *AdditionalLink) Reset() {
*x = AdditionalLink{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4217,7 +4333,7 @@ func (x *AdditionalLink) String() string {
func (*AdditionalLink) ProtoMessage() {}
func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4230,7 +4346,7 @@ func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead.
func (*AdditionalLink) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69}
}
func (x *AdditionalLink) GetTitle() string {
@ -4258,7 +4374,7 @@ type OAuth2Provider struct {
func (x *OAuth2Provider) Reset() {
*x = OAuth2Provider{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4270,7 +4386,7 @@ func (x *OAuth2Provider) String() string {
func (*OAuth2Provider) ProtoMessage() {}
func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4283,7 +4399,7 @@ func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
// Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead.
func (*OAuth2Provider) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70}
}
func (x *OAuth2Provider) GetTitle() string {
@ -4316,7 +4432,7 @@ type GetActionBindingRequest struct {
func (x *GetActionBindingRequest) Reset() {
*x = GetActionBindingRequest{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4328,7 +4444,7 @@ func (x *GetActionBindingRequest) String() string {
func (*GetActionBindingRequest) ProtoMessage() {}
func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4341,7 +4457,7 @@ func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead.
func (*GetActionBindingRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71}
}
func (x *GetActionBindingRequest) GetBindingId() string {
@ -4361,7 +4477,7 @@ type GetActionBindingResponse struct {
func (x *GetActionBindingResponse) Reset() {
*x = GetActionBindingResponse{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4373,7 +4489,7 @@ func (x *GetActionBindingResponse) String() string {
func (*GetActionBindingResponse) ProtoMessage() {}
func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4386,7 +4502,7 @@ func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead.
func (*GetActionBindingResponse) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72}
}
func (x *GetActionBindingResponse) GetAction() *Action {
@ -4415,7 +4531,7 @@ type GetEntitiesRequest struct {
func (x *GetEntitiesRequest) Reset() {
*x = GetEntitiesRequest{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4427,7 +4543,7 @@ func (x *GetEntitiesRequest) String() string {
func (*GetEntitiesRequest) ProtoMessage() {}
func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4440,7 +4556,7 @@ func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead.
func (*GetEntitiesRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73}
}
func (x *GetEntitiesRequest) GetEntityType() string {
@ -4480,7 +4596,7 @@ type GetEntitiesResponse struct {
func (x *GetEntitiesResponse) Reset() {
*x = GetEntitiesResponse{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4492,7 +4608,7 @@ func (x *GetEntitiesResponse) String() string {
func (*GetEntitiesResponse) ProtoMessage() {}
func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4505,7 +4621,7 @@ func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead.
func (*GetEntitiesResponse) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74}
}
func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition {
@ -4529,7 +4645,7 @@ type EntityDefinition struct {
func (x *EntityDefinition) Reset() {
*x = EntityDefinition{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4541,7 +4657,7 @@ func (x *EntityDefinition) String() string {
func (*EntityDefinition) ProtoMessage() {}
func (x *EntityDefinition) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4554,7 +4670,7 @@ func (x *EntityDefinition) ProtoReflect() protoreflect.Message {
// Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead.
func (*EntityDefinition) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75}
}
func (x *EntityDefinition) GetTitle() string {
@ -4609,7 +4725,7 @@ type EntityProperty struct {
func (x *EntityProperty) Reset() {
*x = EntityProperty{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4621,7 +4737,7 @@ func (x *EntityProperty) String() string {
func (*EntityProperty) ProtoMessage() {}
func (x *EntityProperty) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4634,7 +4750,7 @@ func (x *EntityProperty) ProtoReflect() protoreflect.Message {
// Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead.
func (*EntityProperty) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76}
}
func (x *EntityProperty) GetName() string {
@ -4661,7 +4777,7 @@ type GetEntityRequest struct {
func (x *GetEntityRequest) Reset() {
*x = GetEntityRequest{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4673,7 +4789,7 @@ func (x *GetEntityRequest) String() string {
func (*GetEntityRequest) ProtoMessage() {}
func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4686,7 +4802,7 @@ func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead.
func (*GetEntityRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77}
}
func (x *GetEntityRequest) GetUniqueKey() string {
@ -4712,7 +4828,7 @@ type RestartActionRequest struct {
func (x *RestartActionRequest) Reset() {
*x = RestartActionRequest{}
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -4724,7 +4840,7 @@ func (x *RestartActionRequest) String() string {
func (*RestartActionRequest) ProtoMessage() {}
func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -4737,7 +4853,7 @@ func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead.
func (*RestartActionRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77}
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{78}
}
func (x *RestartActionRequest) GetExecutionTrackingId() string {
@ -5044,11 +5160,22 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
"\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" +
"\rLogoutRequest\"\x10\n" +
"\x0eLogoutResponse\"\x17\n" +
"\x15GetDiagnosticsRequest\"b\n" +
"\x15GetDiagnosticsRequest\"\xf5\x01\n" +
"\vConfigIssue\x12\x1a\n" +
"\bseverity\x18\x01 \x01(\tR\bseverity\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12\x18\n" +
"\amessage\x18\x03 \x01(\tR\amessage\x12\x1b\n" +
"\taction_id\x18\x04 \x01(\tR\bactionId\x12!\n" +
"\faction_title\x18\x05 \x01(\tR\vactionTitle\x12#\n" +
"\rargument_name\x18\x06 \x01(\tR\fargumentName\x12\x16\n" +
"\x06source\x18\a \x01(\tR\x06source\x12\x1f\n" +
"\vconfig_file\x18\b \x01(\tR\n" +
"configFile\"\xa5\x01\n" +
"\x16GetDiagnosticsResponse\x12 \n" +
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
"\vInitRequest\"\x8d\t\n" +
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\x12A\n" +
"\rconfig_issues\x18\x03 \x03(\v2\x1c.olivetin.api.v1.ConfigIssueR\fconfigIssues\"\r\n" +
"\vInitRequest\"\xbb\t\n" +
"\fInitResponse\x12\x1e\n" +
"\n" +
"showFooter\x18\x01 \x01(\bR\n" +
@ -5078,7 +5205,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" +
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\x12>\n" +
"\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\"8\n" +
"\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\x12,\n" +
"\x12config_issue_count\x18\x1a \x01(\x05R\x10configIssueCount\"8\n" +
"\x0eAdditionalLink\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
@ -5159,7 +5287,7 @@ func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte {
return file_olivetin_api_v1_olivetin_proto_rawDescData
}
var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 85)
var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 86)
var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
(*Action)(nil), // 0: olivetin.api.v1.Action
(*ActionGroupMembership)(nil), // 1: olivetin.api.v1.ActionGroupMembership
@ -5226,38 +5354,39 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
(*LogoutRequest)(nil), // 62: olivetin.api.v1.LogoutRequest
(*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse
(*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest
(*GetDiagnosticsResponse)(nil), // 65: olivetin.api.v1.GetDiagnosticsResponse
(*InitRequest)(nil), // 66: olivetin.api.v1.InitRequest
(*InitResponse)(nil), // 67: olivetin.api.v1.InitResponse
(*AdditionalLink)(nil), // 68: olivetin.api.v1.AdditionalLink
(*OAuth2Provider)(nil), // 69: olivetin.api.v1.OAuth2Provider
(*GetActionBindingRequest)(nil), // 70: olivetin.api.v1.GetActionBindingRequest
(*GetActionBindingResponse)(nil), // 71: olivetin.api.v1.GetActionBindingResponse
(*GetEntitiesRequest)(nil), // 72: olivetin.api.v1.GetEntitiesRequest
(*GetEntitiesResponse)(nil), // 73: olivetin.api.v1.GetEntitiesResponse
(*EntityDefinition)(nil), // 74: olivetin.api.v1.EntityDefinition
(*EntityProperty)(nil), // 75: olivetin.api.v1.EntityProperty
(*GetEntityRequest)(nil), // 76: olivetin.api.v1.GetEntityRequest
(*RestartActionRequest)(nil), // 77: olivetin.api.v1.RestartActionRequest
nil, // 78: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
nil, // 80: olivetin.api.v1.ActionArgument.SuggestionsEntry
nil, // 81: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
nil, // 82: olivetin.api.v1.Entity.FieldsEntry
nil, // 83: olivetin.api.v1.DumpVarsResponse.ContentsEntry
nil, // 84: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
(*ConfigIssue)(nil), // 65: olivetin.api.v1.ConfigIssue
(*GetDiagnosticsResponse)(nil), // 66: olivetin.api.v1.GetDiagnosticsResponse
(*InitRequest)(nil), // 67: olivetin.api.v1.InitRequest
(*InitResponse)(nil), // 68: olivetin.api.v1.InitResponse
(*AdditionalLink)(nil), // 69: olivetin.api.v1.AdditionalLink
(*OAuth2Provider)(nil), // 70: olivetin.api.v1.OAuth2Provider
(*GetActionBindingRequest)(nil), // 71: olivetin.api.v1.GetActionBindingRequest
(*GetActionBindingResponse)(nil), // 72: olivetin.api.v1.GetActionBindingResponse
(*GetEntitiesRequest)(nil), // 73: olivetin.api.v1.GetEntitiesRequest
(*GetEntitiesResponse)(nil), // 74: olivetin.api.v1.GetEntitiesResponse
(*EntityDefinition)(nil), // 75: olivetin.api.v1.EntityDefinition
(*EntityProperty)(nil), // 76: olivetin.api.v1.EntityProperty
(*GetEntityRequest)(nil), // 77: olivetin.api.v1.GetEntityRequest
(*RestartActionRequest)(nil), // 78: olivetin.api.v1.RestartActionRequest
nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
nil, // 80: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
nil, // 81: olivetin.api.v1.ActionArgument.SuggestionsEntry
nil, // 82: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
nil, // 83: olivetin.api.v1.Entity.FieldsEntry
nil, // 84: olivetin.api.v1.DumpVarsResponse.ContentsEntry
nil, // 85: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
}
var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
3, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument
2, // 1: olivetin.api.v1.Action.exec_on_webhooks:type_name -> olivetin.api.v1.ActionWebhookExecHint
1, // 2: olivetin.api.v1.Action.groups:type_name -> olivetin.api.v1.ActionGroupMembership
78, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
79, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
79, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
80, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
4, // 5: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice
80, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry
81, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry
0, // 7: olivetin.api.v1.EntityRelatedAction.action:type_name -> olivetin.api.v1.Action
81, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
82, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry
82, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
83, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry
5, // 10: olivetin.api.v1.Entity.related_actions:type_name -> olivetin.api.v1.EntityRelatedAction
10, // 11: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard
11, // 12: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent
@ -5275,8 +5404,8 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
28, // 24: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup
22, // 25: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
35, // 26: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
83, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
84, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
84, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
85, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
51, // 29: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
52, // 30: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
54, // 31: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
@ -5285,72 +5414,73 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
53, // 34: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat
22, // 35: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
22, // 36: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
69, // 37: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
68, // 38: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
8, // 39: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
0, // 40: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
35, // 41: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
74, // 42: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
6, // 43: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
75, // 44: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty
43, // 45: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
9, // 46: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
12, // 47: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
15, // 48: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
17, // 49: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
19, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
77, // 51: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
56, // 52: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
34, // 53: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
21, // 54: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
24, // 55: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
26, // 56: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
30, // 57: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
37, // 58: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
39, // 59: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest
41, // 60: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
44, // 61: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
46, // 62: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
58, // 63: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
60, // 64: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
62, // 65: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
48, // 66: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
64, // 67: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
66, // 68: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
70, // 69: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
72, // 70: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
76, // 71: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
7, // 72: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
14, // 73: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
16, // 74: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
18, // 75: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
20, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
14, // 77: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
57, // 78: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
36, // 79: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
23, // 80: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
25, // 81: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
29, // 82: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
31, // 83: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
38, // 84: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
40, // 85: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse
42, // 86: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
45, // 87: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
47, // 88: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
59, // 89: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
61, // 90: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
63, // 91: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
49, // 92: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
65, // 93: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
67, // 94: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
71, // 95: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
73, // 96: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
6, // 97: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
72, // [72:98] is the sub-list for method output_type
46, // [46:72] is the sub-list for method input_type
46, // [46:46] is the sub-list for extension type_name
46, // [46:46] is the sub-list for extension extendee
0, // [0:46] is the sub-list for field type_name
65, // 37: olivetin.api.v1.GetDiagnosticsResponse.config_issues:type_name -> olivetin.api.v1.ConfigIssue
70, // 38: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
69, // 39: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
8, // 40: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
0, // 41: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
35, // 42: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
75, // 43: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
6, // 44: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
76, // 45: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty
43, // 46: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
9, // 47: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
12, // 48: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
15, // 49: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
17, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
19, // 51: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
78, // 52: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
56, // 53: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
34, // 54: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
21, // 55: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
24, // 56: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
26, // 57: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
30, // 58: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
37, // 59: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
39, // 60: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest
41, // 61: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
44, // 62: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
46, // 63: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
58, // 64: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
60, // 65: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
62, // 66: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
48, // 67: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
64, // 68: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
67, // 69: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
71, // 70: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
73, // 71: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
77, // 72: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
7, // 73: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
14, // 74: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
16, // 75: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
18, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
20, // 77: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
14, // 78: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
57, // 79: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
36, // 80: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
23, // 81: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
25, // 82: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
29, // 83: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
31, // 84: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
38, // 85: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
40, // 86: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse
42, // 87: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
45, // 88: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
47, // 89: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
59, // 90: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
61, // 91: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
63, // 92: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
49, // 93: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
66, // 94: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
68, // 95: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
72, // 96: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
74, // 97: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
6, // 98: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
73, // [73:99] is the sub-list for method output_type
47, // [47:73] is the sub-list for method input_type
47, // [47:47] is the sub-list for extension type_name
47, // [47:47] is the sub-list for extension extendee
0, // [0:47] is the sub-list for field type_name
}
func init() { file_olivetin_api_v1_olivetin_proto_init() }
@ -5372,7 +5502,7 @@ func file_olivetin_api_v1_olivetin_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)),
NumEnums: 0,
NumMessages: 85,
NumMessages: 86,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -10,7 +10,7 @@ require (
github.com/MicahParks/keyfunc/v3 v3.8.0
github.com/PaesslerAG/jsonpath v0.1.1
github.com/alexedwards/argon2id v1.0.0
github.com/bufbuild/buf v1.71.0
github.com/bufbuild/buf v1.72.0
github.com/expr-lang/expr v1.17.8
github.com/fsnotify/fsnotify v1.10.1
github.com/go-viper/mapstructure/v2 v2.5.0
@ -23,12 +23,12 @@ require (
github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/providers/rawbytes v1.0.0
github.com/knadh/koanf/v2 v2.3.5
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_golang v1.24.1
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1
go.akshayshah.org/connectproto v0.6.0
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
golang.org/x/exp v0.0.0-20260718201538-764159d718ef
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0
google.golang.org/protobuf v1.36.11
@ -36,15 +36,15 @@ require (
)
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gocheckcompilerdirectives v1.4.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/bufplugin/protocolbuffers/go v1.36.11-20260722160903-4d94f3df3a7b.1 // indirect
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 // indirect
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 // indirect
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1 // indirect
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.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.20260626143626-be153867abea // indirect
buf.build/go/bufplugin v0.10.0 // indirect
buf.build/go/bufprivateusage v0.1.0 // indirect
buf.build/go/interrupt v1.1.0 // indirect
@ -53,21 +53,21 @@ require (
buf.build/go/spdx v0.2.0 // indirect
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
cel.dev/expr v0.25.2 // indirect
charm.land/lipgloss/v2 v2.0.3 // indirect
codeberg.org/chavacava/garif v0.2.0 // indirect
charm.land/lipgloss/v2 v2.0.5 // indirect
codeberg.org/chavacava/garif v0.2.1 // indirect
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
connectrpc.com/otelconnect v0.9.0 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/golib v0.6.0 // indirect
dev.gaijin.team/go/golib v0.8.1 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
github.com/Abirdcfly/dupword v0.1.7 // indirect
github.com/Abirdcfly/dupword v0.1.8 // 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/errname v1.1.2 // indirect
github.com/Antonboom/nilnil v1.1.2 // 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/ClickHouse/clickhouse-go-linter v1.2.1 // 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
@ -75,7 +75,7 @@ require (
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/PaesslerAG/gval v1.2.4 // indirect
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
github.com/alecthomas/chroma/v2 v2.27.0 // 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
@ -92,16 +92,16 @@ require (
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/protocompile v0.14.2-0.20260716165721-bb5762d29672 // 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/butuzov/mirror v1.3.3 // indirect
github.com/catenacyber/perfsprint v0.10.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.11 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac // 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
@ -114,26 +114,26 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.7 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/daixiang0/gci v0.14.0 // indirect
github.com/dave/dst v0.27.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denis-tingaikin/go-header v1.0.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/docker/cli v29.6.1+incompatible // indirect
github.com/dlclark/regexp2/v2 v2.5.2 // indirect
github.com/docker/cli v29.6.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.8 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-connections v0.8.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/firefart/nonamedreturns v1.0.6 // indirect
github.com/firefart/nonamedreturns v1.0.8 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.20 // indirect
github.com/ghostiam/protogetter v0.3.21 // 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.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
@ -154,7 +154,7 @@ require (
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/rowserrcheck v0.0.0-20260701145345-399602f983c8 // 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
@ -168,24 +168,23 @@ require (
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdx/go-netrc v1.0.0 // indirect
github.com/jgautheron/goconst v1.10.0 // indirect
github.com/jgautheron/goconst v1.10.2 // 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/kisielk/errcheck v1.20.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/klauspost/compress v1.19.0 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.2 // 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/gomoddirectives v0.9.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
@ -193,19 +192,17 @@ require (
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
github.com/manuelarte/funcorder v0.6.0 // indirect
github.com/maratori/testableexamples v1.0.1 // indirect
github.com/maratori/testpackage v1.1.2 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mgechev/revive v1.15.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.55.0 // indirect
@ -216,15 +213,14 @@ require (
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/nunnatsa/ginkgolinter v0.23.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/petermattis/goid v0.0.0-20260725062400-500c67a39b75 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.69.0 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
@ -232,45 +228,45 @@ require (
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.60.0 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/quic-go/quic-go v0.61.0 // indirect
github.com/raeperd/recvcheck v0.3.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
github.com/sagikazarmark/locafero v0.12.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/securego/gosec/v2 v2.28.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/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/spf13/viper v1.21.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/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // 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/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.1 // indirect
github.com/uudashr/iface v1.4.2 // indirect
github.com/uudashr/iface v1.5.0 // 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
@ -280,7 +276,7 @@ require (
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.augendre.info/fatcontext v0.10.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
@ -293,19 +289,17 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20260718201538-764159d718ef // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.39.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // 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

View File

@ -1,21 +1,21 @@
4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 h1:bHi5/cwz7IPRhFcrRA4iytTjDI3WwFkv1m5K7y+YOTc=
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ=
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260722160903-4d94f3df3a7b.1 h1:IbnjTSFlFUyPCRF50eY1u9EyPpU1wAyAN5neSgI5jyE=
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260722160903-4d94f3df3a7b.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ=
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY=
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 h1:szEsAEWp6MqAX3NHUFq3ZH3oGKyajcte99KrCz0MpQ0=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1/go.mod h1:VKtlgFOV7s6j5/wPr+d6c0Q80c8MF4OfZev4e0d9pNo=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 h1:JYYze3qnJZxo4PWTSZGKH+NJs0Vi2DYFo/NzUpa8Em4=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1/go.mod h1:5QgvXU4BhJqHgkO27sFJrRQHABWN4X6QnGvL8hWCJKk=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 h1:fXh8CsdNpjRr8R5vFdqtIxPt/Lno2IIJlYOdZBIZn0w=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1 h1:c4w6fQ6wAhZLgVI+HZozlWX9I2aVPN34vrl/8cuovVc=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1/go.mod h1:mbcuFKpnCQsF8H2fBs1lN1dQVAiASPQKwFgTkXqYscI=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1 h1:qJUlFKLFdb3OFCpmA+r5AQbysgBD7WLRapE9lOs08r8=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q=
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts=
buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda h1:eysSyjrJtkxU1A/9+Kv+1Mwq9K6BYBw+STIOVsZ256Y=
buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ=
buf.build/go/app v0.2.1-0.20260626143626-be153867abea h1:krHaIyUJSTnurb0fbYQtsXkZWiWQ/ydOgfm61TPSODc=
buf.build/go/app v0.2.1-0.20260626143626-be153867abea/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ=
buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU=
buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I=
buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=
@ -32,10 +32,10 @@ buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2b
buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY=
codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ=
charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
@ -46,26 +46,26 @@ connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA
connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=
dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=
dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=
github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ=
github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4=
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=
github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=
github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=
github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=
github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ClickHouse/clickhouse-go-linter v1.2.0 h1:zbm174up3hTKjp0wKZVnTzRiG7tSF5XZF0FJG/MuCBI=
github.com/ClickHouse/clickhouse-go-linter v1.2.0/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@ -90,8 +90,8 @@ github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEs
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
@ -132,16 +132,16 @@ github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDw
github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
github.com/bufbuild/buf v1.71.0 h1:/XLs8mFKjlM/LVbBBBQ3uSNcyEClyAV8lJiO7QR04d8=
github.com/bufbuild/buf v1.71.0/go.mod h1:fbHMfu0nEbIPQT3IG7rAfPZXoFYEfZJuqFbAXu1wnRU=
github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 h1:N0ZOKMwHpKsFhMGSxnB6JiVkM7SbH8IAKlGGO0SpQBA=
github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
github.com/bufbuild/buf v1.72.0 h1:VMmGFtCLrxyS2wkpghExmhhiqJDdmc8DcwAvsGJGJ94=
github.com/bufbuild/buf v1.72.0/go.mod h1:bhtIlPDo3q/PDw4yaTdx2+jxkc33Aq+ygFIi6Diz2yU=
github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672 h1:6xykiXQPoF/hhOjAhwiHQ0kgcAvrDZw0Tjl1VQkpu5c=
github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
@ -152,8 +152,8 @@ github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1Di
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac h1:BP8qMDGjmOejoVTklEXXTHI0OwMIt8JHPSPHxscFFwA=
github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac/go.mod h1:psnCZIfwwxVs6v6DhUc6NJ8AQ3ejvs2ejKwoOMeVmUk=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@ -179,27 +179,30 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
github.com/daixiang0/gci v0.14.0 h1:h6AcLqmjIOBgojhtzY2CvBnA6RawPTkBHgtMvYD5YZ8=
github.com/daixiang0/gci v0.14.0/go.mod h1:w9E+SWQ4aPQ+xYPUdqitGDoXpT4mayqOfk7Szz2U6wQ=
github.com/dave/dst v0.27.4 h1:d+EVnOZmphH+lUEXq9rit4GjsFSKJ3AhfRWf7eobTps=
github.com/dave/dst v0.27.4/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denis-tingaikin/go-header v1.0.0 h1:QIwZWb3jLC6pOp9NEFldiD8raqRmCE/n0VUdZKW32x8=
github.com/denis-tingaikin/go-header v1.0.0/go.mod h1:NT3qKwqsXQYp8WHVgkwxL49qB5jsRmdr9dGQCDfpmZ0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8=
github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/dlclark/regexp2/v2 v2.5.2 h1:HAsucWRhsqcDzl6Ua9aR8JwYOTzrZyPrF0/FNxJVAI0=
github.com/dlclark/regexp2/v2 v2.5.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw=
github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0=
github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
@ -212,23 +215,23 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
github.com/ghostiam/protogetter v0.3.21 h1:EeWTGvL/Eyosp653hiWb6Byx4b69iJC4/E+za7vQHoI=
github.com/ghostiam/protogetter v0.3.21/go.mod h1:iAKSpyoHwYzay+OpjoWgwzRtPFthEfuUvmlomTThck0=
github.com/go-critic/go-critic v0.14.4 h1:dSX4C3pWSeuMVxvQh6yG8U0ReSf3YOmKi4nwX5q7n/8=
github.com/go-critic/go-critic v0.14.4/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
@ -284,8 +287,8 @@ github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3H
github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg=
github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk=
github.com/golangci/rowserrcheck v0.0.0-20260701145345-399602f983c8 h1:OtbxoLqMgJUWEHXhlNkvTlxJMQNSddWMZP/e6FaNkBI=
github.com/golangci/rowserrcheck v0.0.0-20260701145345-399602f983c8/go.mod h1:RLgEC6y0EaFAJT8kkHrRxwsGFwYamOB6BYzUZOpfTVs=
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
@ -299,8 +302,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU=
github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0 h1:du0WGc8xSKq/++e0cglxhS/mXVqsR7+c7jLEi5Vqduw=
github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
@ -326,8 +329,6 @@ github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaX
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -336,8 +337,8 @@ github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp
github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
github.com/jgautheron/goconst v1.10.0 h1:Ptt+OoE4NaEWKhLrWrrN3IpZdGLiqaf7WLnEX/iv4Jw=
github.com/jgautheron/goconst v1.10.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
github.com/jgautheron/goconst v1.10.2 h1:0wFg/DbpYB0ChoP94qUM+SuQpqj5oCzPEqAil5dV8Pg=
github.com/jgautheron/goconst v1.10.2/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
@ -346,12 +347,12 @@ github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=
github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
@ -380,8 +381,8 @@ github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=
github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=
github.com/ldez/gomoddirectives v0.9.0 h1:2YV/EX7nVlWL4jySusYTzBKHuE3D2fgcRsQuMa3yIoo=
github.com/ldez/gomoddirectives v0.9.0/go.mod h1:DdZzfm9MdXCjn2/UGYXCFfo+tzrp2Ib4iD2Q0kIJkwE=
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
@ -396,8 +397,6 @@ github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
@ -412,18 +411,16 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=
github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@ -444,12 +441,12 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=
github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps=
github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/nunnatsa/ginkgolinter v0.23.1 h1:1JOE+iDubBmH2YvP485w9AHBI8osHFBdVeMucRRvkDM=
github.com/nunnatsa/ginkgolinter v0.23.1/go.mod h1:jh1sHyjC5TzL24rFUJvxtXV0iD2/9S7C/8N4aQd/lag=
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@ -461,20 +458,19 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 h1:YCvN6o1QTbVkeHMlTjnzfP02SvsmOZLlJdFzw/f9BTk=
github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20260725062400-500c67a39b75 h1:VmZ6mKVkxavKEhEy4ZYyV7BwBYBFBP0TwIqmLk84fpU=
github.com/petermattis/goid v0.0.0-20260725062400-500c67a39b75/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk=
github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=
@ -493,18 +489,18 @@ github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=
github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=
github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA=
github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs=
github.com/raeperd/recvcheck v0.3.0 h1:PM+XYvyxIj3bo+kobJfFTdTuU3Lmfu96mKDbyHDbRt8=
github.com/raeperd/recvcheck v0.3.0/go.mod h1:PZNwG+HztFYMH2ZPq0Hu3QgkV2yiA6VrtNz9c1fXWJo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@ -515,6 +511,8 @@ github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/L
github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
@ -523,8 +521,8 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.26.1 h1:gdkttGhQFVehqRJ8grKH4DrpqM/QlPKNHBnl8QgcEC4=
github.com/securego/gosec/v2 v2.26.1/go.mod h1:57UW4p0uoP3kxoTkhoo3axLdVAi+OWrLg/Ax/kdqtPE=
github.com/securego/gosec/v2 v2.28.0 h1:ZsSdiDb0AtTpLFVol5z91gbMei9ZiLEPG/pZjZujp7c=
github.com/securego/gosec/v2 v2.28.0/go.mod h1:lb4/9AHe+lJy/kjWmWRWWsEipvbwGKuxf+tY1Pmjdnk=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -540,32 +538,29 @@ github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQyt
github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
@ -576,8 +571,8 @@ github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZ
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 h1:SiHe5XLTn9sFWJ5pBwJ5FN/4j34q9ZlOAD//kMoMYp0=
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
@ -590,8 +585,8 @@ github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSW
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
github.com/uudashr/iface v1.4.2 h1:06Vq5RKVYThBsj0Bnw4oasMjD1r+7CE/bcKOA8dVSvg=
github.com/uudashr/iface v1.4.2/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/uudashr/iface v1.5.0 h1:PgdMt4uAettGG8K/Kbamc4B9FABgUgnS3TLbl6fnjEk=
github.com/uudashr/iface v1.5.0/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@ -621,8 +616,8 @@ go.akshayshah.org/connectproto v0.6.0 h1:tqmysQF2AfvUeYS03mRAAZTFpiQeXqhGIDnH1GO
go.akshayshah.org/connectproto v0.6.0/go.mod h1:uA9TR/6MhBlLn0fh8VXRyL26EKTJlimWao4jbz7JHbA=
go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=
go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
go.augendre.info/fatcontext v0.10.0 h1:HhFopmivh8U1+AU7f0kuwUeg2eiIns7YsGQOMHwSJ90=
go.augendre.info/fatcontext v0.10.0/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
go.lsp.dev/jsonrpc2 v1.0.1 h1:JZAchpm6RcOuwEOnZ7kF3kA1EpEjdJZ/6rMSfOF3+F4=
go.lsp.dev/jsonrpc2 v1.0.1/go.mod h1:WN+TesqrPumBmUdsuWyn9DHq3zl4i85L/qG0NLdj8qg=
go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU=
@ -660,22 +655,22 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 h1:GTD/WuaexTazIG/SxLOz4rEKZPDVilmVVC2nz4xhwfE=
golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
golang.org/x/exp/typeparams v0.0.0-20260718201538-764159d718ef h1:HbgfnigKyfbszJsaFQE4IMPTcjdKZMNxyYlF00yVkf4=
golang.org/x/exp/typeparams v0.0.0-20260718201538-764159d718ef/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -686,8 +681,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -721,8 +716,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -731,8 +726,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -744,8 +739,8 @@ golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0t
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@ -754,20 +749,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4=
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df h1:NsJx+hCSwIBI6+C4BuJIkb8xOG1M+nfQDsqIrQHT92k=
google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=

View File

@ -1200,6 +1200,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api
res := &apiv1.GetDiagnosticsResponse{
SshFoundKey: installationinfo.Runtime.SshFoundKey,
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
ConfigIssues: api.buildConfigIssuesForUser(user),
}
return connect.NewResponse(res), nil
}
@ -1242,6 +1243,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
LoginRequired: loginRequired,
AvailableThemes: discoverAvailableThemes(api.cfg),
ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons,
ConfigIssueCount: configIssueCountForUser(api, user),
}
return connect.NewResponse(res), nil

View File

@ -0,0 +1,197 @@
package api
import (
"context"
"testing"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configissues"
)
func TestGetDiagnosticsReturnsConfigIssues(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "Deploy",
ID: "deploy",
Entity: "host",
Arguments: []config.ActionArgument{
{
Name: "ansible_host",
Type: "ascii_identifier",
Default: `{{ host.name | default "All" }}`,
},
},
ExecOnCron: []string{"0 1 * * *"},
},
}
testServer, client := getNewTestServerAndClient(cfg)
defer testServer.Close()
res, err := client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{}))
require.NoError(t, err)
require.NotNil(t, res.Msg)
require.NotEmpty(t, res.Msg.ConfigIssues)
codes := map[string]bool{}
for _, issue := range res.Msg.ConfigIssues {
codes[issue.Code] = true
}
assert.True(t, codes[configissues.CodeTemplateParse])
assert.True(t, codes[configissues.CodeCronEntityBinding])
initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
require.NoError(t, err)
assert.Equal(t, int32(len(res.Msg.ConfigIssues)), initRes.Msg.ConfigIssueCount)
}
func TestInitHidesConfigIssueCountWithoutDiagnostics(t *testing.T) {
cfg := config.DefaultConfig()
cfg.DefaultPolicy.ShowDiagnostics = false
cfg.Actions = []*config.Action{
{
Title: "Deploy",
ID: "deploy",
Entity: "host",
Arguments: []config.ActionArgument{
{
Name: "ansible_host",
Type: "ascii_identifier",
Default: `{{ host.name | default "All" }}`,
},
},
},
}
testServer, client := getNewTestServerAndClient(cfg)
defer testServer.Close()
initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
require.NoError(t, err)
assert.Equal(t, int32(0), initRes.Msg.ConfigIssueCount)
_, err = client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{}))
require.Error(t, err)
assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
}
func attachBrokenTemplateArgToSecretAction(cfg *config.Config) {
for i := range cfg.Actions {
if cfg.Actions[i].ID != "secret_action" {
continue
}
cfg.Actions[i].Arguments = []config.ActionArgument{
{
Name: "target",
Type: "ascii_identifier",
Default: `{{ host.name | default "All" }}`,
},
}
return
}
}
func assertNoActionScopedConfigIssues(t *testing.T, issues []*apiv1.ConfigIssue) {
t.Helper()
for _, issue := range issues {
assert.Empty(t, issue.ActionId,
"user with view:false must not see action-scoped config issues")
}
}
func configIssuesContainActionID(issues []*apiv1.ConfigIssue, actionID string) bool {
for _, issue := range issues {
if issue.ActionId == actionID {
return true
}
}
return false
}
func TestConfigIssuesHideActionsWithoutViewPermission(t *testing.T) {
cfg, _, _ := buildViewPermissionTestConfig(t)
cfg.AuthHttpHeaderUsername = "X-Ot-User"
attachBrokenTemplateArgToSecretAction(cfg)
testServer, client := getNewTestServerAndClient(cfg)
defer testServer.Close()
configissues.Report(configissues.Issue{
Severity: configissues.SeverityWarning,
Code: configissues.CodeEnvUnset,
Message: "TEST_GLOBAL_ENV is not set",
Source: "TEST_GLOBAL_ENV",
})
lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
lowReq.Header().Set("X-Ot-User", "low")
lowRes, err := client.GetDiagnostics(context.Background(), lowReq)
require.NoError(t, err)
assertNoActionScopedConfigIssues(t, lowRes.Msg.ConfigIssues)
require.NotEmpty(t, lowRes.Msg.ConfigIssues, "low user should still see global config issues")
lowInit := connect.NewRequest(&apiv1.InitRequest{})
lowInit.Header().Set("X-Ot-User", "low")
lowInitRes, err := client.Init(context.Background(), lowInit)
require.NoError(t, err)
assert.Equal(t, int32(len(lowRes.Msg.ConfigIssues)), lowInitRes.Msg.ConfigIssueCount)
adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
adminReq.Header().Set("X-Ot-User", "admin")
adminRes, err := client.GetDiagnostics(context.Background(), adminReq)
require.NoError(t, err)
assert.True(t, configIssuesContainActionID(adminRes.Msg.ConfigIssues, "secret_action"),
"admin must still see config issues for secret_action")
assert.Greater(t, len(adminRes.Msg.ConfigIssues), len(lowRes.Msg.ConfigIssues))
}
func TestConfigIssuesHideRuntimeWatcherFailuresWithoutViewPermission(t *testing.T) {
cfg, _, _ := buildViewPermissionTestConfig(t)
cfg.AuthHttpHeaderUsername = "X-Ot-User"
testServer, client := getNewTestServerAndClient(cfg)
defer testServer.Close()
configissues.Report(configissues.Issue{
Severity: configissues.SeverityError,
Code: configissues.CodeWatcherPath,
Message: `Could not create watcher for "/secret/path": permission denied`,
ActionID: "secret_action",
ActionTitle: "Secret Action",
Source: "/secret/path",
ConfigFile: "/etc/OliveTin/secret.yaml",
})
lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
lowReq.Header().Set("X-Ot-User", "low")
lowRes, err := client.GetDiagnostics(context.Background(), lowReq)
require.NoError(t, err)
for _, issue := range lowRes.Msg.ConfigIssues {
assert.NotEqual(t, configissues.CodeWatcherPath, issue.Code,
"user with view:false must not see action-scoped watcher_path issues")
assert.NotContains(t, issue.Source, "/secret/path")
}
adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
adminReq.Header().Set("X-Ot-User", "admin")
adminRes, err := client.GetDiagnostics(context.Background(), adminReq)
require.NoError(t, err)
foundWatcher := false
for _, issue := range adminRes.Msg.ConfigIssues {
if issue.Code == configissues.CodeWatcherPath && issue.ActionId == "secret_action" {
foundWatcher = true
break
}
}
assert.True(t, foundWatcher, "admin must see action-scoped watcher_path issues")
}

View File

@ -0,0 +1,75 @@
package api
import (
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configissues"
)
func configIssueCountForUser(api *oliveTinAPI, user *authpublic.AuthenticatedUser) int32 {
return int32(len(api.buildConfigIssuesForUser(user)))
}
func (api *oliveTinAPI) buildConfigIssuesForUser(user *authpublic.AuthenticatedUser) []*apiv1.ConfigIssue {
if !userCanSeeDiagnostics(user) {
return []*apiv1.ConfigIssue{}
}
return filterConfigIssuesForUser(api, user, configissues.List())
}
func userCanSeeDiagnostics(user *authpublic.AuthenticatedUser) bool {
return user != nil && user.EffectivePolicy != nil && user.EffectivePolicy.ShowDiagnostics
}
func filterConfigIssuesForUser(api *oliveTinAPI, user *authpublic.AuthenticatedUser, src []configissues.Issue) []*apiv1.ConfigIssue {
out := make([]*apiv1.ConfigIssue, 0, len(src))
for _, issue := range src {
if api.includeConfigIssueForUser(user, issue) {
out = append(out, toProtoConfigIssue(issue))
}
}
return out
}
func (api *oliveTinAPI) includeConfigIssueForUser(user *authpublic.AuthenticatedUser, issue configissues.Issue) bool {
if issue.ActionID == "" {
return true
}
return api.canViewActionID(user, issue.ActionID)
}
func (api *oliveTinAPI) canViewActionID(user *authpublic.AuthenticatedUser, actionID string) bool {
action := findActionInList(api.cfg.Actions, actionID)
if action == nil {
return false
}
return api.userCanViewAction(user, action)
}
func findActionInList(actions []*config.Action, actionID string) *config.Action {
for _, action := range actions {
if actionHasID(action, actionID) {
return action
}
}
return nil
}
func actionHasID(action *config.Action, actionID string) bool {
return action != nil && action.ID == actionID
}
func toProtoConfigIssue(issue configissues.Issue) *apiv1.ConfigIssue {
return &apiv1.ConfigIssue{
Severity: issue.Severity,
Code: issue.Code,
Message: issue.Message,
ActionId: issue.ActionID,
ActionTitle: issue.ActionTitle,
ArgumentName: issue.ArgumentName,
Source: issue.Source,
ConfigFile: issue.ConfigFile,
}
}

View File

@ -39,6 +39,8 @@ type Action struct {
EnabledExpression string `koanf:"enabledExpression"`
Groups []string `koanf:"groups"`
Justification string `koanf:"justification"`
// SourceFile is set by OliveTin when loading config (not user YAML).
SourceFile string `koanf:"-"`
}
func (action *Action) RequiresJustification() bool {
@ -110,6 +112,8 @@ type EntityFile struct {
Name string `koanf:"name"`
Icon string `koanf:"icon"`
Properties []EntityProperty `koanf:"properties"`
// SourceFile is set by OliveTin when loading config (not user YAML).
SourceFile string `koanf:"-"`
}
// EntityProperty defines a column shown when listing entity instances in the UI.

View File

@ -1,6 +1,7 @@
package config
import (
"fmt"
"os"
"path/filepath"
"reflect"
@ -8,6 +9,7 @@ import (
"sort"
"strings"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/go-viper/mapstructure/v2"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
@ -40,12 +42,15 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
"configPath": configPath,
}).Info("Appending cfg source")
configissues.BeginConfigLoad()
stampLoadedConfigSources(k, configPath)
loadIncludedConfigsFromDir(k, configPath)
if !unmarshalRoot(k, cfg) {
return
}
applyStampedSourceFiles(k, cfg)
afterLoadFinalize(cfg, configPath)
}
@ -172,7 +177,7 @@ func listYamlFiles(includePath string) ([]string, bool) {
func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
filePath := filepath.Join(includePath, filename)
if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFuncForSource(filePath))); err != nil {
log.Errorf("Error loading included config file %s: %v", filePath, err)
return
}
@ -182,8 +187,15 @@ func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
}).Info("Successfully loaded included config file")
}
func mergeFuncForSource(sourceFile string) func(src, dest map[string]interface{}) error {
return func(src map[string]interface{}, dest map[string]interface{}) error {
return mergeFunc(src, dest, sourceFile)
}
}
// mergeActionsWhenBothExist merges actions when both src and dest have actions.
func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}) {
func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}, sourceFile string) {
stampSourceOnMaps(srcActions, sourceFile)
srcSlice, ok1 := srcActions.([]interface{})
destSlice, ok2 := destActions.([]interface{})
if ok1 && ok2 {
@ -194,10 +206,11 @@ func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{},
}
// mergeActionsFromSource merges actions from source into destination.
func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}) {
func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}, sourceFile string) {
if destActions, ok := dest["actions"]; ok {
mergeActionsWhenBothExist(srcActions, destActions, dest)
mergeActionsWhenBothExist(srcActions, destActions, dest, sourceFile)
} else {
stampSourceOnMaps(srcActions, sourceFile)
dest["actions"] = srcActions
}
}
@ -223,7 +236,8 @@ func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interf
}
// mergeEntitiesWhenBothExist merges entities when both src and dest have entities.
func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}) {
func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}, sourceFile string) {
stampSourceOnMaps(srcEntities, sourceFile)
srcSlice, ok1 := srcEntities.([]interface{})
destSlice, ok2 := destEntities.([]interface{})
if ok1 && ok2 {
@ -234,17 +248,18 @@ func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{
}
// mergeEntitiesFromSource merges entities from source into destination.
func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}) {
func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}, sourceFile string) {
if destEntities, ok := dest["entities"]; ok {
mergeEntitiesWhenBothExist(srcEntities, destEntities, dest)
mergeEntitiesWhenBothExist(srcEntities, destEntities, dest, sourceFile)
} else {
stampSourceOnMaps(srcEntities, sourceFile)
dest["entities"] = srcEntities
}
}
func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
func mergeFunc(src map[string]interface{}, dest map[string]interface{}, sourceFile string) error {
if srcActions, ok := src["actions"]; ok {
mergeActionsFromSource(srcActions, dest)
mergeActionsFromSource(srcActions, dest, sourceFile)
}
if srcDashboards, ok := src["dashboards"]; ok {
@ -252,7 +267,7 @@ func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
}
if srcEntities, ok := src["entities"]; ok {
mergeEntitiesFromSource(srcEntities, dest)
mergeEntitiesFromSource(srcEntities, dest, sourceFile)
}
return nil
@ -285,7 +300,12 @@ func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error
val, set := os.LookupEnv(key)
log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val)
if !set {
log.Warnf("Config file references unset environment variable: \"%s\"", key)
configissues.ReportSticky(configissues.Issue{
Severity: configissues.SeverityWarning,
Code: configissues.CodeEnvUnset,
Message: fmt.Sprintf("Config file references unset environment variable %q", key),
Source: key,
})
}
return val
})

View File

@ -5,6 +5,7 @@ import (
"strings"
"text/template"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/OliveTin/OliveTin/internal/env"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
@ -29,7 +30,6 @@ func (cfg *Config) Sanitize() {
cfg.sanitizeDashboardsForInlineActions()
cfg.sanitizeActionGroups()
cfg.sanitizeActionGroupReferences()
cfg.sanitizeEntities()
if err := cfg.validateReservedActionArgumentNames(); err != nil {
@ -238,7 +238,7 @@ func (action *Action) sanitize(cfg *Config) {
action.Groups = dedupeStrings(action.Groups)
for idx := range action.Arguments {
action.Arguments[idx].sanitize()
action.Arguments[idx].sanitize(action)
}
}
@ -283,14 +283,6 @@ func (cfg *Config) sanitizeActionGroups() {
}
}
func (cfg *Config) sanitizeActionGroupReferences() {
for _, action := range cfg.Actions {
for _, groupName := range action.Groups {
cfg.warnInvalidActionGroupReference(action, groupName)
}
}
}
func (cfg *Config) sanitizeEntities() {
for _, entityFile := range cfg.Entities {
if entityFile == nil {
@ -310,24 +302,6 @@ func sanitizeEntityProperties(entityFile *EntityFile) {
}
}
func (cfg *Config) warnInvalidActionGroupReference(action *Action, groupName string) {
group, found := cfg.ActionGroups[groupName]
if !found {
log.WithFields(log.Fields{
"actionTitle": action.Title,
"groupName": groupName,
}).Warn("Action references unknown action group")
return
}
if group == nil || group.MaxConcurrent < 1 {
log.WithFields(log.Fields{
"actionTitle": action.Title,
"groupName": groupName,
}).Warn("Action references action group that will not be enforced at runtime")
}
}
func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
if cfg.AuthRequireGuestsToLogin {
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
@ -505,7 +479,7 @@ func (cfg *Config) sanitizeOnClickDefaults() {
cfg.DefaultPopupOnStart = cfg.DefaultOnClick
}
func (arg *ActionArgument) sanitize() {
func (arg *ActionArgument) sanitize(action *Action) {
if arg.Title == "" {
arg.Title = arg.Name
}
@ -516,45 +490,24 @@ func (arg *ActionArgument) sanitize() {
}
}
arg.sanitizeNoType()
arg.sanitizeChecklist()
// Default value validation runs in executor at config load (validateArgumentDefaults).
arg.sanitizeNoType(action)
// Checklist configuration problems are reported by configcheck.
}
func (arg *ActionArgument) sanitizeChecklist() {
if arg.Type != "checklist" {
return
}
arg.warnMissingChecklistChoices()
arg.warnInvalidChecklistEntityTemplate()
}
func (arg *ActionArgument) warnMissingChecklistChoices() {
if len(arg.Choices) == 0 {
log.WithFields(log.Fields{
"arg": arg.Name,
}).Warn("Checklist argument has no choices defined")
}
}
func (arg *ActionArgument) warnInvalidChecklistEntityTemplate() {
if arg.Entity == "" || len(arg.Choices) == 1 {
return
}
log.WithFields(log.Fields{
"arg": arg.Name,
"entity": arg.Entity,
}).Warn("Checklist argument with entity should define exactly one choice template")
}
func (arg *ActionArgument) sanitizeNoType() {
func (arg *ActionArgument) sanitizeNoType(action *Action) {
if len(arg.Choices) == 0 && arg.Type == "" {
log.WithFields(log.Fields{
"arg": arg.Name,
}).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.")
issue := configissues.Issue{
Severity: configissues.SeverityWarning,
Code: configissues.CodeArgTypeUnset,
Message: "Argument type isn't set, will default to 'ascii' but this may not be safe",
ArgumentName: arg.Name,
}
if action != nil {
issue.ActionID = action.ID
issue.ActionTitle = action.Title
issue.ConfigFile = action.SourceFile
}
configissues.ReportSticky(issue)
arg.Type = "ascii"
}
}

View File

@ -0,0 +1,96 @@
package config
import (
"github.com/knadh/koanf/v2"
log "github.com/sirupsen/logrus"
)
const sourceFileKey = "x-olivetin-source-file"
// stampSourceOnMaps sets the OliveTin source-file marker on each map in a
// koanf slice value (actions or entities). Always overwrites so user-provided
// x-olivetin-source-file values cannot spoof the real config path.
func stampSourceOnMaps(raw any, sourceFile string) any {
if sourceFile == "" {
return raw
}
items, ok := raw.([]interface{})
if !ok {
return raw
}
for _, item := range items {
stampSourceOnMap(item, sourceFile)
}
return items
}
func stampSourceOnMap(item any, sourceFile string) {
m, ok := item.(map[string]interface{})
if !ok {
return
}
m[sourceFileKey] = sourceFile
}
func stampLoadedConfigSources(k *koanf.Koanf, configPath string) {
stampConfigKey(k, "actions", configPath)
stampConfigKey(k, "entities", configPath)
}
func stampConfigKey(k *koanf.Koanf, key, configPath string) {
if err := k.Set(key, stampSourceOnMaps(k.Get(key), configPath)); err != nil {
log.WithFields(log.Fields{
"key": key,
"configPath": configPath,
}).Errorf("Failed to persist source stamps: %v", err)
}
}
// applyStampedSourceFiles copies stamped source paths from koanf maps onto
// unmarshaled actions/entities. SourceFile uses koanf:"-" so YAML cannot set it.
func applyStampedSourceFiles(k *koanf.Koanf, cfg *Config) {
actionPaths := stampedSourcePaths(k.Get("actions"))
for i, action := range cfg.Actions {
if action != nil {
action.SourceFile = sourcePathAt(actionPaths, i)
}
}
entityPaths := stampedSourcePaths(k.Get("entities"))
for i, entity := range cfg.Entities {
if entity != nil {
entity.SourceFile = sourcePathAt(entityPaths, i)
}
}
}
func sourcePathAt(paths []string, index int) string {
if index >= len(paths) {
return ""
}
return paths[index]
}
func stampedSourcePaths(raw any) []string {
items, ok := raw.([]interface{})
if !ok {
return nil
}
paths := make([]string, len(items))
for i, item := range items {
paths[i] = stampedSourceFromMap(item)
}
return paths
}
func stampedSourceFromMap(item any) string {
m, ok := item.(map[string]interface{})
if !ok {
return ""
}
path, _ := m[sourceFileKey].(string)
return path
}

View File

@ -0,0 +1,80 @@
package config_test
import (
"os"
"path/filepath"
"testing"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAppendSourceStampsActionSourceFiles(t *testing.T) {
dir := t.TempDir()
includeDir := filepath.Join(dir, "config.d")
require.NoError(t, os.Mkdir(includeDir, 0o755))
basePath := filepath.Join(dir, "config.yaml")
require.NoError(t, os.WriteFile(basePath, []byte(`
include: config.d
actions:
- title: From base
shell: echo base
`), 0o644))
includePath := filepath.Join(includeDir, "10-extra.yaml")
require.NoError(t, os.WriteFile(includePath, []byte(`
actions:
- title: From include
shell: echo include
entities:
- name: host
file: hosts.yaml
`), 0o644))
k := koanf.New(".")
require.NoError(t, k.Load(file.Provider(basePath), yaml.Parser()))
cfg := config.DefaultConfig()
config.AppendSource(cfg, k, basePath)
require.Len(t, cfg.Actions, 2)
assert.Equal(t, basePath, cfg.Actions[0].SourceFile)
assert.Equal(t, includePath, cfg.Actions[1].SourceFile)
require.Len(t, cfg.Entities, 1)
assert.Equal(t, includePath, cfg.Entities[0].SourceFile)
}
func TestAppendSourceIgnoresUserProvidedSourceFile(t *testing.T) {
dir := t.TempDir()
basePath := filepath.Join(dir, "config.yaml")
require.NoError(t, os.WriteFile(basePath, []byte(`
actions:
- title: Spoofed
shell: echo hi
x-olivetin-source-file: /tmp/fake-user-path.yaml
entities:
- name: host
file: hosts.yaml
x-olivetin-source-file: /tmp/fake-entity-path.yaml
`), 0o644))
k := koanf.New(".")
require.NoError(t, k.Load(file.Provider(basePath), yaml.Parser()))
cfg := config.DefaultConfig()
config.AppendSource(cfg, k, basePath)
require.Len(t, cfg.Actions, 1)
assert.Equal(t, basePath, cfg.Actions[0].SourceFile)
assert.NotEqual(t, "/tmp/fake-user-path.yaml", cfg.Actions[0].SourceFile)
require.Len(t, cfg.Entities, 1)
assert.Equal(t, basePath, cfg.Entities[0].SourceFile)
assert.NotEqual(t, "/tmp/fake-entity-path.yaml", cfg.Entities[0].SourceFile)
}

View File

@ -0,0 +1,486 @@
package configcheck
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/OliveTin/OliveTin/internal/entities"
"github.com/OliveTin/OliveTin/internal/tpl"
"github.com/robfig/cron/v3"
"gopkg.in/yaml.v3"
)
// Rebuild clears and rebuilds the issue list from the current config, entity
// state, sticky load-time reports, and any extra issues provided by callers
// (for example literal argument default validation from the executor).
func Rebuild(cfg *config.Config, extra ...configissues.Issue) {
if cfg == nil {
configissues.Replace(extra)
return
}
collected := make([]configissues.Issue, 0)
collected = append(collected, configissues.CopySticky()...)
collected = append(collected, collectActionGroupIssues(cfg)...)
collected = append(collected, collectArgumentIssues(cfg)...)
collected = append(collected, collectIncludeIssues(cfg)...)
collected = append(collected, collectTemplateParseIssues(cfg)...)
collected = append(collected, collectEntityFileIssues(cfg)...)
collected = append(collected, collectEntityEmptyIssues(cfg)...)
collected = append(collected, collectCronIssues(cfg)...)
collected = append(collected, collectWatcherPathIssues(cfg)...)
collected = append(collected, extra...)
configissues.Replace(collected)
}
func collectActionGroupIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, action := range cfg.Actions {
if action == nil {
continue
}
for _, groupName := range action.Groups {
out = append(out, actionGroupIssue(cfg, action, groupName)...)
}
}
return out
}
func actionGroupIssue(cfg *config.Config, action *config.Action, groupName string) []configissues.Issue {
group, found := cfg.ActionGroups[groupName]
if !found {
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeActionGroupUnknown,
fmt.Sprintf("Action references unknown action group %q", groupName), groupName, "")}
}
if group == nil || group.MaxConcurrent < 1 {
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeActionGroupUnenforced,
fmt.Sprintf("Action references action group %q that will not be enforced at runtime", groupName), groupName, "")}
}
return nil
}
func actionIssue(action *config.Action, severity, code, message, source, argName string) configissues.Issue {
return configissues.Issue{
Severity: severity,
Code: code,
Message: message,
ActionID: action.ID,
ActionTitle: action.Title,
ArgumentName: argName,
Source: source,
ConfigFile: action.SourceFile,
}
}
func collectArgumentIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, action := range cfg.Actions {
if action == nil {
continue
}
for i := range action.Arguments {
out = append(out, argumentIssues(action, &action.Arguments[i])...)
}
}
return out
}
func argumentIssues(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
if arg.Type != "checklist" {
return nil
}
out := make([]configissues.Issue, 0)
out = append(out, checklistNoChoicesIssue(action, arg)...)
out = append(out, checklistEntityChoicesIssue(action, arg)...)
return out
}
func checklistNoChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
if len(arg.Choices) > 0 {
return nil
}
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeChecklistNoChoices,
"Checklist argument has no choices defined", "", arg.Name)}
}
func checklistEntityChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
if arg.Entity == "" || len(arg.Choices) <= 1 {
return nil
}
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeChecklistEntityChoices,
"Checklist argument with entity should define exactly one choice template", arg.Entity, arg.Name)}
}
func collectIncludeIssues(cfg *config.Config) []configissues.Issue {
if cfg.Include == "" {
return nil
}
includePath := cfg.Include
if !filepath.IsAbs(includePath) {
includePath = filepath.Join(cfg.GetDir(), cfg.Include)
}
info, err := os.Stat(includePath)
if err != nil {
return []configissues.Issue{{
Severity: configissues.SeverityWarning,
Code: configissues.CodeIncludeMissing,
Message: fmt.Sprintf("Include directory not found: %s", includePath),
Source: includePath,
ConfigFile: includePath,
}}
}
if !info.IsDir() {
return []configissues.Issue{{
Severity: configissues.SeverityWarning,
Code: configissues.CodeIncludeMissing,
Message: fmt.Sprintf("Include path is not a directory: %s", includePath),
Source: includePath,
ConfigFile: includePath,
}}
}
return nil
}
func collectTemplateParseIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, action := range cfg.Actions {
if action == nil {
continue
}
for i := range action.Arguments {
arg := &action.Arguments[i]
out = append(out, templateIssuesForArg(action, arg)...)
}
}
return out
}
func templateIssuesForArg(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
out := make([]configissues.Issue, 0)
if looksLikeTemplate(arg.Default) {
if err := tpl.CheckActionTemplateParses(arg.Default); err != nil {
out = append(out, actionIssue(action, configissues.SeverityError, configissues.CodeTemplateParse,
fmt.Sprintf("Argument default template failed to parse: %v", err), arg.Default, arg.Name))
}
}
for _, choice := range arg.Choices {
out = append(out, templateIssueForChoice(action, arg, choice.Title, "title")...)
out = append(out, templateIssueForChoice(action, arg, choice.Value, "value")...)
}
return out
}
func templateIssueForChoice(action *config.Action, arg *config.ActionArgument, value, field string) []configissues.Issue {
if !looksLikeTemplate(value) {
return nil
}
if err := tpl.CheckActionTemplateParses(value); err != nil {
return []configissues.Issue{actionIssue(action, configissues.SeverityError, configissues.CodeTemplateParse,
fmt.Sprintf("Argument choice %s template failed to parse: %v", field, err), value, arg.Name)}
}
return nil
}
func looksLikeTemplate(value string) bool {
return strings.Contains(value, "{{")
}
func collectEntityFileIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir())
for _, ef := range cfg.Entities {
out = append(out, entityFileIssuesFor(ef, baseDir)...)
}
return out
}
func entityFileIssuesFor(ef *config.EntityFile, baseDir string) []configissues.Issue {
if ef == nil || ef.File == "" {
return nil
}
path := resolveEntityPath(ef.File, baseDir)
issue := entityFileIssue(ef, path)
if issue == nil {
return nil
}
return []configissues.Issue{*issue}
}
func resolveEntityPath(file, baseDir string) string {
if filepath.IsAbs(file) {
return file
}
return filepath.Join(baseDir, file)
}
func entityFileIssue(ef *config.EntityFile, path string) *configissues.Issue {
data, err := os.ReadFile(path)
if err != nil {
return entityReadIssue(ef, path, err)
}
if len(strings.TrimSpace(string(data))) == 0 {
return &configissues.Issue{
Severity: configissues.SeverityWarning,
Code: configissues.CodeEntityFile,
Message: fmt.Sprintf("Entity %q file is empty", ef.Name),
Source: path,
ConfigFile: ef.SourceFile,
}
}
if strings.HasSuffix(path, ".json") {
return entityJSONParseIssue(ef, path, data)
}
return entityYAMLParseIssue(ef, path, data)
}
func entityReadIssue(ef *config.EntityFile, path string, err error) *configissues.Issue {
return &configissues.Issue{
Severity: configissues.SeverityError,
Code: configissues.CodeEntityFile,
Message: fmt.Sprintf("Entity %q file could not be read: %v", ef.Name, err),
Source: path,
ConfigFile: ef.SourceFile,
}
}
func entityJSONParseIssue(ef *config.EntityFile, path string, data []byte) *configissues.Issue {
decoder := json.NewDecoder(bytes.NewReader(data))
for decoder.More() {
d := make(map[string]any)
if err := decoder.Decode(&d); err != nil {
return &configissues.Issue{
Severity: configissues.SeverityError,
Code: configissues.CodeEntityFile,
Message: fmt.Sprintf("Entity %q file could not be parsed as JSON: %v", ef.Name, err),
Source: path,
ConfigFile: ef.SourceFile,
}
}
}
return nil
}
func entityYAMLParseIssue(ef *config.EntityFile, path string, data []byte) *configissues.Issue {
var parsed []map[string]any
if err := yaml.Unmarshal(data, &parsed); err != nil {
return &configissues.Issue{
Severity: configissues.SeverityError,
Code: configissues.CodeEntityFile,
Message: fmt.Sprintf("Entity %q file could not be parsed as YAML: %v", ef.Name, err),
Source: path,
ConfigFile: ef.SourceFile,
}
}
return nil
}
func collectEntityEmptyIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, action := range cfg.Actions {
out = append(out, entityEmptyIssueFor(cfg, action)...)
}
return out
}
func entityEmptyIssueFor(cfg *config.Config, action *config.Action) []configissues.Issue {
if !shouldReportEntityEmpty(cfg, action) {
return nil
}
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeEntityEmpty,
fmt.Sprintf("Entity-bound action has no instances of entity %q", action.Entity), action.Entity, "")}
}
func shouldReportEntityEmpty(cfg *config.Config, action *config.Action) bool {
if action == nil || action.Entity == "" {
return false
}
if len(entities.GetEntityInstances(action.Entity)) > 0 {
return false
}
return entityEmptyLoadGatePassed(cfg, action.Entity)
}
func entityEmptyLoadGatePassed(cfg *config.Config, entityName string) bool {
// Avoid startup false positives: entity files load after the first action-map rebuild.
if entityTypeConfigured(cfg, entityName) && !entities.HasEntityLoadAttempted(entityName) {
return false
}
return true
}
func entityTypeConfigured(cfg *config.Config, entityName string) bool {
for _, ef := range cfg.Entities {
if ef != nil && ef.Name == entityName {
return true
}
}
return false
}
func collectCronIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
parser := cronParser(cfg)
for _, action := range cfg.Actions {
out = append(out, cronIssuesForAction(action, parser)...)
}
return out
}
func cronIssuesForAction(action *config.Action, parser cron.Parser) []configissues.Issue {
if action == nil {
return nil
}
out := make([]configissues.Issue, 0)
out = append(out, cronEntityBindingIssue(action)...)
out = append(out, cronScheduleIssues(action, parser)...)
return out
}
func cronEntityBindingIssue(action *config.Action) []configissues.Issue {
if action.Entity == "" || len(action.ExecOnCron) == 0 {
return nil
}
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeCronEntityBinding,
"Entity-bound action uses execOnCron; cron runs without an entity binding and will not execute", action.Entity, "")}
}
func cronScheduleIssues(action *config.Action, parser cron.Parser) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, cronline := range action.ExecOnCron {
out = append(out, cronScheduleIssue(action, parser, cronline)...)
}
return out
}
func cronScheduleIssue(action *config.Action, parser cron.Parser, cronline string) []configissues.Issue {
_, err := parser.Parse(cronline)
if err == nil {
return nil
}
return []configissues.Issue{actionIssue(action, configissues.SeverityError, configissues.CodeCronInvalid,
fmt.Sprintf("Invalid cron schedule %q: %v", cronline, err), cronline, "")}
}
func cronParser(cfg *config.Config) cron.Parser {
if cfg.CronSupportForSeconds {
return cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
}
return cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
}
func collectWatcherPathIssues(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, action := range cfg.Actions {
out = append(out, watcherPathIssuesForAction(action)...)
}
out = append(out, watcherPathIssuesForEntities(cfg)...)
return out
}
func watcherPathIssuesForAction(action *config.Action) []configissues.Issue {
if action == nil {
return nil
}
out := make([]configissues.Issue, 0)
out = append(out, watcherCreatedDirIssues(action)...)
out = append(out, watcherChangedDirIssues(action)...)
out = append(out, watcherCalendarFileIssues(action)...)
return out
}
func watcherCreatedDirIssues(action *config.Action) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, dir := range action.ExecOnFileCreatedInDir {
out = append(out, watcherDirIssue(action, dir)...)
}
return out
}
func watcherChangedDirIssues(action *config.Action) []configissues.Issue {
out := make([]configissues.Issue, 0)
for _, dir := range action.ExecOnFileChangedInDir {
out = append(out, watcherDirIssue(action, dir)...)
}
return out
}
func watcherCalendarFileIssues(action *config.Action) []configissues.Issue {
return watcherFileIssue(action, action.ExecOnCalendarFile)
}
func watcherPathIssuesForEntities(cfg *config.Config) []configissues.Issue {
out := make([]configissues.Issue, 0)
baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir())
for _, ef := range cfg.Entities {
if ef == nil || ef.File == "" {
continue
}
path := resolveEntityPath(ef.File, baseDir)
out = append(out, missingWatchDirIssue(path, filepath.Dir(path), ef.SourceFile, "", "")...)
}
return out
}
func watcherDirIssue(action *config.Action, dir string) []configissues.Issue {
if dir == "" {
return nil
}
return missingWatchDirIssue(dir, dir, action.SourceFile, action.ID, action.Title)
}
func watcherFileIssue(action *config.Action, filePath string) []configissues.Issue {
if filePath == "" {
return nil
}
return missingWatchDirIssue(filePath, filepath.Dir(filePath), action.SourceFile, action.ID, action.Title)
}
func missingWatchDirIssue(displayPath, dirToStat, configFile, actionID, actionTitle string) []configissues.Issue {
_, err := os.Stat(dirToStat)
if err == nil {
return nil
}
return []configissues.Issue{{
Severity: configissues.SeverityError,
Code: configissues.CodeWatcherPath,
Message: fmt.Sprintf("Could not create watcher for %q: %v", displayPath, err),
ActionID: actionID,
ActionTitle: actionTitle,
Source: displayPath,
ConfigFile: configFile,
}}
}

View File

@ -0,0 +1,201 @@
package configcheck_test
import (
"os"
"path/filepath"
"testing"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configcheck"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/OliveTin/OliveTin/internal/entities"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRebuildTemplateParseDefault(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "Deploy",
ID: "deploy",
Entity: "host",
Arguments: []config.ActionArgument{
{
Name: "ansible_host",
Type: "ascii_identifier",
Default: `{{ host.name | default "All" }}`,
},
},
},
}
configcheck.Rebuild(cfg)
issues := configissues.List()
require.NotEmpty(t, issues)
assert.True(t, hasCode(issues, configissues.CodeTemplateParse))
assert.True(t, hasCode(issues, configissues.CodeEntityEmpty))
assert.False(t, hasCode(issues, configissues.CodeCronEntityBinding))
}
func TestRebuildValidTemplateDefault(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "Sleep",
ID: "sleep",
Arguments: []config.ActionArgument{
{
Name: "ssh_host",
Type: "ascii_identifier",
Default: `{{ host.hostname }}`,
},
},
},
}
configcheck.Rebuild(cfg)
assert.False(t, hasCode(configissues.List(), configissues.CodeTemplateParse))
}
func TestRebuildCronEntityBinding(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "Nightly",
ID: "nightly",
Entity: "host",
ExecOnCron: []string{"0 1 * * *"},
},
}
configcheck.Rebuild(cfg)
assert.True(t, hasCode(configissues.List(), configissues.CodeCronEntityBinding))
}
func TestRebuildInvalidCron(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "Broken cron",
ID: "broken",
ExecOnCron: []string{"not a cron line"},
},
}
configcheck.Rebuild(cfg)
assert.True(t, hasCode(configissues.List(), configissues.CodeCronInvalid))
}
func TestRebuildMissingEntityFile(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
dir := t.TempDir()
cfg.SetDir(dir)
cfg.Entities = []*config.EntityFile{
{Name: "host", File: "missing-hosts.yaml"},
}
configcheck.Rebuild(cfg)
assert.True(t, hasCode(configissues.List(), configissues.CodeEntityFile))
}
func TestRebuildEntityFileLoadsAndClearsEmpty(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
dir := t.TempDir()
cfg.SetDir(dir)
entityPath := filepath.Join(dir, "hosts.yaml")
require.NoError(t, os.WriteFile(entityPath, []byte("- name: cradle\n"), 0o644))
cfg.Entities = []*config.EntityFile{
{Name: "host", File: "hosts.yaml"},
}
cfg.Actions = []*config.Action{
{Title: "Wake", ID: "wake", Entity: "host"},
}
entities.AddEntity("host", "0", map[string]any{"name": "cradle"})
t.Cleanup(func() { entities.ClearEntitiesOfType("host") })
configcheck.Rebuild(cfg)
issues := configissues.List()
assert.False(t, hasCode(issues, configissues.CodeEntityFile))
assert.False(t, hasCode(issues, configissues.CodeEntityEmpty))
}
func TestRebuildIncludesActionConfigFile(t *testing.T) {
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "Nightly",
ID: "nightly",
Entity: "host",
SourceFile: "/etc/OliveTin/config.d/16-gitops-sr-soe.yaml",
ExecOnCron: []string{"0 1 * * *"},
},
}
configcheck.Rebuild(cfg)
var found bool
for _, issue := range configissues.List() {
if issue.Code == configissues.CodeCronEntityBinding {
assert.Equal(t, "/etc/OliveTin/config.d/16-gitops-sr-soe.yaml", issue.ConfigFile)
found = true
}
}
assert.True(t, found)
}
func TestRebuildSkipsEntityEmptyBeforeLoadAttempt(t *testing.T) {
entities.ResetEntityLoadAttempts()
configissues.BeginConfigLoad()
cfg := config.DefaultConfig()
cfg.Entities = []*config.EntityFile{{Name: "container", File: "containers.json"}}
cfg.Actions = []*config.Action{
{Title: "Start container", ID: "start", Entity: "container"},
}
configcheck.Rebuild(cfg)
assert.False(t, hasCode(configissues.List(), configissues.CodeEntityEmpty))
entities.MarkEntityLoadAttempted("container")
configcheck.Rebuild(cfg)
assert.True(t, hasCode(configissues.List(), configissues.CodeEntityEmpty))
}
func TestStickyEnvUnsetSurvivesRebuild(t *testing.T) {
configissues.BeginConfigLoad()
configissues.ReportSticky(configissues.Issue{
Severity: configissues.SeverityWarning,
Code: configissues.CodeEnvUnset,
Message: "unset",
Source: "MISSING_VAR",
})
configcheck.Rebuild(config.DefaultConfig())
assert.True(t, hasCode(configissues.List(), configissues.CodeEnvUnset))
}
func hasCode(issues []configissues.Issue, code string) bool {
for _, issue := range issues {
if issue.Code == code {
return true
}
}
return false
}

View File

@ -0,0 +1,46 @@
package configcheck_test
import (
"path/filepath"
"testing"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configcheck"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRebuildWatcherPathMissingDir(t *testing.T) {
configissues.BeginConfigLoad()
configissues.Replace(nil)
missing := filepath.Join(t.TempDir(), "does-not-exist")
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{
Title: "On create",
ID: "on_create",
SourceFile: "/etc/OliveTin/config.yaml",
ExecOnFileCreatedInDir: []string{missing},
},
}
configcheck.Rebuild(cfg)
issues := configissues.List()
require.True(t, hasCode(issues, configissues.CodeWatcherPath))
var found *configissues.Issue
for i := range issues {
if issues[i].Code == configissues.CodeWatcherPath {
found = &issues[i]
break
}
}
require.NotNil(t, found)
assert.Equal(t, missing, found.Source)
assert.Equal(t, "/etc/OliveTin/config.yaml", found.ConfigFile)
assert.Contains(t, found.Message, missing)
assert.Contains(t, found.Message, "Could not create watcher")
}

View File

@ -0,0 +1,33 @@
package configissues
const (
SeverityWarning = "warning"
SeverityError = "error"
CodeActionGroupUnknown = "action_group_unknown"
CodeActionGroupUnenforced = "action_group_unenforced"
CodeArgTypeUnset = "arg_type_unset"
CodeChecklistNoChoices = "checklist_no_choices"
CodeChecklistEntityChoices = "checklist_entity_choices"
CodeEnvUnset = "env_unset"
CodeIncludeMissing = "include_missing"
CodeTemplateParse = "template_parse"
CodeArgDefaultInvalid = "arg_default_invalid"
CodeEntityFile = "entity_file"
CodeEntityEmpty = "entity_empty"
CodeCronInvalid = "cron_invalid"
CodeCronEntityBinding = "cron_entity_binding"
CodeWatcherPath = "watcher_path"
)
// Issue is a configuration warning or error surfaced on Diagnostics.
type Issue struct {
Severity string
Code string
Message string
ActionID string
ActionTitle string
ArgumentName string
Source string
ConfigFile string
}

View File

@ -0,0 +1,125 @@
package configissues
import (
"sync"
log "github.com/sirupsen/logrus"
)
var (
storeMu sync.RWMutex
issues []Issue
stickyMu sync.Mutex
sticky []Issue
)
// BeginConfigLoad clears sticky issues captured during config decode/include load.
func BeginConfigLoad() {
stickyMu.Lock()
defer stickyMu.Unlock()
sticky = nil
}
// ReportSticky records an issue that cannot be re-derived after config decode
// (for example unset environment variables that were already expanded).
func ReportSticky(issue Issue) {
stickyMu.Lock()
defer stickyMu.Unlock()
sticky = append(sticky, issue)
}
func copySticky() []Issue {
stickyMu.Lock()
defer stickyMu.Unlock()
out := make([]Issue, len(sticky))
copy(out, sticky)
return out
}
// CopySticky returns a copy of sticky load-time issues.
func CopySticky() []Issue {
return copySticky()
}
// Replace sets the current issue list and logs only newly appeared issues.
func Replace(next []Issue) {
storeMu.Lock()
defer storeMu.Unlock()
previous := make(map[string]struct{}, len(issues))
for _, issue := range issues {
previous[issueFingerprint(issue)] = struct{}{}
}
issues = make([]Issue, len(next))
copy(issues, next)
for _, issue := range issues {
if _, exists := previous[issueFingerprint(issue)]; exists {
continue
}
logNewIssue(issue)
}
}
func issueFingerprint(issue Issue) string {
return issue.Severity + "\x00" +
issue.Code + "\x00" +
issue.ActionID + "\x00" +
issue.ActionTitle + "\x00" +
issue.ArgumentName + "\x00" +
issue.Source + "\x00" +
issue.ConfigFile + "\x00" +
issue.Message
}
func logNewIssue(issue Issue) {
fields := log.Fields{
"code": issue.Code,
"actionTitle": issue.ActionTitle,
"argumentName": issue.ArgumentName,
"configFile": issue.ConfigFile,
"source": issue.Source,
}
if issue.Severity == SeverityError {
log.WithFields(fields).Error(issue.Message)
return
}
log.WithFields(fields).Warn(issue.Message)
}
// Report adds a live issue if it is not already present and logs it when new.
// Use this for runtime discoveries that happen outside Rebuild (for example
// filesystem watcher setup failures).
func Report(issue Issue) {
storeMu.Lock()
defer storeMu.Unlock()
issueFingerprintValue := issueFingerprint(issue)
for _, existing := range issues {
if issueFingerprint(existing) == issueFingerprintValue {
return
}
}
issues = append(issues, issue)
logNewIssue(issue)
}
// List returns a copy of the current issues.
func List() []Issue {
storeMu.RLock()
defer storeMu.RUnlock()
out := make([]Issue, len(issues))
copy(out, issues)
return out
}
// Count returns the number of current issues.
func Count() int {
storeMu.RLock()
defer storeMu.RUnlock()
return len(issues)
}

View File

@ -0,0 +1,39 @@
package configissues_test
import (
"testing"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/stretchr/testify/assert"
)
func TestStoreReplaceListCount(t *testing.T) {
configissues.BeginConfigLoad()
configissues.Replace([]configissues.Issue{{
Severity: configissues.SeverityError,
Code: configissues.CodeTemplateParse,
Message: "broken",
}})
assert.Equal(t, 1, configissues.Count())
assert.Equal(t, configissues.CodeTemplateParse, configissues.List()[0].Code)
configissues.Replace(nil)
assert.Equal(t, 0, configissues.Count())
}
func TestStickyCopy(t *testing.T) {
configissues.BeginConfigLoad()
configissues.ReportSticky(configissues.Issue{
Code: configissues.CodeEnvUnset,
Message: "missing",
Source: "FOO",
})
copied := configissues.CopySticky()
assert.Len(t, copied, 1)
assert.Equal(t, configissues.CodeEnvUnset, copied[0].Code)
configissues.BeginConfigLoad()
assert.Empty(t, configissues.CopySticky())
}

View File

@ -30,13 +30,18 @@ func AddListener(l func()) {
}
func SetupEntityFileWatchers(cfg *config.Config) {
baseDir := resolveEntitiesBaseDir(cfg.GetDir())
baseDir := ResolveEntitiesBaseDir(cfg.GetDir())
for i := range cfg.Entities { // #337 - iterate by key, not by value
ef := cfg.Entities[i]
watchAndLoadEntity(baseDir, ef)
}
}
// ResolveEntitiesBaseDir returns the directory used to resolve relative entity file paths.
func ResolveEntitiesBaseDir(configDir string) string {
return resolveEntitiesBaseDir(configDir)
}
//gocyclo:ignore
func resolveEntitiesBaseDir(configDir string) string {
absConfigDir, err := filepath.Abs(configDir)
@ -64,15 +69,28 @@ func watchAndLoadEntity(baseDir string, ef *config.EntityFile) {
p = filepath.Join(baseDir, p)
log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path")
}
go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) })
go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }, filehelper.WatchMeta{
ConfigFile: ef.SourceFile,
})
loadEntityFile(p, ef.Name)
}
func loadEntityFile(filename string, entityname string) {
defer func() {
MarkEntityLoadAttempted(entityname)
notifyEntityListeners()
}()
if strings.HasSuffix(filename, ".json") {
loadEntityFileJson(filename, entityname)
} else {
loadEntityFileYaml(filename, entityname)
return
}
loadEntityFileYaml(filename, entityname)
}
func notifyEntityListeners() {
for _, l := range listeners {
l()
}
}
@ -106,7 +124,7 @@ func loadEntityFileJson(filename string, entityname string) {
data = append(data, d)
}
updateSvFromFile(entityname, data)
replaceEntitiesFromFile(entityname, data)
}
func loadEntityFileYaml(filename string, entityname string) {
@ -131,18 +149,27 @@ func loadEntityFileYaml(filename string, entityname string) {
return
}
updateSvFromFile(entityname, data)
replaceEntitiesFromFile(entityname, data)
}
func updateSvFromFile(entityname string, data []map[string]any) {
ClearEntitiesOfType(entityname)
func replaceEntitiesFromFile(entityname string, data []map[string]any) {
rwmutex.Lock()
defer rwmutex.Unlock()
for i, mapp := range data {
AddEntity(entityname, fmt.Sprintf("%d", i), mapp)
delete(entities, entityname)
if len(data) == 0 {
return
}
for _, l := range listeners {
l()
entities[entityname] = make(entityInstancesByKey, 0)
for i, mapp := range data {
entityKey := fmt.Sprintf("%d", i)
entities[entityname][entityKey] = &Entity{
Data: mapp,
UniqueKey: entityKey,
Title: findEntityTitle(mapp),
}
}
}

View File

@ -1,6 +1,8 @@
package entities
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -59,3 +61,34 @@ func TestGetEntityInstancesOrdered_emptyOrMissing(t *testing.T) {
ordered = GetEntityInstancesOrdered("empty_test")
assert.Nil(t, ordered)
}
func TestLoadEntityFile_preservesEntitiesOnTransientFailure(t *testing.T) {
const entityName = "preserve_on_fail"
ClearEntitiesOfType(entityName)
defer ClearEntitiesOfType(entityName)
dir := t.TempDir()
yamlPath := filepath.Join(dir, "hosts.yaml")
require.NoError(t, os.WriteFile(yamlPath, []byte("- title: kept\n"), 0o600))
loadEntityFile(yamlPath, entityName)
require.Len(t, GetEntityInstancesOrdered(entityName), 1)
loadEntityFile(filepath.Join(dir, "missing.yaml"), entityName)
require.Len(t, GetEntityInstancesOrdered(entityName), 1, "read failure should keep last good entities")
require.NoError(t, os.WriteFile(yamlPath, []byte("not: valid: yaml: ["), 0o600))
loadEntityFile(yamlPath, entityName)
require.Len(t, GetEntityInstancesOrdered(entityName), 1, "parse failure should keep last good entities")
jsonPath := filepath.Join(dir, "hosts.json")
require.NoError(t, os.WriteFile(jsonPath, []byte("{\"title\":\"json-kept\"}\n"), 0o600))
loadEntityFile(jsonPath, entityName)
require.Len(t, GetEntityInstancesOrdered(entityName), 1)
require.NoError(t, os.WriteFile(jsonPath, []byte("{bad json"), 0o600))
loadEntityFile(jsonPath, entityName)
ordered := GetEntityInstancesOrdered(entityName)
require.Len(t, ordered, 1, "JSON parse failure should keep last good entities")
assert.Equal(t, "json-kept", ordered[0].Title)
}

View File

@ -0,0 +1,33 @@
package entities
import "sync"
var (
loadAttemptedMu sync.RWMutex
loadAttempted = map[string]bool{}
)
// MarkEntityLoadAttempted records that OliveTin has tried to load this entity
// type from its entity file (successfully or not).
func MarkEntityLoadAttempted(entityName string) {
if entityName == "" {
return
}
loadAttemptedMu.Lock()
defer loadAttemptedMu.Unlock()
loadAttempted[entityName] = true
}
// HasEntityLoadAttempted reports whether a load has been attempted for the entity type.
func HasEntityLoadAttempted(entityName string) bool {
loadAttemptedMu.RLock()
defer loadAttemptedMu.RUnlock()
return loadAttempted[entityName]
}
// ResetEntityLoadAttempts clears load-attempt tracking (used by tests).
func ResetEntityLoadAttempts() {
loadAttemptedMu.Lock()
defer loadAttemptedMu.Unlock()
loadAttempted = map[string]bool{}
}

View File

@ -0,0 +1,45 @@
package executor
import (
"testing"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateArgumentDefaultSkipsTemplates(t *testing.T) {
action := &config.Action{Title: "Deploy", ID: "deploy"}
arg := &config.ActionArgument{
Name: "host",
Type: "ascii_identifier",
Default: `{{ host.name }}`,
}
assert.Nil(t, validateArgumentDefault(action, arg))
}
func TestValidateArgumentDefaultLiteralInvalid(t *testing.T) {
action := &config.Action{Title: "Deploy", ID: "deploy"}
arg := &config.ActionArgument{
Name: "host",
Type: "ascii_identifier",
Default: "not a valid id!!",
}
issue := validateArgumentDefault(action, arg)
require.NotNil(t, issue)
assert.Equal(t, configissues.CodeArgDefaultInvalid, issue.Code)
}
func TestValidateArgumentDefaultLiteralValid(t *testing.T) {
action := &config.Action{Title: "Deploy", ID: "deploy"}
arg := &config.ActionArgument{
Name: "host",
Type: "ascii_identifier",
Default: "example.com",
}
assert.Nil(t, validateArgumentDefault(action, arg))
}

View File

@ -24,6 +24,9 @@ var (
"shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`,
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
}
dnsNameLabelPattern = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
dnsNameAllNumericPattern = regexp.MustCompile(`^[0-9]+$`)
)
// parseExecArray parses all exec arguments in the action.
@ -147,12 +150,7 @@ func redactExecArgs(execArgs []string, arguments []config.ActionArgument, argume
}
func argumentSkipsValidation(arg *config.ActionArgument) bool {
switch arg.Type {
case "confirmation", "html":
return true
default:
return false
}
return arg.Type == "html"
}
func typecheckActionArgument(arg *config.ActionArgument, value string, action *config.Action) error {
@ -160,6 +158,10 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c
return nil
}
if arg.Type == "confirmation" {
return typecheckConfirmation(arg, value)
}
if arg.Name == "" {
return fmt.Errorf("argument name cannot be empty")
}
@ -167,6 +169,20 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c
return typecheckActionArgumentFound(value, arg)
}
// typecheckConfirmation allows unnamed confirmation args as UI-only gates.
// Named confirmation values are only ever "0" or "1", matching the web UI.
func typecheckConfirmation(arg *config.ActionArgument, value string) error {
if arg.Name == "" {
return nil
}
if value == "0" || value == "1" {
return nil
}
return fmt.Errorf("argument %q of type confirmation must be \"0\" or \"1\"", arg.Name)
}
// ValidateArgument validates a single argument value using the same logic as the executor.
// It applies mangling transformations and performs full validation including null checks,
// choice validation, and type safety checks.
@ -223,6 +239,8 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
return typeSafetyCheckUrl(value)
case "datetime":
return typeSafetyCheckDatetime(value)
case "dnsname":
return typeSafetyCheckDnsName(value)
}
return typeSafetyCheckRegex(name, value, argumentType)
@ -304,6 +322,33 @@ func typeSafetyCheckEmail(value string) error {
return nil
}
// typeSafetyCheckDnsName validates a DNS hostname (RFC 1123 LDH labels).
// Accepts short names (e.g. webserver) and FQDNs (e.g. webserver.example.com).
// An optional trailing dot is allowed.
func typeSafetyCheckDnsName(value string) error {
hostname := strings.TrimSuffix(value, ".")
if hostname == "" || len(hostname) > 253 {
return fmt.Errorf("invalid dnsname length")
}
return typeSafetyCheckDnsNameLabels(strings.Split(hostname, "."))
}
func typeSafetyCheckDnsNameLabels(labels []string) error {
for _, label := range labels {
if !dnsNameLabelPattern.MatchString(label) {
return fmt.Errorf("invalid dnsname label %q", label)
}
}
tld := labels[len(labels)-1]
if dnsNameAllNumericPattern.MatchString(tld) {
return fmt.Errorf("dnsname top-level label must not be all-numeric")
}
return nil
}
func typeSafetyCheckDatetime(value string) error {
_, err := time.Parse("2006-01-02T15:04:05", value)
@ -371,7 +416,6 @@ var shellUnsafeArgumentTypes = map[string]struct{}{
"very_dangerous_raw_string": {},
"password": {},
"html": {},
"confirmation": {},
}
func isUnsafeShellArgumentType(arg *config.ActionArgument) bool {

View File

@ -523,8 +523,20 @@ func TestCheckShellArgumentSafetyWithConfirmation(t *testing.T) {
}
err := checkShellArgumentSafety(&a1)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "unsafe argument type 'confirmation'")
assert.Nil(t, err, "confirmation is constrained to 0/1 and is safe with shell")
}
func TestCheckShellArgumentSafetyWithUnnamedConfirmation(t *testing.T) {
a1 := config.Action{
Title: "Confirm shell unnamed",
Shell: "echo ok",
Arguments: []config.ActionArgument{
{Type: "confirmation", Title: "Are you sure?!"},
},
}
err := checkShellArgumentSafety(&a1)
assert.Nil(t, err)
}
func TestCheckShellArgumentSafetyWithChoicelessCheckbox(t *testing.T) {
@ -812,6 +824,44 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
}
}
func TestTypeSafetyCheckDnsName(t *testing.T) {
tests := []struct {
name string
value string
hasError bool
}{
{"Short name", "webserver", false},
{"Localhost", "localhost", false},
{"Simple domain", "example.com", false},
{"Host with subdomain", "webserver.example.com", false},
{"Deep subdomain", "a.b.c.example.co.uk", false},
{"Label starting with digit", "1host.example.com", false},
{"Trailing dot", "example.com.", false},
{"Punycode IDN", "xn--bcher-kva.example", false},
{"Underscore", "my_host.example.com", true},
{"Space", "example .com", true},
{"Leading hyphen label", "-host.example.com", true},
{"Trailing hyphen label", "host-.example.com", true},
{"Empty label", "example..com", true},
{"IP address", "192.168.1.1", true},
{"All numeric TLD", "example.123", true},
{"All numeric short name", "12345", true},
{"Special chars", "exam!ple.com", true},
{"Unicode label", "bücher.example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := TypeSafetyCheck("host", tt.value, "dnsname")
if tt.hasError {
assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
} else {
assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
}
})
}
}
func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
tests := []struct {
name string
@ -892,8 +942,27 @@ func TestTypecheckActionArgumentConfirmation(t *testing.T) {
}
action := config.Action{Title: "Test"}
assert.Nil(t, typecheckActionArgument(&arg, "0", &action))
assert.Nil(t, typecheckActionArgument(&arg, "1", &action))
err := typecheckActionArgument(&arg, "any_value", &action)
assert.Nil(t, err, "Confirmation type should always pass validation")
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "must be \"0\" or \"1\"")
err = typecheckActionArgument(&arg, "", &action)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "must be \"0\" or \"1\"")
}
func TestTypecheckActionArgumentUnnamedConfirmation(t *testing.T) {
arg := config.ActionArgument{
Type: "confirmation",
Title: "Are you sure?!",
}
action := config.Action{Title: "Test"}
assert.Nil(t, typecheckActionArgument(&arg, "", &action))
assert.Nil(t, typecheckActionArgument(&arg, "ignored", &action))
}
func TestTypecheckActionArgumentHtmlWithoutName(t *testing.T) {

View File

@ -1097,6 +1097,7 @@ func appendErrorToStderr(req *ExecutionRequest, err error) {
type OutputStreamer struct {
Req *ExecutionRequest
mu sync.Mutex
output bytes.Buffer
}
@ -1105,10 +1106,31 @@ func (ost *OutputStreamer) Write(o []byte) (n int, err error) {
listener.OnOutputChunk(o, ost.Req.TrackingID)
}
return ost.output.Write(o)
ost.mu.Lock()
n, err = ost.output.Write(o)
outputSoFar := ""
if err == nil {
outputSoFar = ost.output.String()
}
ost.mu.Unlock()
if err != nil {
return n, err
}
// Keep the log entry's Output in sync while the command is still running so
// ExecutionStatus / mid-run result views can show output produced so far.
ost.Req.mutateLogEntry(func(entry *InternalLogEntry) {
entry.Output = outputSoFar
})
return n, nil
}
func (ost *OutputStreamer) String() string {
ost.mu.Lock()
defer ost.mu.Unlock()
return ost.output.String()
}
@ -1155,9 +1177,10 @@ func stepExec(req *ExecutionRequest) bool {
})
ctx.setProcess(cmd.Process)
waiterr := cmd.Wait()
finalOutput := streamer.String()
req.mutateLogEntry(func(entry *InternalLogEntry) {
entry.ExitCode = int32(commandExitCode(cmd))
entry.Output = streamer.String()
entry.Output = finalOutput
})
appendErrorToStderr(req, runerr)
@ -1391,7 +1414,12 @@ func triggerLoop(req *ExecutionRequest) {
}
func stepSaveLog(req *ExecutionRequest) bool {
filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
if !canSaveExecutionLog(req) {
log.Warnf("Cannot save execution log; missing request, log entry, binding/action, or config")
return false
}
filename := fmt.Sprintf("%v.%v.%v", sanitizeLogFilename(req.logEntry.ActionTitle), req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
saveLogResults(req, filename)
saveLogOutput(req, filename)
@ -1399,6 +1427,34 @@ func stepSaveLog(req *ExecutionRequest) bool {
return true
}
func canSaveExecutionLog(req *ExecutionRequest) bool {
return req != nil && req.logEntry != nil && req.Binding != nil && req.Binding.Action != nil && req.Cfg != nil
}
// sanitizeLogFilename replaces characters that are unsafe in filenames so action
// titles like "Create/update Report" do not create nested paths or fail to write.
func sanitizeLogFilename(title string) string {
oldnew := []string{
"/", "_",
"\\", "_",
":", "_",
"*", "_",
"?", "_",
"\"", "_",
"<", "_",
">", "_",
"|", "_",
}
// NUL and other C0 controls plus DEL are invalid or problematic in filenames.
for i := 0; i < 32; i++ {
oldnew = append(oldnew, string(rune(i)), "_")
}
oldnew = append(oldnew, "\x7f", "_")
return strings.NewReplacer(oldnew...).Replace(title)
}
func firstNonEmpty(one, two string) string {
if one != "" {
return one

View File

@ -3,10 +3,12 @@ package executor
import (
"crypto/sha256"
"fmt"
"strings"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/configcheck"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/OliveTin/OliveTin/internal/entities"
log "github.com/sirupsen/logrus"
)
func (e *Executor) FindBindingByID(id string) *ActionBinding {
@ -40,40 +42,54 @@ type RebuildActionMapRequest struct {
dashboardTargets *dashboardTargetIndex
}
func validateArgumentDefaults(cfg *config.Config) {
func validateArgumentDefaults(cfg *config.Config) []configissues.Issue {
if cfg == nil {
return
return nil
}
out := make([]configissues.Issue, 0)
for _, action := range cfg.Actions {
validateActionArgumentDefaults(action)
out = append(out, validateActionArgumentDefaults(action)...)
}
return out
}
func validateActionArgumentDefaults(action *config.Action) {
func validateActionArgumentDefaults(action *config.Action) []configissues.Issue {
if action == nil {
return
return nil
}
out := make([]configissues.Issue, 0)
for i := range action.Arguments {
validateArgumentDefault(action, &action.Arguments[i])
if issue := validateArgumentDefault(action, &action.Arguments[i]); issue != nil {
out = append(out, *issue)
}
}
return out
}
func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) {
func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) *configissues.Issue {
if arg.Default == "" {
return
return nil
}
if strings.Contains(arg.Default, "{{") {
return nil
}
if err := ValidateArgument(arg, arg.Default, action); err != nil {
log.WithFields(log.Fields{
"actionTitle": action.Title,
"argName": arg.Name,
"default": arg.Default,
"error": err,
}).Warn("Argument default value failed validation")
return &configissues.Issue{
Severity: configissues.SeverityWarning,
Code: configissues.CodeArgDefaultInvalid,
Message: fmt.Sprintf("Argument default value failed validation: %v", err),
ActionID: action.ID,
ActionTitle: action.Title,
ArgumentName: arg.Name,
Source: arg.Default,
ConfigFile: action.SourceFile,
}
}
return nil
}
func (e *Executor) RebuildActionMap() {
validateArgumentDefaults(e.Cfg)
defaultIssues := validateArgumentDefaults(e.Cfg)
e.MapActionBindingsLock.Lock()
@ -94,6 +110,8 @@ func (e *Executor) RebuildActionMap() {
e.MapActionBindingsLock.Unlock()
configcheck.Rebuild(e.Cfg, defaultIssues...)
for _, l := range e.copyListeners() {
l.OnActionMapRebuilt()
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/OliveTin/OliveTin/internal/auth"
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
@ -780,3 +781,216 @@ func (c *executionFinishedCollector) OnExecutionFinished(entry *InternalLogEntry
func (c *executionFinishedCollector) OnOutputChunk(_ []byte, _ string) {}
func (c *executionFinishedCollector) OnActionMapRebuilt() {}
func TestSanitizeLogFilename(t *testing.T) {
tests := []struct {
title string
want string
}{
{"Echo Test", "Echo Test"},
{"Create/update Monthly Report", "Create_update Monthly Report"},
{`path\with\backslashes`, "path_with_backslashes"},
{`a:b*c?d"e<f>g|h`, "a_b_c_d_e_f_g_h"},
{"has\x00nul", "has_nul"},
{"tab\there\nand\rreturn", "tab_here_and_return"},
}
for _, tt := range tests {
assert.Equal(t, tt.want, sanitizeLogFilename(tt.title), "title=%q", tt.title)
}
}
func TestStepSaveLogSanitizesSlashInTitle(t *testing.T) {
resultsDir := t.TempDir()
outputDir := t.TempDir()
started := time.Unix(1714333384, 0)
trackingID := "5e2dc9e5-b6b3-445b-bff9-c2082b0bbbb2"
title := "Create/update Monthly Report"
req := &ExecutionRequest{
Cfg: &config.Config{
SaveLogs: config.SaveLogsConfig{
ResultsDirectory: resultsDir,
OutputDirectory: outputDir,
},
},
Binding: &ActionBinding{
Action: &config.Action{},
},
logEntry: &InternalLogEntry{
ActionTitle: title,
DatetimeStarted: started,
ExecutionTrackingID: trackingID,
Output: "report ok",
},
}
assert.True(t, stepSaveLog(req))
expectedBase := "Create_update Monthly Report.1714333384." + trackingID
resultsPath := filepath.Join(resultsDir, expectedBase+".yaml")
outputPath := filepath.Join(outputDir, expectedBase+".log")
assert.FileExists(t, resultsPath)
assert.FileExists(t, outputPath)
resultsEntries, err := os.ReadDir(resultsDir)
assert.NoError(t, err)
assert.Len(t, resultsEntries, 1, "results file must be flat under resultsDirectory, not a subdirectory")
data, err := os.ReadFile(resultsPath)
assert.NoError(t, err)
assert.Contains(t, string(data), title, "YAML content keeps the original action title")
output, err := os.ReadFile(outputPath)
assert.NoError(t, err)
assert.Equal(t, "report ok", string(output))
}
func TestStepSaveLogKeepsSafeTitleFilename(t *testing.T) {
resultsDir := t.TempDir()
started := time.Unix(1714333384, 0)
trackingID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
req := &ExecutionRequest{
Cfg: &config.Config{
SaveLogs: config.SaveLogsConfig{
ResultsDirectory: resultsDir,
},
},
Binding: &ActionBinding{
Action: &config.Action{},
},
logEntry: &InternalLogEntry{
ActionTitle: "Echo Test",
DatetimeStarted: started,
ExecutionTrackingID: trackingID,
},
}
assert.True(t, stepSaveLog(req))
expectedPath := filepath.Join(resultsDir, "Echo Test.1714333384."+trackingID+".yaml")
assert.FileExists(t, expectedPath)
}
func TestStepSaveLogSanitizesNULInTitle(t *testing.T) {
resultsDir := t.TempDir()
outputDir := t.TempDir()
started := time.Unix(1714333384, 0)
trackingID := "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
title := "Bad\x00Title"
req := &ExecutionRequest{
Cfg: &config.Config{
SaveLogs: config.SaveLogsConfig{
ResultsDirectory: resultsDir,
OutputDirectory: outputDir,
},
},
Binding: &ActionBinding{
Action: &config.Action{},
},
logEntry: &InternalLogEntry{
ActionTitle: title,
DatetimeStarted: started,
ExecutionTrackingID: trackingID,
Output: "nul ok",
},
}
assert.True(t, stepSaveLog(req))
expectedBase := "Bad_Title.1714333384." + trackingID
resultsPath := filepath.Join(resultsDir, expectedBase+".yaml")
outputPath := filepath.Join(outputDir, expectedBase+".log")
assert.FileExists(t, resultsPath)
assert.FileExists(t, outputPath)
assert.NotContains(t, resultsPath, "\x00")
assert.NotContains(t, outputPath, "\x00")
output, err := os.ReadFile(outputPath)
assert.NoError(t, err)
assert.Equal(t, "nul ok", string(output))
}
func TestStepSaveLogReturnsFalseWhenDependenciesMissing(t *testing.T) {
started := time.Unix(1714333384, 0)
valid := &ExecutionRequest{
Cfg: &config.Config{},
Binding: &ActionBinding{
Action: &config.Action{},
},
logEntry: &InternalLogEntry{
ActionTitle: "Echo",
DatetimeStarted: started,
ExecutionTrackingID: "cccccccc-dddd-eeee-ffff-000000000000",
},
}
assert.False(t, stepSaveLog(nil))
assert.False(t, stepSaveLog(&ExecutionRequest{}))
missingLog := *valid
missingLog.logEntry = nil
assert.False(t, stepSaveLog(&missingLog))
missingBinding := *valid
missingBinding.Binding = nil
assert.False(t, stepSaveLog(&missingBinding))
missingAction := *valid
missingAction.Binding = &ActionBinding{}
assert.False(t, stepSaveLog(&missingAction))
missingCfg := *valid
missingCfg.Cfg = nil
assert.False(t, stepSaveLog(&missingCfg))
}
func TestLogEntryOutputAvailableWhileRunning(t *testing.T) {
cfg := config.DefaultConfig()
e := DefaultExecutor(cfg)
action := &config.Action{
Title: "Slow output",
Shell: "echo hello-mid-run; sleep 2",
}
cfg.Actions = append(cfg.Actions, action)
cfg.Sanitize()
e.RebuildActionMap()
binding := e.FindBindingWithNoEntity(action)
require.NotNil(t, binding)
wg, trackingID := e.ExecRequest(&ExecutionRequest{
Binding: binding,
Cfg: cfg,
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
})
var sawOutputWhileRunning bool
require.Eventually(t, func() bool {
snapshot, ok := e.SnapshotLog(trackingID)
if !ok {
return false
}
if snapshot.ExecutionFinished {
return false
}
if strings.Contains(snapshot.Output, "hello-mid-run") {
sawOutputWhileRunning = true
return true
}
return false
}, 2*time.Second, 10*time.Millisecond)
wg.Wait()
require.True(t, sawOutputWhileRunning, "expected Output to contain printed text before ExecutionFinished")
snapshot, ok := e.SnapshotLog(trackingID)
require.True(t, ok)
assert.True(t, snapshot.ExecutionFinished)
assert.Contains(t, snapshot.Output, "hello-mid-run")
}

View File

@ -128,7 +128,8 @@ func RestartArgumentsIncomplete(action *config.Action, entity *entities.Entity,
}
func restartArgumentRequired(arg *config.ActionArgument, entity *entities.Entity) bool {
if argumentSkipsValidation(arg) {
// confirmation is a UI gate; html skips validation. Neither needs a stored value to restart.
if argumentSkipsValidation(arg) || arg.Type == "confirmation" {
return false
}

View File

@ -1,12 +1,14 @@
package filehelper
import (
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"fmt"
"path/filepath"
"sync"
"time"
"sync"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
)
var (
@ -28,33 +30,44 @@ const (
debounceDelay = 300 * time.Millisecond
)
// WatchMeta attaches action context to watcher setup failures so Diagnostics
// can apply the same action view ACL as other configuration issues.
type WatchMeta struct {
ActionID string
ActionTitle string
ConfigFile string
}
type watchContext struct {
filename string
filedir string
callback func(filename string)
interestedEvent fsnotify.Op
event *fsnotify.Event
meta WatchMeta
}
func WatchDirectoryCreate(fullpath string, callback func(filename string)) {
func WatchDirectoryCreate(fullpath string, callback func(filename string), meta WatchMeta) {
watchPath(&watchContext{
filedir: fullpath,
filename: "",
callback: callback,
interestedEvent: fsnotify.Create,
meta: meta,
})
}
func WatchDirectoryWrite(fullpath string, callback func(filename string)) {
func WatchDirectoryWrite(fullpath string, callback func(filename string), meta WatchMeta) {
watchPath(&watchContext{
filedir: fullpath,
filename: "",
callback: callback,
interestedEvent: fsnotify.Write,
meta: meta,
})
}
func WatchFileWrite(fullpath string, callback func(filename string)) {
func WatchFileWrite(fullpath string, callback func(filename string), meta WatchMeta) {
filename := filepath.Base(fullpath)
filedir := filepath.Dir(fullpath)
@ -63,69 +76,94 @@ func WatchFileWrite(fullpath string, callback func(filename string)) {
filename: filename,
callback: callback,
interestedEvent: fsnotify.Write,
meta: meta,
})
}
func watchPath(ctx *watchContext) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Errorf("Could not watch for files being created: %v", err)
reportWatcherFailure(ctx, err)
return
}
defer func() {
if err := watcher.Close(); err != nil {
log.Errorf("Failed to close file watcher: %v", err)
}
}()
defer closeWatcher(watcher)
done := make(chan bool)
go func() {
for {
processEvent(ctx, watcher)
}
}()
err = watcher.Add(ctx.filedir)
if err != nil {
log.Errorf("Could not create watcher: %v", err)
if err := watcher.Add(ctx.filedir); err != nil {
reportWatcherFailure(ctx, err)
return
}
<-done
for processEvent(ctx, watcher) {
}
}
func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) {
func closeWatcher(watcher *fsnotify.Watcher) {
if err := watcher.Close(); err != nil {
log.Errorf("Failed to close file watcher: %v", err)
}
}
func watchTarget(ctx *watchContext) string {
if ctx.filename == "" {
return ctx.filedir
}
return filepath.Join(ctx.filedir, ctx.filename)
}
func reportWatcherFailure(ctx *watchContext, err error) {
path := watchTarget(ctx)
message := fmt.Sprintf("Could not create watcher for %q: %v", path, err)
configissues.Report(configissues.Issue{
Severity: configissues.SeverityError,
Code: configissues.CodeWatcherPath,
Message: message,
ActionID: ctx.meta.ActionID,
ActionTitle: ctx.meta.ActionTitle,
Source: path,
ConfigFile: ctx.meta.ConfigFile,
})
}
// processEvent waits for one watcher event. It returns false when the watcher
// channels are closed so the caller can stop looping.
func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) bool {
select {
case event, ok := <-watcher.Events:
ctx.event = &event
if !consumeEvent(ok, ctx) {
return
}
break
case err := <-watcher.Errors:
log.Errorf("Error in fsnotify: %v", err)
return
return handleWatcherEvent(ctx, event, ok)
case err, ok := <-watcher.Errors:
return handleWatcherError(ctx, err, ok)
}
}
func consumeEvent(ok bool, ctx *watchContext) bool {
func handleWatcherEvent(ctx *watchContext, event fsnotify.Event, ok bool) bool {
if !ok {
return false
}
ctx.event = &event
consumeEvent(ctx)
return true
}
func handleWatcherError(ctx *watchContext, err error, ok bool) bool {
if !ok {
return false
}
if err != nil {
log.WithFields(log.Fields{
"path": watchTarget(ctx),
}).Errorf("Error in fsnotify: %v", err)
}
return true
}
func consumeEvent(ctx *watchContext) {
if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename {
log.Tracef("fsnotify irreleventa event different file %+v", ctx.event)
return true
return
}
consumeRelevantEvents(ctx)
return true
}
func consumeRelevantEvents(ctx *watchContext) {

View File

@ -0,0 +1,63 @@
package filehelper_test
import (
"path/filepath"
"testing"
"time"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/OliveTin/OliveTin/internal/filehelper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWatchDirectoryCreateReportsMissingPath(t *testing.T) {
configissues.Replace(nil)
missing := filepath.Join(t.TempDir(), "missing-watch-dir")
done := make(chan struct{})
go func() {
filehelper.WatchDirectoryCreate(missing, func(string) {}, filehelper.WatchMeta{})
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("WatchDirectoryCreate did not return after watcher failure")
}
issues := configissues.List()
require.Len(t, issues, 1)
assert.Equal(t, configissues.CodeWatcherPath, issues[0].Code)
assert.Equal(t, missing, issues[0].Source)
assert.Contains(t, issues[0].Message, missing)
assert.Empty(t, issues[0].ActionID)
}
func TestWatchDirectoryCreateReportsActionMeta(t *testing.T) {
configissues.Replace(nil)
missing := filepath.Join(t.TempDir(), "missing-secret-watch")
done := make(chan struct{})
go func() {
filehelper.WatchDirectoryCreate(missing, func(string) {}, filehelper.WatchMeta{
ActionID: "secret_action",
ActionTitle: "Secret Action",
ConfigFile: "/etc/OliveTin/secret.yaml",
})
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("WatchDirectoryCreate did not return after watcher failure")
}
issues := configissues.List()
require.Len(t, issues, 1)
assert.Equal(t, "secret_action", issues[0].ActionID)
assert.Equal(t, "Secret Action", issues[0].ActionTitle)
assert.Equal(t, "/etc/OliveTin/secret.yaml", issues[0].ConfigFile)
}

View File

@ -37,7 +37,11 @@ func Schedule(cfg *config.Config, ex *executor.Executor) {
parseCalendarFile(captured, cfg, ex, filename)
}
go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x)
go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x, filehelper.WatchMeta{
ActionID: action.ID,
ActionTitle: action.Title,
ConfigFile: action.SourceFile,
})
x(action.ExecOnCalendarFile)
}

View File

@ -17,7 +17,7 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
go func(act *config.Action, dir string) {
filehelper.WatchDirectoryCreate(dir, func(filename string) {
scheduleExec(act, cfg, ex, filename)
})
}, watchMetaForAction(act))
}(action, dirname)
}
for _, dirname := range action.ExecOnFileChangedInDir {
@ -27,12 +27,20 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
go func(act *config.Action, dir string) {
filehelper.WatchDirectoryWrite(dir, func(filename string) {
scheduleExec(act, cfg, ex, filename)
})
}, watchMetaForAction(act))
}(action, dirname)
}
}
}
func watchMetaForAction(action *config.Action) filehelper.WatchMeta {
return filehelper.WatchMeta{
ActionID: action.ID,
ActionTitle: action.Title,
ConfigFile: action.SourceFile,
}
}
func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Executor, path string) {
args := map[string]string{
"filepath": path,

View File

@ -0,0 +1,17 @@
package tpl_test
import (
"testing"
"github.com/OliveTin/OliveTin/internal/tpl"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckActionTemplateParses(t *testing.T) {
assert.NoError(t, tpl.CheckActionTemplateParses(`{{ host.name }}`))
assert.NoError(t, tpl.CheckActionTemplateParses(`{{ host.hostname }}`))
err := tpl.CheckActionTemplateParses(`{{ host.name | default "All" }}`)
require.Error(t, err)
}

View File

@ -219,6 +219,25 @@ func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string
return result
}
// CheckActionTemplateParses migrates legacy template syntax and reports parse
// errors only (execution against entity/argument data is not attempted).
func CheckActionTemplateParses(source string) error {
if source == "" {
return nil
}
source = migrateLegacyArgumentNames(source)
source = migrateLegacyEntityProperties(source)
clone, err := tpl.Clone()
if err != nil {
return err
}
_, err = clone.Parse(source)
return err
}
/*
func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
source = strings.TrimSpace(source)