feat: Show config issues in diagnostics (#1087)

This commit is contained in:
James Read 2026-07-25 21:25:11 +01:00 committed by GitHub
commit ecc5c64046
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2590 additions and 639 deletions

View File

@ -1,11 +1,29 @@
= Diagnostics = 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: 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[] 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 == 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. 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

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

View File

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

View File

@ -5,9 +5,9 @@
"repository": "https://github.com/OliveTin/OliveTin", "repository": "https://github.com/OliveTin/OliveTin",
"source": "index.html", "source": "index.html",
"devDependencies": { "devDependencies": {
"eslint-plugin-vue": "^10.9.2", "eslint-plugin-vue": "^10.10.0",
"process": "^0.11.10", "process": "^0.11.10",
"stylelint": "^17.14.0", "stylelint": "^17.14.1",
"stylelint-config-standard": "^40.0.0" "stylelint-config-standard": "^40.0.0"
}, },
"scripts": { "scripts": {
@ -25,20 +25,20 @@
"dependencies": { "dependencies": {
"@connectrpc/connect": "^2.1.2", "@connectrpc/connect": "^2.1.2",
"@connectrpc/connect-web": "^2.1.2", "@connectrpc/connect-web": "^2.1.2",
"@hugeicons/core-free-icons": "^4.2.2", "@hugeicons/core-free-icons": "^4.2.3",
"@hugeicons/vue": "^1.0.7", "@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-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
"iconify-icon": "^3.0.2", "iconify-icon": "^3.0.2",
"picocrank": "^1.18.2", "picocrank": "^1.21.0",
"standard": "^17.1.2", "standard": "^17.1.2",
"unplugin-vue-components": "^32.1.0", "unplugin-vue-components": "^32.1.0",
"vite": "^8.1.3", "vite": "^8.1.5",
"vue": "^3.5.39", "vue": "^3.5.40",
"vue-i18n": "^11.4.6", "vue-i18n": "^11.4.7",
"vue-router": "^5.1.0" "vue-router": "^5.2.0"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "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) // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
/* eslint-disable */ /* eslint-disable */
@ -1666,6 +1666,57 @@ export declare type GetDiagnosticsRequest = Message<"olivetin.api.v1.GetDiagnost
*/ */
export declare const GetDiagnosticsRequestSchema: GenMessage<GetDiagnosticsRequest>; 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 * @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; * @generated from field: string SshFoundConfig = 2;
*/ */
SshFoundConfig: string; 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; * @generated from field: bool show_navigate_on_start_icons = 25;
*/ */
showNavigateOnStartIcons: boolean; 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 || [] const rootDashboards = window.initResponse?.rootDashboards || []
if (typeof navigation.value.clear === 'function') { if (typeof navigation.value.clearNavigationLinks === 'function') {
navigation.value.clear() navigation.value.clearNavigationLinks()
} }
for (const rootDashboard of rootDashboards) { for (const rootDashboard of rootDashboards) {
@ -367,7 +367,10 @@ function renderNavigation () {
} }
if (showDiagnostics.value) { 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

@ -1,4 +1,28 @@
<template> <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')"> <Section :title="t('diagnostics.get-support')">
<p> <p>
{{ t('diagnostics.get-support-description') }} {{ t('diagnostics.get-support-description') }}
@ -84,19 +108,41 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue' import Section from 'picocrank/vue/components/Section.vue'
import Table from 'picocrank/vue/components/Table.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n() const { t, locale } = useI18n()
const diagnostics = ref({}) const diagnostics = ref({})
const configIssues = ref([])
const loading = ref(false) const loading = ref(false)
const serverDiagnostics = ref('') const serverDiagnostics = ref('')
const browserInfo = ref('') const browserInfo = ref('')
const serverDiagnosticsCopied = ref(false) const serverDiagnosticsCopied = ref(false)
const browserInfoCopied = 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 () { async function fetchDiagnostics () {
loading.value = true loading.value = true
@ -106,12 +152,14 @@ async function fetchDiagnostics () {
sshFoundKey: response.SshFoundKey, sshFoundKey: response.SshFoundKey,
sshFoundConfig: response.SshFoundConfig sshFoundConfig: response.SshFoundConfig
} }
configIssues.value = response.configIssues || []
} catch (err) { } catch (err) {
console.error('Failed to fetch diagnostics:', err) console.error('Failed to fetch diagnostics:', err)
diagnostics.value = { diagnostics.value = {
sshFoundKey: t('diagnostics.unknown'), sshFoundKey: t('diagnostics.unknown'),
sshFoundConfig: t('diagnostics.unknown') sshFoundConfig: t('diagnostics.unknown')
} }
configIssues.value = []
} }
loading.value = false loading.value = false
} }
@ -263,85 +311,12 @@ async function copyBrowserInfo () {
onMounted(() => { onMounted(() => {
fetchDiagnostics() fetchDiagnostics()
window.addEventListener('EventConfigChanged', fetchDiagnostics)
window.addEventListener('EventEntityChanged', fetchDiagnostics)
})
onUnmounted(() => {
window.removeEventListener('EventConfigChanged', fetchDiagnostics)
window.removeEventListener('EventEntityChanged', fetchDiagnostics)
}) })
</script> </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

@ -32,7 +32,7 @@ describe('config: dashboards with basic fieldsets', function () {
await openSidebar() await openSidebar()
const navigationLinks = await getNavigationLinks() 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] const firstLink = await navigationLinks[0]

View File

@ -32,6 +32,6 @@ describe('config: empty dashboards are hidden', function () {
const navigationLinks = await getNavigationLinks() const navigationLinks = await getNavigationLinks()
expect(navigationLinks).to.not.be.empty 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", "connected": "Verbunden",
"diagnostics.browser-info": "Browser-Informationen", "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.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.copied": "Kopiert!",
"diagnostics.copy-to-clipboard": "In Zwischenablage kopieren", "diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
"diagnostics.found-config": "Konfiguration gefunden", "diagnostics.found-config": "Konfiguration gefunden",
@ -84,6 +95,17 @@
"connected": "Connected", "connected": "Connected",
"diagnostics.browser-info": "Browser Info", "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.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.copied": "Copied!",
"diagnostics.copy-to-clipboard": "Copy to Clipboard", "diagnostics.copy-to-clipboard": "Copy to Clipboard",
"diagnostics.found-config": "Found Config", "diagnostics.found-config": "Found Config",
@ -163,6 +185,17 @@
"connected": "Conectado", "connected": "Conectado",
"diagnostics.browser-info": "Información del navegador", "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.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.copied": "¡Copiado!",
"diagnostics.copy-to-clipboard": "Copiar al portapapeles", "diagnostics.copy-to-clipboard": "Copiar al portapapeles",
"diagnostics.found-config": "Configuración encontrada", "diagnostics.found-config": "Configuración encontrada",
@ -242,6 +275,17 @@
"connected": "Connesso", "connected": "Connesso",
"diagnostics.browser-info": "Informazioni del browser", "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.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.copied": "Copiato!",
"diagnostics.copy-to-clipboard": "Copia negli appunti", "diagnostics.copy-to-clipboard": "Copia negli appunti",
"diagnostics.found-config": "Configurazione trovata", "diagnostics.found-config": "Configurazione trovata",
@ -321,6 +365,17 @@
"connected": "已连接", "connected": "已连接",
"diagnostics.browser-info": "浏览器信息", "diagnostics.browser-info": "浏览器信息",
"diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。", "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.copied": "已复制!",
"diagnostics.copy-to-clipboard": "复制到剪贴板", "diagnostics.copy-to-clipboard": "复制到剪贴板",
"diagnostics.found-config": "找到配置", "diagnostics.found-config": "找到配置",
@ -400,6 +455,17 @@
"connected": "已連線", "connected": "已連線",
"diagnostics.browser-info": "瀏覽器資訊", "diagnostics.browser-info": "瀏覽器資訊",
"diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。", "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.copied": "已複製!",
"diagnostics.copy-to-clipboard": "複製到剪貼簿", "diagnostics.copy-to-clipboard": "複製到剪貼簿",
"diagnostics.found-config": "找到設定檔 (Config)", "diagnostics.found-config": "找到設定檔 (Config)",

View File

@ -54,6 +54,17 @@ translations:
diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten diagnostics.get-support: Unterstützung erhalten 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.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.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.ssh: SSH
diagnostics.found-key: Schlüssel gefunden diagnostics.found-key: Schlüssel gefunden
diagnostics.found-config: Konfiguration gefunden diagnostics.found-config: Konfiguration gefunden

View File

@ -54,6 +54,17 @@ translations:
diagnostics.get-support: Get support 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.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.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.ssh: SSH
diagnostics.found-key: Found Key diagnostics.found-key: Found Key
diagnostics.found-config: Found Config diagnostics.found-config: Found Config

View File

@ -54,6 +54,17 @@ translations:
diagnostics.get-support: Obtener soporte Obtener soporte diagnostics.get-support: Obtener soporte 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.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.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.ssh: SSH
diagnostics.found-key: Clave encontrada diagnostics.found-key: Clave encontrada
diagnostics.found-config: Configuración encontrada diagnostics.found-config: Configuración encontrada

View File

@ -54,6 +54,17 @@ translations:
diagnostics.get-support: Ottenere supporto Ottenere supporto diagnostics.get-support: Ottenere supporto 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.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.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.ssh: SSH
diagnostics.found-key: Chiave trovata diagnostics.found-key: Chiave trovata
diagnostics.found-config: Configurazione trovata diagnostics.found-config: Configurazione trovata

View File

@ -63,6 +63,17 @@ translations:
diagnostics.get-support: 获取支持 获取支持 diagnostics.get-support: 获取支持 获取支持
diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。 diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。
diagnostics.where-to-find-help: 在哪里找到帮助 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.ssh: SSH
diagnostics.found-key: 找到密钥 diagnostics.found-key: 找到密钥
diagnostics.found-config: 找到配置 diagnostics.found-config: 找到配置

View File

@ -54,6 +54,17 @@ translations:
diagnostics.get-support: 尋求技術支援 diagnostics.get-support: 尋求技術支援
diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。 diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。
diagnostics.where-to-find-help: 哪裡可以尋求協助 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.ssh: SSH
diagnostics.found-key: 找到金鑰 (Key) diagnostics.found-key: 找到金鑰 (Key)
diagnostics.found-config: 找到設定檔 (Config) diagnostics.found-config: 找到設定檔 (Config)

View File

@ -379,9 +379,21 @@ message LogoutResponse {
message GetDiagnosticsRequest { 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 { message GetDiagnosticsResponse {
string SshFoundKey = 1; string SshFoundKey = 1;
string SshFoundConfig = 2; string SshFoundConfig = 2;
repeated ConfigIssue config_issues = 3;
} }
message InitRequest {} message InitRequest {}
@ -412,6 +424,7 @@ message InitResponse {
bool login_required = 23; bool login_required = 23;
repeated string available_themes = 24; // List of available theme names repeated string available_themes = 24; // List of available theme names
bool show_navigate_on_start_icons = 25; bool show_navigate_on_start_icons = 25;
int32 config_issue_count = 26;
} }
message AdditionalLink { message AdditionalLink {

View File

@ -3871,17 +3871,118 @@ func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) {
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64} 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 { type GetDiagnosticsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"` SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"`
SshFoundConfig string `protobuf:"bytes,2,opt,name=SshFoundConfig,proto3" json:"SshFoundConfig,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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *GetDiagnosticsResponse) Reset() { func (x *GetDiagnosticsResponse) Reset() {
*x = GetDiagnosticsResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -3893,7 +3994,7 @@ func (x *GetDiagnosticsResponse) String() string {
func (*GetDiagnosticsResponse) ProtoMessage() {} func (*GetDiagnosticsResponse) ProtoMessage() {}
func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -3906,7 +4007,7 @@ func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead.
func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) { 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 { func (x *GetDiagnosticsResponse) GetSshFoundKey() string {
@ -3923,6 +4024,13 @@ func (x *GetDiagnosticsResponse) GetSshFoundConfig() string {
return "" return ""
} }
func (x *GetDiagnosticsResponse) GetConfigIssues() []*ConfigIssue {
if x != nil {
return x.ConfigIssues
}
return nil
}
type InitRequest struct { type InitRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -3931,7 +4039,7 @@ type InitRequest struct {
func (x *InitRequest) Reset() { func (x *InitRequest) Reset() {
*x = InitRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -3943,7 +4051,7 @@ func (x *InitRequest) String() string {
func (*InitRequest) ProtoMessage() {} func (*InitRequest) ProtoMessage() {}
func (x *InitRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -3956,7 +4064,7 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. // Deprecated: Use InitRequest.ProtoReflect.Descriptor instead.
func (*InitRequest) Descriptor() ([]byte, []int) { 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 { 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"` 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 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *InitResponse) Reset() { func (x *InitResponse) Reset() {
*x = InitResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4004,7 +4113,7 @@ func (x *InitResponse) String() string {
func (*InitResponse) ProtoMessage() {} func (*InitResponse) ProtoMessage() {}
func (x *InitResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4017,7 +4126,7 @@ func (x *InitResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead. // Deprecated: Use InitResponse.ProtoReflect.Descriptor instead.
func (*InitResponse) Descriptor() ([]byte, []int) { 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 { func (x *InitResponse) GetShowFooter() bool {
@ -4195,6 +4304,13 @@ func (x *InitResponse) GetShowNavigateOnStartIcons() bool {
return false return false
} }
func (x *InitResponse) GetConfigIssueCount() int32 {
if x != nil {
return x.ConfigIssueCount
}
return 0
}
type AdditionalLink struct { type AdditionalLink struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@ -4205,7 +4321,7 @@ type AdditionalLink struct {
func (x *AdditionalLink) Reset() { func (x *AdditionalLink) Reset() {
*x = AdditionalLink{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4217,7 +4333,7 @@ func (x *AdditionalLink) String() string {
func (*AdditionalLink) ProtoMessage() {} func (*AdditionalLink) ProtoMessage() {}
func (x *AdditionalLink) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4230,7 +4346,7 @@ func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead. // Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead.
func (*AdditionalLink) Descriptor() ([]byte, []int) { 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 { func (x *AdditionalLink) GetTitle() string {
@ -4258,7 +4374,7 @@ type OAuth2Provider struct {
func (x *OAuth2Provider) Reset() { func (x *OAuth2Provider) Reset() {
*x = OAuth2Provider{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4270,7 +4386,7 @@ func (x *OAuth2Provider) String() string {
func (*OAuth2Provider) ProtoMessage() {} func (*OAuth2Provider) ProtoMessage() {}
func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4283,7 +4399,7 @@ func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
// Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead. // Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead.
func (*OAuth2Provider) Descriptor() ([]byte, []int) { 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 { func (x *OAuth2Provider) GetTitle() string {
@ -4316,7 +4432,7 @@ type GetActionBindingRequest struct {
func (x *GetActionBindingRequest) Reset() { func (x *GetActionBindingRequest) Reset() {
*x = GetActionBindingRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4328,7 +4444,7 @@ func (x *GetActionBindingRequest) String() string {
func (*GetActionBindingRequest) ProtoMessage() {} func (*GetActionBindingRequest) ProtoMessage() {}
func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4341,7 +4457,7 @@ func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead. // Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead.
func (*GetActionBindingRequest) Descriptor() ([]byte, []int) { 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 { func (x *GetActionBindingRequest) GetBindingId() string {
@ -4361,7 +4477,7 @@ type GetActionBindingResponse struct {
func (x *GetActionBindingResponse) Reset() { func (x *GetActionBindingResponse) Reset() {
*x = GetActionBindingResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4373,7 +4489,7 @@ func (x *GetActionBindingResponse) String() string {
func (*GetActionBindingResponse) ProtoMessage() {} func (*GetActionBindingResponse) ProtoMessage() {}
func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4386,7 +4502,7 @@ func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead.
func (*GetActionBindingResponse) Descriptor() ([]byte, []int) { 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 { func (x *GetActionBindingResponse) GetAction() *Action {
@ -4415,7 +4531,7 @@ type GetEntitiesRequest struct {
func (x *GetEntitiesRequest) Reset() { func (x *GetEntitiesRequest) Reset() {
*x = GetEntitiesRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4427,7 +4543,7 @@ func (x *GetEntitiesRequest) String() string {
func (*GetEntitiesRequest) ProtoMessage() {} func (*GetEntitiesRequest) ProtoMessage() {}
func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4440,7 +4556,7 @@ func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead. // Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead.
func (*GetEntitiesRequest) Descriptor() ([]byte, []int) { 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 { func (x *GetEntitiesRequest) GetEntityType() string {
@ -4480,7 +4596,7 @@ type GetEntitiesResponse struct {
func (x *GetEntitiesResponse) Reset() { func (x *GetEntitiesResponse) Reset() {
*x = GetEntitiesResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4492,7 +4608,7 @@ func (x *GetEntitiesResponse) String() string {
func (*GetEntitiesResponse) ProtoMessage() {} func (*GetEntitiesResponse) ProtoMessage() {}
func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4505,7 +4621,7 @@ func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead.
func (*GetEntitiesResponse) Descriptor() ([]byte, []int) { 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 { func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition {
@ -4529,7 +4645,7 @@ type EntityDefinition struct {
func (x *EntityDefinition) Reset() { func (x *EntityDefinition) Reset() {
*x = EntityDefinition{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4541,7 +4657,7 @@ func (x *EntityDefinition) String() string {
func (*EntityDefinition) ProtoMessage() {} func (*EntityDefinition) ProtoMessage() {}
func (x *EntityDefinition) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4554,7 +4670,7 @@ func (x *EntityDefinition) ProtoReflect() protoreflect.Message {
// Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead. // Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead.
func (*EntityDefinition) Descriptor() ([]byte, []int) { 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 { func (x *EntityDefinition) GetTitle() string {
@ -4609,7 +4725,7 @@ type EntityProperty struct {
func (x *EntityProperty) Reset() { func (x *EntityProperty) Reset() {
*x = EntityProperty{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4621,7 +4737,7 @@ func (x *EntityProperty) String() string {
func (*EntityProperty) ProtoMessage() {} func (*EntityProperty) ProtoMessage() {}
func (x *EntityProperty) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4634,7 +4750,7 @@ func (x *EntityProperty) ProtoReflect() protoreflect.Message {
// Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead. // Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead.
func (*EntityProperty) Descriptor() ([]byte, []int) { 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 { func (x *EntityProperty) GetName() string {
@ -4661,7 +4777,7 @@ type GetEntityRequest struct {
func (x *GetEntityRequest) Reset() { func (x *GetEntityRequest) Reset() {
*x = GetEntityRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4673,7 +4789,7 @@ func (x *GetEntityRequest) String() string {
func (*GetEntityRequest) ProtoMessage() {} func (*GetEntityRequest) ProtoMessage() {}
func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4686,7 +4802,7 @@ func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead. // Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead.
func (*GetEntityRequest) Descriptor() ([]byte, []int) { 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 { func (x *GetEntityRequest) GetUniqueKey() string {
@ -4712,7 +4828,7 @@ type RestartActionRequest struct {
func (x *RestartActionRequest) Reset() { func (x *RestartActionRequest) Reset() {
*x = RestartActionRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -4724,7 +4840,7 @@ func (x *RestartActionRequest) String() string {
func (*RestartActionRequest) ProtoMessage() {} func (*RestartActionRequest) ProtoMessage() {}
func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -4737,7 +4853,7 @@ func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead.
func (*RestartActionRequest) Descriptor() ([]byte, []int) { 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 { 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" + "\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" +
"\rLogoutRequest\"\x10\n" + "\rLogoutRequest\"\x10\n" +
"\x0eLogoutResponse\"\x17\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" + "\x16GetDiagnosticsResponse\x12 \n" +
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" + "\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" + "\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\x12A\n" +
"\vInitRequest\"\x8d\t\n" + "\rconfig_issues\x18\x03 \x03(\v2\x1c.olivetin.api.v1.ConfigIssueR\fconfigIssues\"\r\n" +
"\vInitRequest\"\xbb\t\n" +
"\fInitResponse\x12\x1e\n" + "\fInitResponse\x12\x1e\n" +
"\n" + "\n" +
"showFooter\x18\x01 \x01(\bR\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" + "\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" + "\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" +
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\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" + "\x0eAdditionalLink\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
"\x03url\x18\x02 \x01(\tR\x03url\"L\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 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{ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
(*Action)(nil), // 0: olivetin.api.v1.Action (*Action)(nil), // 0: olivetin.api.v1.Action
(*ActionGroupMembership)(nil), // 1: olivetin.api.v1.ActionGroupMembership (*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 (*LogoutRequest)(nil), // 62: olivetin.api.v1.LogoutRequest
(*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse (*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse
(*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest (*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest
(*GetDiagnosticsResponse)(nil), // 65: olivetin.api.v1.GetDiagnosticsResponse (*ConfigIssue)(nil), // 65: olivetin.api.v1.ConfigIssue
(*InitRequest)(nil), // 66: olivetin.api.v1.InitRequest (*GetDiagnosticsResponse)(nil), // 66: olivetin.api.v1.GetDiagnosticsResponse
(*InitResponse)(nil), // 67: olivetin.api.v1.InitResponse (*InitRequest)(nil), // 67: olivetin.api.v1.InitRequest
(*AdditionalLink)(nil), // 68: olivetin.api.v1.AdditionalLink (*InitResponse)(nil), // 68: olivetin.api.v1.InitResponse
(*OAuth2Provider)(nil), // 69: olivetin.api.v1.OAuth2Provider (*AdditionalLink)(nil), // 69: olivetin.api.v1.AdditionalLink
(*GetActionBindingRequest)(nil), // 70: olivetin.api.v1.GetActionBindingRequest (*OAuth2Provider)(nil), // 70: olivetin.api.v1.OAuth2Provider
(*GetActionBindingResponse)(nil), // 71: olivetin.api.v1.GetActionBindingResponse (*GetActionBindingRequest)(nil), // 71: olivetin.api.v1.GetActionBindingRequest
(*GetEntitiesRequest)(nil), // 72: olivetin.api.v1.GetEntitiesRequest (*GetActionBindingResponse)(nil), // 72: olivetin.api.v1.GetActionBindingResponse
(*GetEntitiesResponse)(nil), // 73: olivetin.api.v1.GetEntitiesResponse (*GetEntitiesRequest)(nil), // 73: olivetin.api.v1.GetEntitiesRequest
(*EntityDefinition)(nil), // 74: olivetin.api.v1.EntityDefinition (*GetEntitiesResponse)(nil), // 74: olivetin.api.v1.GetEntitiesResponse
(*EntityProperty)(nil), // 75: olivetin.api.v1.EntityProperty (*EntityDefinition)(nil), // 75: olivetin.api.v1.EntityDefinition
(*GetEntityRequest)(nil), // 76: olivetin.api.v1.GetEntityRequest (*EntityProperty)(nil), // 76: olivetin.api.v1.EntityProperty
(*RestartActionRequest)(nil), // 77: olivetin.api.v1.RestartActionRequest (*GetEntityRequest)(nil), // 77: olivetin.api.v1.GetEntityRequest
nil, // 78: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry (*RestartActionRequest)(nil), // 78: olivetin.api.v1.RestartActionRequest
nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
nil, // 80: olivetin.api.v1.ActionArgument.SuggestionsEntry nil, // 80: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
nil, // 81: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry nil, // 81: olivetin.api.v1.ActionArgument.SuggestionsEntry
nil, // 82: olivetin.api.v1.Entity.FieldsEntry nil, // 82: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
nil, // 83: olivetin.api.v1.DumpVarsResponse.ContentsEntry nil, // 83: olivetin.api.v1.Entity.FieldsEntry
nil, // 84: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry nil, // 84: olivetin.api.v1.DumpVarsResponse.ContentsEntry
nil, // 85: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
} }
var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
3, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument 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 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 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, // 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 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 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 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, // 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 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 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 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 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 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 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 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, // 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 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 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 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 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 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, // 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 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 65, // 37: olivetin.api.v1.GetDiagnosticsResponse.config_issues:type_name -> olivetin.api.v1.ConfigIssue
68, // 38: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink 70, // 38: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
8, // 39: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy 69, // 39: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
0, // 40: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action 8, // 40: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
35, // 41: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget 0, // 41: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
74, // 42: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition 35, // 42: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
6, // 43: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity 75, // 43: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
75, // 44: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty 6, // 44: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
43, // 45: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding 76, // 45: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty
9, // 46: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest 43, // 46: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
12, // 47: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest 9, // 47: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
15, // 48: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest 12, // 48: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
17, // 49: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest 15, // 49: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
19, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest 17, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
77, // 51: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest 19, // 51: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
56, // 52: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest 78, // 52: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
34, // 53: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest 56, // 53: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
21, // 54: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest 34, // 54: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
24, // 55: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest 21, // 55: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
26, // 56: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest 24, // 56: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
30, // 57: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest 26, // 57: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
37, // 58: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest 30, // 58: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
39, // 59: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest 37, // 59: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
41, // 60: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest 39, // 60: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest
44, // 61: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest 41, // 61: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
46, // 62: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest 44, // 62: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
58, // 63: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest 46, // 63: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
60, // 64: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest 58, // 64: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
62, // 65: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest 60, // 65: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
48, // 66: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest 62, // 66: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
64, // 67: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest 48, // 67: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
66, // 68: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest 64, // 68: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
70, // 69: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest 67, // 69: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
72, // 70: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest 71, // 70: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
76, // 71: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest 73, // 71: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
7, // 72: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse 77, // 72: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
14, // 73: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse 7, // 73: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
16, // 74: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse 14, // 74: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
18, // 75: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse 16, // 75: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
20, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse 18, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
14, // 77: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse 20, // 77: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
57, // 78: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse 14, // 78: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
36, // 79: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse 57, // 79: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
23, // 80: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse 36, // 80: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
25, // 81: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse 23, // 81: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
29, // 82: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse 25, // 82: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
31, // 83: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse 29, // 83: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
38, // 84: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse 31, // 84: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
40, // 85: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse 38, // 85: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
42, // 86: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse 40, // 86: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse
45, // 87: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse 42, // 87: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
47, // 88: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse 45, // 88: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
59, // 89: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse 47, // 89: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
61, // 90: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse 59, // 90: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
63, // 91: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse 61, // 91: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
49, // 92: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse 63, // 92: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
65, // 93: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse 49, // 93: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
67, // 94: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse 66, // 94: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
71, // 95: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse 68, // 95: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
73, // 96: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse 72, // 96: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
6, // 97: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity 74, // 97: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
72, // [72:98] is the sub-list for method output_type 6, // 98: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
46, // [46:72] is the sub-list for method input_type 73, // [73:99] is the sub-list for method output_type
46, // [46:46] is the sub-list for extension type_name 47, // [47:73] is the sub-list for method input_type
46, // [46:46] is the sub-list for extension extendee 47, // [47:47] is the sub-list for extension type_name
0, // [0:46] is the sub-list for field 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() } 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(), 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)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 85, NumMessages: 86,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -1200,6 +1200,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api
res := &apiv1.GetDiagnosticsResponse{ res := &apiv1.GetDiagnosticsResponse{
SshFoundKey: installationinfo.Runtime.SshFoundKey, SshFoundKey: installationinfo.Runtime.SshFoundKey,
SshFoundConfig: installationinfo.Runtime.SshFoundConfig, SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
ConfigIssues: api.buildConfigIssuesForUser(user),
} }
return connect.NewResponse(res), nil return connect.NewResponse(res), nil
} }
@ -1242,6 +1243,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
LoginRequired: loginRequired, LoginRequired: loginRequired,
AvailableThemes: discoverAvailableThemes(api.cfg), AvailableThemes: discoverAvailableThemes(api.cfg),
ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons, ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons,
ConfigIssueCount: configIssueCountForUser(api, user),
} }
return connect.NewResponse(res), nil 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"` EnabledExpression string `koanf:"enabledExpression"`
Groups []string `koanf:"groups"` Groups []string `koanf:"groups"`
Justification string `koanf:"justification"` Justification string `koanf:"justification"`
// SourceFile is set by OliveTin when loading config (not user YAML).
SourceFile string `koanf:"x-olivetin-source-file"`
} }
func (action *Action) RequiresJustification() bool { func (action *Action) RequiresJustification() bool {
@ -110,6 +112,8 @@ type EntityFile struct {
Name string `koanf:"name"` Name string `koanf:"name"`
Icon string `koanf:"icon"` Icon string `koanf:"icon"`
Properties []EntityProperty `koanf:"properties"` Properties []EntityProperty `koanf:"properties"`
// SourceFile is set by OliveTin when loading config (not user YAML).
SourceFile string `koanf:"x-olivetin-source-file"`
} }
// EntityProperty defines a column shown when listing entity instances in the UI. // EntityProperty defines a column shown when listing entity instances in the UI.

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -8,6 +9,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
"github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/file"
@ -40,6 +42,8 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
"configPath": configPath, "configPath": configPath,
}).Info("Appending cfg source") }).Info("Appending cfg source")
configissues.BeginConfigLoad()
stampLoadedConfigSources(k, configPath)
loadIncludedConfigsFromDir(k, configPath) loadIncludedConfigsFromDir(k, configPath)
if !unmarshalRoot(k, cfg) { if !unmarshalRoot(k, cfg) {
@ -172,7 +176,7 @@ func listYamlFiles(includePath string) ([]string, bool) {
func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) { func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
filePath := filepath.Join(includePath, filename) 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) log.Errorf("Error loading included config file %s: %v", filePath, err)
return return
} }
@ -182,8 +186,15 @@ func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
}).Info("Successfully loaded included config file") }).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. // 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{}) srcSlice, ok1 := srcActions.([]interface{})
destSlice, ok2 := destActions.([]interface{}) destSlice, ok2 := destActions.([]interface{})
if ok1 && ok2 { if ok1 && ok2 {
@ -194,10 +205,11 @@ func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{},
} }
// mergeActionsFromSource merges actions from source into destination. // 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 { if destActions, ok := dest["actions"]; ok {
mergeActionsWhenBothExist(srcActions, destActions, dest) mergeActionsWhenBothExist(srcActions, destActions, dest, sourceFile)
} else { } else {
stampSourceOnMaps(srcActions, sourceFile)
dest["actions"] = srcActions dest["actions"] = srcActions
} }
} }
@ -223,7 +235,8 @@ func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interf
} }
// mergeEntitiesWhenBothExist merges entities when both src and dest have entities. // 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{}) srcSlice, ok1 := srcEntities.([]interface{})
destSlice, ok2 := destEntities.([]interface{}) destSlice, ok2 := destEntities.([]interface{})
if ok1 && ok2 { if ok1 && ok2 {
@ -234,17 +247,18 @@ func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{
} }
// mergeEntitiesFromSource merges entities from source into destination. // 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 { if destEntities, ok := dest["entities"]; ok {
mergeEntitiesWhenBothExist(srcEntities, destEntities, dest) mergeEntitiesWhenBothExist(srcEntities, destEntities, dest, sourceFile)
} else { } else {
stampSourceOnMaps(srcEntities, sourceFile)
dest["entities"] = srcEntities 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 { if srcActions, ok := src["actions"]; ok {
mergeActionsFromSource(srcActions, dest) mergeActionsFromSource(srcActions, dest, sourceFile)
} }
if srcDashboards, ok := src["dashboards"]; ok { if srcDashboards, ok := src["dashboards"]; ok {
@ -252,7 +266,7 @@ func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
} }
if srcEntities, ok := src["entities"]; ok { if srcEntities, ok := src["entities"]; ok {
mergeEntitiesFromSource(srcEntities, dest) mergeEntitiesFromSource(srcEntities, dest, sourceFile)
} }
return nil return nil
@ -285,7 +299,12 @@ func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error
val, set := os.LookupEnv(key) val, set := os.LookupEnv(key)
log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val) log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val)
if !set { 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 return val
}) })

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/OliveTin/OliveTin/internal/configissues"
"github.com/OliveTin/OliveTin/internal/env" "github.com/OliveTin/OliveTin/internal/env"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -29,7 +30,6 @@ func (cfg *Config) Sanitize() {
cfg.sanitizeDashboardsForInlineActions() cfg.sanitizeDashboardsForInlineActions()
cfg.sanitizeActionGroups() cfg.sanitizeActionGroups()
cfg.sanitizeActionGroupReferences()
cfg.sanitizeEntities() cfg.sanitizeEntities()
if err := cfg.validateReservedActionArgumentNames(); err != nil { if err := cfg.validateReservedActionArgumentNames(); err != nil {
@ -238,7 +238,7 @@ func (action *Action) sanitize(cfg *Config) {
action.Groups = dedupeStrings(action.Groups) action.Groups = dedupeStrings(action.Groups)
for idx := range action.Arguments { 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() { func (cfg *Config) sanitizeEntities() {
for _, entityFile := range cfg.Entities { for _, entityFile := range cfg.Entities {
if entityFile == nil { 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() { func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
if cfg.AuthRequireGuestsToLogin { if cfg.AuthRequireGuestsToLogin {
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false") log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
@ -505,7 +479,7 @@ func (cfg *Config) sanitizeOnClickDefaults() {
cfg.DefaultPopupOnStart = cfg.DefaultOnClick cfg.DefaultPopupOnStart = cfg.DefaultOnClick
} }
func (arg *ActionArgument) sanitize() { func (arg *ActionArgument) sanitize(action *Action) {
if arg.Title == "" { if arg.Title == "" {
arg.Title = arg.Name arg.Title = arg.Name
} }
@ -516,45 +490,24 @@ func (arg *ActionArgument) sanitize() {
} }
} }
arg.sanitizeNoType() arg.sanitizeNoType(action)
arg.sanitizeChecklist() // Checklist configuration problems are reported by configcheck.
// Default value validation runs in executor at config load (validateArgumentDefaults).
} }
func (arg *ActionArgument) sanitizeChecklist() { func (arg *ActionArgument) sanitizeNoType(action *Action) {
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() {
if len(arg.Choices) == 0 && arg.Type == "" { if len(arg.Choices) == 0 && arg.Type == "" {
log.WithFields(log.Fields{ issue := configissues.Issue{
"arg": arg.Name, Severity: configissues.SeverityWarning,
}).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.") 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" arg.Type = "ascii"
} }
} }

View File

@ -0,0 +1,51 @@
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).
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
}
if existing, ok := m[sourceFileKey].(string); ok && existing != "" {
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)
}
}

View File

@ -0,0 +1,51 @@
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)
}

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) { 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 for i := range cfg.Entities { // #337 - iterate by key, not by value
ef := cfg.Entities[i] ef := cfg.Entities[i]
watchAndLoadEntity(baseDir, ef) watchAndLoadEntity(baseDir, ef)
} }
} }
// ResolveEntitiesBaseDir returns the directory used to resolve relative entity file paths.
func ResolveEntitiesBaseDir(configDir string) string {
return resolveEntitiesBaseDir(configDir)
}
//gocyclo:ignore //gocyclo:ignore
func resolveEntitiesBaseDir(configDir string) string { func resolveEntitiesBaseDir(configDir string) string {
absConfigDir, err := filepath.Abs(configDir) absConfigDir, err := filepath.Abs(configDir)
@ -64,15 +69,28 @@ func watchAndLoadEntity(baseDir string, ef *config.EntityFile) {
p = filepath.Join(baseDir, p) p = filepath.Join(baseDir, p)
log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path") 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) loadEntityFile(p, ef.Name)
} }
func loadEntityFile(filename string, entityname string) { func loadEntityFile(filename string, entityname string) {
defer func() {
MarkEntityLoadAttempted(entityname)
notifyEntityListeners()
}()
if strings.HasSuffix(filename, ".json") { if strings.HasSuffix(filename, ".json") {
loadEntityFileJson(filename, entityname) loadEntityFileJson(filename, entityname)
} else { return
loadEntityFileYaml(filename, entityname) }
loadEntityFileYaml(filename, entityname)
}
func notifyEntityListeners() {
for _, l := range listeners {
l()
} }
} }
@ -86,6 +104,7 @@ func loadEntityFileJson(filename string, entityname string) {
if err != nil { if err != nil {
log.Errorf("ReadIn: %v", err) log.Errorf("ReadIn: %v", err)
ClearEntitiesOfType(entityname)
return return
} }
@ -100,13 +119,14 @@ func loadEntityFileJson(filename string, entityname string) {
if err != nil { if err != nil {
log.Errorf("%v", err) log.Errorf("%v", err)
ClearEntitiesOfType(entityname)
return return
} }
data = append(data, d) data = append(data, d)
} }
updateSvFromFile(entityname, data) replaceEntitiesFromFile(entityname, data)
} }
func loadEntityFileYaml(filename string, entityname string) { func loadEntityFileYaml(filename string, entityname string) {
@ -119,6 +139,7 @@ func loadEntityFileYaml(filename string, entityname string) {
if err != nil { if err != nil {
log.Errorf("ReadIn: %v", err) log.Errorf("ReadIn: %v", err)
ClearEntitiesOfType(entityname)
return return
} }
@ -128,21 +149,31 @@ func loadEntityFileYaml(filename string, entityname string) {
if err != nil { if err != nil {
log.Errorf("Unmarshal: %v", err) log.Errorf("Unmarshal: %v", err)
ClearEntitiesOfType(entityname)
return return
} }
updateSvFromFile(entityname, data) replaceEntitiesFromFile(entityname, data)
} }
func updateSvFromFile(entityname string, data []map[string]any) { func replaceEntitiesFromFile(entityname string, data []map[string]any) {
ClearEntitiesOfType(entityname) rwmutex.Lock()
defer rwmutex.Unlock()
for i, mapp := range data { delete(entities, entityname)
AddEntity(entityname, fmt.Sprintf("%d", i), mapp)
if len(data) == 0 {
return
} }
for _, l := range listeners { entities[entityname] = make(entityInstancesByKey, 0)
l() for i, mapp := range data {
entityKey := fmt.Sprintf("%d", i)
entities[entityname][entityKey] = &Entity{
Data: mapp,
UniqueKey: entityKey,
Title: findEntityTitle(mapp),
}
} }
} }

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

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

View File

@ -1,12 +1,14 @@
package filehelper package filehelper
import ( import (
"github.com/fsnotify/fsnotify" "fmt"
log "github.com/sirupsen/logrus"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"sync" "github.com/OliveTin/OliveTin/internal/configissues"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
) )
var ( var (
@ -28,33 +30,44 @@ const (
debounceDelay = 300 * time.Millisecond 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 { type watchContext struct {
filename string filename string
filedir string filedir string
callback func(filename string) callback func(filename string)
interestedEvent fsnotify.Op interestedEvent fsnotify.Op
event *fsnotify.Event 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{ watchPath(&watchContext{
filedir: fullpath, filedir: fullpath,
filename: "", filename: "",
callback: callback, callback: callback,
interestedEvent: fsnotify.Create, 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{ watchPath(&watchContext{
filedir: fullpath, filedir: fullpath,
filename: "", filename: "",
callback: callback, callback: callback,
interestedEvent: fsnotify.Write, 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) filename := filepath.Base(fullpath)
filedir := filepath.Dir(fullpath) filedir := filepath.Dir(fullpath)
@ -63,69 +76,94 @@ func WatchFileWrite(fullpath string, callback func(filename string)) {
filename: filename, filename: filename,
callback: callback, callback: callback,
interestedEvent: fsnotify.Write, interestedEvent: fsnotify.Write,
meta: meta,
}) })
} }
func watchPath(ctx *watchContext) { func watchPath(ctx *watchContext) {
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
log.Errorf("Could not watch for files being created: %v", err) reportWatcherFailure(ctx, err)
return return
} }
defer func() { defer closeWatcher(watcher)
if err := watcher.Close(); err != nil {
log.Errorf("Failed to close file watcher: %v", err)
}
}()
done := make(chan bool) if err := watcher.Add(ctx.filedir); err != nil {
reportWatcherFailure(ctx, err)
go func() { return
for {
processEvent(ctx, watcher)
}
}()
err = watcher.Add(ctx.filedir)
if err != nil {
log.Errorf("Could not create watcher: %v", err)
} }
<-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 { select {
case event, ok := <-watcher.Events: case event, ok := <-watcher.Events:
ctx.event = &event return handleWatcherEvent(ctx, event, ok)
case err, ok := <-watcher.Errors:
if !consumeEvent(ok, ctx) { return handleWatcherError(ctx, err, ok)
return
}
break
case err := <-watcher.Errors:
log.Errorf("Error in fsnotify: %v", err)
return
} }
} }
func consumeEvent(ok bool, ctx *watchContext) bool { func handleWatcherEvent(ctx *watchContext, event fsnotify.Event, ok bool) bool {
if !ok { if !ok {
return false 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 { if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename {
log.Tracef("fsnotify irreleventa event different file %+v", ctx.event) log.Tracef("fsnotify irreleventa event different file %+v", ctx.event)
return true return
} }
consumeRelevantEvents(ctx) consumeRelevantEvents(ctx)
return true
} }
func consumeRelevantEvents(ctx *watchContext) { 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) 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) x(action.ExecOnCalendarFile)
} }

View File

@ -17,7 +17,7 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
go func(act *config.Action, dir string) { go func(act *config.Action, dir string) {
filehelper.WatchDirectoryCreate(dir, func(filename string) { filehelper.WatchDirectoryCreate(dir, func(filename string) {
scheduleExec(act, cfg, ex, filename) scheduleExec(act, cfg, ex, filename)
}) }, watchMetaForAction(act))
}(action, dirname) }(action, dirname)
} }
for _, dirname := range action.ExecOnFileChangedInDir { for _, dirname := range action.ExecOnFileChangedInDir {
@ -27,12 +27,20 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
go func(act *config.Action, dir string) { go func(act *config.Action, dir string) {
filehelper.WatchDirectoryWrite(dir, func(filename string) { filehelper.WatchDirectoryWrite(dir, func(filename string) {
scheduleExec(act, cfg, ex, filename) scheduleExec(act, cfg, ex, filename)
}) }, watchMetaForAction(act))
}(action, dirname) }(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) { func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Executor, path string) {
args := map[string]string{ args := map[string]string{
"filepath": path, "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 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 { func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
source = strings.TrimSpace(source) source = strings.TrimSpace(source)