diff --git a/application/src/main/data/json/system/widget_bundles/air_quality.json b/application/src/main/data/json/system/widget_bundles/air_quality.json deleted file mode 100644 index 534b509dba..0000000000 --- a/application/src/main/data/json/system/widget_bundles/air_quality.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "widgetsBundle": { - "alias": "air_quality", - "title": "Air quality", - "image": "tb-image:YWlyX3F1YWxpdHlfd2lkZ2V0c19idW5kbGUuc3Zn:YWlyX3F1YWxpdHlfd2lkZ2V0c19idW5kbGUuc3Zn;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijc2IiBmaWxsPSIjZmZmIiByeD0iNCIgLz4KICA8cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9Ijc1IiB4PSIuNSIgeT0iLjUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiByeD0iMy41IiAvPgogIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjkiCiAgICBkPSJNMTIgMTYuMWgxYTIuMyAyLjMgMCAwIDEtLjQgMSAxLjkgMS45IDAgMCAxLS43LjdsLTEuMi4zYy0uNCAwLS43IDAtMS0uMmEyIDIgMCAwIDEtLjctLjUgMi40IDIuNCAwIDAgMS0uNC0uOWwtLjItMS4xdi0uNWEzLjYgMy42IDAgMCAxIC4yLTFjMC0uNC4yLS43LjQtMWwuOC0uNSAxLS4yYy40IDAgLjguMSAxIC4zLjQuMi42LjQuOC43LjIuMy4zLjYuMyAxaC0xYTEuOCAxLjggMCAwIDAtLjEtLjYuOS45IDAgMCAwLS40LS40bC0uNy0uMi0uNi4xYTEuMSAxLjEgMCAwIDAtLjQuNCAxLjggMS44IDAgMCAwLS4yLjYgMy42IDMuNiAwIDAgMC0uMS44djEuM2wuMy42LjQuNC42LjEuNy0uMWEuOS45IDAgMCAwIC40LS40IDEuOCAxLjggMCAwIDAgLjEtLjdabTYuNi0xLjF2LjNjMCAuNCAwIC44LS4yIDEuMmEyLjUgMi41IDAgMCAxLS41LjggMiAyIDAgMCAxLS43LjZsLTEgLjJjLS40IDAtLjYgMC0xLS4yYTIuMSAyLjEgMCAwIDEtLjctLjYgMi42IDIuNiAwIDAgMS0uNS0uOCAzLjYgMy42IDAgMCAxLS4xLTEuMlYxNWwuMS0xLjIuNS0uOGMuMi0uMy41LS40LjgtLjZsMS0uMi45LjIuNy42LjUuOC4yIDEuMlptLTEgLjN2LTEuMWwtLjQtLjdhMS4xIDEuMSAwIDAgMC0uNC0uMyAxLjMgMS4zIDAgMCAwLS42LS4yYy0uMiAwLS40IDAtLjUuMi0uMiAwLS40LjItLjUuM2ExLjggMS44IDAgMCAwLS4zLjcgMy40IDMuNCAwIDAgMCAwIC44djEuMmwuMy42LjUuNGguNWExLjMgMS4zIDAgMCAwIC42IDBsLjQtLjQuMy0uNi4xLS45Wm01LjkgMnYuN2gtMy44di0uNmwxLjgtMmE3IDcgMCAwIDAgLjUtLjdsLjItLjRhMS4xIDEuMSAwIDAgMCAwLS45LjcuNyAwIDAgMC0uMy0uMy45LjkgMCAwIDAtLjQtLjEgMSAxIDAgMCAwLS42LjEuOS45IDAgMCAwLS4zLjRsLS4xLjVoLTFsLjMtLjkuNi0uNmEyIDIgMCAwIDEgMS0uM2wxIC4yLjcuNi4yLjgtLjEuNS0uMy42YTQuNSA0LjUgMCAwIDEtLjMuNWwtLjUuNS0xLjIgMS4zaDIuNlptNC4zLTUuM3Y2aC0xdi02aDFabTMuMSA2aC0uOGExLjkgMS45IDAgMCAxLS43LS41IDIgMiAwIDAgMS0uNC0uN0wyOSAxNlYxNmwuMS0xYTIgMiAwIDAgMSAuNC0uNmwuNi0uNS44LS4xLjguMS42LjUuMy42LjEuOXYuNGgtMy4zdi0uN2gyLjRhMS4zIDEuMyAwIDAgMC0uMS0uNi44LjggMCAwIDAtLjMtLjMuOC44IDAgMCAwLS41LS4yLjguOCAwIDAgMC0uNy40bC0uMi41YTIuNSAyLjUgMCAwIDAgMCAuNnYuNmwuMi41YTEgMSAwIDAgMCAuOS4zIDEuMyAxLjMgMCAwIDAgMS0uNWwuNS41LS4zLjQtLjYuM0gzMVptNC0uNyAxLTMuNWgxTDM1LjUgMThoLS43bC4xLS43Wm0tLjgtMy41IDEgMy41LjEuN2gtLjZsLTEuNS00LjJoMVptNS41IDQuM2MtLjMgMC0uNiAwLS45LS4yYTEuOSAxLjkgMCAwIDEtLjYtLjQgMiAyIDAgMCAxLS40LS43bC0uMi0uOFYxNmMwLS40IDAtLjcuMi0xIDAtLjIuMi0uNC40LS42bC42LS41LjgtLjEuNy4xYy4zLjEuNS4zLjYuNWwuMy42LjIuOXYuNEgzOHYtLjdoMi4zYTEuMyAxLjMgMCAwIDAgMC0uNi44LjggMCAwIDAtLjQtLjMuOC44IDAgMCAwLS41LS4yLjguOCAwIDAgMC0uNC4xLjguOCAwIDAgMC0uMy4zbC0uMi41YTIuNSAyLjUgMCAwIDAgMCAuNnYuNmwuMy41LjMuMi41LjFhMS4zIDEuMyAwIDAgMCAxLS41bC42LjVhMS44IDEuOCAwIDAgMS0xIC43aC0uNlptMy42LTYuMXY2aC0uOXYtNmgxWiIgLz4KICA8cGF0aCBmaWxsPSIjM0ZBNzFBIgogICAgZD0iTTY1LjYgNDl2Mi4ySDUzLjN2LTEuN2w3LjMtMTEuNmgyLjNsLTIuNSA0LjItNC4yIDYuOGg5LjRabS0yLTExdjE3aC0zVjM4aDNabTE1IDd2Mi44YzAgMS40IDAgMi41LS4zIDMuNWE2IDYgMCAwIDEtMS4yIDIuMiA0LjQgNC40IDAgMCAxLTEuNyAxLjMgNiA2IDAgMCAxLTIuMy40IDYuNiA2LjYgMCAwIDEtMS45LS4yIDQuNiA0LjYgMCAwIDEtMS42LS44IDQuOCA0LjggMCAwIDEtMS4xLTEuNSA4IDggMCAwIDEtLjgtMmMtLjItLjktLjItMS44LS4yLTIuOVY0NWMwLTEuMyAwLTIuNC40LTMuNGE2IDYgMCAwIDEgMS0yLjIgNC4zIDQuMyAwIDAgMSAxLjgtMS4zYy43LS4zIDEuNS0uNCAyLjMtLjQuNyAwIDEuNCAwIDIgLjNhNC40IDQuNCAwIDAgMSAxLjUuNyA3IDcgMCAwIDEgMiAzLjVsLjEgMi44Wm0tMi44IDMuMnYtNS40bC0uNC0xLjNhMi42IDIuNiAwIDAgMC0uNi0uOSAyIDIgMCAwIDAtLjgtLjUgMi43IDIuNyAwIDAgMC0xLS4yYy0uNCAwLS44LjEtMS4xLjMtLjQuMi0uNy40LS45LjgtLjIuNC0uNC44LS41IDEuNGwtLjIgMi4ydjMuNmwuMSAxLjhjMCAuNi4yIDEgLjQgMS40bC41IDEgLjguNCAxIC4yIDEuMi0uM2MuMy0uMS42LS40LjgtLjguMy0uNC40LS45LjUtMS41LjItLjYuMi0xLjMuMi0yLjJaTTkyLjMgNDV2Mi44YzAgMS40LS4xIDIuNS0uNCAzLjVhNiA2IDAgMCAxLTEuMSAyLjIgNC40IDQuNCAwIDAgMS0xLjggMS4zIDYgNiAwIDAgMS0yLjMuNCA2LjYgNi42IDAgMCAxLTEuOS0uMiA0LjYgNC42IDAgMCAxLTEuNS0uOCA0LjggNC44IDAgMCAxLTEuMi0xLjUgOCA4IDAgMCAxLS43LTJjLS4yLS45LS4zLTEuOC0uMy0yLjlWNDVjMC0xLjMuMS0yLjQuNC0zLjRhNiA2IDAgMCAxIDEuMS0yLjIgNC4zIDQuMyAwIDAgMSAxLjgtMS4zYy43LS4zIDEuNC0uNCAyLjMtLjQuNyAwIDEuMyAwIDEuOS4zYTQuNCA0LjQgMCAwIDEgMS41LjdjLjUuNC45LjkgMS4yIDEuNS4zLjUuNiAxLjIuNyAyIC4yLjguMyAxLjguMyAyLjhabS0yLjggMy4ydi0zLjZjMC0uNyAwLTEuMy0uMi0xLjggMC0uNS0uMS0xLS4zLTEuM2EyLjYgMi42IDAgMCAwLS42LS45IDIgMiAwIDAgMC0uNy0uNSAyLjcgMi43IDAgMCAwLTEtLjJjLS41IDAtLjguMS0xLjIuM2wtLjkuOGMtLjIuNC0uNC44LS41IDEuNGwtLjIgMi4ydjMuNmMwIC43IDAgMS4zLjIgMS44IDAgLjYuMSAxIC4zIDEuNC4yLjQuMy43LjYgMWwuNy40IDEgLjJjLjUgMCAuOS0uMSAxLjItLjMuMy0uMS42LS40LjktLjhsLjUtMS41LjItMi4yWm0xNC4zLTMuNHYxNUgxMDFWNDIuNGgyLjZsLjIgMi41Wm04LjIgMy43di4zYzAgMSAwIDEuOC0uMyAyLjZhNi4zIDYuMyAwIDAgMS0xIDIgNC40IDQuNCAwIDAgMS0xLjUgMS40Yy0uNi4zLTEuMy40LTIgLjQtLjkgMC0xLjYtLjEtMi4xLS40YTQuMyA0LjMgMCAwIDEtMS41LTEuMyA2LjggNi44IDAgMCAxLTEtMS45IDEyLjIgMTIuMiAwIDAgMS0uNS0yLjR2LTFjMC0uOC4yLTEuNy41LTIuNC4yLS44LjYtMS41IDEtMmE0LjIgNC4yIDAgMCAxIDEuNS0xLjNjLjUtLjMgMS4yLS40IDItLjRzMS41LjEgMi4xLjVhNC4zIDQuMyAwIDAgMSAxLjYgMS4zYy40LjUuNyAxLjIgMSAybC4yIDIuNlptLTIuOC4zdi0uM2wtLjEtMS42YTQuMyA0LjMgMCAwIDAtLjYtMS4zIDIuNSAyLjUgMCAwIDAtLjktMWwtMS4zLS4yYy0uNSAwLS45IDAtMS4yLjJsLTEgLjdhMy4zIDMuMyAwIDAgMC0uNSAxbC0uMyAxLjNWNTBsLjUgMS41Yy4yLjUuNS45IDEgMS4yLjQuMiAxIC40IDEuNi40LjUgMCAuOS0uMSAxLjMtLjNsLjgtMWMuMy0uMy40LS44LjYtMS4zYTcuNyA3LjcgMCAwIDAgLjEtMS42Wm04LTR2MTVoLTIuN1Y0Mi40aDIuNmwuMiAyLjVabTguMyAzLjd2LjNjMCAxIDAgMS44LS4zIDIuNmE2LjMgNi4zIDAgMCAxLTEgMiA0LjQgNC40IDAgMCAxLTEuNSAxLjRjLS42LjMtMS4zLjQtMiAuNC0uOSAwLTEuNi0uMS0yLjEtLjRhNC4zIDQuMyAwIDAgMS0xLjUtMS4zIDYuOCA2LjggMCAwIDEtMS0xLjkgMTIuMiAxMi4yIDAgMCAxLS41LTIuNHYtMWMwLS44LjItMS43LjUtMi40LjItLjguNi0xLjUgMS0yYTQuMiA0LjIgMCAwIDEgMS41LTEuM2MuNS0uMyAxLjItLjQgMi0uNHMxLjUuMSAyLjEuNWE0LjMgNC4zIDAgMCAxIDEuNiAxLjNjLjQuNS43IDEuMiAxIDJsLjIgMi42Wm0tMi44LjN2LS4zbC0uMS0xLjZhNC4zIDQuMyAwIDAgMC0uNi0xLjMgMi41IDIuNSAwIDAgMC0uOS0xbC0xLjMtLjJjLS41IDAtLjkgMC0xLjIuMmwtMSAuN2EzLjMgMy4zIDAgMCAwLS41IDFsLS4zIDEuM1Y1MGwuNSAxLjVjLjIuNS41LjkgMSAxLjIuNC4yIDEgLjQgMS42LjQuNSAwIC45LS4xIDEuMy0uM2wuOC0xYy4zLS4zLjQtLjguNi0xLjNhNy43IDcuNyAwIDAgMCAuMS0xLjZabTgtMy45VjU1SDEyOFY0Mi4zaDIuN2wuMiAyLjZabS0uNCAzLjNoLTFjMC0uOS4yLTEuNy40LTIuNGE1LjggNS44IDAgMCAxIDEtMmMuNC0uNS45LTEgMS41LTEuM2E1IDUgMCAwIDEgMi4yLS40Yy42IDAgMS4xIDAgMS42LjJzLjkuNSAxLjIuOGMuNC40LjcuOC45IDEuNC4yLjYuMyAxLjMuMyAyVjU1aC0yLjl2LTguMmMwLS42IDAtMS4xLS4zLTEuNWExLjYgMS42IDAgMCAwLS43LS43Yy0uNC0uMi0uOC0uMi0xLjItLjItLjYgMC0xIDAtMS40LjMtLjQuMi0uNy40LS45LjhhMy44IDMuOCAwIDAgMC0uNSAxLjIgNi40IDYuNCAwIDAgMC0uMiAxLjVabTcuOS0uOC0xLjMuM2MwLS43IDAtMS41LjMtMi4xLjItLjcuNS0xLjMgMS0xLjhhNC41IDQuNSAwIDAgMSAxLjUtMS4zYy42LS4zIDEuMy0uNCAyLjEtLjQuNiAwIDEuMiAwIDEuNy4zYTMuMiAzLjIgMCAwIDEgMS4zLjhjLjQuNC43LjguOSAxLjQuMi42LjIgMS40LjIgMi4yVjU1aC0yLjh2LTguMmMwLS43IDAtMS4yLS4zLTEuNS0uMS0uNC0uNC0uNi0uNy0uNy0uNC0uMi0uOC0uMi0xLjItLjItLjUgMC0uOSAwLTEuMi4yYTIuMyAyLjMgMCAwIDAtLjguNyAzIDMgMCAwIDAtLjYgMWwtLjEgMS4xWiIgLz4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijc2IiB5PSI4NCIgZmlsbD0iI2ZmZiIgcng9IjQiIC8+CiAgPHJlY3Qgd2lkdGg9IjE5OSIgaGVpZ2h0PSI3NSIgeD0iLjUiIHk9Ijg0LjUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiByeD0iMy41IiAvPgogIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjkiCiAgICBkPSJNMTAuNyA5OS45SDkuM1Y5OWgxLjRsLjctLjFhLjguOCAwIDAgMCAuMy0uNGwuMS0uNXYtLjVhLjkuOSAwIDAgMC0uNC0uNGwtLjctLjFIOS42djQuOWgtMXYtNS43aDIuMWMuNSAwIC45IDAgMS4yLjJsLjcuNy4yIDFjMCAuMyAwIC42LS4yLjlhMS42IDEuNiAwIDAgMS0uNy42bC0xLjIuMlptMy42LTMuNmgxbDEuNiA0LjQgMS42LTQuNGguOWwtMi4yIDUuN2gtLjdsLTIuMi01LjdabS0uNCAwaC45bC4xIDMuOHYxLjloLTF2LTUuN1ptNSAwaC45djUuN2gtMXYtMS45bC4xLTMuOFptNiA1di43SDIxdi0uNmwxLjktMmE3IDcgMCAwIDAgLjUtLjdsLjItLjRhMS4xIDEuMSAwIDAgMCAwLS45LjcuNyAwIDAgMC0uMy0uMy45LjkgMCAwIDAtLjUtLjEgMSAxIDAgMCAwLS41LjEuOS45IDAgMCAwLS4zLjRsLS4xLjVoLTFsLjMtLjljLjEtLjIuMy0uNS42LS42YTIgMiAwIDAgMSAxLS4zbDEgLjJjLjMuMi41LjMuNi42LjIuMi4yLjUuMi44di41bC0uMy42YTQuNSA0LjUgMCAwIDEtLjQuNWwtLjQuNS0xLjMgMS40SDI1Wm0xIC4yYzAtLjEgMC0uMi4yLS4zYS41LjUgMCAwIDEgLjQtLjJjLjIgMCAuMyAwIC40LjJhLjUuNSAwIDAgMSAuMS4zbC0uMS40YS41LjUgMCAwIDEtLjQuMS41LjUgMCAwIDEtLjQtLjEuNS41IDAgMCAxLS4yLS40Wm0zLjQtMi4xLS44LS4yLjQtMi45aDN2LjhoLTIuM2wtLjEgMS40YTEuOCAxLjggMCAwIDEgLjQtLjFsLjUtLjEuNy4xYy4yIDAgLjQuMi41LjQuMi4xLjMuNC40LjZhMi41IDIuNSAwIDAgMSAwIDEuNWMwIC4yLS4yLjQtLjQuNmExLjYgMS42IDAgMCAxLS41LjQgMi4xIDIuMSAwIDAgMS0uOS4ybC0uNi0uMWExLjkgMS45IDAgMCAxLS42LS4zIDEuNiAxLjYgMCAwIDEtLjQtLjUgMS43IDEuNyAwIDAgMS0uMi0uN2guOWMwIC4xIDAgLjMuMi40IDAgLjIuMS4zLjMuM2wuNC4xYTEgMSAwIDAgMCAuNCAwIC44LjggMCAwIDAgLjMtLjNsLjItLjRhMS44IDEuOCAwIDAgMCAwLS45IDEgMSAwIDAgMC0uMi0uNC44LjggMCAwIDAtLjMtLjIgMSAxIDAgMCAwLS41IDBoLS41YTEuNiAxLjYgMCAwIDAtLjMuM1oiIC8+CiAgPGcgY2xpcC1wYXRoPSJ1cmwoI2EpIj4KICAgIDxwYXRoIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9Ii4zIiBkPSJNNy41IDE0MS43SDE5MSIgLz4KICAgIDxwYXRoIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii45IiBzdHJva2Utd2lkdGg9IjEuMyIKICAgICAgZD0iTTguNyAxNDQuOGg5LjhsMTMgMi4yIDExLjItOCAyMS42IDUuOCAyNC44LTUuOSA3LjIuMmM0LjggMi4yIDE0LjUgNy45IDE1IDcuOWgxNC41bDEyLjQtN2gxMC40bDkuMi00IDEyLjQgMS4xIDE5LjYgMi44IiAvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIuNCIgZmlsbC1ydWxlPSJldmVub2RkIgogICAgICBkPSJNOC41IDEzNS41SDh2MmguNVYxMzZIMTB2LS41SDguNVptMCAxMS41di0xLjZIOHYyLjFoMnYtLjVIOC41Wm0xODMtMTEuNWguNXYyaC0uNVYxMzZIMTkwdi0uNWgxLjVabTAgMTEuNXYtMS42aC41djIuMWgtMnYtLjVoMS41WiIKICAgICAgY2xpcC1ydWxlPSJldmVub2RkIiAvPgogIDwvZz4KICA8cGF0aCBmaWxsPSIjRkZBNjAwIgogICAgZD0iTTc4LjEgMTIwLjNoMS43Yy42IDAgMS4yIDAgMS42LS4zLjUtLjIuOC0uNSAxLTEgLjItLjMuMy0uOC4zLTEuMyAwLS42IDAtMS0uMy0xLjRhMiAyIDAgMCAwLS44LS45IDMgMyAwIDAgMC0xLjUtLjMgMyAzIDAgMCAwLTEuNC4zIDIuMyAyLjMgMCAwIDAtMSAuOGMtLjIuNC0uMy44LS4zIDEuM2gtMi44YzAtLjkuMi0xLjcuNy0yLjQuNS0uNyAxLjEtMS4yIDItMS42LjgtLjUgMS43LS43IDIuNy0uN2E3IDcgMCAwIDEgMyAuNmMuOC40IDEuNC45IDEuOSAxLjYuNC43LjYgMS42LjYgMi43IDAgLjUgMCAxLS4zIDEuNS0uMi41LS42IDEtMSAxLjMtLjQuNC0xIC44LTEuNyAxLS42LjMtMS40LjQtMi4zLjRoLTJ2LTEuNlptMCAyLjJWMTIxaDIuMWMxIDAgMS45LjEgMi42LjMuNy4zIDEuMy42IDEuNyAxIC41LjQuOC45IDEgMS40LjIuNS4zIDEgLjMgMS43YTUgNSAwIDAgMS0uNCAyLjEgNC4zIDQuMyAwIDAgMS0xLjIgMS42IDUuNCA1LjQgMCAwIDEtMS45IDEgNy41IDcuNSAwIDAgMS0yLjIuMyA3LjMgNy4zIDAgMCAxLTIuMi0uMyA1LjggNS44IDAgMCAxLTEuOC0xIDQuNSA0LjUgMCAwIDEtMS4yLTEuNGMtLjMtLjctLjUtMS40LS41LTIuMmgyLjhjMCAuNi4yIDEgLjQgMS40YTIuNSAyLjUgMCAwIDAgMSAuOWMuNC4yIDEgLjMgMS41LjMuNiAwIDEuMSAwIDEuNS0uM2EyLjIgMi4yIDAgMCAwIDEtMWMuMy0uMy40LS44LjQtMS40YTMgMyAwIDAgMC0uNC0xLjYgMi4zIDIuMyAwIDAgMC0xLjEtMWMtLjUtLjItMS0uMy0xLjctLjNINzhabTIxLjUtMi4zdjIuOGMwIDEuMy0uMiAyLjQtLjQgMy40YTYgNiAwIDAgMS0xLjIgMi4zIDQuNCA0LjQgMCAwIDEtMS43IDEuMyA2IDYgMCAwIDEtMi4zLjQgNi41IDYuNSAwIDAgMS0xLjktLjMgNC42IDQuNiAwIDAgMS0xLjUtLjggNC44IDQuOCAwIDAgMS0xLjItMS40IDggOCAwIDAgMS0uOC0yLjFjLS4yLS44LS4yLTEuOC0uMi0yLjh2LTIuOGMwLTEuNCAwLTIuNS40LTMuNGE2IDYgMCAwIDEgMS0yLjMgNC4zIDQuMyAwIDAgMSAxLjktMS4zIDYuNSA2LjUgMCAwIDEgNC4yLS4xIDQuNCA0LjQgMCAwIDEgMS41LjhjLjQuMy44LjggMS4xIDEuNC40LjYuNiAxLjMuOCAyIC4yLjkuMyAxLjguMyAyLjlabS0yLjkgMy4yVjExOGwtLjQtMS40YTIuNiAyLjYgMCAwIDAtLjYtLjkgMiAyIDAgMCAwLS44LS41IDIuNyAyLjcgMCAwIDAtMS0uMWMtLjQgMC0uOCAwLTEuMS4yLS40LjItLjcuNS0uOS44bC0uNSAxLjUtLjIgMi4ydjMuNmwuMSAxLjhjMCAuNS4yIDEgLjQgMS4zbC41IDEgLjguNSAxIC4xYy40IDAgLjggMCAxLjItLjIuMy0uMi42LS41LjgtLjkuMy0uMy40LS44LjYtMS40bC4xLTIuMlptMTAuMi40aDF2Ni4zaC0uOWwtLjEtMS40di00LjlabS4yIDMuNGguNGwtLjEgMS4yYTMuMSAzLjEgMCAwIDEtLjQgMSAxLjggMS44IDAgMCAxLS42LjZjLS4zLjItLjYuMy0xIC4zLS4zIDAtLjUgMC0uOC0uMmExLjUgMS41IDAgMCAxLS42LS4zIDIgMiAwIDAgMS0uNC0uN2wtLjItMS43aC43djFjMCAuMi4yLjUuNC42IDAgLjIuMi4yLjQuM2gxLjNsLjYtLjUuMi0uN2E0LjYgNC42IDAgMCAwIDAtMVptLTQuMS0zLjRoMXY4LjhoLTF2LTguOFptMTEuMiAwaDF2Ni4yYzAgLjYtLjEgMS0uMyAxLjRhMi4yIDIuMiAwIDAgMS0xIDEgMyAzIDAgMCAxLTIuMS4xIDMuMSAzLjEgMCAwIDEtMS0uMyAyLjIgMi4yIDAgMCAxLS42LS43bC42LS42LjguNy45LjIuOC0uMi42LS42Yy4yLS4zLjItLjYuMi0xdi00LjhsLjEtMS40Wk0xMTAgMTI3bC4xLTEuNC41LTFhMi4yIDIuMiAwIDAgMSAxLjktMWMuNCAwIC43LjIgMSAuMy4zLjEuNi4zLjguNmwuNSAxYTYgNiAwIDAgMSAuMiAxLjJ2LjVhNS41IDUuNSAwIDAgMS0uMiAxLjMgMyAzIDAgMCAxLS41IDEgMiAyIDAgMCAxLS44LjYgMi41IDIuNSAwIDAgMS0xIC4yYy0uNCAwLS44LS4xLTEtLjNhMi40IDIuNCAwIDAgMS0uOS0uNyAzLjIgMy4yIDAgMCAxLS41LTEgNC42IDQuNiAwIDAgMS0uMS0xLjNabTEgMCAuMSAxIC4zLjcuNi41LjcuMWMuNCAwIC43IDAgMS0uMmwuNS0uNi4zLS44di0xLjRhMi45IDIuOSAwIDAgMC0uMi0uNiAxLjkgMS45IDAgMCAwLS4zLS42IDEuNSAxLjUgMCAwIDAtLjYtLjQgMS43IDEuNyAwIDAgMC0uNy0uMWMtLjMgMC0uNSAwLS43LjJhMS41IDEuNSAwIDAgMC0uNi41IDIuNCAyLjQgMCAwIDAtLjMuNyA0IDQgMCAwIDAgMCAxWm0xMC4xLTUuNC0zLjUgOS4zaC0xbDMuNi05LjNoMVptMi43IDMuNXY1aC0xdi02LjNoMXYxLjNabS0uMiAxLjZoLS41bC4xLTEuMi41LTEgLjgtLjYgMS4xLS4yLjkuMWExLjcgMS43IDAgMCAxIDEgMS4xbC4yIDF2NC4yaC0xVjEyNmMwLS40LS4xLS42LS4zLS44YTEgMSAwIDAgMC0uNC0uNSAxLjcgMS43IDAgMCAwLS43LS4xYy0uNCAwLS42IDAtLjguMmExLjQgMS40IDAgMCAwLS42LjRsLS4yLjdhMy44IDMuOCAwIDAgMC0uMS44Wm00LS42LS42LjNhMy4yIDMuMiAwIDAgMSAuMS0xIDIuNyAyLjcgMCAwIDEgLjUtLjljLjItLjMuNS0uNC44LS42bDEtLjIgMSAuMWMuMi4xLjUuMi42LjRsLjUuNy4xIDF2NC4yaC0xVjEyNmMwLS40LS4xLS43LS4zLS45YS45LjkgMCAwIDAtLjQtLjQgMS45IDEuOSAwIDAgMC0uNy0uMWwtLjcuMWExLjMgMS4zIDAgMCAwLS40LjRjLS4yLjEtLjMuMy0uMy40YTEuOCAxLjggMCAwIDAtLjEuNlptNi44LTIuNmguOWEuNi42IDAgMCAwIC4zLS4zLjYuNiAwIDAgMCAuMS0uMy42LjYgMCAwIDAtLjItLjRsLS42LS4yYTEgMSAwIDAgMC0uNS4xLjUuNSAwIDAgMC0uMi40aC0xYzAtLjIuMS0uNS4zLS42LjEtLjIuMy0uNC42LS41bC44LS4yIDEgLjJjLjIgMCAuNC4yLjUuNC4yLjIuMi41LjIuOGExIDEgMCAwIDEtLjEuNWwtLjYuNC0uOS4yaC0uNnYtLjVabTAgLjd2LS40aC42bDEgLjFjLjIgMCAuNC4yLjUuNC4yLjIuMi40LjIuNiAwIC4zIDAgLjYtLjIuOGExLjUgMS41IDAgMCAxLS43LjRsLS45LjItLjgtLjFhMS41IDEuNSAwIDAgMS0uNy0uNWwtLjItLjhoMWwuMS41LjcuMS42LS4xYS42LjYgMCAwIDAgLjItLjUuNi42IDAgMCAwLS4xLS40LjYuNiAwIDAgMC0uMy0uMmgtMVoiIC8+CiAgPHBhdGggZmlsbD0idXJsKCNiKSIgZmlsbC1vcGFjaXR5PSIuMyIgZD0iTTE1OCAxMjAuN2g1LjR2Ni43bC0yLjcgMi0yLjctMnYtNi43WiIgLz4KICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii45IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMTYwLjUgMTI5LjhhLjMuMyAwIDAgMCAuNSAwbDIuNC0yLjQtLjUtLjUtMS4zIDEuM2EuMy4zIDAgMCAxLS41LS4ydi0zLjNoLS43djMuM2EuMy4zIDAgMCAxLS41LjJsLTEuNC0xLjMtLjUuNSAyLjUgMi40WiIKICAgIGNsaXAtcnVsZT0iZXZlbm9kZCIgLz4KICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii45IgogICAgZD0iTTE3MC4zIDEyNS4ydjFoLTMuMXYtMWgzWm01LTQuNHY4LjZIMTc0di03bC0yIC44VjEyMmwzLjMtMS4yaC4yWm03LjQgMGguMXYxLjFjLS42IDAtMSAuMS0xLjQuM2EyLjQgMi40IDAgMCAwLS45LjZsLS41IDEtLjIgMS4xdjEuNGwuMS45LjQuNi40LjQuNi4yYy4yIDAgLjQgMCAuNi0uMmwuNC0uMy4zLS42YTIuNSAyLjUgMCAwIDAgMC0xLjQgMS43IDEuNyAwIDAgMC0uMy0uNSAxLjIgMS4yIDAgMCAwLS40LS40IDEuMiAxLjIgMCAwIDAtLjYtLjJsLS44LjJhMS42IDEuNiAwIDAgMC0uNS42IDEuMyAxLjMgMCAwIDAtLjMuNmwtLjUtLjJhMyAzIDAgMCAxIC4yLTFsLjYtLjcuNy0uNCAxLS4yYy40IDAgLjcgMCAxIC4ybC44LjdjLjIuMi4zLjUuNC45YTMuNyAzLjcgMCAwIDEgMCAyLjJjLS4xLjQtLjMuNy0uNiAxYTIuNSAyLjUgMCAwIDEtLjguNmwtMS4yLjJjLS40IDAtLjktLjEtMS4yLS4zYTIuNyAyLjcgMCAwIDEtMS0uNyAzLjMgMy4zIDAgMCAxLS41LTEgNC4yIDQuMiAwIDAgMS0uMi0xLjR2LS41YzAtLjcuMS0xLjMuMy0xLjlhNC41IDQuNSAwIDAgMSAuNy0xLjUgMy41IDMuNSAwIDAgMSAxLjMtMWMuNi0uMyAxLjItLjQgMi0uNFptMi44IDMuN3YtLjNsLjEtLjcuNS0uNS43LS4xLjcuMS40LjUuMi43di4zYzAgLjMgMCAuNS0uMi43YTEuMiAxLjIgMCAwIDEtLjQuNWwtLjcuMS0uNy0uMWExLjIgMS4yIDAgMCAxLS41LS41IDEuNCAxLjQgMCAwIDEtLjEtLjdabS42LS4zdi43YS43LjcgMCAwIDAgLjMuMy43LjcgMCAwIDAgLjQuMS43LjcgMCAwIDAgLjQgMCAuNy43IDAgMCAwIC4yLS40IDEgMSAwIDAgMCAwLS40di0uM2ExIDEgMCAwIDAgMC0uNC42LjYgMCAwIDAtLjYtLjQuNy43IDAgMCAwLS40LjEuNy43IDAgMCAwLS4zLjMgMSAxIDAgMCAwIDAgLjRabTIuNSA0di0uNGwuMS0uNy41LS40LjctLjJjLjMgMCAuNSAwIC43LjIuMiAwIC4zLjIuNC40bC4yLjd2LjNjMCAuMyAwIC41LS4yLjdhMS4yIDEuMiAwIDAgMS0uNC41IDEuMyAxLjMgMCAwIDEtLjcuMmwtLjctLjJhMS4zIDEuMyAwIDAgMS0uNS0uNSAxLjQgMS40IDAgMCAxLS4xLS43Wm0uNi0uNHYuN2wuMy4zYS43LjcgMCAwIDAgLjQuMS43LjcgMCAwIDAgLjQgMCAuNy43IDAgMCAwIC4yLS40di0uN2ExIDEgMCAwIDAgMC0uNC42LjYgMCAwIDAtLjYtLjQuNi42IDAgMCAwLS42LjQgMSAxIDAgMCAwLS4xLjRabS45LTQtMy4xIDUtLjUtLjIgMy4xLTUgLjUuM1oiIC8+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImIiIHgxPSIxNjAuNSIgeDI9IjE2MC41IiB5MT0iMTI5LjkiIHkyPSIxMjAuNyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNmZmY7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I2ZmZjtzdG9wLW9wYWNpdHk6MCIgLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8Y2xpcFBhdGggaWQ9ImEiPgogICAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNOCAxMzUuNWgxODR2MTJIOHoiIC8+CiAgICA8L2NsaXBQYXRoPgogIDwvZGVmcz4KPC9zdmc+", - "description": "Contains widgets displaying air quality telemetry.", - "order": 12600, - "externalId": null, - "name": "Air quality" - }, - "widgetTypeFqns": [ - "air_quality_card", - "air_quality_card_with_background", - "horizontal_air_quality_card", - "horizontal_air_quality_card_with_background", - "air_quality_chart_card", - "air_quality_chart_card_with_background", - "simple_air_quality_chart_card", - "simple_air_quality_chart_card_with_background", - "humidity_card", - "humidity_card_with_background", - "horizontal_humidity_card", - "horizontal_humidity_card_with_background", - "humidity_chart_card", - "humidity_chart_card_with_background", - "simple_humidity_chart_card", - "simple_humidity_chart_card_with_background", - "humidity_progress_bar", - "humidity_progress_bar_with_background", - "co2_card", - "co2_card_with_background", - "horizontal_co2_card", - "horizontal_co2_card_with_background", - "co2_chart_card", - "co2_chart_card_with_background", - "simple_co2_chart_card", - "simple_co2_chart_card_with_background", - "pm2_5_card", - "pm2_5_card_with_background", - "horizontal_pm2_5_card", - "horizontal_pm2_5_card_with_background", - "pm2_5_chart_card", - "pm2_5_chart_card_with_background", - "simple_pm2_5_chart_card", - "simple_pm2_5_chart_card_with_background", - "pm10_card", - "pm10_card_with_background", - "horizontal_pm10_card", - "horizontal_pm10_card_with_background", - "pm10_chart_card", - "pm10_chart_card_with_background", - "simple_pm10_chart_card", - "simple_pm10_chart_card_with_background", - "radon_level_card", - "radon_level_card_with_background", - "horizontal_radon_level_card", - "horizontal_radon_level_card_with_background", - "radon_level_chart_card", - "radon_level_chart_card_with_background", - "simple_radon_level_chart_card", - "simple_radon_level_chart_card_with_background", - "volatile_organic_compounds_card", - "volatile_organic_compounds_card_with_background", - "horizontal_volatile_organic_compounds_card", - "horizontal_volatile_organic_compounds_card_with_background", - "volatile_organic_compounds_chart_card", - "volatile_organic_compounds_chart_card_with_background", - "simple_volatile_organic_compounds_chart_card", - "simple_volatile_organic_compounds_chart_card_with_background", - "individual_allergy_index_card", - "individual_allergy_index_card_with_background", - "horizontal_individual_allergy_index_iai_card", - "horizontal_individual_allergy_index_iai_card_with_background", - "individual_allergy_index_iai_chart_card", - "individual_allergy_index_iai_chart_card_with_background", - "simple_individual_allergy_index_iai_chart_card", - "simple_individual_allergy_index_iai_chart_card_with_background2", - "ozone_o3_card", - "ozone_o3_card_with_background", - "horizontal_ozone_o3_card", - "horizontal_ozone_o3_card_with_background", - "ozone_o3_chart_card", - "ozone_o3_chart_card_with_background", - "simple_ozone_o3_chart_card", - "simple_ozone_o3_chart_card_with_background", - "nitrogen_dioxide_no2_card", - "nitrogen_dioxide_no2_card_with_background", - "horizontal_nitrogen_dioxide_no2_card", - "horizontal_nitrogen_dioxide_no2_card_with_background", - "nitrogen_dioxide_no2_chart_card", - "nitrogen_dioxide_no2_chart_card_with_background", - "simple_nitrogen_dioxide_no2_chart_card", - "simple_nitrogen_dioxide_no2_chart_card_with_background", - "sulfur_dioxide_so2_card", - "sulfur_dioxide_so2_card_with_background", - "horizontal_sulfur_dioxide_so2_card", - "horizontal_sulfur_dioxide_so2_card_with_background", - "sulfur_dioxide_so2_chart_card", - "sulfur_dioxide_so2_chart_card_with_background", - "simple_sulfur_dioxide_so2_chart_card", - "simple_sulfur_dioxide_so2_chart_card_with_background", - "carbon_monoxide_co_card", - "carbon_monoxide_co_card_with_background", - "horizontal_carbon_monoxide_co_card", - "horizontal_carbon_monoxide_co_card_with_background", - "carbon_monoxide_co_chart_card", - "carbon_monoxide_co_chart_card_with_background", - "simple_carbon_monoxide_co_chart_card", - "simple_carbon_monoxide_co_chart_card_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/indoor_environment.json b/application/src/main/data/json/system/widget_bundles/indoor_environment.json deleted file mode 100644 index d13fc5ba66..0000000000 --- a/application/src/main/data/json/system/widget_bundles/indoor_environment.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "widgetsBundle": { - "alias": "indoor_environment", - "title": "Indoor Environment", - "image": "data:image/png;base64,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", - "description": "Contains widgets displaying indoor environment telemetry.", - "order": 10000, - "externalId": null, - "name": "Indoor Environment" - }, - "widgetTypeFqns": [ - "indoor_temperature_card", - "indoor_temperature_card_with_background", - "indoor_horizontal_temperature_card", - "indoor_horizontal_temperature_card_with_background", - "indoor_temperature_chart_card", - "indoor_temperature_chart_card_with_background", - "indoor_simple_temperature_chart_card", - "indoor_simple_temperature_chart_card_with_background", - "indoor_temperature_progress_bar", - "indoor_temperature_progress_bar_with_background", - "indoor_temperature_range_chart", - "indoor_temperature_range_chart_with_background", - "indoor_temperature_gauge", - "indoor_humidity_card", - "indoor_humidity_card_with_background", - "indoor_horizontal_humidity_card", - "indoor_horizontal_humidity_card_with_background", - "indoor_humidity_chart_card", - "indoor_humidity_chart_card_with_background", - "indoor_simple_humidity_chart_card", - "indoor_simple_humidity_chart_card_with_background", - "indoor_humidity_progress_bar", - "indoor_humidity_progress_bar_with_background", - "indoor_co2_card", - "indoor_co2_card_with_background", - "indoor_horizontal_co2_card", - "indoor_horizontal_co2_card_with_background", - "indoor_co2_chart_card", - "indoor_co2_chart_card_with_background", - "indoor_simple_co2_chart_card", - "indoor_simple_co2_chart_card_with_background", - "indoor_illuminance_card", - "indoor_illuminance_card_with_background", - "indoor_horizontal_illuminance_card", - "indoor_horizontal_illuminance_card_with_background", - "indoor_illuminance_chart_card", - "indoor_illuminance_chart_card_with_background", - "indoor_simple_illuminance_chart_card", - "indoor_simple_illuminance_chart_card_with_background", - "indoor_illuminance_progress_bar", - "indoor_illuminance_progress_bar_with_background", - "noise_level_card", - "noise_level_card_with_background", - "horizontal_noise_level_card", - "horizontal_noise_level_card_with_background", - "noise_level_chart_card", - "noise_level_chart_card_with_background", - "simple_noise_level_chart_card", - "simple_noise_level_chart_card_with_background", - "indoor_pm2_5_card", - "indoor_pm2_5_card_with_background", - "indoor_horizontal_pm2_5_card", - "indoor_horizontal_pm2_5_card_with_background", - "indoor_pm2_5_chart_card", - "indoor_pm2_5_chart_card_with_background", - "indoor_simple_pm2_5_chart_card", - "indoor_simple_pm2_5_chart_card_with_background", - "indoor_pm10_card", - "indoor_pm10_card_with_background", - "indoor_horizontal_pm10_card", - "indoor_horizontal_pm10_card_with_background", - "indoor_pm10_chart_card", - "indoor_pm10_chart_card_with_background", - "indoor_simple_pm10_chart_card", - "indoor_simple_pm10_chart_card_with_background", - "radon_level_card", - "radon_level_card_with_background", - "horizontal_radon_level_card", - "horizontal_radon_level_card_with_background", - "radon_level_chart_card", - "radon_level_chart_card_with_background", - "simple_radon_level_chart_card", - "simple_radon_level_chart_card_with_background", - "volatile_organic_compounds_card", - "volatile_organic_compounds_card_with_background", - "horizontal_volatile_organic_compounds_card", - "horizontal_volatile_organic_compounds_card_with_background", - "volatile_organic_compounds_chart_card", - "volatile_organic_compounds_chart_card_with_background", - "simple_volatile_organic_compounds_chart_card", - "simple_volatile_organic_compounds_chart_card_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/industial_widgets.json b/application/src/main/data/json/system/widget_bundles/industial_widgets.json deleted file mode 100644 index 940c4e0d31..0000000000 --- a/application/src/main/data/json/system/widget_bundles/industial_widgets.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "widgetsBundle": { - "alias": "industrial_widgets", - "title": "Industrial widgets", - "image": "tb-image:aW5kdXN0cmlhbC13aWRnZXRzLWJ1bmxkZS5zdmc=:aW5kdXN0cmlhbC13aWRnZXRzLWJ1bmxkZS5zdmc=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iNzYiIHJ4PSI0IiBmaWxsPSJ3aGl0ZSIvPgo8cmVjdCB4PSIwLjUiIHk9IjAuNSIgd2lkdGg9IjE5OSIgaGVpZ2h0PSI3NSIgcng9IjMuNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTEyLjMwMDggMTcuMjIyN1YxOEg5LjI4MTI1VjE3LjIyMjdIMTIuMzAwOFpNOS41NTg1OSAxMi4zMTI1VjE4SDguNTc4MTJWMTIuMzEyNUg5LjU1ODU5Wk0xMS45MDYyIDE0LjY4NzVWMTUuNDUzMUg5LjI4MTI1VjE0LjY4NzVIMTEuOTA2MlpNMTIuMjgxMiAxMi4zMTI1VjEzLjA5MzhIOS4yODEyNVYxMi4zMTI1SDEyLjI4MTJaTTE0LjUzMTIgMThIMTMuNTg5OFYxMy4zNjcyQzEzLjU4OTggMTMuMDUyMSAxMy42NDg0IDEyLjc4NzggMTMuNzY1NiAxMi41NzQyQzEzLjg4NTQgMTIuMzU4MSAxNC4wNTYgMTIuMTk1MyAxNC4yNzczIDEyLjA4NTlDMTQuNDk4NyAxMS45NzQgMTQuNzYwNCAxMS45MTggMTUuMDYyNSAxMS45MThDMTUuMTU2MiAxMS45MTggMTUuMjQ4NyAxMS45MjQ1IDE1LjMzOTggMTEuOTM3NUMxNS40MzEgMTEuOTQ3OSAxNS41MTk1IDExLjk2NDggMTUuNjA1NSAxMS45ODgzTDE1LjU4MiAxMi43MTQ4QzE1LjUyOTkgMTIuNzAxOCAxNS40NzI3IDEyLjY5MjcgMTUuNDEwMiAxMi42ODc1QzE1LjM1MDMgMTIuNjgyMyAxNS4yODUyIDEyLjY3OTcgMTUuMjE0OCAxMi42Nzk3QzE1LjA3MTYgMTIuNjc5NyAxNC45NDc5IDEyLjcwNyAxNC44NDM4IDEyLjc2MTdDMTQuNzQyMiAxMi44MTM4IDE0LjY2NDEgMTIuODkwNiAxNC42MDk0IDEyLjk5MjJDMTQuNTU3MyAxMy4wOTM4IDE0LjUzMTIgMTMuMjE4OCAxNC41MzEyIDEzLjM2NzJWMThaTTE1LjQyNTggMTMuNzczNFYxNC40NjA5SDEyLjk0MTRWMTMuNzczNEgxNS40MjU4Wk0xNi45NTMxIDE4SDE2LjAxMTdWMTMuNUMxNi4wMTE3IDEzLjE1ODkgMTYuMDc5NCAxMi44NzExIDE2LjIxNDggMTIuNjM2N0MxNi4zNTAzIDEyLjQwMjMgMTYuNTQ1NiAxMi4yMjQgMTYuODAwOCAxMi4xMDE2QzE3LjA1ODYgMTEuOTc5MiAxNy4zNjQ2IDExLjkxOCAxNy43MTg4IDExLjkxOEMxNy45MjcxIDExLjkxOCAxOC4xMjg5IDExLjk0MDEgMTguMzI0MiAxMS45ODQ0QzE4LjUxOTUgMTIuMDI4NiAxOC43MjAxIDEyLjA4NDYgMTguOTI1OCAxMi4xNTIzTDE4Ljc4MTIgMTIuOTE4QzE4LjY1MSAxMi44NzM3IDE4LjUwMzkgMTIuODMyIDE4LjMzOTggMTIuNzkzQzE4LjE3ODQgMTIuNzUxMyAxNy45OTM1IDEyLjczMDUgMTcuNzg1MiAxMi43MzA1QzE3LjUwMTMgMTIuNzMwNSAxNy4yOTE3IDEyLjc5NTYgMTcuMTU2MiAxMi45MjU4QzE3LjAyMDggMTMuMDUzNCAxNi45NTMxIDEzLjI0NDggMTYuOTUzMSAxMy41VjE4Wk0xNy44MDQ3IDEzLjc3MzRWMTQuNDYwOUgxNS4zNjMzVjEzLjc3MzRIMTcuODA0N1pNMTkuMzc1IDEzLjc3MzRWMThIMTguNDI5N1YxMy43NzM0SDE5LjM3NVpNMjIuMzkwNiAxNy4zMjgxQzIyLjU0NDMgMTcuMzI4MSAyMi42ODIzIDE3LjI5ODIgMjIuODA0NyAxNy4yMzgzQzIyLjkyOTcgMTcuMTc1OCAyMy4wMjk5IDE3LjA4OTggMjMuMTA1NSAxNi45ODA1QzIzLjE4MzYgMTYuODcxMSAyMy4yMjY2IDE2Ljc0NDggMjMuMjM0NCAxNi42MDE2SDI0LjEyMTFDMjQuMTE1OSAxNi44NzUgMjQuMDM1MiAxNy4xMjM3IDIzLjg3ODkgMTcuMzQ3N0MyMy43MjI3IDE3LjU3MTYgMjMuNTE1NiAxNy43NSAyMy4yNTc4IDE3Ljg4MjhDMjMgMTguMDEzIDIyLjcxNDggMTguMDc4MSAyMi40MDIzIDE4LjA3ODFDMjIuMDc5NCAxOC4wNzgxIDIxLjc5ODIgMTguMDIzNCAyMS41NTg2IDE3LjkxNDFDMjEuMzE5IDE3LjgwMjEgMjEuMTE5OCAxNy42NDg0IDIwLjk2MDkgMTcuNDUzMUMyMC44MDIxIDE3LjI1NzggMjAuNjgyMyAxNy4wMzI2IDIwLjYwMTYgMTYuNzc3M0MyMC41MjM0IDE2LjUyMjEgMjAuNDg0NCAxNi4yNDg3IDIwLjQ4NDQgMTUuOTU3VjE1LjgyMDNDMjAuNDg0NCAxNS41Mjg2IDIwLjUyMzQgMTUuMjU1MiAyMC42MDE2IDE1QzIwLjY4MjMgMTQuNzQyMiAyMC44MDIxIDE0LjUxNTYgMjAuOTYwOSAxNC4zMjAzQzIxLjExOTggMTQuMTI1IDIxLjMxOSAxMy45NzI3IDIxLjU1ODYgMTMuODYzM0MyMS43OTgyIDEzLjc1MTMgMjIuMDc4MSAxMy42OTUzIDIyLjM5ODQgMTMuNjk1M0MyMi43MzcgMTMuNjk1MyAyMy4wMzM5IDEzLjc2MyAyMy4yODkxIDEzLjg5ODRDMjMuNTQ0MyAxNC4wMzEyIDIzLjc0NDggMTQuMjE3NCAyMy44OTA2IDE0LjQ1N0MyNC4wMzkxIDE0LjY5NCAyNC4xMTU5IDE0Ljk3MDEgMjQuMTIxMSAxNS4yODUySDIzLjIzNDRDMjMuMjI2NiAxNS4xMjg5IDIzLjE4NzUgMTQuOTg4MyAyMy4xMTcyIDE0Ljg2MzNDMjMuMDQ5NSAxNC43MzU3IDIyLjk1MzEgMTQuNjM0MSAyMi44MjgxIDE0LjU1ODZDMjIuNzA1NyAxNC40ODMxIDIyLjU1ODYgMTQuNDQ1MyAyMi4zODY3IDE0LjQ0NTNDMjIuMTk2NiAxNC40NDUzIDIyLjAzOTEgMTQuNDg0NCAyMS45MTQxIDE0LjU2MjVDMjEuNzg5MSAxNC42MzggMjEuNjkxNCAxNC43NDIyIDIxLjYyMTEgMTQuODc1QzIxLjU1MDggMTUuMDA1MiAyMS41IDE1LjE1MjMgMjEuNDY4OCAxNS4zMTY0QzIxLjQ0MDEgMTUuNDc3OSAyMS40MjU4IDE1LjY0NTggMjEuNDI1OCAxNS44MjAzVjE1Ljk1N0MyMS40MjU4IDE2LjEzMTUgMjEuNDQwMSAxNi4zMDA4IDIxLjQ2ODggMTYuNDY0OEMyMS40OTc0IDE2LjYyODkgMjEuNTQ2OSAxNi43NzYgMjEuNjE3MiAxNi45MDYyQzIxLjY5MDEgMTcuMDMzOSAyMS43ODkxIDE3LjEzNjcgMjEuOTE0MSAxNy4yMTQ4QzIyLjAzOTEgMTcuMjkwNCAyMi4xOTc5IDE3LjMyODEgMjIuMzkwNiAxNy4zMjgxWk0yNi4xMDE2IDEzLjc3MzRWMThIMjUuMTU2MlYxMy43NzM0SDI2LjEwMTZaTTI1LjA5MzggMTIuNjY0MUMyNS4wOTM4IDEyLjUyMDggMjUuMTQwNiAxMi40MDIzIDI1LjIzNDQgMTIuMzA4NkMyNS4zMzA3IDEyLjIxMjIgMjUuNDYzNSAxMi4xNjQxIDI1LjYzMjggMTIuMTY0MUMyNS43OTk1IDEyLjE2NDEgMjUuOTMxIDEyLjIxMjIgMjYuMDI3MyAxMi4zMDg2QzI2LjEyMzcgMTIuNDAyMyAyNi4xNzE5IDEyLjUyMDggMjYuMTcxOSAxMi42NjQxQzI2LjE3MTkgMTIuODA0NyAyNi4xMjM3IDEyLjkyMTkgMjYuMDI3MyAxMy4wMTU2QzI1LjkzMSAxMy4xMDk0IDI1Ljc5OTUgMTMuMTU2MiAyNS42MzI4IDEzLjE1NjJDMjUuNDYzNSAxMy4xNTYyIDI1LjMzMDcgMTMuMTA5NCAyNS4yMzQ0IDEzLjAxNTZDMjUuMTQwNiAxMi45MjE5IDI1LjA5MzggMTIuODA0NyAyNS4wOTM4IDEyLjY2NDFaTTI5LjI0NjEgMTguMDc4MUMyOC45MzM2IDE4LjA3ODEgMjguNjUxIDE4LjAyNzMgMjguMzk4NCAxNy45MjU4QzI4LjE0ODQgMTcuODIxNiAyNy45MzQ5IDE3LjY3NzEgMjcuNzU3OCAxNy40OTIyQzI3LjU4MzMgMTcuMzA3MyAyNy40NDkyIDE3LjA4OTggMjcuMzU1NSAxNi44Mzk4QzI3LjI2MTcgMTYuNTg5OCAyNy4yMTQ4IDE2LjMyMDMgMjcuMjE0OCAxNi4wMzEyVjE1Ljg3NUMyNy4yMTQ4IDE1LjU0NDMgMjcuMjYzIDE1LjI0NDggMjcuMzU5NCAxNC45NzY2QzI3LjQ1NTcgMTQuNzA4MyAyNy41ODk4IDE0LjQ3OTIgMjcuNzYxNyAxNC4yODkxQzI3LjkzMzYgMTQuMDk2NCAyOC4xMzY3IDEzLjk0OTIgMjguMzcxMSAxMy44NDc3QzI4LjYwNTUgMTMuNzQ2MSAyOC44NTk0IDEzLjY5NTMgMjkuMTMyOCAxMy42OTUzQzI5LjQzNDkgMTMuNjk1MyAyOS42OTkyIDEzLjc0NjEgMjkuOTI1OCAxMy44NDc3QzMwLjE1MjMgMTMuOTQ5MiAzMC4zMzk4IDE0LjA5MjQgMzAuNDg4MyAxNC4yNzczQzMwLjYzOTMgMTQuNDU5NiAzMC43NTEzIDE0LjY3NzEgMzAuODI0MiAxNC45Mjk3QzMwLjg5OTcgMTUuMTgyMyAzMC45Mzc1IDE1LjQ2MDkgMzAuOTM3NSAxNS43NjU2VjE2LjE2OEgyNy42NzE5VjE1LjQ5MjJIMzAuMDA3OFYxNS40MThDMzAuMDAyNiAxNS4yNDg3IDI5Ljk2ODggMTUuMDg5OCAyOS45MDYyIDE0Ljk0MTRDMjkuODQ2NCAxNC43OTMgMjkuNzUzOSAxNC42NzMyIDI5LjYyODkgMTQuNTgyQzI5LjUwMzkgMTQuNDkwOSAyOS4zMzcyIDE0LjQ0NTMgMjkuMTI4OSAxNC40NDUzQzI4Ljk3MjcgMTQuNDQ1MyAyOC44MzMzIDE0LjQ3OTIgMjguNzEwOSAxNC41NDY5QzI4LjU5MTEgMTQuNjEyIDI4LjQ5MDkgMTQuNzA3IDI4LjQxMDIgMTQuODMyQzI4LjMyOTQgMTQuOTU3IDI4LjI2NjkgMTUuMTA4MSAyOC4yMjI3IDE1LjI4NTJDMjguMTgxIDE1LjQ1OTYgMjguMTYwMiAxNS42NTYyIDI4LjE2MDIgMTUuODc1VjE2LjAzMTJDMjguMTYwMiAxNi4yMTYxIDI4LjE4NDkgMTYuMzg4IDI4LjIzNDQgMTYuNTQ2OUMyOC4yODY1IDE2LjcwMzEgMjguMzYyIDE2LjgzOTggMjguNDYwOSAxNi45NTdDMjguNTU5OSAxNy4wNzQyIDI4LjY3OTcgMTcuMTY2NyAyOC44MjAzIDE3LjIzNDRDMjguOTYwOSAxNy4yOTk1IDI5LjEyMTEgMTcuMzMyIDI5LjMwMDggMTcuMzMyQzI5LjUyNzMgMTcuMzMyIDI5LjcyOTIgMTcuMjg2NSAyOS45MDYyIDE3LjE5NTNDMzAuMDgzMyAxNy4xMDQyIDMwLjIzNyAxNi45NzUzIDMwLjM2NzIgMTYuODA4NkwzMC44NjMzIDE3LjI4OTFDMzAuNzcyMSAxNy40MjE5IDMwLjY1MzYgMTcuNTQ5NSAzMC41MDc4IDE3LjY3MTlDMzAuMzYyIDE3Ljc5MTcgMzAuMTgzNiAxNy44ODkzIDI5Ljk3MjcgMTcuOTY0OEMyOS43NjQzIDE4LjA0MDQgMjkuNTIyMSAxOC4wNzgxIDI5LjI0NjEgMTguMDc4MVpNMzIuODU1NSAxNC42NzU4VjE4SDMxLjkxNDFWMTMuNzczNEgzMi44MDA4TDMyLjg1NTUgMTQuNjc1OFpNMzIuNjg3NSAxNS43MzA1TDMyLjM4MjggMTUuNzI2NkMzMi4zODU0IDE1LjQyNzEgMzIuNDI3MSAxNS4xNTIzIDMyLjUwNzggMTQuOTAyM0MzMi41OTExIDE0LjY1MjMgMzIuNzA1NyAxNC40Mzc1IDMyLjg1MTYgMTQuMjU3OEMzMyAxNC4wNzgxIDMzLjE3NzEgMTMuOTQwMSAzMy4zODI4IDEzLjg0MzhDMzMuNTg4NSAxMy43NDQ4IDMzLjgxNzcgMTMuNjk1MyAzNC4wNzAzIDEzLjY5NTNDMzQuMjczNCAxMy42OTUzIDM0LjQ1NyAxMy43MjQgMzQuNjIxMSAxMy43ODEyQzM0Ljc4NzggMTMuODM1OSAzNC45Mjk3IDEzLjkyNTggMzUuMDQ2OSAxNC4wNTA4QzM1LjE2NjcgMTQuMTc1OCAzNS4yNTc4IDE0LjMzODUgMzUuMzIwMyAxNC41MzkxQzM1LjM4MjggMTQuNzM3IDM1LjQxNDEgMTQuOTgwNSAzNS40MTQxIDE1LjI2OTVWMThIMzQuNDY4OFYxNS4yNjU2QzM0LjQ2ODggMTUuMDYyNSAzNC40Mzg4IDE0LjkwMjMgMzQuMzc4OSAxNC43ODUyQzM0LjMyMTYgMTQuNjY1NCAzNC4yMzcgMTQuNTgwNyAzNC4xMjUgMTQuNTMxMkMzNC4wMTU2IDE0LjQ3OTIgMzMuODc4OSAxNC40NTMxIDMzLjcxNDggMTQuNDUzMUMzMy41NTM0IDE0LjQ1MzEgMzMuNDA4OSAxNC40ODcgMzMuMjgxMiAxNC41NTQ3QzMzLjE1MzYgMTQuNjIyNCAzMy4wNDU2IDE0LjcxNDggMzIuOTU3IDE0LjgzMkMzMi44NzExIDE0Ljk0OTIgMzIuODA0NyAxNS4wODQ2IDMyLjc1NzggMTUuMjM4M0MzMi43MTA5IDE1LjM5MTkgMzIuNjg3NSAxNS41NTYgMzIuNjg3NSAxNS43MzA1Wk0zOC4zNTE2IDE3LjMyODFDMzguNTA1MiAxNy4zMjgxIDM4LjY0MzIgMTcuMjk4MiAzOC43NjU2IDE3LjIzODNDMzguODkwNiAxNy4xNzU4IDM4Ljk5MDkgMTcuMDg5OCAzOS4wNjY0IDE2Ljk4MDVDMzkuMTQ0NSAxNi44NzExIDM5LjE4NzUgMTYuNzQ0OCAzOS4xOTUzIDE2LjYwMTZINDAuMDgyQzQwLjA3NjggMTYuODc1IDM5Ljk5NjEgMTcuMTIzNyAzOS44Mzk4IDE3LjM0NzdDMzkuNjgzNiAxNy41NzE2IDM5LjQ3NjYgMTcuNzUgMzkuMjE4OCAxNy44ODI4QzM4Ljk2MDkgMTguMDEzIDM4LjY3NTggMTguMDc4MSAzOC4zNjMzIDE4LjA3ODFDMzguMDQwNCAxOC4wNzgxIDM3Ljc1OTEgMTguMDIzNCAzNy41MTk1IDE3LjkxNDFDMzcuMjc5OSAxNy44MDIxIDM3LjA4MDcgMTcuNjQ4NCAzNi45MjE5IDE3LjQ1MzFDMzYuNzYzIDE3LjI1NzggMzYuNjQzMiAxNy4wMzI2IDM2LjU2MjUgMTYuNzc3M0MzNi40ODQ0IDE2LjUyMjEgMzYuNDQ1MyAxNi4yNDg3IDM2LjQ0NTMgMTUuOTU3VjE1LjgyMDNDMzYuNDQ1MyAxNS41Mjg2IDM2LjQ4NDQgMTUuMjU1MiAzNi41NjI1IDE1QzM2LjY0MzIgMTQuNzQyMiAzNi43NjMgMTQuNTE1NiAzNi45MjE5IDE0LjMyMDNDMzcuMDgwNyAxNC4xMjUgMzcuMjc5OSAxMy45NzI3IDM3LjUxOTUgMTMuODYzM0MzNy43NTkxIDEzLjc1MTMgMzguMDM5MSAxMy42OTUzIDM4LjM1OTQgMTMuNjk1M0MzOC42OTc5IDEzLjY5NTMgMzguOTk0OCAxMy43NjMgMzkuMjUgMTMuODk4NEMzOS41MDUyIDE0LjAzMTIgMzkuNzA1NyAxNC4yMTc0IDM5Ljg1MTYgMTQuNDU3QzQwIDE0LjY5NCA0MC4wNzY4IDE0Ljk3MDEgNDAuMDgyIDE1LjI4NTJIMzkuMTk1M0MzOS4xODc1IDE1LjEyODkgMzkuMTQ4NCAxNC45ODgzIDM5LjA3ODEgMTQuODYzM0MzOS4wMTA0IDE0LjczNTcgMzguOTE0MSAxNC42MzQxIDM4Ljc4OTEgMTQuNTU4NkMzOC42NjY3IDE0LjQ4MzEgMzguNTE5NSAxNC40NDUzIDM4LjM0NzcgMTQuNDQ1M0MzOC4xNTc2IDE0LjQ0NTMgMzggMTQuNDg0NCAzNy44NzUgMTQuNTYyNUMzNy43NSAxNC42MzggMzcuNjUyMyAxNC43NDIyIDM3LjU4MiAxNC44NzVDMzcuNTExNyAxNS4wMDUyIDM3LjQ2MDkgMTUuMTUyMyAzNy40Mjk3IDE1LjMxNjRDMzcuNDAxIDE1LjQ3NzkgMzcuMzg2NyAxNS42NDU4IDM3LjM4NjcgMTUuODIwM1YxNS45NTdDMzcuMzg2NyAxNi4xMzE1IDM3LjQwMSAxNi4zMDA4IDM3LjQyOTcgMTYuNDY0OEMzNy40NTgzIDE2LjYyODkgMzcuNTA3OCAxNi43NzYgMzcuNTc4MSAxNi45MDYyQzM3LjY1MSAxNy4wMzM5IDM3Ljc1IDE3LjEzNjcgMzcuODc1IDE3LjIxNDhDMzggMTcuMjkwNCAzOC4xNTg5IDE3LjMyODEgMzguMzUxNiAxNy4zMjgxWk00Mi4yODEyIDE3LjUzOTFMNDMuNDI5NyAxMy43NzM0SDQ0LjQzNzVMNDIuNzQyMiAxOC42NDQ1QzQyLjcwMzEgMTguNzQ4NyA0Mi42NTIzIDE4Ljg2MiA0Mi41ODk4IDE4Ljk4NDRDNDIuNTI3MyAxOS4xMDY4IDQyLjQ0NTMgMTkuMjIyNyA0Mi4zNDM4IDE5LjMzMkM0Mi4yNDQ4IDE5LjQ0NCA0Mi4xMjExIDE5LjUzMzkgNDEuOTcyNyAxOS42MDE2QzQxLjgyNDIgMTkuNjcxOSA0MS42NDQ1IDE5LjcwNyA0MS40MzM2IDE5LjcwN0M0MS4zNTAzIDE5LjcwNyA0MS4yNjk1IDE5LjY5OTIgNDEuMTkxNCAxOS42ODM2QzQxLjExNTkgMTkuNjcwNiA0MS4wNDQzIDE5LjY1NjIgNDAuOTc2NiAxOS42NDA2TDQwLjk3MjcgMTguOTIxOUM0MC45OTg3IDE4LjkyNDUgNDEuMDI5OSAxOC45MjcxIDQxLjA2NjQgMTguOTI5N0M0MS4xMDU1IDE4LjkzMjMgNDEuMTM2NyAxOC45MzM2IDQxLjE2MDIgMTguOTMzNkM0MS4zMTY0IDE4LjkzMzYgNDEuNDQ2NiAxOC45MTQxIDQxLjU1MDggMTguODc1QzQxLjY1NDkgMTguODM4NSA0MS43Mzk2IDE4Ljc3ODYgNDEuODA0NyAxOC42OTUzQzQxLjg3MjQgMTguNjEyIDQxLjkyOTcgMTguNSA0MS45NzY2IDE4LjM1OTRMNDIuMjgxMiAxNy41MzkxWk00MS42MzI4IDEzLjc3MzRMNDIuNjM2NyAxNi45Mzc1TDQyLjgwNDcgMTcuOTI5N0w0Mi4xNTIzIDE4LjA5NzdMNDAuNjE3MiAxMy43NzM0SDQxLjYzMjhaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yMC42MjExIDQ4LjkzNzVWNTAuNDg0NEwxMy44MDA4IDY2SDEwLjgyNDJMMTcuNjMyOCA1MS4xODc1SDguNzk2ODhWNDguOTM3NUgyMC42MjExWk0zNC4wNTA4IDU2LjAzOTFWNTguODI4MUMzNC4wNTA4IDYwLjE2NDEgMzMuOTE4IDYxLjMwNDcgMzMuNjUyMyA2Mi4yNUMzMy4zOTQ1IDYzLjE4NzUgMzMuMDE5NSA2My45NDkyIDMyLjUyNzMgNjQuNTM1MkMzMi4wMzUyIDY1LjEyMTEgMzEuNDQ1MyA2NS41NTA4IDMwLjc1NzggNjUuODI0MkMzMC4wNzgxIDY2LjA5NzcgMjkuMzE2NCA2Ni4yMzQ0IDI4LjQ3MjcgNjYuMjM0NEMyNy44MDA4IDY2LjIzNDQgMjcuMTc1OCA2Ni4xNDg0IDI2LjU5NzcgNjUuOTc2NkMyNi4wMjczIDY1LjgwNDcgMjUuNTExNyA2NS41MzUyIDI1LjA1MDggNjUuMTY4QzI0LjU4OTggNjQuODAwOCAyNC4xOTUzIDY0LjMyODEgMjMuODY3MiA2My43NUMyMy41NDY5IDYzLjE2NDEgMjMuMjk2OSA2Mi40NjQ4IDIzLjExNzIgNjEuNjUyM0MyMi45NDUzIDYwLjgzOTggMjIuODU5NCA1OS44OTg0IDIyLjg1OTQgNTguODI4MVY1Ni4wMzkxQzIyLjg1OTQgNTQuNjk1MyAyMi45OTIyIDUzLjU2MjUgMjMuMjU3OCA1Mi42NDA2QzIzLjUyMzQgNTEuNzEwOSAyMy45MDIzIDUwLjk1NyAyNC4zOTQ1IDUwLjM3ODlDMjQuODg2NyA0OS43OTMgMjUuNDcyNyA0OS4zNjcyIDI2LjE1MjMgNDkuMTAxNkMyNi44Mzk4IDQ4LjgzNTkgMjcuNjA1NSA0OC43MDMxIDI4LjQ0OTIgNDguNzAzMUMyOS4xMjg5IDQ4LjcwMzEgMjkuNzUzOSA0OC43ODkxIDMwLjMyNDIgNDguOTYwOUMzMC45MDIzIDQ5LjEyNSAzMS40MTggNDkuMzg2NyAzMS44NzExIDQ5Ljc0NjFDMzIuMzMyIDUwLjEwNTUgMzIuNzIyNyA1MC41NzQyIDMzLjA0MyA1MS4xNTIzQzMzLjM3MTEgNTEuNzIyNyAzMy42MjExIDUyLjQxNDEgMzMuNzkzIDUzLjIyNjZDMzMuOTY0OCA1NC4wMzEyIDM0LjA1MDggNTQuOTY4OCAzNC4wNTA4IDU2LjAzOTFaTTMxLjIyNjYgNTkuMjI2NlY1NS42MTcyQzMxLjIyNjYgNTQuOTM3NSAzMS4xODc1IDU0LjMzOTggMzEuMTA5NCA1My44MjQyQzMxLjAzMTIgNTMuMzAwOCAzMC45MTQxIDUyLjg1OTQgMzAuNzU3OCA1Mi41QzMwLjYwOTQgNTIuMTMyOCAzMC40MjE5IDUxLjgzNTkgMzAuMTk1MyA1MS42MDk0QzI5Ljk2ODggNTEuMzc1IDI5LjcxMDkgNTEuMjA3IDI5LjQyMTkgNTEuMTA1NUMyOS4xMzI4IDUwLjk5NjEgMjguODA4NiA1MC45NDE0IDI4LjQ0OTIgNTAuOTQxNEMyOC4wMTE3IDUwLjk0MTQgMjcuNjIxMSA1MS4wMjczIDI3LjI3NzMgNTEuMTk5MkMyNi45MzM2IDUxLjM2MzMgMjYuNjQ0NSA1MS42Mjg5IDI2LjQxMDIgNTEuOTk2MUMyNi4xNzU4IDUyLjM2MzMgMjUuOTk2MSA1Mi44NDc3IDI1Ljg3MTEgNTMuNDQ5MkMyNS43NTM5IDU0LjA0MyAyNS42OTUzIDU0Ljc2NTYgMjUuNjk1MyA1NS42MTcyVjU5LjIyNjZDMjUuNjk1MyA1OS45MTQxIDI1LjczNDQgNjAuNTE5NSAyNS44MTI1IDYxLjA0M0MyNS44OTA2IDYxLjU2NjQgMjYuMDA3OCA2Mi4wMTU2IDI2LjE2NDEgNjIuMzkwNkMyNi4zMjAzIDYyLjc1NzggMjYuNTA3OCA2My4wNjI1IDI2LjcyNjYgNjMuMzA0N0MyNi45NTMxIDYzLjUzOTEgMjcuMjEwOSA2My43MTA5IDI3LjUgNjMuODIwM0MyNy43OTY5IDYzLjkyOTcgMjguMTIxMSA2My45ODQ0IDI4LjQ3MjcgNjMuOTg0NEMyOC45MTggNjMuOTg0NCAyOS4zMTI1IDYzLjg5ODQgMjkuNjU2MiA2My43MjY2QzMwIDYzLjU1NDcgMzAuMjg5MSA2My4yODEyIDMwLjUyMzQgNjIuOTA2MkMzMC43NTc4IDYyLjUyMzQgMzAuOTMzNiA2Mi4wMjczIDMxLjA1MDggNjEuNDE4QzMxLjE2OCA2MC44MDg2IDMxLjIyNjYgNjAuMDc4MSAzMS4yMjY2IDU5LjIyNjZaIiBmaWxsPSIjM0ZBNzFBIi8+CjxwYXRoIGQ9Ik0zOS4xNTcyIDU4LjQ4NzNWNTcuOTYwOUMzOS4xNTcyIDU3LjU4MjcgMzkuMjM5MyA1Ny4yMzg2IDM5LjQwMzMgNTYuOTI4N0MzOS41Njc0IDU2LjYxODggMzkuODA2NiA1Ni4zNzA0IDQwLjEyMTEgNTYuMTgzNkM0MC40MzU1IDU1Ljk5NjcgNDAuODEzOCA1NS45MDMzIDQxLjI1NTkgNTUuOTAzM0M0MS43MTE2IDU1LjkwMzMgNDIuMDk0NCA1NS45OTY3IDQyLjQwNDMgNTYuMTgzNkM0Mi43MTg4IDU2LjM3MDQgNDIuOTU4IDU2LjYxODggNDMuMTIyMSA1Ni45Mjg3QzQzLjI4NjEgNTcuMjM4NiA0My4zNjgyIDU3LjU4MjcgNDMuMzY4MiA1Ny45NjA5VjU4LjQ4NzNDNDMuMzY4MiA1OC44NTY0IDQzLjI4NjEgNTkuMTk2IDQzLjEyMjEgNTkuNTA1OUM0Mi45NjI2IDU5LjgxNTggNDIuNzI1NiA2MC4wNjQxIDQyLjQxMTEgNjAuMjUxQzQyLjEwMTIgNjAuNDM3OCA0MS43MjA3IDYwLjUzMTIgNDEuMjY5NSA2MC41MzEyQzQwLjgyMjkgNjAuNTMxMiA0MC40NDAxIDYwLjQzNzggNDAuMTIxMSA2MC4yNTFDMzkuODA2NiA2MC4wNjQxIDM5LjU2NzQgNTkuODE1OCAzOS40MDMzIDU5LjUwNTlDMzkuMjM5MyA1OS4xOTYgMzkuMTU3MiA1OC44NTY0IDM5LjE1NzIgNTguNDg3M1pNNDAuMjk4OCA1Ny45NjA5VjU4LjQ4NzNDNDAuMjk4OCA1OC42Njk2IDQwLjMzMyA1OC44NDI4IDQwLjQwMTQgNTkuMDA2OEM0MC40NzQzIDU5LjE3MDkgNDAuNTgzNyA1OS4zMDMxIDQwLjcyOTUgNTkuNDAzM0M0MC44NzUzIDU5LjUwMzYgNDEuMDU1MyA1OS41NTM3IDQxLjI2OTUgNTkuNTUzN0M0MS40ODgzIDU5LjU1MzcgNDEuNjY2IDU5LjUwMzYgNDEuODAyNyA1OS40MDMzQzQxLjk0NCA1OS4zMDMxIDQyLjA0ODggNTkuMTcwOSA0Mi4xMTcyIDU5LjAwNjhDNDIuMTg1NSA1OC44NDI4IDQyLjIxOTcgNTguNjY5NiA0Mi4yMTk3IDU4LjQ4NzNWNTcuOTYwOUM0Mi4yMTk3IDU3Ljc3NDEgNDIuMTgzMyA1Ny41OTg2IDQyLjExMDQgNTcuNDM0NkM0Mi4wNDIgNTcuMjY2IDQxLjkzNzIgNTcuMTMxNSA0MS43OTU5IDU3LjAzMTJDNDEuNjU0NiA1Ni45MzEgNDEuNDc0NiA1Ni44ODA5IDQxLjI1NTkgNTYuODgwOUM0MS4wNDYyIDU2Ljg4MDkgNDAuODY4NSA1Ni45MzEgNDAuNzIyNyA1Ny4wMzEyQzQwLjU4MTQgNTcuMTMxNSA0MC40NzQzIDU3LjI2NiA0MC40MDE0IDU3LjQzNDZDNDAuMzMzIDU3LjU5ODYgNDAuMjk4OCA1Ny43NzQxIDQwLjI5ODggNTcuOTYwOVpNNDMuOTU2MSA2NC4wOTI4VjYzLjU1OTZDNDMuOTU2MSA2My4xODU5IDQ0LjAzODEgNjIuODQ0MSA0NC4yMDIxIDYyLjUzNDJDNDQuMzcwOCA2Mi4yMjQzIDQ0LjYxMjMgNjEuOTc1OSA0NC45MjY4IDYxLjc4OTFDNDUuMjQxMiA2MS42MDIyIDQ1LjYxOTUgNjEuNTA4OCA0Ni4wNjE1IDYxLjUwODhDNDYuNTE3MyA2MS41MDg4IDQ2LjkwMDEgNjEuNjAyMiA0Ny4yMSA2MS43ODkxQzQ3LjUyNDQgNjEuOTc1OSA0Ny43NjE0IDYyLjIyNDMgNDcuOTIwOSA2Mi41MzQyQzQ4LjA4NSA2Mi44NDQxIDQ4LjE2NyA2My4xODU5IDQ4LjE2NyA2My41NTk2VjY0LjA5MjhDNDguMTY3IDY0LjQ2NjUgNDguMDg1IDY0LjgwODMgNDcuOTIwOSA2NS4xMTgyQzQ3Ljc2MTQgNjUuNDI4MSA0Ny41MjY3IDY1LjY3NjQgNDcuMjE2OCA2NS44NjMzQzQ2LjkwNjkgNjYuMDUwMSA0Ni41Mjg2IDY2LjE0MzYgNDYuMDgyIDY2LjE0MzZDNDUuNjMwOSA2Ni4xNDM2IDQ1LjI0NTggNjYuMDUwMSA0NC45MjY4IDY1Ljg2MzNDNDQuNjEyMyA2NS42NzY0IDQ0LjM3MDggNjUuNDI4MSA0NC4yMDIxIDY1LjExODJDNDQuMDM4MSA2NC44MDgzIDQzLjk1NjEgNjQuNDY2NSA0My45NTYxIDY0LjA5MjhaTTQ1LjEwNDUgNjMuNTU5NlY2NC4wOTI4QzQ1LjEwNDUgNjQuMjc1MSA0NS4xNDEgNjQuNDQ4MiA0NS4yMTM5IDY0LjYxMjNDNDUuMjkxMyA2NC43NzY0IDQ1LjQwMyA2NC45MTA4IDQ1LjU0ODggNjUuMDE1NkM0NS42OTQ3IDY1LjExNTkgNDUuODcwMSA2NS4xNjYgNDYuMDc1MiA2NS4xNjZDNDYuMzA3NiA2NS4xNjYgNDYuNDk0NSA2NS4xMTU5IDQ2LjYzNTcgNjUuMDE1NkM0Ni43NzcgNjQuOTEwOCA0Ni44NzczIDY0Ljc3ODYgNDYuOTM2NSA2NC42MTkxQzQ3LjAwMDMgNjQuNDU1MSA0Ny4wMzIyIDY0LjI3OTYgNDcuMDMyMiA2NC4wOTI4VjYzLjU1OTZDNDcuMDMyMiA2My4zNzI3IDQ2Ljk5NTggNjMuMTk3MyA0Ni45MjI5IDYzLjAzMzJDNDYuODU0NSA2Mi44NjkxIDQ2Ljc0NzQgNjIuNzM3IDQ2LjYwMTYgNjIuNjM2N0M0Ni40NjAzIDYyLjUzNjUgNDYuMjgwMyA2Mi40ODYzIDQ2LjA2MTUgNjIuNDg2M0M0NS44NDczIDYyLjQ4NjMgNDUuNjY5NiA2Mi41MzY1IDQ1LjUyODMgNjIuNjM2N0M0NS4zODcgNjIuNzM3IDQ1LjI3OTkgNjIuODY5MSA0NS4yMDcgNjMuMDMzMkM0NS4xMzg3IDYzLjE5NzMgNDUuMTA0NSA2My4zNzI3IDQ1LjEwNDUgNjMuNTU5NlpNNDYuNDcxNyA1Ny40Njg4TDQxLjYxMTMgNjUuMjQ4TDQwLjc3MDUgNjQuNzYyN0w0NS42MzA5IDU2Ljk4MzRMNDYuNDcxNyA1Ny40Njg4WiIgZmlsbD0iIzNGQTcxQSIvPgo8cGF0aCBkPSJNMTg2LjM2OCAyN0MxNzAuNDM4IDI3IDE3Mi4zMjUgNTguMDk3OSAxNTUuNzU3IDU4LjA5NzlDMTQxLjczOCA1OC4wOTc5IDEzOS40OSA0NC44NzIzIDEyOC42NTcgNDQuODcyM0MxMTcuODI0IDQ0Ljg3MjMgMTEzLjA2MyA2MyA5OS4wNDM4IDYzQzg3LjU3MzYgNjMgODEuMjAxMyA1OS4zMjM0IDcwLjM2ODQgNTkuMzIzNCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuODciIHN0cm9rZS13aWR0aD0iMS4zIi8+CjxyZWN0IHk9Ijg0IiB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijc2IiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iMC41IiB5PSI4NC41IiB3aWR0aD0iMTk5IiBoZWlnaHQ9Ijc1IiByeD0iMy41IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMTAuNTI3MyA5OC44MTY0TDEyLjAyMzQgOTQuMTY4SDEzLjEwNTVMMTEuMDY2NCA5OS44NTU1SDEwLjMzNTlMMTAuNTI3MyA5OC44MTY0Wk05LjE0NDUzIDk0LjE2OEwxMC42Mjg5IDk4LjgxNjRMMTAuODMyIDk5Ljg1NTVIMTAuMDk3N0w4LjA2NjQxIDk0LjE2OEg5LjE0NDUzWk0xNC45MDYyIDk1LjYyODlWOTkuODU1NUgxMy45NjA5Vjk1LjYyODlIMTQuOTA2MlpNMTMuODk4NCA5NC41MTk1QzEzLjg5ODQgOTQuMzc2MyAxMy45NDUzIDk0LjI1NzggMTQuMDM5MSA5NC4xNjQxQzE0LjEzNTQgOTQuMDY3NyAxNC4yNjgyIDk0LjAxOTUgMTQuNDM3NSA5NC4wMTk1QzE0LjYwNDIgOTQuMDE5NSAxNC43MzU3IDk0LjA2NzcgMTQuODMyIDk0LjE2NDFDMTQuOTI4NCA5NC4yNTc4IDE0Ljk3NjYgOTQuMzc2MyAxNC45NzY2IDk0LjUxOTVDMTQuOTc2NiA5NC42NjAyIDE0LjkyODQgOTQuNzc3MyAxNC44MzIgOTQuODcxMUMxNC43MzU3IDk0Ljk2NDggMTQuNjA0MiA5NS4wMTE3IDE0LjQzNzUgOTUuMDExN0MxNC4yNjgyIDk1LjAxMTcgMTQuMTM1NCA5NC45NjQ4IDE0LjAzOTEgOTQuODcxMUMxMy45NDUzIDk0Ljc3NzMgMTMuODk4NCA5NC42NjAyIDEzLjg5ODQgOTQuNTE5NVpNMTYuMTkxNCA5My44NTU1SDE3LjEzMjhWOTguOTUzMUwxNy4wNDMgOTkuODU1NUgxNi4xOTE0VjkzLjg1NTVaTTE5Ljg5MDYgOTcuNzAzMVY5Ny43ODUyQzE5Ljg5MDYgOTguMDk3NyAxOS44NTU1IDk4LjM4NTQgMTkuNzg1MiA5OC42NDg0QzE5LjcxNzQgOTguOTA4OSAxOS42MTMzIDk5LjEzNTQgMTkuNDcyNyA5OS4zMjgxQzE5LjMzNDYgOTkuNTIwOCAxOS4xNjI4IDk5LjY3MDYgMTguOTU3IDk5Ljc3NzNDMTguNzUzOSA5OS44ODE1IDE4LjUxODIgOTkuOTMzNiAxOC4yNSA5OS45MzM2QzE3Ljk4NyA5OS45MzM2IDE3Ljc1NzggOTkuODg0MSAxNy41NjI1IDk5Ljc4NTJDMTcuMzY3MiA5OS42ODYyIDE3LjIwMzEgOTkuNTQ1NiAxNy4wNzAzIDk5LjM2MzNDMTYuOTQwMSA5OS4xODEgMTYuODM0NiA5OC45NjM1IDE2Ljc1MzkgOTguNzEwOUMxNi42NzMyIDk4LjQ1ODMgMTYuNjE1OSA5OC4xNzk3IDE2LjU4MiA5Ny44NzVWOTcuNjEzM0MxNi42MTU5IDk3LjMwNiAxNi42NzMyIDk3LjAyNzMgMTYuNzUzOSA5Ni43NzczQzE2LjgzNDYgOTYuNTI0NyAxNi45NDAxIDk2LjMwNzMgMTcuMDcwMyA5Ni4xMjVDMTcuMjAzMSA5NS45NDAxIDE3LjM2NTkgOTUuNzk4MiAxNy41NTg2IDk1LjY5OTJDMTcuNzUzOSA5NS42MDAzIDE3Ljk4MTggOTUuNTUwOCAxOC4yNDIyIDk1LjU1MDhDMTguNTEzIDk1LjU1MDggMTguNzUxMyA5NS42MDI5IDE4Ljk1NyA5NS43MDdDMTkuMTY1NCA5NS44MTEyIDE5LjMzODUgOTUuOTU5NiAxOS40NzY2IDk2LjE1MjNDMTkuNjE0NiA5Ni4zNDI0IDE5LjcxNzQgOTYuNTY5IDE5Ljc4NTIgOTYuODMyQzE5Ljg1NTUgOTcuMDk1MSAxOS44OTA2IDk3LjM4NTQgMTkuODkwNiA5Ny43MDMxWk0xOC45NDkyIDk3Ljc4NTJWOTcuNzAzMUMxOC45NDkyIDk3LjUxMyAxOC45MzM2IDk3LjMzNDYgMTguOTAyMyA5Ny4xNjhDMTguODcxMSA5Ni45OTg3IDE4LjgxOSA5Ni44NTAzIDE4Ljc0NjEgOTYuNzIyN0MxOC42NzU4IDk2LjU5NTEgMTguNTc5NCA5Ni40OTQ4IDE4LjQ1NyA5Ni40MjE5QzE4LjMzNzIgOTYuMzQ2NCAxOC4xODc1IDk2LjMwODYgMTguMDA3OCA5Ni4zMDg2QzE3Ljg0MTEgOTYuMzA4NiAxNy42OTc5IDk2LjMzNzIgMTcuNTc4MSA5Ni4zOTQ1QzE3LjQ1ODMgOTYuNDUxOCAxNy4zNTgxIDk2LjUyOTkgMTcuMjc3MyA5Ni42Mjg5QzE3LjE5NjYgOTYuNzI3OSAxNy4xMzI4IDk2Ljg0MjQgMTcuMDg1OSA5Ni45NzI3QzE3LjA0MTcgOTcuMTAyOSAxNy4wMTE3IDk3LjI0MzUgMTYuOTk2MSA5Ny4zOTQ1Vjk4LjEwMTZDMTcuMDE5NSA5OC4yOTY5IDE3LjA2OSA5OC40NzY2IDE3LjE0NDUgOTguNjQwNkMxNy4yMjI3IDk4LjgwMjEgMTcuMzMyIDk4LjkzMjMgMTcuNDcyNyA5OS4wMzEyQzE3LjYxMzMgOTkuMTI3NiAxNy43OTQzIDk5LjE3NTggMTguMDE1NiA5OS4xNzU4QzE4LjE5MDEgOTkuMTc1OCAxOC4zMzcyIDk5LjE0MDYgMTguNDU3IDk5LjA3MDNDMTguNTc2OCA5OSAxOC42NzE5IDk4LjkwMjMgMTguNzQyMiA5OC43NzczQzE4LjgxNTEgOTguNjQ5NyAxOC44NjcyIDk4LjUwMTMgMTguODk4NCA5OC4zMzJDMTguOTMyMyA5OC4xNjI4IDE4Ljk0OTIgOTcuOTgwNSAxOC45NDkyIDk3Ljc4NTJaTTIxLjg4MjggOTYuNDMzNlY5OS44NTU1SDIwLjk0MTRWOTUuNjI4OUgyMS44Mzk4TDIxLjg4MjggOTYuNDMzNlpNMjMuMTc1OCA5NS42MDE2TDIzLjE2OCA5Ni40NzY2QzIzLjExMDcgOTYuNDY2MSAyMy4wNDgyIDk2LjQ1ODMgMjIuOTgwNSA5Ni40NTMxQzIyLjkxNTQgOTYuNDQ3OSAyMi44NTAzIDk2LjQ0NTMgMjIuNzg1MiA5Ni40NDUzQzIyLjYyMzcgOTYuNDQ1MyAyMi40ODE4IDk2LjQ2ODggMjIuMzU5NCA5Ni41MTU2QzIyLjIzNyA5Ni41NTk5IDIyLjEzNDEgOTYuNjI1IDIyLjA1MDggOTYuNzEwOUMyMS45NzAxIDk2Ljc5NDMgMjEuOTA3NiA5Ni44OTU4IDIxLjg2MzMgOTcuMDE1NkMyMS44MTkgOTcuMTM1NCAyMS43OTMgOTcuMjY5NSAyMS43ODUyIDk3LjQxOEwyMS41NzAzIDk3LjQzMzZDMjEuNTcwMyA5Ny4xNjggMjEuNTk2NCA5Ni45MjE5IDIxLjY0ODQgOTYuNjk1M0MyMS43MDA1IDk2LjQ2ODggMjEuNzc4NiA5Ni4yNjk1IDIxLjg4MjggOTYuMDk3N0MyMS45ODk2IDk1LjkyNTggMjIuMTIyNCA5NS43OTE3IDIyLjI4MTIgOTUuNjk1M0MyMi40NDI3IDk1LjU5OSAyMi42Mjg5IDk1LjU1MDggMjIuODM5OCA5NS41NTA4QzIyLjg5NzEgOTUuNTUwOCAyMi45NTgzIDk1LjU1NiAyMy4wMjM0IDk1LjU2NjRDMjMuMDkxMSA5NS41NzY4IDIzLjE0MTkgOTUuNTg4NSAyMy4xNzU4IDk1LjYwMTZaTTI2LjI1NzggOTkuMDA3OFY5Ni45OTIyQzI2LjI1NzggOTYuODQxMSAyNi4yMzA1IDk2LjcxMDkgMjYuMTc1OCA5Ni42MDE2QzI2LjEyMTEgOTYuNDkyMiAyNi4wMzc4IDk2LjQwNzYgMjUuOTI1OCA5Ni4zNDc3QzI1LjgxNjQgOTYuMjg3OCAyNS42Nzg0IDk2LjI1NzggMjUuNTExNyA5Ni4yNTc4QzI1LjM1ODEgOTYuMjU3OCAyNS4yMjUzIDk2LjI4MzkgMjUuMTEzMyA5Ni4zMzU5QzI1LjAwMTMgOTYuMzg4IDI0LjkxNDEgOTYuNDU4MyAyNC44NTE2IDk2LjU0NjlDMjQuNzg5MSA5Ni42MzU0IDI0Ljc1NzggOTYuNzM1NyAyNC43NTc4IDk2Ljg0NzdIMjMuODIwM0MyMy44MjAzIDk2LjY4MSAyMy44NjA3IDk2LjUxOTUgMjMuOTQxNCA5Ni4zNjMzQzI0LjAyMjEgOTYuMjA3IDI0LjEzOTMgOTYuMDY3NyAyNC4yOTMgOTUuOTQ1M0MyNC40NDY2IDk1LjgyMjkgMjQuNjMwMiA5NS43MjY2IDI0Ljg0MzggOTUuNjU2MkMyNS4wNTczIDk1LjU4NTkgMjUuMjk2OSA5NS41NTA4IDI1LjU2MjUgOTUuNTUwOEMyNS44ODAyIDk1LjU1MDggMjYuMTYxNSA5NS42MDQyIDI2LjQwNjIgOTUuNzEwOUMyNi42NTM2IDk1LjgxNzcgMjYuODQ3NyA5NS45NzkyIDI2Ljk4ODMgOTYuMTk1M0MyNy4xMzE1IDk2LjQwODkgMjcuMjAzMSA5Ni42NzcxIDI3LjIwMzEgOTdWOTguODc4OUMyNy4yMDMxIDk5LjA3MTYgMjcuMjE2MSA5OS4yNDQ4IDI3LjI0MjIgOTkuMzk4NEMyNy4yNzA4IDk5LjU0OTUgMjcuMzExMiA5OS42ODEgMjcuMzYzMyA5OS43OTNWOTkuODU1NUgyNi4zOTg0QzI2LjM1NDIgOTkuNzUzOSAyNi4zMTkgOTkuNjI1IDI2LjI5MyA5OS40Njg4QzI2LjI2OTUgOTkuMzA5OSAyNi4yNTc4IDk5LjE1NjIgMjYuMjU3OCA5OS4wMDc4Wk0yNi4zOTQ1IDk3LjI4NTJMMjYuNDAyMyA5Ny44NjcySDI1LjcyNjZDMjUuNTUyMSA5Ny44NjcyIDI1LjM5ODQgOTcuODg0MSAyNS4yNjU2IDk3LjkxOEMyNS4xMzI4IDk3Ljk0OTIgMjUuMDIyMSA5Ny45OTYxIDI0LjkzMzYgOTguMDU4NkMyNC44NDUxIDk4LjEyMTEgMjQuNzc4NiA5OC4xOTY2IDI0LjczNDQgOTguMjg1MkMyNC42OTAxIDk4LjM3MzcgMjQuNjY4IDk4LjQ3NCAyNC42NjggOTguNTg1OUMyNC42NjggOTguNjk3OSAyNC42OTQgOTguODAwOCAyNC43NDYxIDk4Ljg5NDVDMjQuNzk4MiA5OC45ODU3IDI0Ljg3MzcgOTkuMDU3MyAyNC45NzI3IDk5LjEwOTRDMjUuMDc0MiA5OS4xNjE1IDI1LjE5NjYgOTkuMTg3NSAyNS4zMzk4IDk5LjE4NzVDMjUuNTMyNiA5OS4xODc1IDI1LjcwMDUgOTkuMTQ4NCAyNS44NDM4IDk5LjA3MDNDMjUuOTg5NiA5OC45ODk2IDI2LjEwNDIgOTguODkxOSAyNi4xODc1IDk4Ljc3NzNDMjYuMjcwOCA5OC42NjAyIDI2LjMxNTEgOTguNTQ5NSAyNi4zMjAzIDk4LjQ0NTNMMjYuNjI1IDk4Ljg2MzNDMjYuNTkzOCA5OC45NzAxIDI2LjU0MDQgOTkuMDg0NiAyNi40NjQ4IDk5LjIwN0MyNi4zODkzIDk5LjMyOTQgMjYuMjkwNCA5OS40NDY2IDI2LjE2OCA5OS41NTg2QzI2LjA0ODIgOTkuNjY4IDI1LjkwMzYgOTkuNzU3OCAyNS43MzQ0IDk5LjgyODFDMjUuNTY3NyA5OS44OTg0IDI1LjM3NSA5OS45MzM2IDI1LjE1NjIgOTkuOTMzNkMyNC44ODAyIDk5LjkzMzYgMjQuNjM0MSA5OS44Nzg5IDI0LjQxOCA5OS43Njk1QzI0LjIwMTggOTkuNjU3NiAyNC4wMzI2IDk5LjUwNzggMjMuOTEwMiA5OS4zMjAzQzIzLjc4NzggOTkuMTMwMiAyMy43MjY2IDk4LjkxNTQgMjMuNzI2NiA5OC42NzU4QzIzLjcyNjYgOTguNDUxOCAyMy43NjgyIDk4LjI1MzkgMjMuODUxNiA5OC4wODJDMjMuOTM3NSA5Ny45MDc2IDI0LjA2MjUgOTcuNzYxNyAyNC4yMjY2IDk3LjY0NDVDMjQuMzkzMiA5Ny41MjczIDI0LjU5NjQgOTcuNDM4OCAyNC44MzU5IDk3LjM3ODlDMjUuMDc1NSA5Ny4zMTY0IDI1LjM0OSA5Ny4yODUyIDI1LjY1NjIgOTcuMjg1MkgyNi4zOTQ1Wk0zMC4zNzUgOTUuNjI4OVY5Ni4zMTY0SDI3Ljk5MjJWOTUuNjI4OUgzMC4zNzVaTTI4LjY3OTcgOTQuNTkzOEgyOS42MjExVjk4LjY4NzVDMjkuNjIxMSA5OC44MTc3IDI5LjYzOTMgOTguOTE4IDI5LjY3NTggOTguOTg4M0MyOS43MTQ4IDk5LjA1NiAyOS43NjgyIDk5LjEwMTYgMjkuODM1OSA5OS4xMjVDMjkuOTAzNiA5OS4xNDg0IDI5Ljk4MzEgOTkuMTYwMiAzMC4wNzQyIDk5LjE2MDJDMzAuMTM5MyA5OS4xNjAyIDMwLjIwMTggOTkuMTU2MiAzMC4yNjE3IDk5LjE0ODRDMzAuMzIxNiA5OS4xNDA2IDMwLjM2OTggOTkuMTMyOCAzMC40MDYyIDk5LjEyNUwzMC40MTAyIDk5Ljg0MzhDMzAuMzMyIDk5Ljg2NzIgMzAuMjQwOSA5OS44ODggMzAuMTM2NyA5OS45MDYyQzMwLjAzNTIgOTkuOTI0NSAyOS45MTggOTkuOTMzNiAyOS43ODUyIDk5LjkzMzZDMjkuNTY5IDk5LjkzMzYgMjkuMzc3NiA5OS44OTU4IDI5LjIxMDkgOTkuODIwM0MyOS4wNDQzIDk5Ljc0MjIgMjguOTE0MSA5OS42MTU5IDI4LjgyMDMgOTkuNDQxNEMyOC43MjY2IDk5LjI2NjkgMjguNjc5NyA5OS4wMzUyIDI4LjY3OTcgOTguNzQ2MVY5NC41OTM4Wk0zMi4zNTk0IDk1LjYyODlWOTkuODU1NUgzMS40MTQxVjk1LjYyODlIMzIuMzU5NFpNMzEuMzUxNiA5NC41MTk1QzMxLjM1MTYgOTQuMzc2MyAzMS4zOTg0IDk0LjI1NzggMzEuNDkyMiA5NC4xNjQxQzMxLjU4ODUgOTQuMDY3NyAzMS43MjE0IDk0LjAxOTUgMzEuODkwNiA5NC4wMTk1QzMyLjA1NzMgOTQuMDE5NSAzMi4xODg4IDk0LjA2NzcgMzIuMjg1MiA5NC4xNjQxQzMyLjM4MTUgOTQuMjU3OCAzMi40Mjk3IDk0LjM3NjMgMzIuNDI5NyA5NC41MTk1QzMyLjQyOTcgOTQuNjYwMiAzMi4zODE1IDk0Ljc3NzMgMzIuMjg1MiA5NC44NzExQzMyLjE4ODggOTQuOTY0OCAzMi4wNTczIDk1LjAxMTcgMzEuODkwNiA5NS4wMTE3QzMxLjcyMTQgOTUuMDExNyAzMS41ODg1IDk0Ljk2NDggMzEuNDkyMiA5NC44NzExQzMxLjM5ODQgOTQuNzc3MyAzMS4zNTE2IDk0LjY2MDIgMzEuMzUxNiA5NC41MTk1Wk0zMy40NjA5IDk3Ljc4OTFWOTcuNjk5MkMzMy40NjA5IDk3LjM5NDUgMzMuNTA1MiA5Ny4xMTIgMzMuNTkzOCA5Ni44NTE2QzMzLjY4MjMgOTYuNTg4NSAzMy44MDk5IDk2LjM2MDcgMzMuOTc2NiA5Ni4xNjhDMzQuMTQ1OCA5NS45NzI3IDM0LjM1MTYgOTUuODIxNiAzNC41OTM4IDk1LjcxNDhDMzQuODM4NSA5NS42MDU1IDM1LjExNDYgOTUuNTUwOCAzNS40MjE5IDk1LjU1MDhDMzUuNzMxOCA5NS41NTA4IDM2LjAwNzggOTUuNjA1NSAzNi4yNSA5NS43MTQ4QzM2LjQ5NDggOTUuODIxNiAzNi43MDE4IDk1Ljk3MjcgMzYuODcxMSA5Ni4xNjhDMzcuMDQwNCA5Ni4zNjA3IDM3LjE2OTMgOTYuNTg4NSAzNy4yNTc4IDk2Ljg1MTZDMzcuMzQ2NCA5Ny4xMTIgMzcuMzkwNiA5Ny4zOTQ1IDM3LjM5MDYgOTcuNjk5MlY5Ny43ODkxQzM3LjM5MDYgOTguMDkzOCAzNy4zNDY0IDk4LjM3NjMgMzcuMjU3OCA5OC42MzY3QzM3LjE2OTMgOTguODk3MSAzNy4wNDA0IDk5LjEyNSAzNi44NzExIDk5LjMyMDNDMzYuNzAxOCA5OS41MTMgMzYuNDk2MSA5OS42NjQxIDM2LjI1MzkgOTkuNzczNEMzNi4wMTE3IDk5Ljg4MDIgMzUuNzM3IDk5LjkzMzYgMzUuNDI5NyA5OS45MzM2QzM1LjExOTggOTkuOTMzNiAzNC44NDI0IDk5Ljg4MDIgMzQuNTk3NyA5OS43NzM0QzM0LjM1NTUgOTkuNjY0MSAzNC4xNDk3IDk5LjUxMyAzMy45ODA1IDk5LjMyMDNDMzMuODExMiA5OS4xMjUgMzMuNjgyMyA5OC44OTcxIDMzLjU5MzggOTguNjM2N0MzMy41MDUyIDk4LjM3NjMgMzMuNDYwOSA5OC4wOTM4IDMzLjQ2MDkgOTcuNzg5MVpNMzQuNDAyMyA5Ny42OTkyVjk3Ljc4OTFDMzQuNDAyMyA5Ny45NzkyIDM0LjQyMTkgOTguMTU4OSAzNC40NjA5IDk4LjMyODFDMzQuNSA5OC40OTc0IDM0LjU2MTIgOTguNjQ1OCAzNC42NDQ1IDk4Ljc3MzRDMzQuNzI3OSA5OC45MDEgMzQuODM0NiA5OS4wMDEzIDM0Ljk2NDggOTkuMDc0MkMzNS4wOTUxIDk5LjE0NzEgMzUuMjUgOTkuMTgzNiAzNS40Mjk3IDk5LjE4MzZDMzUuNjA0MiA5OS4xODM2IDM1Ljc1NTIgOTkuMTQ3MSAzNS44ODI4IDk5LjA3NDJDMzYuMDEzIDk5LjAwMTMgMzYuMTE5OCA5OC45MDEgMzYuMjAzMSA5OC43NzM0QzM2LjI4NjUgOTguNjQ1OCAzNi4zNDc3IDk4LjQ5NzQgMzYuMzg2NyA5OC4zMjgxQzM2LjQyODQgOTguMTU4OSAzNi40NDkyIDk3Ljk3OTIgMzYuNDQ5MiA5Ny43ODkxVjk3LjY5OTJDMzYuNDQ5MiA5Ny41MTE3IDM2LjQyODQgOTcuMzM0NiAzNi4zODY3IDk3LjE2OEMzNi4zNDc3IDk2Ljk5ODcgMzYuMjg1MiA5Ni44NDkgMzYuMTk5MiA5Ni43MTg4QzM2LjExNTkgOTYuNTg4NSAzNi4wMDkxIDk2LjQ4NyAzNS44Nzg5IDk2LjQxNDFDMzUuNzUxMyA5Ni4zMzg1IDM1LjU5OSA5Ni4zMDA4IDM1LjQyMTkgOTYuMzAwOEMzNS4yNDQ4IDk2LjMwMDggMzUuMDkxMSA5Ni4zMzg1IDM0Ljk2MDkgOTYuNDE0MUMzNC44MzMzIDk2LjQ4NyAzNC43Mjc5IDk2LjU4ODUgMzQuNjQ0NSA5Ni43MTg4QzM0LjU2MTIgOTYuODQ5IDM0LjUgOTYuOTk4NyAzNC40NjA5IDk3LjE2OEMzNC40MjE5IDk3LjMzNDYgMzQuNDAyMyA5Ny41MTE3IDM0LjQwMjMgOTcuNjk5MlpNMzkuMzYzMyA5Ni41MzEyVjk5Ljg1NTVIMzguNDIxOVY5NS42Mjg5SDM5LjMwODZMMzkuMzYzMyA5Ni41MzEyWk0zOS4xOTUzIDk3LjU4NTlMMzguODkwNiA5Ny41ODJDMzguODkzMiA5Ny4yODI2IDM4LjkzNDkgOTcuMDA3OCAzOS4wMTU2IDk2Ljc1NzhDMzkuMDk5IDk2LjUwNzggMzkuMjEzNSA5Ni4yOTMgMzkuMzU5NCA5Ni4xMTMzQzM5LjUwNzggOTUuOTMzNiAzOS42ODQ5IDk1Ljc5NTYgMzkuODkwNiA5NS42OTkyQzQwLjA5NjQgOTUuNjAwMyA0MC4zMjU1IDk1LjU1MDggNDAuNTc4MSA5NS41NTA4QzQwLjc4MTIgOTUuNTUwOCA0MC45NjQ4IDk1LjU3OTQgNDEuMTI4OSA5NS42MzY3QzQxLjI5NTYgOTUuNjkxNCA0MS40Mzc1IDk1Ljc4MTIgNDEuNTU0NyA5NS45MDYyQzQxLjY3NDUgOTYuMDMxMiA0MS43NjU2IDk2LjE5NCA0MS44MjgxIDk2LjM5NDVDNDEuODkwNiA5Ni41OTI0IDQxLjkyMTkgOTYuODM1OSA0MS45MjE5IDk3LjEyNVY5OS44NTU1SDQwLjk3NjZWOTcuMTIxMUM0MC45NzY2IDk2LjkxOCA0MC45NDY2IDk2Ljc1NzggNDAuODg2NyA5Ni42NDA2QzQwLjgyOTQgOTYuNTIwOCA0MC43NDQ4IDk2LjQzNjIgNDAuNjMyOCA5Ni4zODY3QzQwLjUyMzQgOTYuMzM0NiA0MC4zODY3IDk2LjMwODYgNDAuMjIyNyA5Ni4zMDg2QzQwLjA2MTIgOTYuMzA4NiAzOS45MTY3IDk2LjM0MjQgMzkuNzg5MSA5Ni40MTAyQzM5LjY2MTUgOTYuNDc3OSAzOS41NTM0IDk2LjU3MDMgMzkuNDY0OCA5Ni42ODc1QzM5LjM3ODkgOTYuODA0NyAzOS4zMTI1IDk2Ljk0MDEgMzkuMjY1NiA5Ny4wOTM4QzM5LjIxODggOTcuMjQ3NCAzOS4xOTUzIDk3LjQxMTUgMzkuMTk1MyA5Ny41ODU5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNDY3M185Nzc0KSI+CjxwYXRoIGQ9Ik00NC44ODgzIDEyMi41NzdMMTcuNDk2MSAxNDIuMzExTDggMTQ4LjM3MVYxNTUuMTQ1SDE5MlYxNDYuMjM3VjEzNi45ODdMMTc4LjM4OSAxNTEuMDI3TDE3MC44MDMgMTQyLjI0NkwxNjcuNTExIDE0NC44MjhMMTUzLjQ0MSAxNTQuNTE1QzE0NC4wNjIgMTQzLjk3OSAxMzYuMjQ2IDE0MC4wNDggMTI2LjAzOCAxMjkuNjk2TDExNS41MDUgMTQ1LjE1NkgxMDEuOTU5TDk5LjM0MjYgMTIwLjAzNEw5NC4wNzI4IDExMC4xNDVMNjguOTk3OSAxMzQuODE4TDYzLjAzNTQgMTM0LjA0M0w0NC44ODgzIDEyMi41NzdaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNDY3M185Nzc0KSIvPgo8cGF0aCBkPSJNOCAxNDguMDI3TDE3LjU4NTQgMTQyLjAyMkw0NS4yMzUzIDEyMi40NjVMNjMuNTUzMSAxMzMuODI5TDY5LjU3MTcgMTM0LjU5Nkw5NCAxMTFMOTkuNSAxMTkuOTY2QzEwMC41MzEgMTI5LjY4OSAxMDAuOTY5IDEzNS4xMTkgMTAyIDE0NC44NDFIMTE1LjVMMTI2IDEyOS4zOTVMMTUzLjUgMTU0LjE0NUwxNzAuOTUyIDE0MS45OTRMMTc4LjUgMTUwLjYwOUwxOTIgMTM2LjgzOSIgc3Ryb2tlPSJ1cmwoI3BhaW50MV9saW5lYXJfNDY3M185Nzc0KSIgc3Ryb2tlLXdpZHRoPSIwLjkwMTMxOCIvPgo8L2c+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMV80NjczXzk3NzQpIj4KPHBhdGggZD0iTTggMTEwLjY0NUwxOTIgMTEwLjY0NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTIwLjM5NUwxOTIgMTIwLjM5NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTMwLjE0NUwxOTIgMTMwLjE0NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTM5Ljg5NUwxOTIgMTM5Ljg5NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTQ5LjY0NUwxOTIgMTQ5LjY0NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzQ2NzNfOTc3NCIgeDE9Ijk1LjU4ODkiIHkxPSIxNjAuMzI1IiB4Mj0iOTUuNTg4OSIgeTI9Ijk4LjMzODciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjE3MzM4OSIgc3RvcC1jb2xvcj0iIzZGQ0M0QyIvPgo8c3RvcCBvZmZzZXQ9IjAuMzI3MDc0IiBzdG9wLWNvbG9yPSIjNzBDQzRDIi8+CjxzdG9wIG9mZnNldD0iMC4zMzE2NTYiIHN0b3AtY29sb3I9IiNGRUJBM0QiLz4KPHN0b3Agb2Zmc2V0PSIwLjQ4MDg3NiIgc3RvcC1jb2xvcj0iI0ZFQkEzRCIvPgo8c3RvcCBvZmZzZXQ9IjAuNDkxMjQyIiBzdG9wLWNvbG9yPSIjRkQ4RjNDIi8+CjxzdG9wIG9mZnNldD0iMC42Mzc0MzEiIHN0b3AtY29sb3I9IiNGRDhGM0MiLz4KPHN0b3Agb2Zmc2V0PSIwLjY0NTY0OSIgc3RvcC1jb2xvcj0iI0Y1NjE0OCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfNDY3M185Nzc0IiB4MT0iOTQuNzg2MiIgeTE9IjE1NC41MzciIHgyPSI5NC43NzY0IiB5Mj0iOTguNDQ0OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjU4NDUyIiBzdG9wLWNvbG9yPSIjM0ZBNzFBIi8+CjxzdG9wIG9mZnNldD0iMC4yNjk2MDEiIHN0b3AtY29sb3I9IiNGRkE2MDAiLz4KPHN0b3Agb2Zmc2V0PSIwLjQyNzI1OSIgc3RvcC1jb2xvcj0iI0ZGQTYwMCIvPgo8c3RvcCBvZmZzZXQ9IjAuNDQzODQ3IiBzdG9wLWNvbG9yPSIjRjU2RTA4Ii8+CjxzdG9wIG9mZnNldD0iMC42MDM1NDciIHN0b3AtY29sb3I9IiNGNTZFMDgiLz4KPHN0b3Agb2Zmc2V0PSIwLjYwNjQ2NyIgc3RvcC1jb2xvcj0iI0ZGQTYwMCIvPgo8c3RvcCBvZmZzZXQ9IjAuNjExNTkxIiBzdG9wLWNvbG9yPSIjRjA0MDIyIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNDY3M185Nzc0Ij4KPHJlY3Qgd2lkdGg9IjE4NCIgaGVpZ2h0PSI0NSIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDggMTEwLjE0NSkiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwMV80NjczXzk3NzQiPgo8cmVjdCB3aWR0aD0iMTg0IiBoZWlnaHQ9IjQ1IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAxMTAuMTQ1KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", - "description": "Contains widgets displaying industrial telemetry.", - "order": 9500, - "externalId": null, - "name": "Industrial widgets" - }, - "widgetTypeFqns": [ - "flow_rate_card", - "flow_rate_card_with_background", - "horizontal_flow_rate_card", - "horizontal_flow_rate_card_with_background", - "flow_rate_chart_card", - "flow_rate_chart_card_with_background", - "simple_flow_rate_chart_card", - "simple_flow_rate_chart_card_with_background", - "flow_rate_progress_bar", - "flow_rate_progress_bar_with_background", - "flow_rate_range_chart", - "flow_rate_range_chart_with_background", - "flow_rate_gauge", - "fluid_pressure_card", - "fluid_pressure_card_with_background", - "horizontal_fluid_pressure_card", - "horizontal_fluid_pressure_card_with_background", - "fluid_pressure_chart_card", - "fluid_pressure_chart_card_with_background", - "simple_fluid_pressure_chart_card", - "simple_fluid_pressure_chart_card_with_background", - "fluid_pressure_progress_bar", - "fluid_pressure_progress_bar_with_background", - "fluid_pressure_range_chart", - "fluid_pressure_range_chart_with_background", - "fluid_pressure_gauge", - "pump_vibration_card", - "pump_vibration_card_with_background", - "horizontal_pump_vibration_card", - "horizontal_pump_vibration_card_with_background", - "pump_vibration_chart_card", - "pump_vibration_chart_card_with_background", - "simple_pump_vibration_chart_card", - "simple_pump_vibration_chart_card_with_background", - "pump_vibration_range_chart", - "pump_vibration_range_chart_with_background", - "power_consumption_card", - "power_consumption_card_with_background", - "horizontal_power_consumption_card", - "horizontal_power_consumption_card_with_background", - "power_consumption_chart_card", - "power_consumption_chart_card_with_background", - "simple_power_consumption_chart_card", - "simple_power_consumption_chart_card_with_background", - "power_consumption_range_chart", - "power_consumption_range_chart_with_background", - "rotational_speed_card", - "rotational_speed_card_with_background", - "horizontal_rotational_speed_card", - "horizontal_rotational_speed_card_with_background", - "rotational_speed_chart_card", - "rotational_speed_chart_card_with_background", - "simple_rotational_speed_chart_card", - "simple_rotational_speed_chart_card_with_background", - "rotational_speed_progress_bar", - "rotational_speed_progress_bar_with_background", - "rotational_speed_range_chart", - "rotational_speed_range_chart_with_background", - "rotational_speed_gauge", - "efficiency_card", - "efficiency_card_with_background", - "horizontal_efficiency_card", - "horizontal_efficiency_card_with_background", - "efficiency_chart_card", - "efficiency_chart_card_with_background", - "simple_efficiency_chart_card", - "simple_efficiency_chart_card_with_background", - "efficiency_progress_bar", - "efficiency_progress_bar_with_background", - "efficiency_range_chart", - "efficiency_range_chart_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/outdoor_environment.json b/application/src/main/data/json/system/widget_bundles/outdoor_environment.json deleted file mode 100644 index c7705c5e91..0000000000 --- a/application/src/main/data/json/system/widget_bundles/outdoor_environment.json +++ /dev/null @@ -1,190 +0,0 @@ -{ - "widgetsBundle": { - "alias": "outdoor_environment", - "title": "Outdoor Environment", - "image": "data:image/png;base64,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", - "description": "Contains widgets displaying outdoor environment telemetry.", - "order": 11000, - "externalId": null, - "name": "Outdoor Environment" - }, - "widgetTypeFqns": [ - "temperature_card", - "temperature_card_with_background", - "horizontal_temperature_card", - "horizontal_temperature_card_with_background", - "temperature_chart_card", - "temperature_chart_card_with_background", - "simple_temperature_chart_card", - "simple_temperature_chart_card_with_background", - "temperature_range_chart", - "temperature_range_chart_with_background", - "temperature_gauge", - "humidity_card", - "humidity_card_with_background", - "horizontal_humidity_card", - "horizontal_humidity_card_with_background", - "humidity_chart_card", - "humidity_chart_card_with_background", - "simple_humidity_chart_card", - "simple_humidity_chart_card_with_background", - "humidity_progress_bar", - "humidity_progress_bar_with_background", - "pressure_card", - "pressure_card_with_background", - "horizontal_pressure_card", - "horizontal_pressure_card_with_background", - "pressure_chart_card", - "pressure_chart_card_with_background", - "simple_pressure_chart_card", - "simple_pressure_chart_card_with_background", - "pressure_progress_bar", - "pressure_progress_bar_with_background", - "wind_speed_card", - "wind_speed_card_with_background", - "horizontal_wind_speed_card", - "horizontal_wind_speed_card_with_background", - "wind_speed_chart_card", - "wind_speed_chart_card_with_background", - "simple_wind_speed_chart_card", - "simple_wind_speed_chart_card_with_background", - "wind_speed_and_direction", - "wind_speed_and_direction_with_background", - "rainfall_card", - "rainfall_card_with_background", - "horizontal_rainfall_card", - "horizontal_rainfall_card_with_background", - "rainfall_chart_card", - "rainfall_chart_card_with_background", - "simple_rainfall_chart_card", - "simple_rainfall_chart_card_with_background", - "solar_radiation_card", - "solar_radiation_card_with_background", - "horizontal_solar_radiation_card", - "horizontal_solar_radiation_card_with_background", - "solar_radiation_chart_card", - "solar_radiation_chart_card_with_background", - "simple_solar_radiation_chart_card", - "simple_solar_radiation_chart_card_with_background", - "uv_index_card", - "uv_index_card_with_background", - "horizontal_uv_index_card", - "horizontal_uv_index_card_with_background", - "uv_index_chart_card", - "uv_index_chart_card_with_background", - "simple_uv_index_chart_card", - "simple_uv_index_chart_card_with_background", - "air_quality_card", - "air_quality_card_with_background", - "horizontal_air_quality_card", - "horizontal_air_quality_card_with_background", - "air_quality_chart_card", - "air_quality_chart_card_with_background", - "simple_air_quality_chart_card", - "simple_air_quality_chart_card_with_background", - "visibility_card", - "visibility_card_with_background", - "horizontal_visibility_card", - "horizontal_visibility_card_with_background", - "visibility_chart_card", - "visibility_chart_card_with_background", - "simple_visibility_chart_card", - "simple_visibility_chart_card_with_background", - "ground_temperature_card", - "ground_temperature_card_with_background", - "horizontal_ground_temperature_card", - "horizontal_ground_temperature_card_with_background", - "ground_temperature_chart_card", - "ground_temperature_chart_card_with_background", - "simple_ground_temperature_chart_card", - "simple_ground_temperature_chart_card_with_background", - "soil_moisture_card", - "soil_moisture_card_with_background", - "horizontal_soil_moisture_card", - "horizontal_soil_moisture_card_with_background", - "soil_moisture_chart_card", - "soil_moisture_chart_card_with_background", - "simple_soil_moisture_chart_card", - "simple_soil_moisture_chart_card_with_background", - "soil_moisture_progress_bar", - "soil_moisture_progress_bar_with_background", - "noise_level_card", - "noise_level_card_with_background", - "horizontal_noise_level_card", - "horizontal_noise_level_card_with_background", - "noise_level_chart_card", - "noise_level_chart_card_with_background", - "simple_noise_level_chart_card", - "simple_noise_level_chart_card_with_background", - "vibration_card", - "vibration_card_with_background", - "horizontal_vibration_card", - "horizontal_vibration_card_with_background", - "vibration_chart_card", - "vibration_chart_card_with_background", - "simple_vibration_chart_card", - "simple_vibration_chart_card_with_background", - "leaf_wetness_card", - "leaf_wetness_card_with_background", - "horizontal_leaf_wetness_card", - "horizontal_leaf_wetness_card_with_background", - "leaf_wetness_chart_card", - "leaf_wetness_chart_card_with_background", - "simple_leaf_wetness_chart_card", - "simple_leaf_wetness_chart_card_with_background", - "leaf_wetness_progress_bar", - "leaf_wetness_progress_bar_with_background", - "snow_depth_card", - "snow_depth_card_with_background", - "horizontal_snow_depth_card", - "horizontal_snow_depth_card_with_background", - "snow_depth_chart_card", - "snow_depth_chart_card_with_background", - "simple_snow_depth_chart_card", - "simple_snow_depth_chart_card_with_background", - "co2_card", - "co2_card_with_background", - "horizontal_co2_card", - "horizontal_co2_card_with_background", - "co2_chart_card", - "co2_chart_card_with_background", - "simple_co2_chart_card", - "simple_co2_chart_card_with_background", - "flooding_level_card", - "flooding_level_card_with_background", - "horizontal_flooding_level_card", - "horizontal_flooding_level_card_with_background", - "flooding_level_chart_card", - "flooding_level_chart_card_with_background", - "simple_flooding_level_chart_card", - "simple_flooding_level_chart_card_with_background", - "flooding_level_progress_bar", - "flooding_level_progress_bar_with_background", - "illuminance_card", - "illuminance_card_with_background", - "horizontal_illuminance_card", - "horizontal_illuminance_card_with_background", - "illuminance_chart_card", - "illuminance_chart_card_with_background", - "simple_illuminance_chart_card", - "simple_illuminance_chart_card_with_background", - "illuminance_progress_bar", - "illuminance_progress_bar_with_background", - "pm2_5_card", - "pm2_5_card_with_background", - "horizontal_pm2_5_card", - "horizontal_pm2_5_card_with_background", - "pm2_5_chart_card", - "pm2_5_chart_card_with_background", - "simple_pm2_5_chart_card", - "simple_pm2_5_chart_card_with_background", - "pm10_card", - "pm10_card_with_background", - "horizontal_pm10_card", - "horizontal_pm10_card_with_background", - "pm10_chart_card", - "pm10_chart_card_with_background", - "simple_pm10_chart_card", - "simple_pm10_chart_card_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java index 09a30f9266..2af21a4af5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -46,6 +46,8 @@ import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.iot_hub.InstallItemVersionResult; +import org.thingsboard.server.service.iot_hub.InstallPlan; +import org.thingsboard.server.service.iot_hub.InstallPlanResult; import org.thingsboard.server.service.iot_hub.UpdateItemVersionResult; import org.thingsboard.server.service.iot_hub.IotHubService; @@ -71,6 +73,23 @@ public class IotHubController extends BaseController { return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request); } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/versions/{versionId}/installPlan") + @ResponseBody + public InstallPlan resolveInstallPlan(@PathVariable String versionId) throws ThingsboardException { + return iotHubService.resolveInstallPlan(getCurrentUser(), versionId); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/installPlan") + @ResponseBody + public InstallPlanResult installPlan(@RequestBody InstallPlanRequest body, + HttpServletRequest request) throws ThingsboardException { + return iotHubService.installPlan(getCurrentUser(), body.plan(), body.data(), request); + } + + public record InstallPlanRequest(InstallPlan plan, JsonNode data) {} + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/device/register") @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index bbe2ffcdae..5b64384283 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -75,8 +75,11 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.HexFormat; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Set; import java.util.UUID; @Service @@ -115,50 +118,28 @@ public class DefaultIotHubService implements IotHubService { private final TbDeviceService tbDeviceService; private final SolutionService solutionService; + // Field names of the marketplace version JSON payload. Both the install path and the + // install-plan resolver parse the same shape, so the contract lives here in one place. + private static final String FIELD_ID = "id"; + private static final String FIELD_ITEM_ID = "itemId"; + private static final String FIELD_TYPE = "type"; + private static final String FIELD_NAME = "name"; + private static final String FIELD_VERSION = "version"; + private static final String FIELD_RELATED_ITEMS = "relatedItems"; + @Override public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) { TenantId tenantId = user.getTenantId(); log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); - try { - JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); - if (versionInfo == null) { - throw new IllegalArgumentException("Failed to get version info from IoT Hub"); - } - String itemType = versionInfo.get("type").asText(); - String itemName = versionInfo.get("name").asText(); - UUID itemId = UUID.fromString(versionInfo.get("itemId").asText()); - String version = versionInfo.get("version").asText(); - log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); - - byte[] fileData = iotHubRestClient.getVersionFileData(versionId); - log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); - - IotHubInstalledItemDescriptor descriptor = switch (itemType) { - case "WIDGET" -> installWidget(user, tenantId, fileData); - case "DASHBOARD" -> installDashboard(user, tenantId, fileData); - case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data); - case "ALARM_RULE" -> installAlarmRule(user, tenantId, fileData, data); - case "RULE_CHAIN" -> installRuleChain(user, tenantId, fileData, data); - case "DEVICE" -> installDeviceProfile(user, tenantId, fileData); - case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request); - default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); - }; - - IotHubInstalledItem installedItem = new IotHubInstalledItem(); - installedItem.setTenantId(tenantId); - installedItem.setItemId(itemId); - installedItem.setItemVersionId(UUID.fromString(versionId)); - installedItem.setItemName(itemName); - installedItem.setItemType(itemType); - installedItem.setVersion(version); - installedItem.setDescriptor(descriptor); - iotHubInstalledItemService.save(tenantId, installedItem); - - iotHubRestClient.reportVersionInstalled(versionId); - log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + if (versionInfo == null) { + throw new IllegalArgumentException("Failed to get version info from IoT Hub"); + } - return InstallItemVersionResult.success(descriptor); + try { + IotHubInstalledItem installedItem = doInstallVersion(user, versionId, data, request); + return InstallItemVersionResult.success(installedItem.getDescriptor()); } catch (Exception e) { log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e); if (e instanceof EntitiesLimitExceededException el) { @@ -168,6 +149,54 @@ public class DefaultIotHubService implements IotHubService { } } + /** + * Fetch + apply a single marketplace version, persist the installed-item record, and ping + * the marketplace install counter. Throws on failure so callers (cascade install) can roll back. + * Package-private so the cascade-install / rollback orchestration can be unit-tested in isolation. + */ + IotHubInstalledItem doInstallVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) throws Exception { + TenantId tenantId = user.getTenantId(); + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + String itemType = versionInfo.get(FIELD_TYPE).asText(); + String itemName = versionInfo.get(FIELD_NAME).asText(); + UUID itemId = UUID.fromString(versionInfo.get(FIELD_ITEM_ID).asText()); + String version = versionInfo.get(FIELD_VERSION).asText(); + log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); + + byte[] fileData = iotHubRestClient.getVersionFileData(versionId); + log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); + + IotHubInstalledItemDescriptor descriptor = switch (itemType) { + case "WIDGET" -> installWidget(user, tenantId, fileData); + case "DASHBOARD" -> installDashboard(user, tenantId, fileData); + case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data); + case "ALARM_RULE" -> installAlarmRule(user, tenantId, fileData, data); + case "RULE_CHAIN" -> installRuleChain(user, tenantId, fileData, data); + case "DEVICE" -> installDeviceProfile(user, tenantId, fileData); + case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request); + default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); + }; + + IotHubInstalledItem installedItem = new IotHubInstalledItem(); + installedItem.setTenantId(tenantId); + installedItem.setItemId(itemId); + installedItem.setItemVersionId(UUID.fromString(versionId)); + installedItem.setItemName(itemName); + installedItem.setItemType(itemType); + installedItem.setVersion(version); + installedItem.setDescriptor(descriptor); + installedItem = iotHubInstalledItemService.save(tenantId, installedItem); + + try { + iotHubRestClient.reportVersionInstalled(versionId); + } catch (Exception e) { + // Counter ping is best-effort — do not fail the install if it errors. + log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + } + log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); + return installedItem; + } + private WidgetInstalledItemDescriptor installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { WidgetTypeDetails widgetTypeDetails; try { @@ -744,6 +773,257 @@ public class DefaultIotHubService implements IotHubService { log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); } + @Override + public InstallPlan resolveInstallPlan(SecurityUser user, String versionId) { + TenantId tenantId = user.getTenantId(); + log.debug("[{}] Resolving install plan for version: {}", tenantId, versionId); + + JsonNode rootVersion = iotHubRestClient.getVersionInfo(versionId); + if (rootVersion == null) { + throw new IllegalArgumentException("Marketplace version not found: " + versionId); + } + + // Null-safe: a malformed payload without an itemId must surface the friendly + // IllegalArgumentException thrown by addPlanEntry(root) below, not an NPE here. + String rootItemId = optText(rootVersion, FIELD_ITEM_ID); + JsonNode related = rootVersion.get(FIELD_RELATED_ITEMS); + + // The plan only ever touches the root and its (one-level-deep) related items, so we ask the + // DB whether just those item ids are already installed instead of loading every installed id + // for the tenant — a tenant may have thousands installed while a plan checks a handful. + Set candidateItemIds = new HashSet<>(); + addCandidateItemId(candidateItemIds, rootItemId); + if (related != null && related.isArray()) { + for (JsonNode relatedNode : related) { + addCandidateItemId(candidateItemIds, relatedNode.asText()); + } + } + Set alreadyInstalledItemIds = candidateItemIds.isEmpty() + ? Set.of() + : new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId, candidateItemIds)); + + // LinkedHashMap preserves insertion order; we add the related items (deps) before the + // root so a single forward iteration yields the correct install sequence (deps first, + // root last). Related items are leaf dependencies — IoT Hub items are only ever one + // level deep, so there is no need to walk a related item's own related items. + LinkedHashMap entries = new LinkedHashMap<>(); + + if (related != null && related.isArray()) { + for (JsonNode relatedNode : related) { + String relatedItemId = relatedNode.asText(); + if (relatedItemId == null || relatedItemId.isEmpty() + || relatedItemId.equals(rootItemId) || entries.containsKey(relatedItemId)) { + continue; + } + JsonNode relatedVersion; + try { + relatedVersion = iotHubRestClient.getPublishedVersionByItemId(relatedItemId); + } catch (Exception e) { + log.warn("Failed to fetch related item {}: {}", relatedItemId, e.getMessage()); + entries.put(relatedItemId, missingEntry(relatedItemId, e.getMessage())); + continue; + } + if (relatedVersion == null) { + log.warn("Related IoT Hub item {} is missing or unpublished — recording in plan", relatedItemId); + entries.put(relatedItemId, missingEntry(relatedItemId, "Item not found or not published")); + continue; + } + addPlanEntry(relatedVersion, alreadyInstalledItemIds, entries, false); + } + } + + addPlanEntry(rootVersion, alreadyInstalledItemIds, entries, true); + + return new InstallPlan(versionId, new ArrayList<>(entries.values())); + } + + /** + * Build an {@link InstallPlanEntry} for a single marketplace version and append it to the + * plan. No traversal of {@code relatedItems} happens here — related items are resolved one + * level deep by {@link #resolveInstallPlan}. + */ + private void addPlanEntry(JsonNode versionInfo, + Set alreadyInstalledItemIds, + LinkedHashMap entries, + boolean root) { + String itemId = optText(versionInfo, FIELD_ITEM_ID); + String versionId = optText(versionInfo, FIELD_ID); + if (itemId == null || versionId == null) { + // The marketplace payload is missing its identifiers. The root must have them to be + // installable; a related item without them is recorded as missing so the rest of the + // plan can still proceed. + if (root) { + throw new IllegalArgumentException("Marketplace version is missing required '" + + FIELD_ITEM_ID + "'/'" + FIELD_ID + "' fields"); + } + String key = itemId != null ? itemId : versionId; + log.warn("Related IoT Hub item is missing required identifiers — recording in plan: {}", versionInfo); + entries.putIfAbsent(key != null ? key : versionInfo.toString(), + missingEntry(itemId, "Item descriptor is incomplete")); + return; + } + if (entries.containsKey(itemId)) { + return; + } + + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId); + entry.setVersionId(versionId); + entry.setName(optText(versionInfo, FIELD_NAME)); + entry.setType(optText(versionInfo, FIELD_TYPE)); + entry.setVersion(optText(versionInfo, FIELD_VERSION)); + entry.setRoot(root); + entry.setStatus(isAlreadyInstalled(entry, alreadyInstalledItemIds) + ? InstallPlanEntry.Status.ALREADY_INSTALLED + : InstallPlanEntry.Status.WILL_INSTALL); + entries.put(itemId, entry); + } + + private static InstallPlanEntry missingEntry(String itemId, String errorMessage) { + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId); + entry.setStatus(InstallPlanEntry.Status.MISSING); + entry.setErrorMessage(errorMessage); + return entry; + } + + private static String optText(JsonNode node, String field) { + return node != null && node.hasNonNull(field) ? node.get(field).asText() : null; + } + + @Override + public InstallPlanResult installPlan(SecurityUser user, InstallPlan plan, JsonNode data, HttpServletRequest request) { + TenantId tenantId = user.getTenantId(); + if (plan == null || plan.getEntries() == null || plan.getEntries().isEmpty()) { + return new InstallPlanResult(false, false, "Install plan is empty", null, new ArrayList<>(), new ArrayList<>()); + } + + // The plan is resolved by a separate request, so by the time it is submitted an item may + // already have been installed (stale or replayed plan). Re-check the current state for only + // the items this plan would install, so we never install the same item twice. + Set candidateItemIds = new HashSet<>(); + for (InstallPlanEntry entry : plan.getEntries()) { + if (entry.getStatus() == InstallPlanEntry.Status.WILL_INSTALL) { + addCandidateItemId(candidateItemIds, entry.getItemId()); + } + } + // Mutable on purpose: as the cascade installs each item we add its id below, so a later + // duplicate entry for the same item in this plan is recognised as installed and skipped + // instead of installed twice. A client-supplied plan is not de-duplicated the way a freshly + // resolved one is, so the loop has to guard against duplicates itself. + Set alreadyInstalledItemIds = new HashSet<>(); + if (!candidateItemIds.isEmpty()) { + alreadyInstalledItemIds.addAll(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId, candidateItemIds)); + } + + InstallPlanResult result = new InstallPlanResult(); + List resultEntries = new ArrayList<>(); + List missingItemIds = new ArrayList<>(); + List rollbackIds = new ArrayList<>(); + IotHubInstalledItemDescriptor rootDescriptor = null; + + for (InstallPlanEntry entry : plan.getEntries()) { + InstallPlanEntry resultEntry = cloneEntry(entry); + switch (entry.getStatus()) { + case MISSING -> { + missingItemIds.add(entry.getItemId()); + resultEntries.add(resultEntry); + } + case ALREADY_INSTALLED -> resultEntries.add(resultEntry); + case WILL_INSTALL -> { + if (isAlreadyInstalled(entry, alreadyInstalledItemIds)) { + // Installed since the plan was resolved — skip rather than create a duplicate. + resultEntry.setStatus(InstallPlanEntry.Status.ALREADY_INSTALLED); + resultEntries.add(resultEntry); + break; + } + try { + // Only the root entry receives the user's install data (target profile entityId); + // transitive deps install with defaults. + JsonNode entryData = entry.isRoot() ? data : null; + IotHubInstalledItem installed = doInstallVersion(user, entry.getVersionId(), entryData, request); + rollbackIds.add(installed.getId()); + // Record it as installed so a duplicate entry for the same item later in this + // plan is skipped rather than installed a second time. + addCandidateItemId(alreadyInstalledItemIds, entry.getItemId()); + if (entry.isRoot()) { + rootDescriptor = installed.getDescriptor(); + } + resultEntries.add(resultEntry); + } catch (Exception e) { + log.error("[{}] Cascade install failed at entry {} ({}): {}", tenantId, + entry.getName(), entry.getVersionId(), e.getMessage(), e); + resultEntry.setErrorMessage(e.getMessage()); + resultEntries.add(resultEntry); + boolean rolledBack = rollbackInstalledItems(user, rollbackIds); + result.setSuccess(false); + result.setRolledBack(rolledBack); + result.setErrorMessage("Failed to install '" + entry.getName() + "': " + e.getMessage()); + result.setEntries(resultEntries); + result.setMissingItemIds(missingItemIds); + return result; + } + } + } + } + + result.setSuccess(true); + result.setRolledBack(false); + result.setRootDescriptor(rootDescriptor); + result.setEntries(resultEntries); + result.setMissingItemIds(missingItemIds); + return result; + } + + /** + * Deletes the supplied installed items in reverse install order. Returns {@code true} only if + * every deletion succeeded — a {@code false} result means some entities were left behind and + * the rollback is partial. + */ + private boolean rollbackInstalledItems(SecurityUser user, List installedIds) { + boolean fullyRolledBack = true; + for (int i = installedIds.size() - 1; i >= 0; i--) { + IotHubInstalledItemId id = installedIds.get(i); + try { + deleteInstalledItem(user, id); + } catch (Exception e) { + fullyRolledBack = false; + log.error("[{}] Failed to roll back installed item {}: {}", user.getTenantId(), id, e.getMessage(), e); + } + } + return fullyRolledBack; + } + + private static void addCandidateItemId(Set candidates, String itemId) { + if (itemId == null || itemId.isEmpty()) { + return; + } + try { + candidates.add(UUID.fromString(itemId)); + } catch (IllegalArgumentException ignored) { + // A non-UUID item id can never match an installed record — leave it out of the lookup. + } + } + + private static boolean isAlreadyInstalled(InstallPlanEntry entry, Set alreadyInstalledItemIds) { + if (entry == null || entry.getItemId() == null) { + return false; + } + if (entry.isRoot() && (!"WIDGET".equals(entry.getType()) && !"SOLUTION_TEMPLATE".equals(entry.getType()))) { + return false; + } + try { + return alreadyInstalledItemIds.contains(UUID.fromString(entry.getItemId())); + } catch (IllegalArgumentException ex) { + return false; + } + } + + private static InstallPlanEntry cloneEntry(InstallPlanEntry src) { + return new InstallPlanEntry(src.getItemId(), src.getVersionId(), src.getName(), src.getType(), + src.getVersion(), src.getStatus(), src.isRoot(), src.getErrorMessage()); + } + private static Exception parseFailure(String action, String itemTypeName, Exception cause) { return parseFailure(action, itemTypeName, SECTION_PACKAGE_DATA, cause); } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java new file mode 100644 index 0000000000..12c268a6b4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallPlan { + + private String rootVersionId; + private List entries; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java new file mode 100644 index 0000000000..c073ff8e0e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallPlanEntry { + + public enum Status { + WILL_INSTALL, + ALREADY_INSTALLED, + MISSING + } + + private String itemId; + private String versionId; + private String name; + private String type; + private String version; + private Status status; + private boolean root; + private String errorMessage; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java new file mode 100644 index 0000000000..2c3f49e071 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallPlanResult { + + private boolean success; + private boolean rolledBack; + private String errorMessage; + private IotHubInstalledItemDescriptor rootDescriptor; + private List entries = new ArrayList<>(); + private List missingItemIds = new ArrayList<>(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java index 1ea8b60d2e..c67fef55a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java @@ -21,7 +21,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -63,6 +65,23 @@ public class IotHubRestClient { return restTemplate.getForObject(url, JsonNode.class); } + /** + * Returns the latest published version of an item, or {@code null} if the marketplace + * responds with 404 (item missing or never published). Other 4xx/5xx still propagate. + */ + public JsonNode getPublishedVersionByItemId(String itemId) { + String url = baseUrl + "/api/items/" + itemId + "/published"; + log.debug("Fetching IoT Hub published version for item: {}", url); + try { + return restTemplate.getForObject(url, JsonNode.class); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return null; + } + throw e; + } + } + public byte[] getVersionFileData(String versionId) { String url = baseUrl + "/api/versions/" + versionId + "/fileData"; log.debug("Fetching IoT Hub version file data: {}", url); diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java index e2f0ff0c39..fac0f94b10 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java @@ -30,4 +30,8 @@ public interface IotHubService { InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor); void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId); + + InstallPlan resolveInstallPlan(SecurityUser user, String versionId); + + InstallPlanResult installPlan(SecurityUser user, InstallPlan plan, JsonNode data, HttpServletRequest request); } diff --git a/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java b/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java new file mode 100644 index 0000000000..d505509e4a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java @@ -0,0 +1,368 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultIotHubServiceTest { + + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + private final SecurityUser user = mock(SecurityUser.class); + + @Mock + private IotHubRestClient iotHubRestClient; + @Mock + private IotHubInstalledItemService iotHubInstalledItemService; + + @Spy + @InjectMocks + private DefaultIotHubService service; + + private void mockTenant() { + when(user.getTenantId()).thenReturn(tenantId); + } + + private IotHubInstalledItem installedItem(UUID id) { + IotHubInstalledItem item = new IotHubInstalledItem(); + item.setId(new IotHubInstalledItemId(id)); + item.setTenantId(tenantId); + return item; + } + + private InstallPlanEntry willInstall(String versionId, boolean root) { + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(UUID.randomUUID().toString()); + entry.setVersionId(versionId); + entry.setName(root ? "Root" : "Dep"); + entry.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + entry.setRoot(root); + return entry; + } + + private ObjectNode version(String id, String itemId, String type, String name, String ver) { + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("id", id); + node.put("itemId", itemId); + node.put("type", type); + node.put("name", name); + node.put("version", ver); + return node; + } + + @Test + void resolveInstallPlan_rootOnly_noRelated_marksWillInstall() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String itemId = UUID.randomUUID().toString(); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(version(versionId, itemId, "DASHBOARD", "Root", "1.0")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries()).hasSize(1); + InstallPlanEntry root = plan.getEntries().get(0); + assertThat(root.isRoot()).isTrue(); + assertThat(root.getStatus()).isEqualTo(InstallPlanEntry.Status.WILL_INSTALL); + assertThat(root.getItemId()).isEqualTo(itemId); + } + + @Test + void resolveInstallPlan_ordersDependenciesBeforeRoot() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String rootItemId = UUID.randomUUID().toString(); + String relatedItemId = UUID.randomUUID().toString(); + ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); + root.putArray("relatedItems").add(relatedItemId); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); + when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)) + .thenReturn(version(UUID.randomUUID().toString(), relatedItemId, "WIDGET", "Dep", "2.0")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries()).hasSize(2); + assertThat(plan.getEntries().get(0).getItemId()).isEqualTo(relatedItemId); + assertThat(plan.getEntries().get(0).isRoot()).isFalse(); + assertThat(plan.getEntries().get(1).getItemId()).isEqualTo(rootItemId); + assertThat(plan.getEntries().get(1).isRoot()).isTrue(); + } + + @Test + void resolveInstallPlan_missingRelated_recordedAsMissing() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String rootItemId = UUID.randomUUID().toString(); + String relatedItemId = UUID.randomUUID().toString(); + ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); + root.putArray("relatedItems").add(relatedItemId); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); + when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenReturn(null); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + InstallPlanEntry missing = plan.getEntries().get(0); + assertThat(missing.getStatus()).isEqualTo(InstallPlanEntry.Status.MISSING); + assertThat(missing.getItemId()).isEqualTo(relatedItemId); + assertThat(missing.getErrorMessage()).isNotBlank(); + } + + @Test + void resolveInstallPlan_relatedFetchThrows_recordedAsMissing() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String rootItemId = UUID.randomUUID().toString(); + String relatedItemId = UUID.randomUUID().toString(); + ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); + root.putArray("relatedItems").add(relatedItemId); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); + when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenThrow(new RuntimeException("boom")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.MISSING); + assertThat(plan.getEntries().get(0).getErrorMessage()).contains("boom"); + } + + @Test + void resolveInstallPlan_alreadyInstalledRoot_marksAlreadyInstalled() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + UUID rootItemId = UUID.randomUUID(); + when(iotHubRestClient.getVersionInfo(versionId)) + .thenReturn(version(versionId, rootItemId.toString(), "DASHBOARD", "Root", "1.0")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(rootItemId)); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); + } + + @Test + void resolveInstallPlan_nullVersion_throws() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(null); + + assertThatThrownBy(() -> service.resolveInstallPlan(user, versionId)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void installPlan_emptyPlan_returnsFailureWithoutInstalling() { + InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of()), null, null); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getErrorMessage()).contains("empty"); + verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); + } + + @Test + void installPlan_missingEntry_collectedAndNotInstalled() { + mockTenant(); + InstallPlanEntry missing = new InstallPlanEntry(); + missing.setItemId(UUID.randomUUID().toString()); + missing.setStatus(InstallPlanEntry.Status.MISSING); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(missing)), null, null); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getMissingItemIds()).containsExactly(missing.getItemId()); + assertThat(result.getRootDescriptor()).isNull(); + verify(iotHubRestClient, never()).getVersionInfo(anyString()); + // A plan with nothing to install never hits the DB to check what is already installed. + verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); + } + + @Test + void installPlan_stalePlan_skipsItemInstalledSinceResolution() { + mockTenant(); + UUID itemId = UUID.randomUUID(); + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId.toString()); + entry.setVersionId(UUID.randomUUID().toString()); + entry.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + entry.setRoot(true); + // The item is reported as already installed at install time, even though the plan says WILL_INSTALL. + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(itemId)); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(entry)), null, null); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); + // No marketplace fetch happened — the duplicate install was avoided. + verify(iotHubRestClient, never()).getVersionInfo(anyString()); + } + + @Test + void installPlan_duplicateItemId_installsOnce() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + UUID itemId = UUID.randomUUID(); + String firstVersionId = UUID.randomUUID().toString(); + String secondVersionId = UUID.randomUUID().toString(); + // Two entries point at the same item — a client-supplied plan is not de-duplicated, so the + // cascade itself must not install the same item twice. + InstallPlanEntry first = new InstallPlanEntry(); + first.setItemId(itemId.toString()); + first.setVersionId(firstVersionId); + first.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + first.setRoot(true); + InstallPlanEntry duplicate = new InstallPlanEntry(); + duplicate.setItemId(itemId.toString()); + duplicate.setVersionId(secondVersionId); + duplicate.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + duplicate.setRoot(false); + + // Nothing is installed yet when the plan is submitted. + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + doReturn(installedItem(UUID.randomUUID())).when(service).doInstallVersion(eq(user), eq(firstVersionId), any(), any()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(firstVersionId, List.of(first, duplicate)), null, request); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getEntries()).hasSize(2); + assertThat(result.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.WILL_INSTALL); + // The duplicate is recognised as freshly installed and skipped. + assertThat(result.getEntries().get(1).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); + verify(service).doInstallVersion(eq(user), eq(firstVersionId), any(), any()); + verify(service, never()).doInstallVersion(eq(user), eq(secondVersionId), any(), any()); + } + + @Test + void installPlan_cascade_installsDependencyBeforeRoot_andRoutesDataToRootOnly() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + JsonNode data = JacksonUtil.newObjectNode().put("entityId", UUID.randomUUID().toString()); + String depVersionId = UUID.randomUUID().toString(); + String rootVersionId = UUID.randomUUID().toString(); + InstallPlanEntry dep = willInstall(depVersionId, false); + InstallPlanEntry root = willInstall(rootVersionId, true); + + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); + IotHubInstalledItem rootItem = installedItem(UUID.randomUUID()); + DashboardInstalledItemDescriptor rootDescriptor = new DashboardInstalledItemDescriptor(); + rootItem.setDescriptor(rootDescriptor); + doReturn(depItem).when(service).doInstallVersion(eq(user), eq(depVersionId), any(), any()); + doReturn(rootItem).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep, root)), data, request); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.isRolledBack()).isFalse(); + assertThat(result.getRootDescriptor()).isSameAs(rootDescriptor); + assertThat(result.getMissingItemIds()).isEmpty(); + assertThat(result.getEntries()).hasSize(2); + + // The dependency installs before the root, and only the root receives the user's install data. + InOrder inOrder = inOrder(service); + inOrder.verify(service).doInstallVersion(eq(user), eq(depVersionId), isNull(), eq(request)); + inOrder.verify(service).doInstallVersion(eq(user), eq(rootVersionId), eq(data), eq(request)); + verify(service, never()).deleteInstalledItem(any(), any()); + } + + @Test + void installPlan_dependencyFails_rollsBackInstalledItemsInReverseOrder() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + String dep1VersionId = UUID.randomUUID().toString(); + String dep2VersionId = UUID.randomUUID().toString(); + String rootVersionId = UUID.randomUUID().toString(); + InstallPlanEntry dep1 = willInstall(dep1VersionId, false); + InstallPlanEntry dep2 = willInstall(dep2VersionId, false); + InstallPlanEntry root = willInstall(rootVersionId, true); + + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + IotHubInstalledItem dep1Item = installedItem(UUID.randomUUID()); + IotHubInstalledItem dep2Item = installedItem(UUID.randomUUID()); + doReturn(dep1Item).when(service).doInstallVersion(eq(user), eq(dep1VersionId), any(), any()); + doReturn(dep2Item).when(service).doInstallVersion(eq(user), eq(dep2VersionId), any(), any()); + doThrow(new IllegalStateException("install failed")).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); + doNothing().when(service).deleteInstalledItem(eq(user), any()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep1, dep2, root)), null, request); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.isRolledBack()).isTrue(); + assertThat(result.getErrorMessage()).contains("install failed"); + + // Successfully installed dependencies are rolled back in reverse install order (dep2 before dep1). + InOrder inOrder = inOrder(service); + inOrder.verify(service).deleteInstalledItem(user, dep2Item.getId()); + inOrder.verify(service).deleteInstalledItem(user, dep1Item.getId()); + } + + @Test + void installPlan_rollbackFailure_reportsNotFullyRolledBack() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + String depVersionId = UUID.randomUUID().toString(); + String rootVersionId = UUID.randomUUID().toString(); + InstallPlanEntry dep = willInstall(depVersionId, false); + InstallPlanEntry root = willInstall(rootVersionId, true); + + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); + doReturn(depItem).when(service).doInstallVersion(eq(user), eq(depVersionId), any(), any()); + doThrow(new IllegalStateException("install failed")).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); + // Rolling back the one installed dependency itself fails — the result must report a partial rollback. + doThrow(new RuntimeException("delete failed")).when(service).deleteInstalledItem(user, depItem.getId()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep, root)), null, request); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.isRolledBack()).isFalse(); + assertThat(result.getErrorMessage()).contains("install failed"); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java index 22692f6ee4..8e20a68eb0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -35,6 +36,8 @@ public interface IotHubInstalledItemDao extends Dao { List findInstalledItemIdsByTenantId(TenantId tenantId); + List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds); + long countByTenantId(TenantId tenantId, String itemType); Map findInstalledItemCounts(TenantId tenantId, String itemType); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java index 3fa15852d8..874e9bfcc5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; @@ -35,6 +36,8 @@ public interface IotHubInstalledItemService { List findInstalledItemIdsByTenantId(TenantId tenantId); + List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds); + long countByTenantId(TenantId tenantId, String itemType); Map findInstalledItemCounts(TenantId tenantId, String itemType); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java index 5bb857ca2c..d89c21f548 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; @@ -56,6 +57,11 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { return iotHubInstalledItemDao.findInstalledItemIdsByTenantId(tenantId); } + @Override + public List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds) { + return iotHubInstalledItemDao.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId, itemIds); + } + @Override public long countByTenantId(TenantId tenantId, String itemType) { return iotHubInstalledItemDao.countByTenantId(tenantId, itemType); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java index 8f06beaa39..393e40ea86 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java @@ -24,6 +24,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; @@ -36,6 +37,11 @@ interface IotHubInstalledItemRepository extends JpaRepository findInstalledItemIdsByTenantId(@Param("tenantId") UUID tenantId); + @Query("SELECT DISTINCT item.itemId FROM IotHubInstalledItemEntity item " + + "WHERE item.tenantId = :tenantId AND item.itemId IN :itemIds") + List findInstalledItemIdsByTenantIdAndItemIdIn(@Param("tenantId") UUID tenantId, + @Param("itemIds") Collection itemIds); + @Query(""" SELECT item FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java index 9d61b13209..5cd305b5ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java @@ -33,6 +33,7 @@ import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,6 +69,11 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds) { + return repository.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId.getId(), itemIds); + } + @Override public long countByTenantId(TenantId tenantId, String itemType) { return repository.countByTenantId(tenantId.getId(), itemType); diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index b0f90ddeef..ad4eefbdc6 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -22,7 +22,7 @@ import { PageData } from '@shared/models/page/page-data'; import { PageLink } from '@shared/models/page/page-link'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models'; -import { IotHubInstalledItem, InstallItemVersionResult, UpdateItemVersionResult, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { IotHubInstalledItem, InstallItemVersionResult, InstallPlan, InstallPlanResult, UpdateItemVersionResult, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { ItemType, ItemTypeFilterInfo, WidgetCategory } from '@shared/models/iot-hub/iot-hub-item.models'; import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params'; import { InterceptorConfig } from '@core/interceptors/interceptor-config'; @@ -195,6 +195,21 @@ export class IotHubApiService { ); } + public resolveInstallPlan(versionId: string, config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/versions/${versionId}/installPlan`, + { params: this.buildParams(config) } + ); + } + + public installPlan(plan: InstallPlan, data?: any, config?: IotHubRequestConfig): Observable { + return this.http.post( + `/api/iot-hub/installPlan`, + { plan, data: data ?? null }, + { params: this.buildParams(config) } + ); + } + public updateItemVersion(installedItemId: string, versionId: string, config?: IotHubRequestConfig, force?: boolean): Observable { let params = this.buildParams(config); if (force) { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts index 6c573aba45..dd8d3473a7 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts @@ -143,7 +143,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent

} + @case ('plan') { +

{{ 'iot-hub.install-plan-title' | translate:{ name: item.name } }}

+

+ @if (planSummary.willInstall > 0) { + {{ 'iot-hub.install-plan-will-install' | translate:{ count: planSummary.willInstall } }} + } + @if (planSummary.alreadyInstalled > 0) { + {{ 'iot-hub.install-plan-will-update' | translate:{ count: planSummary.alreadyInstalled } }} + } +

+ @if (planSummary.missing > 0) { +

{{ 'iot-hub.install-plan-missing-warning' | translate:{ count: planSummary.missing } }}

+ } +
    + @for (entry of installPlan?.entries; track entry.itemId) { +
  • +
    + {{ entry.name || entry.itemId }} + @if (entry.type) { + {{ (typeTranslations.get(entry.type) || entry.type) | translate }} + } + @if (entry.version) { + v{{ entry.version }} + } +
    + + @switch (entry.status) { + @case (PlanStatus.WILL_INSTALL) { {{ 'iot-hub.install-plan-status-will' | translate }} } + @case (PlanStatus.ALREADY_INSTALLED) { {{ 'iot-hub.install-plan-status-installed' | translate }} } + @case (PlanStatus.MISSING) { {{ 'iot-hub.install-plan-status-missing' | translate }} } + } + +
  • + } +
+ } @case ('installing') {

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

{{ 'iot-hub.install-desc' | translate }}

@@ -85,6 +124,20 @@

{{ 'iot-hub.install-success-title' | translate }}

{{ 'iot-hub.install-success-desc' | translate:{ name: item.name } }}

} + @case ('partial') { +

{{ 'iot-hub.install-partial-title' | translate }}

+

{{ 'iot-hub.install-partial-desc' | translate:{ name: item.name } }}

+
    + @for (entry of missingEntries; track entry.itemId) { +
  • + {{ entry.name || entry.itemId }} + @if (entry.errorMessage) { + {{ entry.errorMessage }} + } +
  • + } +
+ } @case ('error') {

{{ 'iot-hub.install-error-title' | translate }}

{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}

@@ -101,31 +154,53 @@ @switch (state) { @case ('confirm') { - - + + } @case ('select-entity') { - + @if (item.type === ItemType.RULE_CHAIN) { } @else { } } @case ('confirm-overwrite') { - - + } + @case ('plan') { + + @if (planSummary.willInstall > 0) { + + } + } @case ('installing') { } } + @case ('partial') { + + @if (entityDetailsUrl) { + + } + } @case ('error') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss index 6fe3d0bfa2..bf6356a331 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss @@ -57,3 +57,92 @@ tb-entity-select { border-radius: 8px; padding-bottom: 24px; } + +.tb-iot-hub-install-warning { + color: #b26a00; + background: #fff8e1; + border-left: 3px solid #f5b324; + padding: 8px 12px; + font-size: 13px; + line-height: 18px; + border-radius: 4px; + margin: 0; +} + +.tb-iot-hub-plan-list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 8px; + max-height: 320px; + overflow-y: auto; +} + +.tb-iot-hub-plan-entry { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 14px; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + + &:last-child { + border-bottom: none; + } +} + +.tb-iot-hub-plan-entry-main { + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; + min-width: 0; +} + +.tb-iot-hub-plan-entry-name { + font-weight: 500; + color: rgba(0, 0, 0, 0.87); +} + +.tb-iot-hub-plan-entry-type { + font-size: 12px; + color: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.05); + padding: 2px 6px; + border-radius: 10px; +} + +.tb-iot-hub-plan-entry-version { + font-size: 12px; + color: rgba(0, 0, 0, 0.54); +} + +.tb-iot-hub-plan-entry-error { + font-size: 12px; + color: #b26a00; + margin-left: 8px; +} + +.tb-iot-hub-plan-entry-status { + font-size: 12px; + font-weight: 500; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; + + &.tb-status-will-install { + color: #1565c0; + background: rgba(21, 101, 192, 0.08); + } + + &.tb-status-installed { + color: #2e7d32; + background: rgba(46, 125, 50, 0.08); + } + + &.tb-status-missing { + color: #b26a00; + background: rgba(245, 179, 36, 0.12); + } +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts index 6a28336f70..3f2687a2a8 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts @@ -26,6 +26,11 @@ import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { getInstalledItemUrl, + InstallPlan, + InstallPlanEntry, + InstallPlanEntryStatus, + InstallPlanResult, + IotHubInstalledItemDescriptor, SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; @@ -61,8 +66,10 @@ export type InstallState = | 'select-entity' | 'confirm-overwrite' | 'confirm' + | 'plan' | 'installing' | 'success' + | 'partial' | 'error'; @Component({ @@ -74,6 +81,7 @@ export type InstallState = export class TbIotHubInstallDialogComponent extends DialogComponent { ItemType = ItemType; + PlanStatus = InstallPlanEntryStatus; EntityType = EntityType; item: MpItemVersionView; @@ -90,6 +98,15 @@ export class TbIotHubInstallDialogComponent extends DialogComponent; }>; + installPlan: InstallPlan | null = null; + planSummary: { willInstall: number; alreadyInstalled: number; missing: number } = { + willInstall: 0, + alreadyInstalled: 0, + missing: 0 + }; + missingEntries: InstallPlanEntry[] = []; + resolvingPlan = false; + private readonly selectEntityConfig: Partial> = { [ItemType.CALCULATED_FIELD]: { allowed: [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE], @@ -196,11 +213,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent { - this.state = 'error'; - this.errorMessage = err?.error?.message || err?.message || - this.translate.instant('iot-hub.install-error', { name: this.item.name }); - } + error: (err) => this.handleApiError(err) }); } @@ -240,44 +253,141 @@ export class TbIotHubInstallDialogComponent extends DialogComponent { + this.resolvingPlan = false; + const summary = this.summarizePlan(plan); + // Show the plan only when there's something worth telling the user about: extra items to + // install, items already installed, or missing dependencies. A lone root with nothing to + // skip installs directly so the dialog doesn't flash a one-line "plan". + const shouldShowPlan = plan.entries.length > 1 + || summary.alreadyInstalled > 0 + || summary.missing > 0; + + if (!shouldShowPlan) { + this.installItem(); + return; + } + + this.installPlan = plan; + this.planSummary = summary; + this.missingEntries = plan.entries.filter(e => e.status === InstallPlanEntryStatus.MISSING); + this.state = 'plan'; + }, + error: (err) => { + this.resolvingPlan = false; + this.handleApiError(err); + } + }); + } + + installItemWithDependencies(): void { + if (!this.installPlan) { + return; + } + this.state = 'installing'; + this.iotHubApiService.installPlan(this.installPlan, this.installData(), { ignoreLoading: true }).subscribe({ + next: (result) => this.handlePlanResult(result), + error: (err) => this.handleApiError(err) + }); + } + + private installItem(): void { this.state = 'installing'; const versionId = this.item.id as string; - const data = this.selectedEntityId ? { entityId: this.selectedEntityId } : undefined; - this.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }, data).subscribe({ + this.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }, this.installData()).subscribe({ next: (result) => { - if (result.success) { - if (result.descriptor?.type === 'SOLUTION_TEMPLATE') { - const timeout = this.item.dataDescriptor?.installTimeoutMs; - const openSolutionDialog = () => { - this.dialogRef.close('installed'); - this.dialog.open(SolutionInstallDialogComponent, { - disableClose: true, - autoFocus: false, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { descriptor: result.descriptor as SolutionTemplateInstalledItemDescriptor } - }); - }; - if (timeout > 0) { - setTimeout(openSolutionDialog, timeout); - } else { - openSolutionDialog(); - } - } else { - this.state = 'success'; - this.entityDetailsUrl = getInstalledItemUrl(result.descriptor); - } - } else { + if (!result.success) { this.state = 'error'; this.errorMessage = result.errorMessage || this.translate.instant('iot-hub.install-error', { name: this.item.name }); + return; } + this.handleInstalledDescriptor(result.descriptor); }, - error: (err) => { - this.state = 'error'; - this.errorMessage = err?.error?.message || err?.message || this.translate.instant('iot-hub.install-error', { name: this.item.name }); - } + error: (err) => this.handleApiError(err) }); } + private summarizePlan(plan: InstallPlan): { willInstall: number; alreadyInstalled: number; missing: number } { + const summary = { willInstall: 0, alreadyInstalled: 0, missing: 0 }; + for (const entry of plan.entries) { + switch (entry.status) { + case InstallPlanEntryStatus.WILL_INSTALL: summary.willInstall++; break; + case InstallPlanEntryStatus.ALREADY_INSTALLED: summary.alreadyInstalled++; break; + case InstallPlanEntryStatus.MISSING: summary.missing++; break; + } + } + return summary; + } + + private handleApiError(err: any): void { + this.state = 'error'; + this.errorMessage = err?.error?.message || err?.message || + this.translate.instant('iot-hub.install-error', { name: this.item.name }); + } + + private handlePlanResult(result: InstallPlanResult): void { + if (!result.success) { + this.state = 'error'; + let message = result.errorMessage || this.translate.instant('iot-hub.install-error', { name: this.item.name }); + // A failed cascade rolls back the items installed so far. When something was actually being + // installed and the rollback came back partial (rolledBack === false), some entities are left + // behind — tell the admin so they know manual cleanup may be needed. The willInstall guard + // avoids the misleading warning on the "empty plan" failure, where nothing was installed. + if (!result.rolledBack && this.planSummary.willInstall > 0) { + message += ' ' + this.translate.instant('iot-hub.install-rollback-partial'); + } + this.errorMessage = message; + return; + } + if (this.installPlan) { + this.installPlan = { ...this.installPlan, entries: result.entries ?? this.installPlan.entries }; + this.missingEntries = (result.entries ?? []).filter(e => e.status === InstallPlanEntryStatus.MISSING); + } + const hasMissing = (result.missingItemIds?.length ?? 0) > 0; + if (result.rootDescriptor) { + this.handleInstalledDescriptor(result.rootDescriptor, hasMissing); + } else { + this.state = hasMissing ? 'partial' : 'success'; + } + } + + private handleInstalledDescriptor(descriptor: IotHubInstalledItemDescriptor, partial = false): void { + if (descriptor?.type === 'SOLUTION_TEMPLATE') { + const timeout = this.item.dataDescriptor?.installTimeoutMs; + const openSolutionDialog = () => { + this.dialogRef.close('installed'); + this.dialog.open(SolutionInstallDialogComponent, { + disableClose: true, + autoFocus: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { descriptor: descriptor as SolutionTemplateInstalledItemDescriptor } + }); + }; + if (timeout > 0) { + setTimeout(openSolutionDialog, timeout); + } else { + openSolutionDialog(); + } + return; + } + this.entityDetailsUrl = getInstalledItemUrl(descriptor); + this.state = partial ? 'partial' : 'success'; + } + + private installData(): any | undefined { + return this.selectedEntityId ? { entityId: this.selectedEntityId } : undefined; + } + + cancelPlan(): void { + this.installPlan = null; + this.state = this.computeInitialState(); + } + openEntityDetails(): void { if (this.entityDetailsUrl) { this.dialogRef.close('installed'); @@ -286,7 +396,8 @@ export class TbIotHubInstallDialogComponent extends DialogComponent