diff --git a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc index 854ec96..b0861c8 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc +++ b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc @@ -1,11 +1,29 @@ = Diagnostics -OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. It's mainly designed for checking SSH configuration at the moment. +OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar: image::advanced_configuration/diagnostics/diagnostics.png[] +== Configuration issues + +The Diagnostics page includes a **Configuration issues** section. OliveTin checks the loaded configuration for problems such as: + +* Filesystem watch paths that cannot be created (for example missing directories used by `execOnFileCreatedInDir`) +* Argument templates that fail to parse +* Missing entity files or empty entity data for entity-bound actions +* Invalid cron schedules +* Entity-bound actions that also use `execOnCron` (cron cannot bind an entity) +* Unset environment variables referenced from configuration +* Other sanitize-time warnings (for example unknown action groups) + +When one or more issues exist, the Diagnostics navigation link shows a count badge with the number of issues so operators notice without opening the page first. The list and badge refresh after configuration reload and after entity file changes. + +Issues that refer to a specific action are only shown to users who are allowed to view that action. The badge count uses the same per-user filtering. Issues that are not tied to an action (for example unset environment variables) remain visible to anyone who can open Diagnostics. + +Where possible, each issue includes the configuration source file that defined the related action or entity (for example a file under an `include` directory). + == Disabling Diagnostics The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods. diff --git a/frontend/js/websocket.js b/frontend/js/websocket.js index 01397c6..f44c010 100644 --- a/frontend/js/websocket.js +++ b/frontend/js/websocket.js @@ -199,6 +199,11 @@ async function handleConfigChangedEvent (j) { window.dispatchEvent(j) } +async function handleEntityChangedEvent (j) { + await refreshInitAfterConfigChange() + window.dispatchEvent(j) +} + const eventCaseToTypeName = { entityChanged: 'EventEntityChanged', configChanged: 'EventConfigChanged', @@ -230,9 +235,13 @@ function handleEvent (msg) { case 'EventHeartbeat': break case 'EventOutputChunk': - case 'EventEntityChanged': window.dispatchEvent(j) break + case 'EventEntityChanged': + handleEntityChangedEvent(j).catch((err) => { + console.error('EventEntityChanged handler failed:', err) + }) + break case 'EventExecutionFinished': case 'EventExecutionStarted': window.dispatchEvent(j) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4916cf4..b4f3e44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,25 +11,25 @@ "dependencies": { "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", - "@hugeicons/core-free-icons": "^4.2.2", + "@hugeicons/core-free-icons": "^4.2.3", "@hugeicons/vue": "^1.0.7", - "@vitejs/plugin-vue": "^6.0.7", + "@vitejs/plugin-vue": "^6.0.8", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.18.2", + "picocrank": "^1.21.0", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", - "vite": "^8.1.3", - "vue": "^3.5.39", - "vue-i18n": "^11.4.6", - "vue-router": "^5.1.0" + "vite": "^8.1.5", + "vue": "^3.5.40", + "vue-i18n": "^11.4.7", + "vue-router": "^5.2.0" }, "devDependencies": { - "eslint-plugin-vue": "^10.9.2", + "eslint-plugin-vue": "^10.10.0", "process": "^0.11.10", - "stylelint": "^17.14.0", + "stylelint": "^17.14.1", "stylelint-config-standard": "^40.0.0" }, "engines": { @@ -52,13 +52,13 @@ } }, "node_modules/@babel/generator": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz", - "integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", "license": "MIT", "dependencies": { - "@babel/parser": "^8.0.0-rc.5", - "@babel/types": "^8.0.0-rc.5", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", @@ -69,30 +69,30 @@ } }, "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.5.tgz", - "integrity": "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", "license": "MIT", "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.5.tgz", - "integrity": "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", "license": "MIT", "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.5.tgz", - "integrity": "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", "license": "MIT", "dependencies": { - "@babel/types": "^8.0.0-rc.5" + "@babel/types": "^8.0.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -102,13 +102,13 @@ } }, "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.5.tgz", - "integrity": "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^8.0.0-rc.5", - "@babel/helper-validator-identifier": "^8.0.0-rc.5" + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" }, "engines": { "node": "^22.18.0 || >=24.11.0" @@ -168,13 +168,13 @@ "peer": true }, "node_modules/@cacheable/memory": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", - "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/utils": "^2.4.1", + "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" @@ -208,9 +208,9 @@ } }, "node_modules/@cacheable/utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", - "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", "dev": true, "license": "MIT", "dependencies": { @@ -295,9 +295,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", - "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -883,9 +883,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -942,9 +942,9 @@ } }, "node_modules/@hugeicons/core-free-icons": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz", - "integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.3.tgz", + "integrity": "sha512-fpiU0qFZkv4ABi23xJ2m4qNr0BBfuIaYjPboN8WDA6aj9D+OzXa7eykxKOpACZRuGbDz1U9S5DsNx1d5KNnsgA==", "license": "MIT" }, "node_modules/@hugeicons/vue": { @@ -998,14 +998,14 @@ "license": "MIT" }, "node_modules/@intlify/core-base": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz", - "integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz", + "integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==", "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.4.6", - "@intlify/message-compiler": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/devtools-types": "11.4.7", + "@intlify/message-compiler": "11.4.7", + "@intlify/shared": "11.4.7" }, "engines": { "node": ">= 22" @@ -1015,13 +1015,13 @@ } }, "node_modules/@intlify/devtools-types": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz", - "integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz", + "integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/core-base": "11.4.7", + "@intlify/shared": "11.4.7" }, "engines": { "node": ">= 22" @@ -1031,12 +1031,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz", - "integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz", + "integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.4.6", + "@intlify/shared": "11.4.7", "source-map-js": "^1.0.2" }, "engines": { @@ -1047,9 +1047,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz", - "integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz", + "integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==", "license": "MIT", "engines": { "node": ">= 22" @@ -1481,9 +1481,9 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", - "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", "license": "MIT", "dependencies": { "@rolldown/pluginutils": "^1.0.1" @@ -1497,9 +1497,9 @@ } }, "node_modules/@vue-macros/common": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", - "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", "license": "MIT", "dependencies": { "@vue/compiler-sfc": "^3.5.22", @@ -1524,53 +1524,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", - "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.39", + "@vue/shared": "3.5.40", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", - "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", - "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.39", - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.15", + "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", - "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/devtools-api": { @@ -1580,71 +1580,69 @@ "license": "MIT" }, "node_modules/@vue/devtools-kit": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz", - "integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz", + "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.1.2", + "@vue/devtools-shared": "^8.1.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "node_modules/@vue/devtools-shared": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz", - "integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz", + "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==", "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", - "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.39" + "@vue/shared": "3.5.40" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", - "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", - "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/runtime-core": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", - "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39" - }, - "peerDependencies": { - "vue": "3.5.39" + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/shared": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", - "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", "license": "MIT" }, "node_modules/@xterm/addon-fit": { @@ -1974,9 +1972,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2018,14 +2016,14 @@ } }, "node_modules/cacheable": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", - "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/memory": "^2.0.8", - "@cacheable/utils": "^2.4.1", + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" @@ -3015,18 +3013,18 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz", - "integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", + "@eslint-community/eslint-utils": "^4.9.1", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^7.1.0", - "semver": "^7.6.3", - "xml-name-validator": "^4.0.0" + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3232,9 +3230,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -3597,9 +3595,9 @@ } }, "node_modules/globby": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", - "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", + "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3618,9 +3616,9 @@ } }, "node_modules/globby/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -4311,9 +4309,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -4934,9 +4932,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -4966,6 +4964,12 @@ "node": ">=0.10.0" } }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5268,9 +5272,9 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz", - "integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.0.tgz", + "integrity": "sha512-KM2xyrHfgrKwAwFtyr/o4cuHYqSeTS5sNAOpSUBw/zFvTGPBUWP/K82cYXRU4Tuz7NoQD6D0VbwAs/Q+rvEjkA==", "license": "ISC", "dependencies": { "@hugeicons/core-free-icons": "^4.2.2", @@ -5400,9 +5404,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "funding": [ { "type": "opencollective", @@ -5419,7 +5423,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6233,9 +6237,9 @@ } }, "node_modules/stylelint": { - "version": "17.14.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz", - "integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==", + "version": "17.14.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz", + "integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==", "dev": true, "funding": [ { @@ -6251,7 +6255,7 @@ "dependencies": { "@csstools/css-calc": "^3.2.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.1.5", + "@csstools/css-syntax-patches-for-csstree": "^1.1.6", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", @@ -6263,9 +6267,9 @@ "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^11.1.3", + "file-entry-cache": "^11.1.5", "global-modules": "^2.0.0", - "globby": "^16.2.0", + "globby": "^16.2.1", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", @@ -6275,12 +6279,12 @@ "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.5.15", + "postcss": "^8.5.16", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.4", "postcss-value-parser": "^4.2.0", "string-width": "^8.2.1", - "supports-hyperlinks": "^4.4.0", + "supports-hyperlinks": "^4.5.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.1" @@ -6355,23 +6359,23 @@ } }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz", - "integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.22" + "flat-cache": "^6.1.23" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.22", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz", - "integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^2.3.4", + "cacheable": "^2.5.0", "flatted": "^3.4.2", "hookified": "^1.15.0" } @@ -6431,9 +6435,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz", - "integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", "dev": true, "license": "MIT", "dependencies": { @@ -6750,27 +6754,67 @@ } }, "node_modules/unplugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", - "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/unplugin-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", - "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", "license": "MIT", "dependencies": { "pathe": "^2.0.3", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=20.19.0" @@ -6872,15 +6916,15 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { @@ -6949,9 +6993,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -6961,16 +7005,16 @@ } }, "node_modules/vue": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", - "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-sfc": "3.5.39", - "@vue/runtime-dom": "3.5.39", - "@vue/server-renderer": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" }, "peerDependencies": { "typescript": "*" @@ -7074,14 +7118,14 @@ } }, "node_modules/vue-i18n": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz", - "integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz", + "integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/devtools-types": "11.4.6", - "@intlify/shared": "11.4.6", + "@intlify/core-base": "11.4.7", + "@intlify/devtools-types": "11.4.7", + "@intlify/shared": "11.4.7", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -7095,27 +7139,28 @@ } }, "node_modules/vue-router": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz", - "integrity": "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", + "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", "license": "MIT", "dependencies": { - "@babel/generator": "^8.0.0-rc.4", - "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.1.2", + "@babel/generator": "^8.0.0", + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", "ast-walker-scope": "^0.9.0", "chokidar": "^5.0.0", "json5": "^2.2.3", - "local-pkg": "^1.1.2", + "local-pkg": "^1.2.1", "magic-string": "^0.30.21", "mlly": "^1.8.2", "muggle-string": "^0.4.1", + "nostics": "^1.1.4", "pathe": "^2.0.3", - "picomatch": "^4.0.3", + "picomatch": "^4.0.5", "scule": "^1.3.0", - "tinyglobby": "^0.2.16", - "unplugin": "^3.0.0", - "unplugin-utils": "^0.3.1", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2", "yaml": "^2.9.0" }, "funding": { @@ -7123,10 +7168,10 @@ }, "peerDependencies": { "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.34", - "pinia": "^3.0.4", - "vite": "^7.0.0 || ^8.0.0", - "vue": "^3.5.34" + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" }, "peerDependenciesMeta": { "@pinia/colada": { @@ -7144,12 +7189,12 @@ } }, "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz", - "integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz", + "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.1.2" + "@vue/devtools-kit": "^8.1.5" } }, "node_modules/vue-router/node_modules/json5": { @@ -7165,9 +7210,9 @@ } }, "node_modules/vue-router/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -7320,13 +7365,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/yaml": { diff --git a/frontend/package.json b/frontend/package.json index e812077..e7cd836 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,9 +5,9 @@ "repository": "https://github.com/OliveTin/OliveTin", "source": "index.html", "devDependencies": { - "eslint-plugin-vue": "^10.9.2", + "eslint-plugin-vue": "^10.10.0", "process": "^0.11.10", - "stylelint": "^17.14.0", + "stylelint": "^17.14.1", "stylelint-config-standard": "^40.0.0" }, "scripts": { @@ -25,20 +25,20 @@ "dependencies": { "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", - "@hugeicons/core-free-icons": "^4.2.2", + "@hugeicons/core-free-icons": "^4.2.3", "@hugeicons/vue": "^1.0.7", - "@vitejs/plugin-vue": "^6.0.7", + "@vitejs/plugin-vue": "^6.0.8", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.18.2", + "picocrank": "^1.21.0", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", - "vite": "^8.1.3", - "vue": "^3.5.39", - "vue-i18n": "^11.4.6", - "vue-router": "^5.1.0" + "vite": "^8.1.5", + "vue": "^3.5.40", + "vue-i18n": "^11.4.7", + "vue-router": "^5.2.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index 6450b63..0f482a3 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.12.1 +// @generated by protoc-gen-es v2.13.0 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -1666,6 +1666,57 @@ export declare type GetDiagnosticsRequest = Message<"olivetin.api.v1.GetDiagnost */ export declare const GetDiagnosticsRequestSchema: GenMessage; +/** + * @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; + /** * @generated from message olivetin.api.v1.GetDiagnosticsResponse */ @@ -1679,6 +1730,11 @@ export declare type GetDiagnosticsResponse = Message<"olivetin.api.v1.GetDiagnos * @generated from field: string SshFoundConfig = 2; */ SshFoundConfig: string; + + /** + * @generated from field: repeated olivetin.api.v1.ConfigIssue config_issues = 3; + */ + configIssues: ConfigIssue[]; }; /** @@ -1829,6 +1885,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & { * @generated from field: bool show_navigate_on_start_icons = 25; */ showNavigateOnStartIcons: boolean; + + /** + * @generated from field: int32 config_issue_count = 26; + */ + configIssueCount: number; }; /** diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index 825efe7..df4fcd9 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.12.1 +// @generated by protoc-gen-es v2.13.0 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSKLAQoYR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlEicKBmFjdGlvbhgBIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiWgoSR2V0RW50aXRpZXNSZXF1ZXN0EhMKC2VudGl0eV90eXBlGAEgASgJEg4KBmZpbHRlchgCIAEoCRIMCgRwYWdlGAMgASgFEhEKCXBhZ2Vfc2l6ZRgEIAEoBSJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uIsUBChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJEgwKBGljb24YBCABKAkSMwoKcHJvcGVydGllcxgFIAMoCzIfLm9saXZldGluLmFwaS52MS5FbnRpdHlQcm9wZXJ0eRIXCg90b3RhbF9pbnN0YW5jZXMYBiABKAUiLQoORW50aXR5UHJvcGVydHkSDAoEbmFtZRgBIAEoCRINCgV0aXRsZRgCIAEoCSI0ChBHZXRFbnRpdHlSZXF1ZXN0EhIKCnVuaXF1ZV9rZXkYASABKAkSDAoEdHlwZRgCIAEoCSI1ChRSZXN0YXJ0QWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAky7hMKEk9saXZlVGluQXBpU2VydmljZRJdCgxHZXREYXNoYm9hcmQSJC5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXNwb25zZSIAEloKC1N0YXJ0QWN0aW9uEiMub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASbwoSU3RhcnRBY3Rpb25BbmRXYWl0Eioub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QaKy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2UiABJpChBTdGFydEFjdGlvbkJ5R2V0Eigub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZSIAEn4KF1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0Ei8ub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBowLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlIgASXgoNUmVzdGFydEFjdGlvbhIlLm9saXZldGluLmFwaS52MS5SZXN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASVwoKS2lsbEFjdGlvbhIiLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVxdWVzdBojLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVzcG9uc2UiABJmCg9FeGVjdXRpb25TdGF0dXMSJy5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBooLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXNwb25zZSIAEk4KB0dldExvZ3MSHy5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1JlcXVlc3QaIC5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1Jlc3BvbnNlIgASYAoNR2V0QWN0aW9uTG9ncxIlLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVxdWVzdBomLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVzcG9uc2UiABJsChFHZXRFeGVjdXRpb25RdWV1ZRIpLm9saXZldGluLmFwaS52MS5HZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QaKi5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJsChFTZXJ2ZXJEaWFnbm9zdGljcxIpLm9saXZldGluLmFwaS52MS5TZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QaKi5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiowEKC0NvbmZpZ0lzc3VlEhAKCHNldmVyaXR5GAEgASgJEgwKBGNvZGUYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIRCglhY3Rpb25faWQYBCABKAkSFAoMYWN0aW9uX3RpdGxlGAUgASgJEhUKDWFyZ3VtZW50X25hbWUYBiABKAkSDgoGc291cmNlGAcgASgJEhMKC2NvbmZpZ19maWxlGAggASgJInoKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkSMwoNY29uZmlnX2lzc3VlcxgDIAMoCzIcLm9saXZldGluLmFwaS52MS5Db25maWdJc3N1ZSINCgtJbml0UmVxdWVzdCKHBgoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCBIaChJjb25maWdfaXNzdWVfY291bnQYGiABKAUiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IloKEkdldEVudGl0aWVzUmVxdWVzdBITCgtlbnRpdHlfdHlwZRgBIAEoCRIOCgZmaWx0ZXIYAiABKAkSDAoEcGFnZRgDIAEoBRIRCglwYWdlX3NpemUYBCABKAUiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiLFAQoQRW50aXR5RGVmaW5pdGlvbhINCgV0aXRsZRgBIAEoCRIqCglpbnN0YW5jZXMYAiADKAsyFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5EhoKEnVzZWRfb25fZGFzaGJvYXJkcxgDIAMoCRIMCgRpY29uGAQgASgJEjMKCnByb3BlcnRpZXMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5UHJvcGVydHkSFwoPdG90YWxfaW5zdGFuY2VzGAYgASgFIi0KDkVudGl0eVByb3BlcnR5EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMu4TChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASbAoRU2VydmVyRGlhZ25vc3RpY3MSKS5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLlNlcnZlckRpYWdub3N0aWNzUmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. @@ -465,96 +465,103 @@ export const LogoutResponseSchema = /*@__PURE__*/ export const GetDiagnosticsRequestSchema = /*@__PURE__*/ messageDesc(file_olivetin_api_v1_olivetin, 64); +/** + * Describes the message olivetin.api.v1.ConfigIssue. + * Use `create(ConfigIssueSchema)` to create a new message. + */ +export const ConfigIssueSchema = /*@__PURE__*/ + messageDesc(file_olivetin_api_v1_olivetin, 65); + /** * Describes the message olivetin.api.v1.GetDiagnosticsResponse. * Use `create(GetDiagnosticsResponseSchema)` to create a new message. */ export const GetDiagnosticsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 65); + messageDesc(file_olivetin_api_v1_olivetin, 66); /** * Describes the message olivetin.api.v1.InitRequest. * Use `create(InitRequestSchema)` to create a new message. */ export const InitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 66); + messageDesc(file_olivetin_api_v1_olivetin, 67); /** * Describes the message olivetin.api.v1.InitResponse. * Use `create(InitResponseSchema)` to create a new message. */ export const InitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 67); + messageDesc(file_olivetin_api_v1_olivetin, 68); /** * Describes the message olivetin.api.v1.AdditionalLink. * Use `create(AdditionalLinkSchema)` to create a new message. */ export const AdditionalLinkSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 68); + messageDesc(file_olivetin_api_v1_olivetin, 69); /** * Describes the message olivetin.api.v1.OAuth2Provider. * Use `create(OAuth2ProviderSchema)` to create a new message. */ export const OAuth2ProviderSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 69); + messageDesc(file_olivetin_api_v1_olivetin, 70); /** * Describes the message olivetin.api.v1.GetActionBindingRequest. * Use `create(GetActionBindingRequestSchema)` to create a new message. */ export const GetActionBindingRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 70); + messageDesc(file_olivetin_api_v1_olivetin, 71); /** * Describes the message olivetin.api.v1.GetActionBindingResponse. * Use `create(GetActionBindingResponseSchema)` to create a new message. */ export const GetActionBindingResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 71); + messageDesc(file_olivetin_api_v1_olivetin, 72); /** * Describes the message olivetin.api.v1.GetEntitiesRequest. * Use `create(GetEntitiesRequestSchema)` to create a new message. */ export const GetEntitiesRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 72); + messageDesc(file_olivetin_api_v1_olivetin, 73); /** * Describes the message olivetin.api.v1.GetEntitiesResponse. * Use `create(GetEntitiesResponseSchema)` to create a new message. */ export const GetEntitiesResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 73); + messageDesc(file_olivetin_api_v1_olivetin, 74); /** * Describes the message olivetin.api.v1.EntityDefinition. * Use `create(EntityDefinitionSchema)` to create a new message. */ export const EntityDefinitionSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 74); + messageDesc(file_olivetin_api_v1_olivetin, 75); /** * Describes the message olivetin.api.v1.EntityProperty. * Use `create(EntityPropertySchema)` to create a new message. */ export const EntityPropertySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 75); + messageDesc(file_olivetin_api_v1_olivetin, 76); /** * Describes the message olivetin.api.v1.GetEntityRequest. * Use `create(GetEntityRequestSchema)` to create a new message. */ export const GetEntityRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 76); + messageDesc(file_olivetin_api_v1_olivetin, 77); /** * Describes the message olivetin.api.v1.RestartActionRequest. * Use `create(RestartActionRequestSchema)` to create a new message. */ export const RestartActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 77); + messageDesc(file_olivetin_api_v1_olivetin, 78); /** * @generated from service olivetin.api.v1.OliveTinApiService diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 709f27c..cc3a388 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -345,8 +345,8 @@ function renderNavigation () { const rootDashboards = window.initResponse?.rootDashboards || [] - if (typeof navigation.value.clear === 'function') { - navigation.value.clear() + if (typeof navigation.value.clearNavigationLinks === 'function') { + navigation.value.clearNavigationLinks() } for (const rootDashboard of rootDashboards) { @@ -367,7 +367,10 @@ function renderNavigation () { } if (showDiagnostics.value) { - navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics')) + const issueCount = window.initResponse?.configIssueCount || 0 + navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'), { + count: issueCount + }) } } diff --git a/frontend/resources/vue/views/DiagnosticsView.vue b/frontend/resources/vue/views/DiagnosticsView.vue index ab80c7b..4c68e44 100644 --- a/frontend/resources/vue/views/DiagnosticsView.vue +++ b/frontend/resources/vue/views/DiagnosticsView.vue @@ -1,4 +1,28 @@ - - diff --git a/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js b/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js index b67be0b..64b690c 100644 --- a/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js +++ b/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js @@ -32,7 +32,7 @@ describe('config: dashboards with basic fieldsets', function () { await openSidebar() const navigationLinks = await getNavigationLinks() - assert.equal(navigationLinks.length, 5, 'Expected the nav to only have 5 links') // test dashboard + logs + diagnostics + entities + separator + assert.equal(navigationLinks.length, 4, 'Expected the nav to only have 4 links') // test dashboard + entities + logs + diagnostics const firstLink = await navigationLinks[0] @@ -44,7 +44,7 @@ describe('config: dashboards with basic fieldsets', function () { // Check that we have the expected number of fieldsets const dashboardRows = await webdriver.findElements(By.css('.dashboard-row')) expect(dashboardRows).to.have.length(3, 'Expected 3 dashboard rows total') - + // Check that we have fieldsets with the expected titles const fieldsetTitles = [] for (let i = 0; i < dashboardRows.length; i++) { @@ -54,7 +54,7 @@ describe('config: dashboards with basic fieldsets', function () { fieldsetTitles.push(title) } } - + // We should have fieldsets for: Fieldset 1, Fieldset 2, and Actions fieldsets expect(fieldsetTitles).to.include('Fieldset 1') expect(fieldsetTitles).to.include('Fieldset 2') diff --git a/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js b/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js index e5d498b..6cc133d 100644 --- a/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js +++ b/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js @@ -32,6 +32,6 @@ describe('config: empty dashboards are hidden', function () { const navigationLinks = await getNavigationLinks() expect(navigationLinks).to.not.be.empty - expect(navigationLinks.length).to.be.equal(4, 'Expected the nav to only have 4 links') + expect(navigationLinks.length).to.be.equal(3, 'Expected the nav to only have 3 links') // entities + logs + diagnostics }) }) diff --git a/lang/combined_output.json b/lang/combined_output.json index cffc185..f47b5df 100644 --- a/lang/combined_output.json +++ b/lang/combined_output.json @@ -5,6 +5,17 @@ "connected": "Verbunden", "diagnostics.browser-info": "Browser-Informationen", "diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.", + "diagnostics.config-issue-action": "Aktion", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Konfigurationsdatei", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Meldung", + "diagnostics.config-issue-severity": "Schweregrad", + "diagnostics.config-issue-source": "Quelle", + "diagnostics.config-issues": "Konfigurationsprobleme", + "diagnostics.config-issues-description": "In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.", + "diagnostics.config-issues-none": "Keine Konfigurationsprobleme erkannt.", "diagnostics.copied": "Kopiert!", "diagnostics.copy-to-clipboard": "In Zwischenablage kopieren", "diagnostics.found-config": "Konfiguration gefunden", @@ -84,6 +95,17 @@ "connected": "Connected", "diagnostics.browser-info": "Browser Info", "diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "Copied!", "diagnostics.copy-to-clipboard": "Copy to Clipboard", "diagnostics.found-config": "Found Config", @@ -163,6 +185,17 @@ "connected": "Conectado", "diagnostics.browser-info": "Información del navegador", "diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.", + "diagnostics.config-issue-action": "Acción", + "diagnostics.config-issue-argument": "Argumento", + "diagnostics.config-issue-code": "Código", + "diagnostics.config-issue-config-file": "Archivo de configuración", + "diagnostics.config-issue-detail": "Detalle", + "diagnostics.config-issue-message": "Mensaje", + "diagnostics.config-issue-severity": "Severidad", + "diagnostics.config-issue-source": "Origen", + "diagnostics.config-issues": "Problemas de configuración", + "diagnostics.config-issues-description": "Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.", + "diagnostics.config-issues-none": "No se detectaron problemas de configuración.", "diagnostics.copied": "¡Copiado!", "diagnostics.copy-to-clipboard": "Copiar al portapapeles", "diagnostics.found-config": "Configuración encontrada", @@ -242,6 +275,17 @@ "connected": "Connesso", "diagnostics.browser-info": "Informazioni del browser", "diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.", + "diagnostics.config-issue-action": "Azione", + "diagnostics.config-issue-argument": "Argomento", + "diagnostics.config-issue-code": "Codice", + "diagnostics.config-issue-config-file": "File di configurazione", + "diagnostics.config-issue-detail": "Dettaglio", + "diagnostics.config-issue-message": "Messaggio", + "diagnostics.config-issue-severity": "Gravità", + "diagnostics.config-issue-source": "Origine", + "diagnostics.config-issues": "Problemi di configurazione", + "diagnostics.config-issues-description": "Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.", + "diagnostics.config-issues-none": "Nessun problema di configurazione rilevato.", "diagnostics.copied": "Copiato!", "diagnostics.copy-to-clipboard": "Copia negli appunti", "diagnostics.found-config": "Configurazione trovata", @@ -321,6 +365,17 @@ "connected": "已连接", "diagnostics.browser-info": "浏览器信息", "diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。", + "diagnostics.config-issue-action": "操作", + "diagnostics.config-issue-argument": "参数", + "diagnostics.config-issue-code": "代码", + "diagnostics.config-issue-config-file": "配置文件", + "diagnostics.config-issue-detail": "详情", + "diagnostics.config-issue-message": "消息", + "diagnostics.config-issue-severity": "严重程度", + "diagnostics.config-issue-source": "来源", + "diagnostics.config-issues": "配置问题", + "diagnostics.config-issues-description": "当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。", + "diagnostics.config-issues-none": "未检测到配置问题。", "diagnostics.copied": "已复制!", "diagnostics.copy-to-clipboard": "复制到剪贴板", "diagnostics.found-config": "找到配置", @@ -400,6 +455,17 @@ "connected": "已連線", "diagnostics.browser-info": "瀏覽器資訊", "diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。", + "diagnostics.config-issue-action": "動作", + "diagnostics.config-issue-argument": "參數", + "diagnostics.config-issue-code": "代碼", + "diagnostics.config-issue-config-file": "設定檔", + "diagnostics.config-issue-detail": "詳細資料", + "diagnostics.config-issue-message": "訊息", + "diagnostics.config-issue-severity": "嚴重程度", + "diagnostics.config-issue-source": "來源", + "diagnostics.config-issues": "設定問題", + "diagnostics.config-issues-description": "目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。", + "diagnostics.config-issues-none": "未偵測到設定問題。", "diagnostics.copied": "已複製!", "diagnostics.copy-to-clipboard": "複製到剪貼簿", "diagnostics.found-config": "找到設定檔 (Config)", diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml index 813e533..b8db0d3 100644 --- a/lang/de-DE.yaml +++ b/lang/de-DE.yaml @@ -54,6 +54,17 @@ translations: 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.where-to-find-help: Wo Sie Hilfe finden + diagnostics.config-issues: Konfigurationsprobleme + diagnostics.config-issues-description: In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu. + diagnostics.config-issues-none: Keine Konfigurationsprobleme erkannt. + diagnostics.config-issue-severity: Schweregrad + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Meldung + diagnostics.config-issue-action: Aktion + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Konfigurationsdatei + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Quelle diagnostics.ssh: SSH diagnostics.found-key: Schlüssel gefunden diagnostics.found-config: Konfiguration gefunden diff --git a/lang/en.yaml b/lang/en.yaml index a334965..9d1cc4c 100644 --- a/lang/en.yaml +++ b/lang/en.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: Get support diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page. diagnostics.where-to-find-help: Where to find help + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: Found Key diagnostics.found-config: Found Config diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml index bb834aa..a442417 100644 --- a/lang/es-ES.yaml +++ b/lang/es-ES.yaml @@ -54,6 +54,17 @@ translations: 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.where-to-find-help: Dónde encontrar ayuda + diagnostics.config-issues: Problemas de configuración + diagnostics.config-issues-description: Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin. + diagnostics.config-issues-none: No se detectaron problemas de configuración. + diagnostics.config-issue-severity: Severidad + diagnostics.config-issue-code: Código + diagnostics.config-issue-message: Mensaje + diagnostics.config-issue-action: Acción + diagnostics.config-issue-argument: Argumento + diagnostics.config-issue-config-file: Archivo de configuración + diagnostics.config-issue-detail: Detalle + diagnostics.config-issue-source: Origen diagnostics.ssh: SSH diagnostics.found-key: Clave encontrada diagnostics.found-config: Configuración encontrada diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml index d7f54cf..bb2f1ee 100644 --- a/lang/it-IT.yaml +++ b/lang/it-IT.yaml @@ -54,6 +54,17 @@ translations: 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.where-to-find-help: Dove trovare aiuto + diagnostics.config-issues: Problemi di configurazione + diagnostics.config-issues-description: Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin. + diagnostics.config-issues-none: Nessun problema di configurazione rilevato. + diagnostics.config-issue-severity: Gravità + diagnostics.config-issue-code: Codice + diagnostics.config-issue-message: Messaggio + diagnostics.config-issue-action: Azione + diagnostics.config-issue-argument: Argomento + diagnostics.config-issue-config-file: File di configurazione + diagnostics.config-issue-detail: Dettaglio + diagnostics.config-issue-source: Origine diagnostics.ssh: SSH diagnostics.found-key: Chiave trovata diagnostics.found-config: Configurazione trovata diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml index 23ac07d..6937946 100644 --- a/lang/zh-Hans-CN.yaml +++ b/lang/zh-Hans-CN.yaml @@ -63,6 +63,17 @@ translations: diagnostics.get-support: 获取支持 获取支持 diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。 diagnostics.where-to-find-help: 在哪里找到帮助 + diagnostics.config-issues: 配置问题 + diagnostics.config-issues-description: 当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。 + diagnostics.config-issues-none: 未检测到配置问题。 + diagnostics.config-issue-severity: 严重程度 + diagnostics.config-issue-code: 代码 + diagnostics.config-issue-message: 消息 + diagnostics.config-issue-action: 操作 + diagnostics.config-issue-argument: 参数 + diagnostics.config-issue-config-file: 配置文件 + diagnostics.config-issue-detail: 详情 + diagnostics.config-issue-source: 来源 diagnostics.ssh: SSH diagnostics.found-key: 找到密钥 diagnostics.found-config: 找到配置 diff --git a/lang/zh-Hant-TW.yaml b/lang/zh-Hant-TW.yaml index 7f20e2a..462ffd7 100644 --- a/lang/zh-Hant-TW.yaml +++ b/lang/zh-Hant-TW.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: 尋求技術支援 diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。 diagnostics.where-to-find-help: 哪裡可以尋求協助 + diagnostics.config-issues: 設定問題 + diagnostics.config-issues-description: 目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。 + diagnostics.config-issues-none: 未偵測到設定問題。 + diagnostics.config-issue-severity: 嚴重程度 + diagnostics.config-issue-code: 代碼 + diagnostics.config-issue-message: 訊息 + diagnostics.config-issue-action: 動作 + diagnostics.config-issue-argument: 參數 + diagnostics.config-issue-config-file: 設定檔 + diagnostics.config-issue-detail: 詳細資料 + diagnostics.config-issue-source: 來源 diagnostics.ssh: SSH diagnostics.found-key: 找到金鑰 (Key) diagnostics.found-config: 找到設定檔 (Config) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index 1b16b5f..0d49b28 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -379,9 +379,21 @@ message LogoutResponse { message GetDiagnosticsRequest { } +message ConfigIssue { + string severity = 1; + string code = 2; + string message = 3; + string action_id = 4; + string action_title = 5; + string argument_name = 6; + string source = 7; + string config_file = 8; +} + message GetDiagnosticsResponse { string SshFoundKey = 1; string SshFoundConfig = 2; + repeated ConfigIssue config_issues = 3; } message InitRequest {} @@ -412,6 +424,7 @@ message InitResponse { bool login_required = 23; repeated string available_themes = 24; // List of available theme names bool show_navigate_on_start_icons = 25; + int32 config_issue_count = 26; } message AdditionalLink { diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index e0a0c96..ba38dd5 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -3871,17 +3871,118 @@ func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) { return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64} } +type ConfigIssue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Severity string `protobuf:"bytes,1,opt,name=severity,proto3" json:"severity,omitempty"` + Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + ActionId string `protobuf:"bytes,4,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"` + ActionTitle string `protobuf:"bytes,5,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"` + ArgumentName string `protobuf:"bytes,6,opt,name=argument_name,json=argumentName,proto3" json:"argument_name,omitempty"` + Source string `protobuf:"bytes,7,opt,name=source,proto3" json:"source,omitempty"` + ConfigFile string `protobuf:"bytes,8,opt,name=config_file,json=configFile,proto3" json:"config_file,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigIssue) Reset() { + *x = ConfigIssue{} + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigIssue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigIssue) ProtoMessage() {} + +func (x *ConfigIssue) ProtoReflect() protoreflect.Message { + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigIssue.ProtoReflect.Descriptor instead. +func (*ConfigIssue) Descriptor() ([]byte, []int) { + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65} +} + +func (x *ConfigIssue) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *ConfigIssue) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *ConfigIssue) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ConfigIssue) GetActionId() string { + if x != nil { + return x.ActionId + } + return "" +} + +func (x *ConfigIssue) GetActionTitle() string { + if x != nil { + return x.ActionTitle + } + return "" +} + +func (x *ConfigIssue) GetArgumentName() string { + if x != nil { + return x.ArgumentName + } + return "" +} + +func (x *ConfigIssue) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ConfigIssue) GetConfigFile() string { + if x != nil { + return x.ConfigFile + } + return "" +} + type GetDiagnosticsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"` SshFoundConfig string `protobuf:"bytes,2,opt,name=SshFoundConfig,proto3" json:"SshFoundConfig,omitempty"` + ConfigIssues []*ConfigIssue `protobuf:"bytes,3,rep,name=config_issues,json=configIssues,proto3" json:"config_issues,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetDiagnosticsResponse) Reset() { *x = GetDiagnosticsResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3893,7 +3994,7 @@ func (x *GetDiagnosticsResponse) String() string { func (*GetDiagnosticsResponse) ProtoMessage() {} func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3906,7 +4007,7 @@ func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead. func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66} } func (x *GetDiagnosticsResponse) GetSshFoundKey() string { @@ -3923,6 +4024,13 @@ func (x *GetDiagnosticsResponse) GetSshFoundConfig() string { return "" } +func (x *GetDiagnosticsResponse) GetConfigIssues() []*ConfigIssue { + if x != nil { + return x.ConfigIssues + } + return nil +} + type InitRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -3931,7 +4039,7 @@ type InitRequest struct { func (x *InitRequest) Reset() { *x = InitRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3943,7 +4051,7 @@ func (x *InitRequest) String() string { func (*InitRequest) ProtoMessage() {} func (x *InitRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3956,7 +4064,7 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. func (*InitRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67} } type InitResponse struct { @@ -3986,13 +4094,14 @@ type InitResponse struct { LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"` AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names ShowNavigateOnStartIcons bool `protobuf:"varint,25,opt,name=show_navigate_on_start_icons,json=showNavigateOnStartIcons,proto3" json:"show_navigate_on_start_icons,omitempty"` + ConfigIssueCount int32 `protobuf:"varint,26,opt,name=config_issue_count,json=configIssueCount,proto3" json:"config_issue_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InitResponse) Reset() { *x = InitResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4004,7 +4113,7 @@ func (x *InitResponse) String() string { func (*InitResponse) ProtoMessage() {} func (x *InitResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4017,7 +4126,7 @@ func (x *InitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InitResponse.ProtoReflect.Descriptor instead. func (*InitResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68} } func (x *InitResponse) GetShowFooter() bool { @@ -4195,6 +4304,13 @@ func (x *InitResponse) GetShowNavigateOnStartIcons() bool { return false } +func (x *InitResponse) GetConfigIssueCount() int32 { + if x != nil { + return x.ConfigIssueCount + } + return 0 +} + type AdditionalLink struct { state protoimpl.MessageState `protogen:"open.v1"` Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` @@ -4205,7 +4321,7 @@ type AdditionalLink struct { func (x *AdditionalLink) Reset() { *x = AdditionalLink{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4217,7 +4333,7 @@ func (x *AdditionalLink) String() string { func (*AdditionalLink) ProtoMessage() {} func (x *AdditionalLink) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4230,7 +4346,7 @@ func (x *AdditionalLink) ProtoReflect() protoreflect.Message { // Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead. func (*AdditionalLink) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69} } func (x *AdditionalLink) GetTitle() string { @@ -4258,7 +4374,7 @@ type OAuth2Provider struct { func (x *OAuth2Provider) Reset() { *x = OAuth2Provider{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4270,7 +4386,7 @@ func (x *OAuth2Provider) String() string { func (*OAuth2Provider) ProtoMessage() {} func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4283,7 +4399,7 @@ func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead. func (*OAuth2Provider) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70} } func (x *OAuth2Provider) GetTitle() string { @@ -4316,7 +4432,7 @@ type GetActionBindingRequest struct { func (x *GetActionBindingRequest) Reset() { *x = GetActionBindingRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4328,7 +4444,7 @@ func (x *GetActionBindingRequest) String() string { func (*GetActionBindingRequest) ProtoMessage() {} func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4341,7 +4457,7 @@ func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead. func (*GetActionBindingRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71} } func (x *GetActionBindingRequest) GetBindingId() string { @@ -4361,7 +4477,7 @@ type GetActionBindingResponse struct { func (x *GetActionBindingResponse) Reset() { *x = GetActionBindingResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4373,7 +4489,7 @@ func (x *GetActionBindingResponse) String() string { func (*GetActionBindingResponse) ProtoMessage() {} func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4386,7 +4502,7 @@ func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead. func (*GetActionBindingResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72} } func (x *GetActionBindingResponse) GetAction() *Action { @@ -4415,7 +4531,7 @@ type GetEntitiesRequest struct { func (x *GetEntitiesRequest) Reset() { *x = GetEntitiesRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4427,7 +4543,7 @@ func (x *GetEntitiesRequest) String() string { func (*GetEntitiesRequest) ProtoMessage() {} func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4440,7 +4556,7 @@ func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead. func (*GetEntitiesRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73} } func (x *GetEntitiesRequest) GetEntityType() string { @@ -4480,7 +4596,7 @@ type GetEntitiesResponse struct { func (x *GetEntitiesResponse) Reset() { *x = GetEntitiesResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4492,7 +4608,7 @@ func (x *GetEntitiesResponse) String() string { func (*GetEntitiesResponse) ProtoMessage() {} func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4505,7 +4621,7 @@ func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead. func (*GetEntitiesResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74} } func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition { @@ -4529,7 +4645,7 @@ type EntityDefinition struct { func (x *EntityDefinition) Reset() { *x = EntityDefinition{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4541,7 +4657,7 @@ func (x *EntityDefinition) String() string { func (*EntityDefinition) ProtoMessage() {} func (x *EntityDefinition) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4554,7 +4670,7 @@ func (x *EntityDefinition) ProtoReflect() protoreflect.Message { // Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead. func (*EntityDefinition) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75} } func (x *EntityDefinition) GetTitle() string { @@ -4609,7 +4725,7 @@ type EntityProperty struct { func (x *EntityProperty) Reset() { *x = EntityProperty{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4621,7 +4737,7 @@ func (x *EntityProperty) String() string { func (*EntityProperty) ProtoMessage() {} func (x *EntityProperty) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4634,7 +4750,7 @@ func (x *EntityProperty) ProtoReflect() protoreflect.Message { // Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead. func (*EntityProperty) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76} } func (x *EntityProperty) GetName() string { @@ -4661,7 +4777,7 @@ type GetEntityRequest struct { func (x *GetEntityRequest) Reset() { *x = GetEntityRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4673,7 +4789,7 @@ func (x *GetEntityRequest) String() string { func (*GetEntityRequest) ProtoMessage() {} func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4686,7 +4802,7 @@ func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead. func (*GetEntityRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77} } func (x *GetEntityRequest) GetUniqueKey() string { @@ -4712,7 +4828,7 @@ type RestartActionRequest struct { func (x *RestartActionRequest) Reset() { *x = RestartActionRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4724,7 +4840,7 @@ func (x *RestartActionRequest) String() string { func (*RestartActionRequest) ProtoMessage() {} func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4737,7 +4853,7 @@ func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead. func (*RestartActionRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{78} } func (x *RestartActionRequest) GetExecutionTrackingId() string { @@ -5044,11 +5160,22 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" + "\rLogoutRequest\"\x10\n" + "\x0eLogoutResponse\"\x17\n" + - "\x15GetDiagnosticsRequest\"b\n" + + "\x15GetDiagnosticsRequest\"\xf5\x01\n" + + "\vConfigIssue\x12\x1a\n" + + "\bseverity\x18\x01 \x01(\tR\bseverity\x12\x12\n" + + "\x04code\x18\x02 \x01(\tR\x04code\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x12\x1b\n" + + "\taction_id\x18\x04 \x01(\tR\bactionId\x12!\n" + + "\faction_title\x18\x05 \x01(\tR\vactionTitle\x12#\n" + + "\rargument_name\x18\x06 \x01(\tR\fargumentName\x12\x16\n" + + "\x06source\x18\a \x01(\tR\x06source\x12\x1f\n" + + "\vconfig_file\x18\b \x01(\tR\n" + + "configFile\"\xa5\x01\n" + "\x16GetDiagnosticsResponse\x12 \n" + "\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" + - "\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" + - "\vInitRequest\"\x8d\t\n" + + "\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\x12A\n" + + "\rconfig_issues\x18\x03 \x03(\v2\x1c.olivetin.api.v1.ConfigIssueR\fconfigIssues\"\r\n" + + "\vInitRequest\"\xbb\t\n" + "\fInitResponse\x12\x1e\n" + "\n" + "showFooter\x18\x01 \x01(\bR\n" + @@ -5078,7 +5205,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" + "\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" + "\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\x12>\n" + - "\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\"8\n" + + "\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\x12,\n" + + "\x12config_issue_count\x18\x1a \x01(\x05R\x10configIssueCount\"8\n" + "\x0eAdditionalLink\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\"L\n" + @@ -5159,7 +5287,7 @@ func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte { return file_olivetin_api_v1_olivetin_proto_rawDescData } -var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 85) +var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 86) var file_olivetin_api_v1_olivetin_proto_goTypes = []any{ (*Action)(nil), // 0: olivetin.api.v1.Action (*ActionGroupMembership)(nil), // 1: olivetin.api.v1.ActionGroupMembership @@ -5226,38 +5354,39 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{ (*LogoutRequest)(nil), // 62: olivetin.api.v1.LogoutRequest (*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse (*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest - (*GetDiagnosticsResponse)(nil), // 65: olivetin.api.v1.GetDiagnosticsResponse - (*InitRequest)(nil), // 66: olivetin.api.v1.InitRequest - (*InitResponse)(nil), // 67: olivetin.api.v1.InitResponse - (*AdditionalLink)(nil), // 68: olivetin.api.v1.AdditionalLink - (*OAuth2Provider)(nil), // 69: olivetin.api.v1.OAuth2Provider - (*GetActionBindingRequest)(nil), // 70: olivetin.api.v1.GetActionBindingRequest - (*GetActionBindingResponse)(nil), // 71: olivetin.api.v1.GetActionBindingResponse - (*GetEntitiesRequest)(nil), // 72: olivetin.api.v1.GetEntitiesRequest - (*GetEntitiesResponse)(nil), // 73: olivetin.api.v1.GetEntitiesResponse - (*EntityDefinition)(nil), // 74: olivetin.api.v1.EntityDefinition - (*EntityProperty)(nil), // 75: olivetin.api.v1.EntityProperty - (*GetEntityRequest)(nil), // 76: olivetin.api.v1.GetEntityRequest - (*RestartActionRequest)(nil), // 77: olivetin.api.v1.RestartActionRequest - nil, // 78: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry - nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry - nil, // 80: olivetin.api.v1.ActionArgument.SuggestionsEntry - nil, // 81: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry - nil, // 82: olivetin.api.v1.Entity.FieldsEntry - nil, // 83: olivetin.api.v1.DumpVarsResponse.ContentsEntry - nil, // 84: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + (*ConfigIssue)(nil), // 65: olivetin.api.v1.ConfigIssue + (*GetDiagnosticsResponse)(nil), // 66: olivetin.api.v1.GetDiagnosticsResponse + (*InitRequest)(nil), // 67: olivetin.api.v1.InitRequest + (*InitResponse)(nil), // 68: olivetin.api.v1.InitResponse + (*AdditionalLink)(nil), // 69: olivetin.api.v1.AdditionalLink + (*OAuth2Provider)(nil), // 70: olivetin.api.v1.OAuth2Provider + (*GetActionBindingRequest)(nil), // 71: olivetin.api.v1.GetActionBindingRequest + (*GetActionBindingResponse)(nil), // 72: olivetin.api.v1.GetActionBindingResponse + (*GetEntitiesRequest)(nil), // 73: olivetin.api.v1.GetEntitiesRequest + (*GetEntitiesResponse)(nil), // 74: olivetin.api.v1.GetEntitiesResponse + (*EntityDefinition)(nil), // 75: olivetin.api.v1.EntityDefinition + (*EntityProperty)(nil), // 76: olivetin.api.v1.EntityProperty + (*GetEntityRequest)(nil), // 77: olivetin.api.v1.GetEntityRequest + (*RestartActionRequest)(nil), // 78: olivetin.api.v1.RestartActionRequest + nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry + nil, // 80: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry + nil, // 81: olivetin.api.v1.ActionArgument.SuggestionsEntry + nil, // 82: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry + nil, // 83: olivetin.api.v1.Entity.FieldsEntry + nil, // 84: olivetin.api.v1.DumpVarsResponse.ContentsEntry + nil, // 85: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry } var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 3, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument 2, // 1: olivetin.api.v1.Action.exec_on_webhooks:type_name -> olivetin.api.v1.ActionWebhookExecHint 1, // 2: olivetin.api.v1.Action.groups:type_name -> olivetin.api.v1.ActionGroupMembership - 78, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry - 79, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry + 79, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry + 80, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry 4, // 5: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice - 80, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry + 81, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry 0, // 7: olivetin.api.v1.EntityRelatedAction.action:type_name -> olivetin.api.v1.Action - 81, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry - 82, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry + 82, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry + 83, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry 5, // 10: olivetin.api.v1.Entity.related_actions:type_name -> olivetin.api.v1.EntityRelatedAction 10, // 11: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard 11, // 12: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent @@ -5275,8 +5404,8 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 28, // 24: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup 22, // 25: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry 35, // 26: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget - 83, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry - 84, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + 84, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry + 85, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry 51, // 29: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged 52, // 30: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged 54, // 31: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished @@ -5285,72 +5414,73 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 53, // 34: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat 22, // 35: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry 22, // 36: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry - 69, // 37: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider - 68, // 38: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink - 8, // 39: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy - 0, // 40: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action - 35, // 41: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget - 74, // 42: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition - 6, // 43: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity - 75, // 44: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty - 43, // 45: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding - 9, // 46: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest - 12, // 47: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest - 15, // 48: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest - 17, // 49: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest - 19, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest - 77, // 51: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest - 56, // 52: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest - 34, // 53: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest - 21, // 54: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest - 24, // 55: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest - 26, // 56: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest - 30, // 57: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest - 37, // 58: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest - 39, // 59: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest - 41, // 60: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest - 44, // 61: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest - 46, // 62: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest - 58, // 63: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest - 60, // 64: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest - 62, // 65: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest - 48, // 66: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest - 64, // 67: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest - 66, // 68: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest - 70, // 69: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest - 72, // 70: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest - 76, // 71: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest - 7, // 72: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse - 14, // 73: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse - 16, // 74: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse - 18, // 75: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse - 20, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse - 14, // 77: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse - 57, // 78: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse - 36, // 79: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse - 23, // 80: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse - 25, // 81: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse - 29, // 82: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse - 31, // 83: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse - 38, // 84: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse - 40, // 85: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse - 42, // 86: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse - 45, // 87: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse - 47, // 88: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse - 59, // 89: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse - 61, // 90: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse - 63, // 91: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse - 49, // 92: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse - 65, // 93: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse - 67, // 94: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse - 71, // 95: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse - 73, // 96: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse - 6, // 97: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity - 72, // [72:98] is the sub-list for method output_type - 46, // [46:72] is the sub-list for method input_type - 46, // [46:46] is the sub-list for extension type_name - 46, // [46:46] is the sub-list for extension extendee - 0, // [0:46] is the sub-list for field type_name + 65, // 37: olivetin.api.v1.GetDiagnosticsResponse.config_issues:type_name -> olivetin.api.v1.ConfigIssue + 70, // 38: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider + 69, // 39: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink + 8, // 40: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy + 0, // 41: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action + 35, // 42: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget + 75, // 43: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition + 6, // 44: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity + 76, // 45: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty + 43, // 46: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding + 9, // 47: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest + 12, // 48: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest + 15, // 49: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest + 17, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest + 19, // 51: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest + 78, // 52: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest + 56, // 53: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest + 34, // 54: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest + 21, // 55: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest + 24, // 56: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest + 26, // 57: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest + 30, // 58: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest + 37, // 59: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest + 39, // 60: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest + 41, // 61: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest + 44, // 62: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest + 46, // 63: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest + 58, // 64: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest + 60, // 65: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest + 62, // 66: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest + 48, // 67: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest + 64, // 68: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest + 67, // 69: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest + 71, // 70: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest + 73, // 71: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest + 77, // 72: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest + 7, // 73: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse + 14, // 74: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse + 16, // 75: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse + 18, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse + 20, // 77: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse + 14, // 78: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse + 57, // 79: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse + 36, // 80: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse + 23, // 81: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse + 25, // 82: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse + 29, // 83: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse + 31, // 84: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse + 38, // 85: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse + 40, // 86: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse + 42, // 87: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse + 45, // 88: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse + 47, // 89: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse + 59, // 90: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse + 61, // 91: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse + 63, // 92: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse + 49, // 93: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse + 66, // 94: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse + 68, // 95: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse + 72, // 96: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse + 74, // 97: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse + 6, // 98: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity + 73, // [73:99] is the sub-list for method output_type + 47, // [47:73] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_olivetin_api_v1_olivetin_proto_init() } @@ -5372,7 +5502,7 @@ func file_olivetin_api_v1_olivetin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)), NumEnums: 0, - NumMessages: 85, + NumMessages: 86, NumExtensions: 0, NumServices: 1, }, diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 9f9432e..81b83ee 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -1200,6 +1200,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api res := &apiv1.GetDiagnosticsResponse{ SshFoundKey: installationinfo.Runtime.SshFoundKey, SshFoundConfig: installationinfo.Runtime.SshFoundConfig, + ConfigIssues: api.buildConfigIssuesForUser(user), } return connect.NewResponse(res), nil } @@ -1242,6 +1243,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq LoginRequired: loginRequired, AvailableThemes: discoverAvailableThemes(api.cfg), ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons, + ConfigIssueCount: configIssueCountForUser(api, user), } return connect.NewResponse(res), nil diff --git a/service/internal/api/api_config_issues_test.go b/service/internal/api/api_config_issues_test.go new file mode 100644 index 0000000..f142593 --- /dev/null +++ b/service/internal/api/api_config_issues_test.go @@ -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") +} diff --git a/service/internal/api/config_issues.go b/service/internal/api/config_issues.go new file mode 100644 index 0000000..306fd0b --- /dev/null +++ b/service/internal/api/config_issues.go @@ -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, + } +} diff --git a/service/internal/config/config.go b/service/internal/config/config.go index de85262..4b15077 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -39,6 +39,8 @@ type Action struct { EnabledExpression string `koanf:"enabledExpression"` Groups []string `koanf:"groups"` Justification string `koanf:"justification"` + // SourceFile is set by OliveTin when loading config (not user YAML). + SourceFile string `koanf:"x-olivetin-source-file"` } func (action *Action) RequiresJustification() bool { @@ -110,6 +112,8 @@ type EntityFile struct { Name string `koanf:"name"` Icon string `koanf:"icon"` Properties []EntityProperty `koanf:"properties"` + // SourceFile is set by OliveTin when loading config (not user YAML). + SourceFile string `koanf:"x-olivetin-source-file"` } // EntityProperty defines a column shown when listing entity instances in the UI. diff --git a/service/internal/config/config_reloader.go b/service/internal/config/config_reloader.go index bc8631d..39a2484 100644 --- a/service/internal/config/config_reloader.go +++ b/service/internal/config/config_reloader.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" "reflect" @@ -8,6 +9,7 @@ import ( "sort" "strings" + "github.com/OliveTin/OliveTin/internal/configissues" "github.com/go-viper/mapstructure/v2" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" @@ -40,6 +42,8 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) { "configPath": configPath, }).Info("Appending cfg source") + configissues.BeginConfigLoad() + stampLoadedConfigSources(k, configPath) loadIncludedConfigsFromDir(k, configPath) if !unmarshalRoot(k, cfg) { @@ -172,7 +176,7 @@ func listYamlFiles(includePath string) ([]string, bool) { func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) { filePath := filepath.Join(includePath, filename) - if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil { + if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFuncForSource(filePath))); err != nil { log.Errorf("Error loading included config file %s: %v", filePath, err) return } @@ -182,8 +186,15 @@ func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) { }).Info("Successfully loaded included config file") } +func mergeFuncForSource(sourceFile string) func(src, dest map[string]interface{}) error { + return func(src map[string]interface{}, dest map[string]interface{}) error { + return mergeFunc(src, dest, sourceFile) + } +} + // mergeActionsWhenBothExist merges actions when both src and dest have actions. -func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}) { +func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}, sourceFile string) { + stampSourceOnMaps(srcActions, sourceFile) srcSlice, ok1 := srcActions.([]interface{}) destSlice, ok2 := destActions.([]interface{}) if ok1 && ok2 { @@ -194,10 +205,11 @@ func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, } // mergeActionsFromSource merges actions from source into destination. -func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}) { +func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}, sourceFile string) { if destActions, ok := dest["actions"]; ok { - mergeActionsWhenBothExist(srcActions, destActions, dest) + mergeActionsWhenBothExist(srcActions, destActions, dest, sourceFile) } else { + stampSourceOnMaps(srcActions, sourceFile) dest["actions"] = srcActions } } @@ -223,7 +235,8 @@ func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interf } // mergeEntitiesWhenBothExist merges entities when both src and dest have entities. -func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}) { +func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}, sourceFile string) { + stampSourceOnMaps(srcEntities, sourceFile) srcSlice, ok1 := srcEntities.([]interface{}) destSlice, ok2 := destEntities.([]interface{}) if ok1 && ok2 { @@ -234,17 +247,18 @@ func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{ } // mergeEntitiesFromSource merges entities from source into destination. -func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}) { +func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}, sourceFile string) { if destEntities, ok := dest["entities"]; ok { - mergeEntitiesWhenBothExist(srcEntities, destEntities, dest) + mergeEntitiesWhenBothExist(srcEntities, destEntities, dest, sourceFile) } else { + stampSourceOnMaps(srcEntities, sourceFile) dest["entities"] = srcEntities } } -func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error { +func mergeFunc(src map[string]interface{}, dest map[string]interface{}, sourceFile string) error { if srcActions, ok := src["actions"]; ok { - mergeActionsFromSource(srcActions, dest) + mergeActionsFromSource(srcActions, dest, sourceFile) } if srcDashboards, ok := src["dashboards"]; ok { @@ -252,7 +266,7 @@ func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error { } if srcEntities, ok := src["entities"]; ok { - mergeEntitiesFromSource(srcEntities, dest) + mergeEntitiesFromSource(srcEntities, dest, sourceFile) } return nil @@ -285,7 +299,12 @@ func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error val, set := os.LookupEnv(key) log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val) if !set { - log.Warnf("Config file references unset environment variable: \"%s\"", key) + configissues.ReportSticky(configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeEnvUnset, + Message: fmt.Sprintf("Config file references unset environment variable %q", key), + Source: key, + }) } return val }) diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index c085f23..9b9e3fb 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -5,6 +5,7 @@ import ( "strings" "text/template" + "github.com/OliveTin/OliveTin/internal/configissues" "github.com/OliveTin/OliveTin/internal/env" "github.com/google/uuid" log "github.com/sirupsen/logrus" @@ -29,7 +30,6 @@ func (cfg *Config) Sanitize() { cfg.sanitizeDashboardsForInlineActions() cfg.sanitizeActionGroups() - cfg.sanitizeActionGroupReferences() cfg.sanitizeEntities() if err := cfg.validateReservedActionArgumentNames(); err != nil { @@ -238,7 +238,7 @@ func (action *Action) sanitize(cfg *Config) { action.Groups = dedupeStrings(action.Groups) for idx := range action.Arguments { - action.Arguments[idx].sanitize() + action.Arguments[idx].sanitize(action) } } @@ -283,14 +283,6 @@ func (cfg *Config) sanitizeActionGroups() { } } -func (cfg *Config) sanitizeActionGroupReferences() { - for _, action := range cfg.Actions { - for _, groupName := range action.Groups { - cfg.warnInvalidActionGroupReference(action, groupName) - } - } -} - func (cfg *Config) sanitizeEntities() { for _, entityFile := range cfg.Entities { if entityFile == nil { @@ -310,24 +302,6 @@ func sanitizeEntityProperties(entityFile *EntityFile) { } } -func (cfg *Config) warnInvalidActionGroupReference(action *Action, groupName string) { - group, found := cfg.ActionGroups[groupName] - if !found { - log.WithFields(log.Fields{ - "actionTitle": action.Title, - "groupName": groupName, - }).Warn("Action references unknown action group") - return - } - - if group == nil || group.MaxConcurrent < 1 { - log.WithFields(log.Fields{ - "actionTitle": action.Title, - "groupName": groupName, - }).Warn("Action references action group that will not be enforced at runtime") - } -} - func (cfg *Config) sanitizeAuthRequireGuestsToLogin() { if cfg.AuthRequireGuestsToLogin { log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false") @@ -505,7 +479,7 @@ func (cfg *Config) sanitizeOnClickDefaults() { cfg.DefaultPopupOnStart = cfg.DefaultOnClick } -func (arg *ActionArgument) sanitize() { +func (arg *ActionArgument) sanitize(action *Action) { if arg.Title == "" { arg.Title = arg.Name } @@ -516,45 +490,24 @@ func (arg *ActionArgument) sanitize() { } } - arg.sanitizeNoType() - arg.sanitizeChecklist() - - // Default value validation runs in executor at config load (validateArgumentDefaults). + arg.sanitizeNoType(action) + // Checklist configuration problems are reported by configcheck. } -func (arg *ActionArgument) sanitizeChecklist() { - if arg.Type != "checklist" { - return - } - - arg.warnMissingChecklistChoices() - arg.warnInvalidChecklistEntityTemplate() -} - -func (arg *ActionArgument) warnMissingChecklistChoices() { - if len(arg.Choices) == 0 { - log.WithFields(log.Fields{ - "arg": arg.Name, - }).Warn("Checklist argument has no choices defined") - } -} - -func (arg *ActionArgument) warnInvalidChecklistEntityTemplate() { - if arg.Entity == "" || len(arg.Choices) == 1 { - return - } - - log.WithFields(log.Fields{ - "arg": arg.Name, - "entity": arg.Entity, - }).Warn("Checklist argument with entity should define exactly one choice template") -} - -func (arg *ActionArgument) sanitizeNoType() { +func (arg *ActionArgument) sanitizeNoType(action *Action) { if len(arg.Choices) == 0 && arg.Type == "" { - log.WithFields(log.Fields{ - "arg": arg.Name, - }).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.") + issue := configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeArgTypeUnset, + Message: "Argument type isn't set, will default to 'ascii' but this may not be safe", + ArgumentName: arg.Name, + } + if action != nil { + issue.ActionID = action.ID + issue.ActionTitle = action.Title + issue.ConfigFile = action.SourceFile + } + configissues.ReportSticky(issue) arg.Type = "ascii" } } diff --git a/service/internal/config/source_file.go b/service/internal/config/source_file.go new file mode 100644 index 0000000..817ec52 --- /dev/null +++ b/service/internal/config/source_file.go @@ -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) + } +} diff --git a/service/internal/config/source_file_test.go b/service/internal/config/source_file_test.go new file mode 100644 index 0000000..a237c55 --- /dev/null +++ b/service/internal/config/source_file_test.go @@ -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) +} diff --git a/service/internal/configcheck/rebuild.go b/service/internal/configcheck/rebuild.go new file mode 100644 index 0000000..9063fee --- /dev/null +++ b/service/internal/configcheck/rebuild.go @@ -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, + }} +} diff --git a/service/internal/configcheck/rebuild_test.go b/service/internal/configcheck/rebuild_test.go new file mode 100644 index 0000000..339484f --- /dev/null +++ b/service/internal/configcheck/rebuild_test.go @@ -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 +} diff --git a/service/internal/configcheck/watcher_path_test.go b/service/internal/configcheck/watcher_path_test.go new file mode 100644 index 0000000..2827cca --- /dev/null +++ b/service/internal/configcheck/watcher_path_test.go @@ -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") +} diff --git a/service/internal/configissues/issue.go b/service/internal/configissues/issue.go new file mode 100644 index 0000000..63489f1 --- /dev/null +++ b/service/internal/configissues/issue.go @@ -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 +} diff --git a/service/internal/configissues/store.go b/service/internal/configissues/store.go new file mode 100644 index 0000000..e83871e --- /dev/null +++ b/service/internal/configissues/store.go @@ -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) +} diff --git a/service/internal/configissues/store_test.go b/service/internal/configissues/store_test.go new file mode 100644 index 0000000..b33c3d2 --- /dev/null +++ b/service/internal/configissues/store_test.go @@ -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()) +} diff --git a/service/internal/entities/entities.go b/service/internal/entities/entities.go index 386fb20..0cbd7d9 100644 --- a/service/internal/entities/entities.go +++ b/service/internal/entities/entities.go @@ -30,13 +30,18 @@ func AddListener(l func()) { } func SetupEntityFileWatchers(cfg *config.Config) { - baseDir := resolveEntitiesBaseDir(cfg.GetDir()) + baseDir := ResolveEntitiesBaseDir(cfg.GetDir()) for i := range cfg.Entities { // #337 - iterate by key, not by value ef := cfg.Entities[i] watchAndLoadEntity(baseDir, ef) } } +// ResolveEntitiesBaseDir returns the directory used to resolve relative entity file paths. +func ResolveEntitiesBaseDir(configDir string) string { + return resolveEntitiesBaseDir(configDir) +} + //gocyclo:ignore func resolveEntitiesBaseDir(configDir string) string { absConfigDir, err := filepath.Abs(configDir) @@ -64,15 +69,28 @@ func watchAndLoadEntity(baseDir string, ef *config.EntityFile) { p = filepath.Join(baseDir, p) log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path") } - go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }) + go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }, filehelper.WatchMeta{ + ConfigFile: ef.SourceFile, + }) loadEntityFile(p, ef.Name) } func loadEntityFile(filename string, entityname string) { + defer func() { + MarkEntityLoadAttempted(entityname) + notifyEntityListeners() + }() + if strings.HasSuffix(filename, ".json") { loadEntityFileJson(filename, entityname) - } else { - loadEntityFileYaml(filename, entityname) + return + } + loadEntityFileYaml(filename, entityname) +} + +func notifyEntityListeners() { + for _, l := range listeners { + l() } } @@ -86,6 +104,7 @@ func loadEntityFileJson(filename string, entityname string) { if err != nil { log.Errorf("ReadIn: %v", err) + ClearEntitiesOfType(entityname) return } @@ -100,13 +119,14 @@ func loadEntityFileJson(filename string, entityname string) { if err != nil { log.Errorf("%v", err) + ClearEntitiesOfType(entityname) return } data = append(data, d) } - updateSvFromFile(entityname, data) + replaceEntitiesFromFile(entityname, data) } func loadEntityFileYaml(filename string, entityname string) { @@ -119,6 +139,7 @@ func loadEntityFileYaml(filename string, entityname string) { if err != nil { log.Errorf("ReadIn: %v", err) + ClearEntitiesOfType(entityname) return } @@ -128,21 +149,31 @@ func loadEntityFileYaml(filename string, entityname string) { if err != nil { log.Errorf("Unmarshal: %v", err) + ClearEntitiesOfType(entityname) return } - updateSvFromFile(entityname, data) + replaceEntitiesFromFile(entityname, data) } -func updateSvFromFile(entityname string, data []map[string]any) { - ClearEntitiesOfType(entityname) +func replaceEntitiesFromFile(entityname string, data []map[string]any) { + rwmutex.Lock() + defer rwmutex.Unlock() - for i, mapp := range data { - AddEntity(entityname, fmt.Sprintf("%d", i), mapp) + delete(entities, entityname) + + if len(data) == 0 { + return } - for _, l := range listeners { - l() + entities[entityname] = make(entityInstancesByKey, 0) + for i, mapp := range data { + entityKey := fmt.Sprintf("%d", i) + entities[entityname][entityKey] = &Entity{ + Data: mapp, + UniqueKey: entityKey, + Title: findEntityTitle(mapp), + } } } diff --git a/service/internal/entities/load_state.go b/service/internal/entities/load_state.go new file mode 100644 index 0000000..979bc75 --- /dev/null +++ b/service/internal/entities/load_state.go @@ -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{} +} diff --git a/service/internal/executor/argument_defaults_test.go b/service/internal/executor/argument_defaults_test.go new file mode 100644 index 0000000..b846c36 --- /dev/null +++ b/service/internal/executor/argument_defaults_test.go @@ -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)) +} diff --git a/service/internal/executor/executor_actions.go b/service/internal/executor/executor_actions.go index 75017d3..08a129e 100644 --- a/service/internal/executor/executor_actions.go +++ b/service/internal/executor/executor_actions.go @@ -3,10 +3,12 @@ package executor import ( "crypto/sha256" "fmt" + "strings" - config "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configcheck" + "github.com/OliveTin/OliveTin/internal/configissues" "github.com/OliveTin/OliveTin/internal/entities" - log "github.com/sirupsen/logrus" ) func (e *Executor) FindBindingByID(id string) *ActionBinding { @@ -40,40 +42,54 @@ type RebuildActionMapRequest struct { dashboardTargets *dashboardTargetIndex } -func validateArgumentDefaults(cfg *config.Config) { +func validateArgumentDefaults(cfg *config.Config) []configissues.Issue { if cfg == nil { - return + return nil } + out := make([]configissues.Issue, 0) for _, action := range cfg.Actions { - validateActionArgumentDefaults(action) + out = append(out, validateActionArgumentDefaults(action)...) } + return out } -func validateActionArgumentDefaults(action *config.Action) { +func validateActionArgumentDefaults(action *config.Action) []configissues.Issue { if action == nil { - return + return nil } + out := make([]configissues.Issue, 0) for i := range action.Arguments { - validateArgumentDefault(action, &action.Arguments[i]) + if issue := validateArgumentDefault(action, &action.Arguments[i]); issue != nil { + out = append(out, *issue) + } } + return out } -func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) { +func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) *configissues.Issue { if arg.Default == "" { - return + return nil + } + if strings.Contains(arg.Default, "{{") { + return nil } if err := ValidateArgument(arg, arg.Default, action); err != nil { - log.WithFields(log.Fields{ - "actionTitle": action.Title, - "argName": arg.Name, - "default": arg.Default, - "error": err, - }).Warn("Argument default value failed validation") + return &configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeArgDefaultInvalid, + Message: fmt.Sprintf("Argument default value failed validation: %v", err), + ActionID: action.ID, + ActionTitle: action.Title, + ArgumentName: arg.Name, + Source: arg.Default, + ConfigFile: action.SourceFile, + } } + return nil } func (e *Executor) RebuildActionMap() { - validateArgumentDefaults(e.Cfg) + defaultIssues := validateArgumentDefaults(e.Cfg) e.MapActionBindingsLock.Lock() @@ -94,6 +110,8 @@ func (e *Executor) RebuildActionMap() { e.MapActionBindingsLock.Unlock() + configcheck.Rebuild(e.Cfg, defaultIssues...) + for _, l := range e.copyListeners() { l.OnActionMapRebuilt() } diff --git a/service/internal/filehelper/file_change_notify.go b/service/internal/filehelper/file_change_notify.go index a39eaa1..5173903 100644 --- a/service/internal/filehelper/file_change_notify.go +++ b/service/internal/filehelper/file_change_notify.go @@ -1,12 +1,14 @@ package filehelper import ( - "github.com/fsnotify/fsnotify" - log "github.com/sirupsen/logrus" + "fmt" "path/filepath" + "sync" "time" - "sync" + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/fsnotify/fsnotify" + log "github.com/sirupsen/logrus" ) var ( @@ -28,33 +30,44 @@ const ( debounceDelay = 300 * time.Millisecond ) +// WatchMeta attaches action context to watcher setup failures so Diagnostics +// can apply the same action view ACL as other configuration issues. +type WatchMeta struct { + ActionID string + ActionTitle string + ConfigFile string +} + type watchContext struct { filename string filedir string callback func(filename string) interestedEvent fsnotify.Op event *fsnotify.Event + meta WatchMeta } -func WatchDirectoryCreate(fullpath string, callback func(filename string)) { +func WatchDirectoryCreate(fullpath string, callback func(filename string), meta WatchMeta) { watchPath(&watchContext{ filedir: fullpath, filename: "", callback: callback, interestedEvent: fsnotify.Create, + meta: meta, }) } -func WatchDirectoryWrite(fullpath string, callback func(filename string)) { +func WatchDirectoryWrite(fullpath string, callback func(filename string), meta WatchMeta) { watchPath(&watchContext{ filedir: fullpath, filename: "", callback: callback, interestedEvent: fsnotify.Write, + meta: meta, }) } -func WatchFileWrite(fullpath string, callback func(filename string)) { +func WatchFileWrite(fullpath string, callback func(filename string), meta WatchMeta) { filename := filepath.Base(fullpath) filedir := filepath.Dir(fullpath) @@ -63,69 +76,94 @@ func WatchFileWrite(fullpath string, callback func(filename string)) { filename: filename, callback: callback, interestedEvent: fsnotify.Write, + meta: meta, }) } func watchPath(ctx *watchContext) { watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Errorf("Could not watch for files being created: %v", err) + reportWatcherFailure(ctx, err) return } - defer func() { - if err := watcher.Close(); err != nil { - log.Errorf("Failed to close file watcher: %v", err) - } - }() + defer closeWatcher(watcher) - done := make(chan bool) - - go func() { - for { - processEvent(ctx, watcher) - } - }() - - err = watcher.Add(ctx.filedir) - - if err != nil { - log.Errorf("Could not create watcher: %v", err) + if err := watcher.Add(ctx.filedir); err != nil { + reportWatcherFailure(ctx, err) + return } - <-done + for processEvent(ctx, watcher) { + } } -func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) { +func closeWatcher(watcher *fsnotify.Watcher) { + if err := watcher.Close(); err != nil { + log.Errorf("Failed to close file watcher: %v", err) + } +} + +func watchTarget(ctx *watchContext) string { + if ctx.filename == "" { + return ctx.filedir + } + return filepath.Join(ctx.filedir, ctx.filename) +} + +func reportWatcherFailure(ctx *watchContext, err error) { + path := watchTarget(ctx) + message := fmt.Sprintf("Could not create watcher for %q: %v", path, err) + configissues.Report(configissues.Issue{ + Severity: configissues.SeverityError, + Code: configissues.CodeWatcherPath, + Message: message, + ActionID: ctx.meta.ActionID, + ActionTitle: ctx.meta.ActionTitle, + Source: path, + ConfigFile: ctx.meta.ConfigFile, + }) +} + +// processEvent waits for one watcher event. It returns false when the watcher +// channels are closed so the caller can stop looping. +func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) bool { select { case event, ok := <-watcher.Events: - ctx.event = &event - - if !consumeEvent(ok, ctx) { - return - } - - break - case err := <-watcher.Errors: - log.Errorf("Error in fsnotify: %v", err) - return + return handleWatcherEvent(ctx, event, ok) + case err, ok := <-watcher.Errors: + return handleWatcherError(ctx, err, ok) } } -func consumeEvent(ok bool, ctx *watchContext) bool { +func handleWatcherEvent(ctx *watchContext, event fsnotify.Event, ok bool) bool { if !ok { return false } + ctx.event = &event + consumeEvent(ctx) + return true +} +func handleWatcherError(ctx *watchContext, err error, ok bool) bool { + if !ok { + return false + } + if err != nil { + log.WithFields(log.Fields{ + "path": watchTarget(ctx), + }).Errorf("Error in fsnotify: %v", err) + } + return true +} + +func consumeEvent(ctx *watchContext) { if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename { log.Tracef("fsnotify irreleventa event different file %+v", ctx.event) - return true + return } consumeRelevantEvents(ctx) - - return true } func consumeRelevantEvents(ctx *watchContext) { diff --git a/service/internal/filehelper/watcher_path_test.go b/service/internal/filehelper/watcher_path_test.go new file mode 100644 index 0000000..a79af2a --- /dev/null +++ b/service/internal/filehelper/watcher_path_test.go @@ -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) +} diff --git a/service/internal/oncalendarfile/calendar.go b/service/internal/oncalendarfile/calendar.go index 21d8ba0..32bbc6d 100644 --- a/service/internal/oncalendarfile/calendar.go +++ b/service/internal/oncalendarfile/calendar.go @@ -37,7 +37,11 @@ func Schedule(cfg *config.Config, ex *executor.Executor) { parseCalendarFile(captured, cfg, ex, filename) } - go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x) + go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x, filehelper.WatchMeta{ + ActionID: action.ID, + ActionTitle: action.Title, + ConfigFile: action.SourceFile, + }) x(action.ExecOnCalendarFile) } diff --git a/service/internal/onfileindir/fileindir.go b/service/internal/onfileindir/fileindir.go index b6085d1..ca786d6 100644 --- a/service/internal/onfileindir/fileindir.go +++ b/service/internal/onfileindir/fileindir.go @@ -17,7 +17,7 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) { go func(act *config.Action, dir string) { filehelper.WatchDirectoryCreate(dir, func(filename string) { scheduleExec(act, cfg, ex, filename) - }) + }, watchMetaForAction(act)) }(action, dirname) } for _, dirname := range action.ExecOnFileChangedInDir { @@ -27,12 +27,20 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) { go func(act *config.Action, dir string) { filehelper.WatchDirectoryWrite(dir, func(filename string) { scheduleExec(act, cfg, ex, filename) - }) + }, watchMetaForAction(act)) }(action, dirname) } } } +func watchMetaForAction(action *config.Action) filehelper.WatchMeta { + return filehelper.WatchMeta{ + ActionID: action.ID, + ActionTitle: action.Title, + ConfigFile: action.SourceFile, + } +} + func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Executor, path string) { args := map[string]string{ "filepath": path, diff --git a/service/internal/tpl/check_template_test.go b/service/internal/tpl/check_template_test.go new file mode 100644 index 0000000..1ed8208 --- /dev/null +++ b/service/internal/tpl/check_template_test.go @@ -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) +} diff --git a/service/internal/tpl/templates.go b/service/internal/tpl/templates.go index e2e23d3..f2ca256 100644 --- a/service/internal/tpl/templates.go +++ b/service/internal/tpl/templates.go @@ -219,6 +219,25 @@ func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string return result } +// CheckActionTemplateParses migrates legacy template syntax and reports parse +// errors only (execution against entity/argument data is not attempted). +func CheckActionTemplateParses(source string) error { + if source == "" { + return nil + } + + source = migrateLegacyArgumentNames(source) + source = migrateLegacyEntityProperties(source) + + clone, err := tpl.Clone() + if err != nil { + return err + } + + _, err = clone.Parse(source) + return err +} + /* func ParseTemplateBoolWith(source string, ent *entities.Entity) bool { source = strings.TrimSpace(source)