From 7cca8463c98666d358bde17c83466308be862a36 Mon Sep 17 00:00:00 2001 From: dpinkevych Date: Fri, 22 May 2026 13:15:26 +0300 Subject: [PATCH 01/37] add related items logic --- .../server/controller/IotHubController.java | 19 ++ .../service/iot_hub/DefaultIotHubService.java | 254 +++++++++++++++--- .../server/service/iot_hub/InstallPlan.java | 32 +++ .../service/iot_hub/InstallPlanEntry.java | 42 +++ .../service/iot_hub/InstallPlanResult.java | 38 +++ .../service/iot_hub/IotHubRestClient.java | 19 ++ .../server/service/iot_hub/IotHubService.java | 4 + .../src/app/core/http/iot-hub-api.service.ts | 17 +- .../iot-hub-install-dialog.component.html | 108 +++++++- .../iot-hub-install-dialog.component.scss | 89 ++++++ .../iot-hub-install-dialog.component.ts | 172 +++++++++--- .../iot-hub/iot-hub-installed-item.models.ts | 31 +++ .../models/iot-hub/iot-hub-version.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 10 + 14 files changed, 755 insertions(+), 82 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java 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 5c08b4f12b..ace31effbe 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 @@ -71,8 +71,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 @@ -103,48 +106,61 @@ public class DefaultIotHubService implements IotHubService { 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); - 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" -> throw new IllegalArgumentException( - "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages."); - 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 = 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); + return InstallItemVersionResult.error(e.getMessage()); + } + } - 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); + /** + * 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. + */ + private 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("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" -> throw new IllegalArgumentException( + "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages."); + 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); - log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); - - return InstallItemVersionResult.success(descriptor); } catch (Exception e) { - log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e); - return InstallItemVersionResult.error(e.getMessage()); + // 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 { @@ -654,4 +670,170 @@ public class DefaultIotHubService implements IotHubService { iotHubInstalledItemService.deleteById(tenantId, installedItemId); 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); + } + + Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)); + + // LinkedHashMap preserves insertion order; we add deps before the root so a single + // forward iteration yields the correct topological install sequence. + LinkedHashMap entries = new LinkedHashMap<>(); + Set visiting = new HashSet<>(); + + collectDependencies(rootVersion, alreadyInstalledItemIds, entries, visiting, true); + + return new InstallPlan(versionId, new ArrayList<>(entries.values())); + } + + /** + * Depth-first walk over {@code relatedItems}. Children are added BEFORE the current + * version so the resulting LinkedHashMap iterates deps-first, root-last. + */ + private void collectDependencies(JsonNode versionInfo, + Set alreadyInstalledItemIds, + LinkedHashMap entries, + Set visiting, + boolean root) { + String itemId = versionInfo.get("itemId").asText(); + if (entries.containsKey(itemId)) { + return; + } + if (!visiting.add(itemId)) { + log.warn("Dependency cycle detected involving IoT Hub item {} — breaking", itemId); + return; + } + + JsonNode related = versionInfo.get("relatedItems"); + if (related != null && related.isArray()) { + for (JsonNode relatedNode : related) { + String relatedItemId = relatedNode.asText(); + if (relatedItemId == null || relatedItemId.isEmpty() || 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; + } + collectDependencies(relatedVersion, alreadyInstalledItemIds, entries, visiting, false); + } + } + + visiting.remove(itemId); + + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId); + entry.setVersionId(versionInfo.get("id").asText()); + entry.setName(versionInfo.hasNonNull("name") ? versionInfo.get("name").asText() : null); + entry.setType(versionInfo.hasNonNull("type") ? versionInfo.get("type").asText() : null); + entry.setVersion(versionInfo.hasNonNull("version") ? versionInfo.get("version").asText() : null); + entry.setRoot(root); + boolean alreadyInstalled; + try { + alreadyInstalled = alreadyInstalledItemIds.contains(UUID.fromString(itemId)); + } catch (IllegalArgumentException ex) { + alreadyInstalled = false; + } + entry.setStatus(alreadyInstalled + ? 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; + } + + @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<>()); + } + + 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 -> { + 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()); + 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); + rollbackInstalledItems(user, rollbackIds); + result.setSuccess(false); + result.setRolledBack(true); + 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; + } + + private void rollbackInstalledItems(SecurityUser user, List installedIds) { + for (int i = installedIds.size() - 1; i >= 0; i--) { + IotHubInstalledItemId id = installedIds.get(i); + try { + deleteInstalledItem(user, id); + } catch (Exception e) { + log.error("[{}] Failed to roll back installed item {}: {}", user.getTenantId(), id, e.getMessage(), e); + } + } + } + + 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()); + } } 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 1155bcf0ad..2de1fc8635 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 @@ -18,8 +18,10 @@ package org.thingsboard.server.service.iot_hub; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -39,6 +41,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/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index 12878ac0fc..3a98526b60 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'; @@ -162,6 +162,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/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index 19a43fcb30..46e7f08264 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -48,6 +48,45 @@ existing: pendingOverwrite?.existingRuleChainName }">

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

@@ -56,6 +95,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 } }}

@@ -72,39 +125,66 @@ @switch (state) { @case ('confirm') { - + @if (item.type === ItemType.RULE_CHAIN) { - - + } @else { - + } } @case ('select-entity') { - + @if (item.type === ItemType.RULE_CHAIN) { - + } @else { } } @case ('confirm-overwrite') { - - + } + @case ('plan') { + + + } @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 be37e6e1bb..161e8ca909 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 @@ -22,7 +22,14 @@ import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; -import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { + 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'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; @@ -57,8 +64,10 @@ export type InstallState = | 'select-entity' | 'confirm-overwrite' | 'confirm' + | 'plan' | 'installing' | 'success' + | 'partial' | 'error'; @Component({ @@ -70,6 +79,7 @@ export type InstallState = export class TbIotHubInstallDialogComponent extends DialogComponent { ItemType = ItemType; + PlanStatus = InstallPlanEntryStatus; item: MpItemVersionView; typeTranslations = itemTypeTranslations; @@ -80,6 +90,15 @@ export class TbIotHubInstallDialogComponent extends DialogComponent> = { [ItemType.CALCULATED_FIELD]: { allowed: [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE], @@ -155,11 +174,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) }); } @@ -199,44 +214,130 @@ export class TbIotHubInstallDialogComponent extends DialogComponent { + this.resolvingPlan = false; + const summary = this.summarizePlan(plan); + const hasDependencies = plan.entries.length > 1 + || summary.alreadyInstalled > 0 + || summary.missing > 0; + + if (!hasDependencies && summary.willInstall === 1) { + // Single root version, no deps, nothing to skip — install directly without showing the plan. + this.legacyInstall(); + 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); + } + }); + } + + confirmPlan(): 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 legacyInstall(): 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 = resolveEntityDetailsUrl(result.descriptor, this.item.type); - } - } 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'; + this.errorMessage = result.errorMessage || this.translate.instant('iot-hub.install-error', { name: this.item.name }); + return; + } + if (this.installPlan) { + this.installPlan = { ...this.installPlan, entries: result.entries ?? this.installPlan.entries }; + this.missingEntries = (result.entries ?? []).filter(e => e.status === InstallPlanEntryStatus.MISSING); + } + if (result.rootDescriptor) { + this.handleInstalledDescriptor(result.rootDescriptor, result.missingItemIds?.length > 0); + } else { + this.state = (result.missingItemIds?.length ?? 0) > 0 ? '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 = resolveEntityDetailsUrl(descriptor, this.item.type); + this.state = partial ? 'partial' : 'success'; + } + + private installData(): any | undefined { + return this.selectedEntityId ? { entityId: this.selectedEntityId } : undefined; + } + + cancelPlan(): void { + this.installPlan = null; + this.state = 'confirm'; + } + openEntityDetails(): void { if (this.entityDetailsUrl) { this.dialogRef.close('installed'); @@ -245,7 +346,8 @@ export class TbIotHubInstallDialogComponent extends DialogComponent Date: Wed, 10 Jun 2026 17:59:39 +0300 Subject: [PATCH 02/37] refactor(iot-hub): centralise item-type icons and chip colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add itemTypeIcons + getItemTypeIcon helper to iot-hub-item.models, mirroring mp-item.models in the iot-hub project: widgets / dashboard / apps / mdi:function-variant / settings_ethernet / mdi:bell-cog / devices_other with `category` as the fallback. - Replace every hand-rolled item-type → icon switch with calls to getItemTypeIcon: item detail dialog (getTypeIcon / getCompactIcon fallback path), installed-items table (getItemTypeIcon), iot-hub-home (getCompactIcon / getItemTypeIcon), item-card (getPlaceholderIcon default + per-type fallbacks), item-link-card (getCompactIcon / getTypeIcon). - Swap the remaining mat-icon placeholders that render an item-type icon to tb-icon (installed-items table chip, home search popup thumb placeholder, item-link-card thumb fallback). Generic status / action icons stay on mat-icon. - Refresh the colored item-type chip palette in the installed items table to match itemTypeChipColors from mp-item.models, and add the previously-missing tb-type-alarm-rule rule so that chip picks up the new orange palette instead of falling through to no background. --- ...t-hub-installed-items-table.component.html | 2 +- ...t-hub-installed-items-table.component.scss | 14 ++++++----- ...iot-hub-installed-items-table.component.ts | 13 ++-------- .../iot-hub/iot-hub-item-card.component.ts | 22 ++++++++++------- .../iot-hub-item-detail-dialog.component.ts | 19 ++++----------- .../iot-hub-item-link-card.component.html | 2 +- .../iot-hub-item-link-card.component.ts | 24 +++---------------- .../pages/iot-hub/iot-hub-home.component.html | 2 +- .../pages/iot-hub/iot-hub-home.component.ts | 21 +++------------- .../models/iot-hub/iot-hub-item.models.ts | 17 +++++++++++++ 10 files changed, 54 insertions(+), 82 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html index 7bbff19a85..545d219596 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html @@ -29,7 +29,7 @@ {{ 'iot-hub.item-type' | translate }} - {{ getItemTypeIcon(item.itemType) }} + {{ getItemTypeIcon(item.itemType) }} {{ getItemTypeLabel(item.itemType) }} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss index d83bb3e192..1edef65112 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss @@ -122,12 +122,14 @@ --mat-icon-color: currentColor; } -.tb-type-widget { color: rgb(32, 115, 61); background: rgba(46, 166, 88, 0.06); } -.tb-type-dashboard { color: rgb(61, 76, 166); background: rgba(61, 76, 166, 0.06); } -.tb-type-solution-template { color: rgb(0, 137, 186); background: rgba(0, 137, 186, 0.06); } -.tb-type-calc-field { color: rgb(40, 120, 148); background: rgba(40, 120, 148, 0.06); } -.tb-type-rule-chain { color: rgb(178, 121, 29); background: rgba(179, 121, 29, 0.06); } -.tb-type-device { color: rgb(24, 146, 110); background: rgba(24, 146, 110, 0.06); } +// Colors mirror itemTypeChipColors from the iot-hub mp-item.models. +.tb-type-device { color: #3d4ca6; background: rgba(61, 76, 166, 0.06); } +.tb-type-solution-template { color: #2c6cb4; background: rgba(44, 108, 180, 0.06); } +.tb-type-widget { color: #20733d; background: rgba(46, 166, 88, 0.06); } +.tb-type-calc-field { color: #3db5e0; background: rgba(61, 181, 224, 0.06); } +.tb-type-alarm-rule { color: #d7702f; background: rgba(215, 112, 47, 0.06); } +.tb-type-rule-chain { color: #aa5be3; background: rgba(170, 91, 227, 0.06); } +.tb-type-dashboard { color: #607d8b; background: rgba(96, 125, 139, 0.06); } // "vX.X.X Available" button — Design: border rgba(0,0,0,0.12), rounded-4, px-12 py-6, gap-8 .tb-installed-version-btn { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts index 09917528be..b625d96e2b 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts @@ -44,7 +44,7 @@ import { ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { EntityType } from '@shared/models/entity-type.models'; import { getEntityDetailsPageURL } from '@core/utils'; import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; @@ -153,16 +153,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges, } getItemTypeIcon(itemType: string): string { - switch (itemType) { - case 'WIDGET': return 'widgets'; - case 'DASHBOARD': return 'dashboard'; - case 'SOLUTION_TEMPLATE': return 'integration_instructions'; - case 'CALCULATED_FIELD': return 'functions'; - case 'ALARM_RULE': return 'notification_important'; - case 'RULE_CHAIN': return 'settings_ethernet'; - case 'DEVICE': return 'memory'; - default: return 'category'; - } + return getItemTypeIcon(itemType); } deleteItem(item: IotHubInstalledItem): void { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index 38c1e26dcf..bdf50acddf 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts @@ -16,7 +16,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { TranslateService } from '@ngx-translate/core'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; @@ -69,17 +69,21 @@ export class TbIotHubItemCardComponent { getPlaceholderIcon(): string { switch (this.item.type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; case ItemType.CALCULATED_FIELD: - return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions'; + return this.item.icon + || cfTypeIcons.get(this.item.dataDescriptor?.cfType) + || getItemTypeIcon(ItemType.CALCULATED_FIELD); case ItemType.ALARM_RULE: - return this.item.icon || 'notification_important'; + return this.item.icon || getItemTypeIcon(ItemType.ALARM_RULE); case ItemType.RULE_CHAIN: - return this.item.icon || (this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'router' : 'device_hub'); - case ItemType.DEVICE: return 'memory'; - default: return 'extension'; + return this.item.icon + || (this.item.dataDescriptor?.ruleChainType === 'EDGE' + ? 'router' + : (this.item.dataDescriptor?.ruleChainType === 'CORE' + ? 'device_hub' + : getItemTypeIcon(ItemType.RULE_CHAIN))); + default: + return getItemTypeIcon(this.item.type); } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts index c770045ddc..382f2d0f88 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; @@ -103,16 +103,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent } @else { - {{ getTypeIcon() }} + {{ getTypeIcon() }} } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts index c2ca0ba1ea..743f02bea4 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts @@ -17,7 +17,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; type CardState = 'loading' | 'loaded' | 'unavailable'; @@ -71,29 +71,11 @@ export class TbIotHubItemLinkCardComponent implements OnInit { getCompactIcon(): string { const item = this.item!; - if (item.icon) { - return item.icon; - } - switch (item.type) { - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'account_tree'; - default: return 'category'; - } + return item.icon || getItemTypeIcon(item.type); } getTypeIcon(): string { - const item = this.item!; - switch (item.type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'account_tree'; - case ItemType.DEVICE: return 'memory'; - default: return 'category'; - } + return getItemTypeIcon(this.item!.type); } getCompactColor(): string { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html index 682411d884..f9f7285c3b 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html @@ -109,7 +109,7 @@ @if (getItemImage(item); as imgUrl) { } @else { - {{ getItemTypeIcon(item.type) }} + {{ getItemTypeIcon(item.type) }} } } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index 72a5513056..c8fff1ea7e 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -25,7 +25,7 @@ import { MediaBreakpoints } from '@shared/models/constants'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; @@ -266,15 +266,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { } getCompactIcon(item: MpItemVersionView): string { - if (item.icon) { - return item.icon; - } - switch (item.type) { - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'settings_ethernet'; - default: return 'category'; - } + return item.icon || getItemTypeIcon(item.type); } getItemImage(item: MpItemVersionView): string | null { @@ -282,14 +274,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { } getItemTypeIcon(type: ItemType): string { - switch (type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.DEVICE: return 'memory'; - default: return 'category'; - } + return getItemTypeIcon(type); } getSearchGroupLabel(type: ItemType): string { diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts index 6451bc5d52..6c528384c4 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts @@ -36,6 +36,23 @@ export const itemTypeTranslations = new Map( ] ); +// Canonical icon lookup per item type. Values are tb-icon +// identifiers (Material symbol names or `mdi:*` strings) and should +// be used everywhere an icon is rendered for an item type so the +// mapping stays consistent across the app. +export const itemTypeIcons: Record = { + [ItemType.WIDGET]: 'widgets', + [ItemType.DASHBOARD]: 'dashboard', + [ItemType.SOLUTION_TEMPLATE]: 'apps', + [ItemType.CALCULATED_FIELD]: 'mdi:function-variant', + [ItemType.RULE_CHAIN]: 'settings_ethernet', + [ItemType.ALARM_RULE]: 'mdi:bell-cog', + [ItemType.DEVICE]: 'devices_other' +}; + +export const getItemTypeIcon = (type?: string | null): string => + type && itemTypeIcons[type] ? itemTypeIcons[type] : 'category'; + /** * Item types discoverable to creators in the marketplace UI. * DASHBOARD is intentionally absent (IoT Hub no longer accepts Dashboard contributions). From 18232e9c8c7acc3ec0e84be93b7b4898b407d295 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Jun 2026 19:49:13 +0300 Subject: [PATCH 03/37] feat(iot-hub): force markdown links to new tab + verified item-link card - TbIotHubMarkdownComponent.parseData now runs through a forceLinksOpenInNewTab post-pass that (a) appends the {:target="_blank"} suffix to the TEXT of any markdown link [text](url) that doesn't already carry it, matching the MarkedOptionsService.renderer.link contract that strips the suffix and rewrites the with target="_blank", and (b) injects target="_blank" into raw HTML anchors that don't already declare a target= attribute. Image syntax ![alt](url) is excluded via lookbehind. - iot-hub-markdown SCSS narrows the link styling selector to a:not(.mdc-button):not(.tb-iot-hub-item-link-card) so the embedded item-link card anchor keeps its own typography. - iot-hub-item-link-card now renders a verified icon (teal #00695c via a new tb-iot-hub-item-link-verified-icon class) when item.creatorVerified is true, matching the item-card treatment; the default person icon is used otherwise. --- .../iot-hub-item-link-card.component.html | 6 ++++- .../iot-hub-item-link-card.component.scss | 8 ++++++ .../iot-hub/iot-hub-markdown.component.scss | 2 +- .../iot-hub/iot-hub-markdown.component.ts | 27 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html index 8d2d8b25c9..fbf00c7dd7 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html @@ -47,7 +47,11 @@ diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss index 2713303d6c..cdf4005199 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss @@ -37,6 +37,9 @@ background: rgba(4, 138, 211, 0.04); border-color: rgba(4, 138, 211, 0.32); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + .tb-iot-hub-item-link-name, .tb-iot-hub-item-link-author { + text-decoration: underline; + } } &--unavailable { @@ -128,6 +131,11 @@ width: 14px; height: 14px; } + + // Verified-creator accent — matches the item card chip color. + .tb-iot-hub-item-link-verified-icon { + color: #00695c; + } } .tb-iot-hub-item-link-skeleton-block, diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss index 57e3d25101..be2ab69a76 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss @@ -128,7 +128,7 @@ $gallery-border-hover-color: #2a7dec; max-width: 100%; } - a:not(.mdc-button) { + a:not(.mdc-button):not(.tb-iot-hub-item-link-card) { font-weight: 500; color: $link-color; text-decoration: none; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts index 57df5a8074..a63d17c914 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts @@ -109,9 +109,36 @@ export class TbIotHubMarkdownComponent implements OnInit, OnChanges { parsed = replaceItemLinkPlaceholders(parsed); parsed = this.resolveImages(parsed); parsed = this.resolveVariables(parsed); + parsed = this.forceLinksOpenInNewTab(parsed); return parsed; } + // Ensure every link rendered through tb-markdown opens in a new tab. + // 1) For markdown links `[text](url)` — append the + // `{:target="_blank"}` suffix to the link TEXT (not the URL) + // so MarkedOptionsService.renderer.link, which checks + // `token.text.endsWith(targetBlankBlock)`, emits + // `target="_blank"` on the rendered . Skip links whose + // text already ends with the suffix and skip image syntax + // (`![alt](url)`). + // 2) For raw HTML anchors authored in the markdown — add + // `target="_blank"` when no `target=` attribute is already + // present. + private forceLinksOpenInNewTab(content: string): string { + const TARGET_BLANK_BLOCK = '{:target="_blank"}'; + content = content.replace( + /(? + text.endsWith(TARGET_BLANK_BLOCK) ? match : `[${text}${TARGET_BLANK_BLOCK}](${url})` + ); + content = content.replace( + /]*)>/gi, + (match, attrs: string) => + /\btarget\s*=/i.test(attrs) ? match : `` + ); + return content; + } + private prefixResourceUrls(markdown: string): string { const baseUrl = this.iotHubApiService.baseUrl; return markdown.replace(/([("])(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`); From 8afc94cfc97728cc8b58723511de90980a40d036 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Jun 2026 20:23:45 +0300 Subject: [PATCH 04/37] feat(chip-overflow): optional showOverflowedTitle native tooltip - Add a `showOverflowedTitle` boolean input (defaults to false, coerceBoolean-decorated) to ChipOverflowDirective. When true and hiddenCount > 0, the overflow `+N` chip gets a `title` attribute set to the comma-joined trimmed textContent of every overflowing chip, surfacing them via the native browser tooltip. When the flag is off, no items overflow, or hiddenCount drops back to zero, the title attribute is removed. - Keep `pointer-events` in sync with the title so the native tooltip actually fires: remove `pointer-events: none` while a title is present; reapply it otherwise (matches the initial state set in createOverflowChip). - Opt the IoT Hub item detail dialog's three tb-chip-overflow meta-chip groups (connectivity, categories, use-cases) into showOverflowedTitle so hovering the `+N` chip lists the hidden values. --- .../iot-hub-item-detail-dialog.component.html | 6 ++--- .../directives/chip-overflow.directive.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index 6c0318abbc..3827599242 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -203,7 +203,7 @@ @if (item.dataDescriptor?.connectivity?.length) {
{{ 'iot-hub.connectivity' | translate }} -
+
@for (conn of item.dataDescriptor.connectivity; track conn) { {{ conn }} } @@ -214,7 +214,7 @@ @if (item.categories?.length) {
{{ 'iot-hub.category' | translate }} -
+
@for (cat of item.categories; track cat) { {{ cat }} } @@ -225,7 +225,7 @@ @if (item.useCases?.length) {
{{ 'iot-hub.use-cases' | translate }} -
+
@for (uc of item.useCases; track uc) { {{ uc }} } diff --git a/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts b/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts index 7ed84f10e7..10585f1453 100644 --- a/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts +++ b/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts @@ -15,6 +15,7 @@ /// import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy, Renderer2 } from '@angular/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Directive({ selector: '[tb-chip-overflow]', @@ -28,6 +29,10 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { @Input() overflowClass = 'tb-overflow-chip'; @Input() minChips = 1; + @Input() + @coerceBoolean() + showOverflowedTitle = false; + private resizeObserver?: ResizeObserver; private mutationObserver?: MutationObserver; private overflowEl!: HTMLElement; @@ -151,6 +156,7 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { // 4. Determine which chips fit let usedWidth = 0; let hiddenCount = 0; + const hiddenChips: HTMLElement[] = []; const minChips = Math.max(1, this.minChips); for (let i = 0; i < chips.length; i++) { @@ -163,12 +169,29 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { usedWidth = nextUsedWidth; } else { hiddenCount++; + hiddenChips.push(chips[i]); } } if (hiddenCount > 0) { this.renderer.setProperty(this.overflowEl, 'textContent', this.overflowTemplate.replace('{n}', String(hiddenCount))); this.renderer.setStyle(this.overflowEl, 'display', 'inline-flex'); + if (this.showOverflowedTitle) { + const title = hiddenChips + .map(chip => (chip.textContent || '').trim()) + .filter(text => !!text) + .join(', '); + this.renderer.setAttribute(this.overflowEl, 'title', title); + // Allow pointer interaction so the browser's native title + // tooltip appears on hover. + this.renderer.removeStyle(this.overflowEl, 'pointerEvents'); + } else { + this.renderer.removeAttribute(this.overflowEl, 'title'); + this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none'); + } + } else { + this.renderer.removeAttribute(this.overflowEl, 'title'); + this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none'); } } } From f6ed8b2fdadea62c019de12b53515696df57c915 Mon Sep 17 00:00:00 2001 From: dpinkevych Date: Thu, 11 Jun 2026 08:45:20 +0300 Subject: [PATCH 05/37] avoid recursion to find relatedItems and change method names --- .../service/iot_hub/DefaultIotHubService.java | 57 +++++++++---------- .../iot-hub-install-dialog.component.html | 2 +- .../iot-hub-install-dialog.component.ts | 8 +-- 3 files changed, 32 insertions(+), 35 deletions(-) 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 ace31effbe..b7e4cb2a5d 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 @@ -683,39 +683,19 @@ public class DefaultIotHubService implements IotHubService { Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)); - // LinkedHashMap preserves insertion order; we add deps before the root so a single - // forward iteration yields the correct topological install sequence. + // 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<>(); - Set visiting = new HashSet<>(); - collectDependencies(rootVersion, alreadyInstalledItemIds, entries, visiting, true); - - return new InstallPlan(versionId, new ArrayList<>(entries.values())); - } - - /** - * Depth-first walk over {@code relatedItems}. Children are added BEFORE the current - * version so the resulting LinkedHashMap iterates deps-first, root-last. - */ - private void collectDependencies(JsonNode versionInfo, - Set alreadyInstalledItemIds, - LinkedHashMap entries, - Set visiting, - boolean root) { - String itemId = versionInfo.get("itemId").asText(); - if (entries.containsKey(itemId)) { - return; - } - if (!visiting.add(itemId)) { - log.warn("Dependency cycle detected involving IoT Hub item {} — breaking", itemId); - return; - } - - JsonNode related = versionInfo.get("relatedItems"); + String rootItemId = rootVersion.get("itemId").asText(); + JsonNode related = rootVersion.get("relatedItems"); if (related != null && related.isArray()) { for (JsonNode relatedNode : related) { String relatedItemId = relatedNode.asText(); - if (relatedItemId == null || relatedItemId.isEmpty() || entries.containsKey(relatedItemId)) { + if (relatedItemId == null || relatedItemId.isEmpty() + || relatedItemId.equals(rootItemId) || entries.containsKey(relatedItemId)) { continue; } JsonNode relatedVersion; @@ -731,11 +711,28 @@ public class DefaultIotHubService implements IotHubService { entries.put(relatedItemId, missingEntry(relatedItemId, "Item not found or not published")); continue; } - collectDependencies(relatedVersion, alreadyInstalledItemIds, entries, visiting, false); + addPlanEntry(relatedVersion, alreadyInstalledItemIds, entries, false); } } - visiting.remove(itemId); + 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 = versionInfo.get("itemId").asText(); + if (entries.containsKey(itemId)) { + return; + } InstallPlanEntry entry = new InstallPlanEntry(); entry.setItemId(itemId); diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index 3f2b79fbfc..354399e5c8 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -197,7 +197,7 @@ } 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 9c64353ef3..ccd8731ec8 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 @@ -259,9 +259,9 @@ export class TbIotHubInstallDialogComponent extends DialogComponent 0 || summary.missing > 0; - if (!hasDependencies && summary.willInstall === 1) { + if (!hasDependencies) { // Single root version, no deps, nothing to skip — install directly without showing the plan. - this.legacyInstall(); + this.installItem(); return; } @@ -277,7 +277,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent Date: Thu, 11 Jun 2026 09:39:55 +0300 Subject: [PATCH 06/37] refactor + add tests --- .../service/iot_hub/DefaultIotHubService.java | 94 ++++++-- .../iot_hub/DefaultIotHubServiceTest.java | 215 ++++++++++++++++++ .../iot-hub-install-dialog.component.html | 10 +- .../iot-hub-install-dialog.component.ts | 13 +- .../assets/locale/locale.constant-en_US.json | 4 +- 5 files changed, 303 insertions(+), 33 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java 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 b7e4cb2a5d..a96f95c2c3 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 @@ -102,6 +102,15 @@ 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(); @@ -122,10 +131,10 @@ public class DefaultIotHubService implements IotHubService { private 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("type").asText(); - String itemName = versionInfo.get("name").asText(); - UUID itemId = UUID.fromString(versionInfo.get("itemId").asText()); - String version = versionInfo.get("version").asText(); + 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); @@ -689,8 +698,8 @@ public class DefaultIotHubService implements IotHubService { // level deep, so there is no need to walk a related item's own related items. LinkedHashMap entries = new LinkedHashMap<>(); - String rootItemId = rootVersion.get("itemId").asText(); - JsonNode related = rootVersion.get("relatedItems"); + String rootItemId = rootVersion.get(FIELD_ITEM_ID).asText(); + JsonNode related = rootVersion.get(FIELD_RELATED_ITEMS); if (related != null && related.isArray()) { for (JsonNode relatedNode : related) { String relatedItemId = relatedNode.asText(); @@ -729,25 +738,34 @@ public class DefaultIotHubService implements IotHubService { Set alreadyInstalledItemIds, LinkedHashMap entries, boolean root) { - String itemId = versionInfo.get("itemId").asText(); + 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(versionInfo.get("id").asText()); - entry.setName(versionInfo.hasNonNull("name") ? versionInfo.get("name").asText() : null); - entry.setType(versionInfo.hasNonNull("type") ? versionInfo.get("type").asText() : null); - entry.setVersion(versionInfo.hasNonNull("version") ? versionInfo.get("version").asText() : null); + entry.setVersionId(versionId); + entry.setName(optText(versionInfo, FIELD_NAME)); + entry.setType(optText(versionInfo, FIELD_TYPE)); + entry.setVersion(optText(versionInfo, FIELD_VERSION)); entry.setRoot(root); - boolean alreadyInstalled; - try { - alreadyInstalled = alreadyInstalledItemIds.contains(UUID.fromString(itemId)); - } catch (IllegalArgumentException ex) { - alreadyInstalled = false; - } - entry.setStatus(alreadyInstalled + entry.setStatus(isAlreadyInstalled(itemId, alreadyInstalledItemIds) ? InstallPlanEntry.Status.ALREADY_INSTALLED : InstallPlanEntry.Status.WILL_INSTALL); entries.put(itemId, entry); @@ -761,6 +779,10 @@ public class DefaultIotHubService implements IotHubService { 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(); @@ -768,6 +790,11 @@ public class DefaultIotHubService implements IotHubService { 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 against the current state + // so we never install the same item twice. + Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)); + InstallPlanResult result = new InstallPlanResult(); List resultEntries = new ArrayList<>(); List missingItemIds = new ArrayList<>(); @@ -783,6 +810,12 @@ public class DefaultIotHubService implements IotHubService { } case ALREADY_INSTALLED -> resultEntries.add(resultEntry); case WILL_INSTALL -> { + if (isAlreadyInstalled(entry.getItemId(), 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. @@ -798,9 +831,9 @@ public class DefaultIotHubService implements IotHubService { entry.getName(), entry.getVersionId(), e.getMessage(), e); resultEntry.setErrorMessage(e.getMessage()); resultEntries.add(resultEntry); - rollbackInstalledItems(user, rollbackIds); + boolean rolledBack = rollbackInstalledItems(user, rollbackIds); result.setSuccess(false); - result.setRolledBack(true); + result.setRolledBack(rolledBack); result.setErrorMessage("Failed to install '" + entry.getName() + "': " + e.getMessage()); result.setEntries(resultEntries); result.setMissingItemIds(missingItemIds); @@ -818,15 +851,34 @@ public class DefaultIotHubService implements IotHubService { return result; } - private void rollbackInstalledItems(SecurityUser user, List installedIds) { + /** + * 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 boolean isAlreadyInstalled(String itemId, Set alreadyInstalledItemIds) { + if (itemId == null) { + return false; + } + try { + return alreadyInstalledItemIds.contains(UUID.fromString(itemId)); + } catch (IllegalArgumentException ex) { + return false; + } } private static InstallPlanEntry cloneEntry(InstallPlanEntry src) { 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..5c0559c4d4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java @@ -0,0 +1,215 @@ +/** + * 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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +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.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; + + @InjectMocks + private DefaultIotHubService service; + + private void mockTenant() { + when(user.getTenantId()).thenReturn(tenantId); + } + + 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.findInstalledItemIdsByTenantId(tenantId)).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.findInstalledItemIdsByTenantId(tenantId)).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.findInstalledItemIdsByTenantId(tenantId)).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.findInstalledItemIdsByTenantId(tenantId)).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.findInstalledItemIdsByTenantId(tenantId)).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()).findInstalledItemIdsByTenantId(any()); + } + + @Test + void installPlan_missingEntry_collectedAndNotInstalled() { + mockTenant(); + InstallPlanEntry missing = new InstallPlanEntry(); + missing.setItemId(UUID.randomUUID().toString()); + missing.setStatus(InstallPlanEntry.Status.MISSING); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + + 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()); + } + + @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.findInstalledItemIdsByTenantId(tenantId)).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()); + } +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index 354399e5c8..bb551a4106 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -195,11 +195,11 @@ } @case ('plan') { - + @if (planSummary.willInstall > 0) { + + } } @case ('installing') { 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 ccd8731ec8..33bb2af0f7 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 @@ -255,12 +255,14 @@ export class TbIotHubInstallDialogComponent extends DialogComponent { this.resolvingPlan = false; const summary = this.summarizePlan(plan); - const hasDependencies = plan.entries.length > 1 + // 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 (!hasDependencies) { - // Single root version, no deps, nothing to skip — install directly without showing the plan. + if (!shouldShowPlan) { this.installItem(); return; } @@ -332,10 +334,11 @@ export class TbIotHubInstallDialogComponent extends DialogComponent e.status === InstallPlanEntryStatus.MISSING); } + const hasMissing = (result.missingItemIds?.length ?? 0) > 0; if (result.rootDescriptor) { - this.handleInstalledDescriptor(result.rootDescriptor, result.missingItemIds?.length > 0); + this.handleInstalledDescriptor(result.rootDescriptor, hasMissing); } else { - this.state = (result.missingItemIds?.length ?? 0) > 0 ? 'partial' : 'success'; + this.state = hasMissing ? 'partial' : 'success'; } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index becae3d7be..664ab07b91 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3845,10 +3845,10 @@ "install-error-message": "Failed to install '{{name}}'.", "install-plan-title": "Install '{{name}}' and its dependencies", "install-plan-will-install": "{{count}} item(s) will be installed.", - "install-plan-will-update": "{{count}} item(s) will be updated.", + "install-plan-will-update": "{{count}} item(s) already installed and will be skipped.", "install-plan-missing-warning": "{{count}} required item(s) could not be found on the marketplace. '{{ name }}' may not work fully without them.", "install-plan-status-will": "Install", - "install-plan-status-installed": "Update", + "install-plan-status-installed": "Already installed", "install-plan-status-missing": "Missing", "install-plan-confirm": "Install", "install-partial-title": "Installed with warnings", From 65e005fe97c282beef791c60846ec29d0326d941 Mon Sep 17 00:00:00 2001 From: dpinkevych Date: Thu, 11 Jun 2026 11:02:48 +0300 Subject: [PATCH 07/37] add tests, refactor translation map --- .../service/iot_hub/DefaultIotHubService.java | 7 +- .../iot_hub/DefaultIotHubServiceTest.java | 117 ++++++++++++++++++ .../iot-hub-install-dialog.component.ts | 10 +- .../assets/locale/locale.constant-en_US.json | 1 + 4 files changed, 132 insertions(+), 3 deletions(-) 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 a96f95c2c3..6b61c05a7a 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 @@ -127,8 +127,9 @@ 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. */ - private IotHubInstalledItem doInstallVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) throws Exception { + 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(); @@ -698,7 +699,9 @@ public class DefaultIotHubService implements IotHubService { // level deep, so there is no need to walk a related item's own related items. LinkedHashMap entries = new LinkedHashMap<>(); - String rootItemId = rootVersion.get(FIELD_ITEM_ID).asText(); + // 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); if (related != null && related.isArray()) { for (JsonNode relatedNode : related) { 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 index 5c0559c4d4..9631129a81 100644 --- 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 @@ -17,13 +17,19 @@ 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; @@ -34,6 +40,12 @@ 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; @@ -50,6 +62,7 @@ class DefaultIotHubServiceTest { @Mock private IotHubInstalledItemService iotHubInstalledItemService; + @Spy @InjectMocks private DefaultIotHubService service; @@ -57,6 +70,23 @@ class DefaultIotHubServiceTest { 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); @@ -212,4 +242,91 @@ class DefaultIotHubServiceTest { // No marketplace fetch happened — the duplicate install was avoided. verify(iotHubRestClient, never()).getVersionInfo(anyString()); } + + @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.findInstalledItemIdsByTenantId(tenantId)).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.findInstalledItemIdsByTenantId(tenantId)).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.findInstalledItemIdsByTenantId(tenantId)).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/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 33bb2af0f7..95de4a289e 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 @@ -327,7 +327,15 @@ export class TbIotHubInstallDialogComponent extends DialogComponent 0) { + message += ' ' + this.translate.instant('iot-hub.install-rollback-partial'); + } + this.errorMessage = message; return; } if (this.installPlan) { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 664ab07b91..7b63f52b97 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3853,6 +3853,7 @@ "install-plan-confirm": "Install", "install-partial-title": "Installed with warnings", "install-partial-desc": "'{{name}}' was installed, but some required items were missing.", + "install-rollback-partial": "Some of the items installed before the failure could not be removed automatically and may need to be deleted manually.", "check-for-updates": "Check for updates", "updates": "Updates", "up-to-date": "Up to date", From 54a0b80d22ebaa5ba719bdc3f6652658b56cb6f5 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 11:06:13 +0300 Subject: [PATCH 08/37] refactor(iot-hub): centralise installed-item entity URL + selectedTab deep link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a single getInstalledItemUrl(descriptor) helper to iot-hub-installed-item.models that maps every descriptor type (WIDGET / DASHBOARD / CALCULATED_FIELD / ALARM_RULE / RULE_CHAIN / SOLUTION_TEMPLATE) to its entity-details URL via the shared getEntityDetailsPageURL utility, appending `?selectedTab=cf` for calculated-field installs so the entity tabs page lands on the CF tab on first paint. - Drop the parallel resolveEntityDetailsUrl helper + iot-hub-components.models.ts file and rewire every installed-item open path (install dialog, update dialog, installed-items table, item detail dialog) to call getInstalledItemUrl. Installed-items table openEntity collapses to parseUrl + serializeUrl on the helper's output now that any selectedTab is part of the URL. - Teach EntityDetailsPageComponent to honour a `selectedTab=…` query param: read it from the current route on entityId change, strip it (replaceUrl) so refresh doesn't keep jumping the tab, then resolve a tab index via the new EntityTabsComponent.resolveTabIndex(tab) hook. Asset, device, asset-profile, and device-profile tabs override the hook to map the `cf` shortcut to their respective Calculated Fields tab position; other entities fall back to index 0. - EntityDetailsPanelComponent now applies this.selectedTab to the underlying MatTabGroup both when the entity-tabs query list refreshes and after view init, so the initial tab matches the resolved index instead of staying on 0. --- .../entity/entity-details-page.component.ts | 19 ++++++- .../entity/entity-details-panel.component.ts | 2 + .../entity/entity-tabs.component.ts | 4 ++ .../iot-hub/iot-hub-components.models.ts | 50 ------------------- .../iot-hub-install-dialog.component.ts | 8 +-- ...iot-hub-installed-items-table.component.ts | 42 ++-------------- .../iot-hub-item-detail-dialog.component.ts | 31 ++---------- .../iot-hub-update-dialog.component.ts | 8 +-- .../asset-profile-tabs.component.ts | 8 +++ .../home/pages/asset/asset-tabs.component.ts | 8 +++ .../device-profile-tabs.component.ts | 8 +++ .../pages/device/device-tabs.component.ts | 8 +++ .../iot-hub/iot-hub-installed-item.models.ts | 34 +++++++++++++ 13 files changed, 110 insertions(+), 120 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts index 8988a02380..7d9b9e07f7 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -98,7 +98,24 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl const id = paramMap.get('entityId'); this.currentEntityId = { id, entityType }; this.reload(); - this.selectedTab = 0; + const queryParams = this.route.snapshot.queryParams; + let selectedTabIndex = 0; + if (queryParams['selectedTab']) { + this.router.navigate([], { + queryParams: { + selectedTab: null + }, + queryParamsHandling: 'merge', + replaceUrl: true + }); + if (this.entityTabsComponent) { + const selectedTab: string = queryParams['selectedTab']; + if (selectedTab) { + selectedTabIndex = this.entityTabsComponent.resolveTabIndex(selectedTab); + } + } + } + this.selectedTab = selectedTabIndex; } })); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index 8c6fc004d8..b9ed698a47 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -215,6 +215,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV if (entityTabs) { if (this.viewInited) { this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...entityTabs]); + this.matTabGroup.selectedIndex = this.selectedTab; this.matTabGroup._tabs.notifyOnChanges(); } else { this.pendingTabs = entityTabs; @@ -321,6 +322,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV this.viewInited = true; if (this.pendingTabs) { this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...this.pendingTabs]); + this.matTabGroup.selectedIndex = this.selectedTab; this.matTabGroup._tabs.notifyOnChanges(); this.pendingTabs = null; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts index ec6e46fb72..c5b2eebeb4 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts @@ -117,6 +117,10 @@ export abstract class EntityTabsComponent, ); } + resolveTabIndex(tab: string): number { + return 0; + } + protected setEntity(entity: T) { this.entityValue = entity; } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts deleted file mode 100644 index 1233dfa89c..0000000000 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts +++ /dev/null @@ -1,50 +0,0 @@ -/// -/// 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. -/// - -import { EntityType } from '@shared/models/entity-type.models'; -import { IotHubInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; -import { getEntityDetailsPageURL } from '@core/utils'; - -export const ITEM_TYPE_TO_ENTITY_TYPE: Record = { - 'WIDGET': EntityType.WIDGET_TYPE, - 'DASHBOARD': EntityType.DASHBOARD, - 'CALCULATED_FIELD': EntityType.CALCULATED_FIELD, - 'ALARM_RULE': EntityType.CALCULATED_FIELD, - 'RULE_CHAIN': EntityType.RULE_CHAIN, - 'DEVICE': EntityType.DEVICE_PROFILE -}; - -export function resolveEntityDetailsUrl(descriptor: IotHubInstalledItemDescriptor, itemType: string): string | null { - if (!descriptor) { - return null; - } - const entityType = ITEM_TYPE_TO_ENTITY_TYPE[itemType]; - if (!entityType) { - return null; - } - let entityId: string | null = null; - switch (descriptor.type) { - case 'WIDGET': entityId = descriptor.widgetTypeId?.id; break; - case 'DASHBOARD': entityId = descriptor.dashboardId?.id; break; - case 'CALCULATED_FIELD': entityId = descriptor.calculatedFieldId?.id; break; - case 'ALARM_RULE': entityId = descriptor.calculatedFieldId?.id; break; - case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; break; - } - if (!entityId) { - return null; - } - return getEntityDetailsPageURL(entityId, entityType) || null; -} 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 6484a32c4b..34056abbfe 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 @@ -24,12 +24,14 @@ import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; -import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { + getInstalledItemUrl, + SolutionTemplateInstalledItemDescriptor +} from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { EntityId } from '@shared/models/id/entity-id'; -import { resolveEntityDetailsUrl } from './iot-hub-components.models'; import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; @@ -256,7 +258,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent super.ngOnInit(); } + resolveTabIndex(tab: string): number { + if (tab === 'cf') { + return 1; + } else { + return super.resolveTabIndex(tab); + } + } + } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts index 10ef434755..03aa7a1fa1 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts @@ -36,4 +36,12 @@ export class AssetTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + resolveTabIndex(tab: string): number { + if (tab === 'cf') { + return 3; + } else { + return super.resolveTabIndex(tab); + } + } + } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts index 1a762ac881..bfefdf5d4c 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -56,6 +56,14 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + resolveTabIndex(tab: string): number { + if (tab === 'cf') { + return 3; + } else { + return super.resolveTabIndex(tab); + } + } + } diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts index 25ac09d161..b46bdb2829 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts @@ -15,6 +15,8 @@ /// import { BaseData } from '@shared/models/base-data'; +import { EntityType } from '@shared/models/entity-type.models'; +import { getEntityDetailsPageURL } from '@core/utils'; export interface WidgetInstalledItemDescriptor { type: 'WIDGET'; @@ -97,3 +99,35 @@ export interface IotHubInstalledItem extends BaseData<{id: string}> { version: string; descriptor: IotHubInstalledItemDescriptor; } + +export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): string | null => { + let entityId: string | null = null; + let entityType: EntityType | null = null; + let query: string | null = null; + switch (descriptor.type) { + case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; break; + case 'DASHBOARD': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + case 'CALCULATED_FIELD': + case 'ALARM_RULE': + entityId = descriptor.entityId?.id; + entityType = descriptor.entityId?.entityType as EntityType; + if (descriptor.type === 'CALCULATED_FIELD') { + query = 'selectedTab=cf'; + } + break; + case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break; + case 'SOLUTION_TEMPLATE': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + } + if (entityType && entityId) { + let url = getEntityDetailsPageURL(entityId, entityType); + if (url) { + if (query) { + url = `${url}?${query}`; + } + return url; + } else { + return null; + } + } + return null; +} From 8861bc6c9089965c7659a95557b134ea86df30c4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 11:15:56 +0300 Subject: [PATCH 09/37] style(iot-hub): polish getInstalledItemUrl readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard the helper against a missing descriptor by short-circuiting to null up front instead of letting the switch crash on `undefined.type`. - Expand the inline single-line `case 'X': … break;` arms in the switch so each branch reads on its own line, matching the formatting of the already-multi-line CALCULATED_FIELD / ALARM_RULE arms. --- .../iot-hub/iot-hub-installed-item.models.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts index b46bdb2829..3bdaa87e66 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts @@ -101,12 +101,21 @@ export interface IotHubInstalledItem extends BaseData<{id: string}> { } export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): string | null => { + if (!descriptor) { + return null; + } let entityId: string | null = null; let entityType: EntityType | null = null; let query: string | null = null; switch (descriptor.type) { - case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; break; - case 'DASHBOARD': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + case 'WIDGET': + entityId = descriptor.widgetTypeId?.id; + entityType = EntityType.WIDGET_TYPE; + break; + case 'DASHBOARD': + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + break; case 'CALCULATED_FIELD': case 'ALARM_RULE': entityId = descriptor.entityId?.id; @@ -115,8 +124,14 @@ export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): query = 'selectedTab=cf'; } break; - case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break; - case 'SOLUTION_TEMPLATE': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break; + case 'RULE_CHAIN': + entityId = descriptor.ruleChainId?.id; + entityType = EntityType.RULE_CHAIN; + break; + case 'SOLUTION_TEMPLATE': + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + break; } if (entityType && entityId) { let url = getEntityDetailsPageURL(entityId, entityType); From bf3e405ea2c23f2201ce80fc721f9876d9decb3c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 11:56:04 +0300 Subject: [PATCH 10/37] feat(iot-hub): show install time in installed-items table createdTime column Switch the createdTime column formatter from 'mediumDate' (date only) to the custom pattern 'MMM d, y, h:mm a' so each row also surfaces the install time. Angular's built-in 'medium' alias was not used because it includes seconds. --- .../iot-hub/iot-hub-installed-items-table.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html index 545d219596..a64f9ab73e 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html @@ -44,7 +44,7 @@ {{ 'iot-hub.installed-date' | translate }} - {{ item.createdTime | date:'mediumDate' }} + {{ item.createdTime | date:'MMM d, y, h:mm a' }} From 428a6d5ded5268a7315ad9385813f08bf971c1e2 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 12:29:13 +0300 Subject: [PATCH 11/37] style(iot-hub): widen createdTime column for added install time Bump the installed-items table .mat-column-createdTime width from 100px to 160px so the new "MMM d, y, h:mm a" format (introduced when the column started showing the install time) fits without wrapping or ellipsis. --- .../iot-hub/iot-hub-installed-items-table.component.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss index 1edef65112..400bb5778b 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss @@ -72,9 +72,9 @@ } .mat-column-createdTime { - width: 100px; - min-width: 100px; - max-width: 100px; + width: 160px; + min-width: 160px; + max-width: 160px; } .mat-column-updates { From f8f7e00cce4a934cf333d38d9d625eba915b6d14 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 12:35:31 +0300 Subject: [PATCH 12/37] fix(iot-hub): resolve DEVICE installed-item url to its dashboard Add a DEVICE arm to getInstalledItemUrl so device-installs link to their generated dashboard (descriptor.dashboardId / EntityType.DASHBOARD), matching the existing SOLUTION_TEMPLATE handling instead of falling through to no URL. --- .../shared/models/iot-hub/iot-hub-installed-item.models.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts index 3bdaa87e66..c568c96a71 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts @@ -108,6 +108,10 @@ export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): let entityType: EntityType | null = null; let query: string | null = null; switch (descriptor.type) { + case 'DEVICE': + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + break; case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; From cab7c5102769854b388147d39fcc99a82d816844 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 15:08:28 +0300 Subject: [PATCH 13/37] feat(shared): add AutocompleteAutoScrollRepositionDirective - New input[matAutocomplete]/textarea[matAutocomplete] directive that keeps an open MatAutocomplete panel attached to its anchor while the page scrolls or the window resizes: tracks the input's visibility via IntersectionObserver and re-requests panel position (or closes the panel when off-screen) when the nearest scroll container or window emits a resize/scroll event. - Declare and export the directive from SharedModule so it lights up across every screen that uses mat-autocomplete. - Drop the transparent backdrop from MAT_AUTOCOMPLETE_DEFAULT_OPTIONS (the directive now handles scroll-and-resize repositioning, so the backdrop-driven outside- click close is no longer needed) and keep the existing hideSingleSelectionIndicator default. --- ...mplete-auto-scroll-reposition.directive.ts | 80 +++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 7 +- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 ui-ngx/src/app/shared/directives/autocomplete-auto-scroll-reposition.directive.ts diff --git a/ui-ngx/src/app/shared/directives/autocomplete-auto-scroll-reposition.directive.ts b/ui-ngx/src/app/shared/directives/autocomplete-auto-scroll-reposition.directive.ts new file mode 100644 index 0000000000..5f620789f1 --- /dev/null +++ b/ui-ngx/src/app/shared/directives/autocomplete-auto-scroll-reposition.directive.ts @@ -0,0 +1,80 @@ +/// +/// 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. +/// + +import { + Directive, + ElementRef, + OnDestroy, + inject, AfterViewInit, Renderer2, +} from '@angular/core'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { Subscription } from 'rxjs'; +import { onParentScrollOrWindowResize } from '@core/utils'; + +@Directive({ + selector: 'input[matAutocomplete], textarea[matAutocomplete]', + standalone: false +}) +export class AutocompleteAutoScrollRepositionDirective implements AfterViewInit, OnDestroy { + private readonly trigger = inject(MatAutocompleteTrigger, { host: true }); + private readonly elementRef = inject(ElementRef); + private readonly renderer = inject(Renderer2); + + private parentScrollSubscription: Subscription = null; + private isIntersecting: boolean = false; + + private intersectionObserver = new IntersectionObserver((entries) => { + if (this.isIntersecting !== entries[0].isIntersecting) { + this.isIntersecting = entries[0].isIntersecting; + this.updatePanelVisibility(); + } + }, {threshold: [0.5]}); + + constructor() { + } + + ngAfterViewInit(): void { + this.parentScrollSubscription = onParentScrollOrWindowResize(this.elementRef.nativeElement).subscribe(() => { + if (this.trigger.panelOpen) { + this.trigger.updatePosition(); + } + }); + this.intersectionObserver.observe(this.elementRef.nativeElement); + } + + ngOnDestroy(): void { + if (this.parentScrollSubscription) { + this.parentScrollSubscription.unsubscribe(); + this.parentScrollSubscription = null; + } + if (this.intersectionObserver) { + this.intersectionObserver.unobserve(this.elementRef.nativeElement); + this.intersectionObserver.disconnect(); + this.intersectionObserver = null; + } + } + + private updatePanelVisibility(): void { + if (this.trigger.panelOpen) { + if (this.isIntersecting) { + this.renderer.removeStyle(this.trigger.autocomplete.panel.nativeElement, 'display'); + } else { + this.renderer.setStyle(this.trigger.autocomplete.panel.nativeElement, 'display', 'none'); + } + } + } + +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 6fe78f7ee0..d5073332d2 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -230,6 +230,9 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle'; import { PhotoSwipeGalleryDirective } from '@shared/directives/photoswipe-gallery.directive'; +import { + AutocompleteAutoScrollRepositionDirective +} from '@shared/directives/autocomplete-auto-scroll-reposition.directive'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -278,8 +281,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: { - hasBackdrop: true, - backdropClass: 'cdk-overlay-transparent-backdrop', hideSingleSelectionIndicator: true } }, @@ -389,6 +390,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ContextMenuDirective, ChipOverflowDirective, PhotoSwipeGalleryDirective, + AutocompleteAutoScrollRepositionDirective, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -655,6 +657,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ContextMenuDirective, ChipOverflowDirective, PhotoSwipeGalleryDirective, + AutocompleteAutoScrollRepositionDirective, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, From a880a8635a03daef73959b8349301058cb24f9e8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Jun 2026 16:53:31 +0300 Subject: [PATCH 14/37] feat(iot-hub): hardening + dao-level lookups - IotHubRestClient.getVersionFileData now streams the response and enforces a configurable size cap (iot-hub.max-file-data-size-bytes, env IOT_HUB_MAX_FILE_DATA_SIZE_BYTES, default 100 MiB). Oversized payloads are rejected up front via Content-Length and mid-stream for chunked transfers, preventing OOMs from unknown bodies. - DefaultIotHubService guards every iotHubRestClient.getVersionInfo call against a null response, and rejects update attempts where the installed item's type does not match the new version's type ("Installed item type does not match the new version's item type."). Solution-template descriptor is also populated with tenantTelemetryKeys / tenantAttributeKeys captured during install. Drops the empty updateDeviceProfile stub. - Add a tenant-scoped findByTenantIdAndId + deleteByTenantIdAndId to IotHubInstalledItemDao (and the bulk deleteByTenantIdAndIdIn JPA query). IotHubInstalledItemServiceImpl now uses these so reads and deletes always honour the tenant boundary. - Database: add lts schema_update.sql + schema-entities-idx.sql indexes on iot_hub_installed_item(tenant_id), (tenant_id, item_type), and (tenant_id, item_id) for the new tenant-scoped lookups. - DefaultSolutionService: minor cleanup touched alongside. - .gitignore trailing-newline fix; remove stale superpowers plan/spec docs that have been folded into the implementation. --- .gitignore | 1 + .../main/data/upgrade/lts/schema_update.sql | 6 + .../service/iot_hub/DefaultIotHubService.java | 27 +- .../service/iot_hub/IotHubRestClient.java | 65 +- .../solutions/DefaultSolutionService.java | 2 +- .../src/main/resources/thingsboard.yml | 3 + .../dao/iot_hub/IotHubInstalledItemDao.java | 6 + .../IotHubInstalledItemServiceImpl.java | 4 +- .../IotHubInstalledItemRepository.java | 9 + .../iot_hub/JpaIotHubInstalledItemDao.java | 13 + .../server/dao/tenant/TenantServiceImpl.java | 3 +- .../resources/sql/schema-entities-idx.sql | 6 + .../2026-04-22-iot-hub-item-deep-link.md | 939 ----------------- .../plans/2026-04-27-install-method-sync.md | 685 ------------ .../plans/2026-05-06-iot-hub-item-link.md | 974 ------------------ .../2026-04-02-gateway-support-design.md | 267 ----- .../2026-04-02-transport-variables-design.md | 72 -- ...-04-09-provisioning-conflict-resolution.md | 102 -- ...026-04-22-iot-hub-item-deep-link-design.md | 289 ------ .../specs/2026-05-06-iot-hub-item-link.md | 145 --- 20 files changed, 132 insertions(+), 3486 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-22-iot-hub-item-deep-link.md delete mode 100644 docs/superpowers/plans/2026-04-27-install-method-sync.md delete mode 100644 docs/superpowers/plans/2026-05-06-iot-hub-item-link.md delete mode 100644 docs/superpowers/specs/2026-04-02-gateway-support-design.md delete mode 100644 docs/superpowers/specs/2026-04-02-transport-variables-design.md delete mode 100644 docs/superpowers/specs/2026-04-09-provisioning-conflict-resolution.md delete mode 100644 docs/superpowers/specs/2026-04-22-iot-hub-item-deep-link-design.md delete mode 100644 docs/superpowers/specs/2026-05-06-iot-hub-item-link.md diff --git a/.gitignore b/.gitignore index ee8c78dad3..833038e151 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ rebuild-docker.sh .run .claude .planning +docs/superpowers/ \ No newline at end of file diff --git a/application/src/main/data/upgrade/lts/schema_update.sql b/application/src/main/data/upgrade/lts/schema_update.sql index 443b54ed78..b52d8ee14e 100644 --- a/application/src/main/data/upgrade/lts/schema_update.sql +++ b/application/src/main/data/upgrade/lts/schema_update.sql @@ -32,4 +32,10 @@ CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( descriptor JSONB NOT NULL ); +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_tenant_id ON iot_hub_installed_item(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_type ON iot_hub_installed_item(tenant_id, item_type); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_id ON iot_hub_installed_item(tenant_id, item_id); + -- IOT HUB INSTALLED ITEM END 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 5c08b4f12b..26d208dbf6 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 @@ -106,6 +106,9 @@ public class DefaultIotHubService implements IotHubService { 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()); @@ -320,6 +323,9 @@ public class DefaultIotHubService implements IotHubService { // Skip checksum validation for solution templates if (!"SOLUTION_TEMPLATE".equals(itemType)) { JsonNode installedVersionInfo = iotHubRestClient.getVersionInfo(installedItem.getItemVersionId().toString()); + if (installedVersionInfo == null) { + throw new IllegalArgumentException("Failed to get installed version info from IoT Hub"); + } String installedChecksum = installedVersionInfo.has("checksum") ? installedVersionInfo.get("checksum").asText() : null; log.info("[{}] Installed version info: name={}, version={}, checksum={}", tenantId, installedItem.getItemName(), installedItem.getVersion(), installedChecksum); @@ -335,6 +341,16 @@ public class DefaultIotHubService implements IotHubService { } JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + + if (versionInfo == null) { + throw new IllegalArgumentException("Failed to get version info from IoT Hub"); + } + + String updateItemType = versionInfo.get("type").asText(); + if (!itemType.equals(updateItemType)) { + throw new IllegalArgumentException("Installed item type does not match the new version's item type."); + } + String itemName = versionInfo.get("name").asText(); String version = versionInfo.get("version").asText(); @@ -360,6 +376,8 @@ public class DefaultIotHubService implements IotHubService { throw new RuntimeException(response.getDetails()); } stDescriptor.setCreatedEntityIds(response.getCreatedEntityIds()); + stDescriptor.setTenantTelemetryKeys(response.getTenantTelemetryKeys()); + stDescriptor.setTenantAttributeKeys(response.getTenantAttributeKeys()); stDescriptor.setDashboardId(response.getDashboardId()); stDescriptor.setPublicId(response.getPublicId()); stDescriptor.setMainDashboardPublic(response.isMainDashboardPublic()); @@ -456,10 +474,6 @@ public class DefaultIotHubService implements IotHubService { ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration); } - private void updateDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { - // TODO: implement device profile update - } - private String calculateEntityChecksum(TenantId tenantId, IotHubInstalledItem installedItem) { IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); if (descriptor instanceof WidgetInstalledItemDescriptor wd) { @@ -549,6 +563,11 @@ public class DefaultIotHubService implements IotHubService { try { JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + + if (versionInfo == null) { + throw new IllegalArgumentException("Failed to get version info from IoT Hub"); + } + String itemName = versionInfo.get("name").asText(); UUID itemId = UUID.fromString(versionInfo.get("itemId").asText()); String version = versionInfo.get("version").asText(); 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 1155bcf0ad..1ea8b60d2e 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 @@ -16,23 +16,47 @@ package org.thingsboard.server.service.iot_hub; import com.fasterxml.jackson.databind.JsonNode; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import org.thingsboard.server.queue.util.TbCoreComponent; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; + @Component @TbCoreComponent @Slf4j public class IotHubRestClient { - private final RestTemplate restTemplate = new RestTemplate(); - @Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}") private String baseUrl; + @Value("${iot-hub.connect-timeout-sec:5}") + private int connectTimeoutSec; + + @Value("${iot-hub.read-timeout-sec:10}") + private int readTimeoutSec; + + @Value("${iot-hub.max-file-data-size-bytes:104857600}") + private long maxFileDataSizeBytes; + + private RestTemplate restTemplate; + + @PostConstruct + public void initRestClient() { + restTemplate = new RestTemplateBuilder() + .connectTimeout(Duration.ofSeconds(connectTimeoutSec)) + .readTimeout(Duration.ofSeconds(readTimeoutSec)) + .build(); + } + public JsonNode getVersionInfo(String versionId) { String url = baseUrl + "/api/versions/" + versionId; log.debug("Fetching IoT Hub version info: {}", url); @@ -42,8 +66,39 @@ public class IotHubRestClient { public byte[] getVersionFileData(String versionId) { String url = baseUrl + "/api/versions/" + versionId + "/fileData"; log.debug("Fetching IoT Hub version file data: {}", url); - ResponseEntity response = restTemplate.getForEntity(url, byte[].class); - return response.getBody(); + // Stream the response so we can reject oversized payloads before + // they get loaded fully into memory: + // 1) refuse early if the server advertises a Content-Length + // larger than the configured cap; + // 2) refuse mid-stream once we have already buffered more than + // the cap (for chunked transfers without Content-Length). + final long limit = maxFileDataSizeBytes; + return restTemplate.execute(url, HttpMethod.GET, null, response -> { + long contentLength = response.getHeaders().getContentLength(); + if (contentLength > limit) { + throw new IllegalStateException("IoT Hub file data size " + contentLength + + " bytes exceeds the configured limit of " + limit + " bytes"); + } + int initialCapacity = contentLength > 0 && contentLength <= Integer.MAX_VALUE + ? (int) contentLength : 8192; + try (InputStream in = response.getBody(); + ByteArrayOutputStream out = new ByteArrayOutputStream(initialCapacity)) { + byte[] buffer = new byte[8192]; + long total = 0; + int read; + while ((read = in.read(buffer)) != -1) { + total += read; + if (total > limit) { + throw new IllegalStateException("IoT Hub file data stream exceeded the configured limit of " + + limit + " bytes"); + } + out.write(buffer, 0, read); + } + return out.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException("Failed to read IoT Hub file data", e); + } + }); } public void reportVersionInstalled(String versionId) { diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index b68fe5515c..9d13f15b96 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -269,7 +269,7 @@ public class DefaultSolutionService implements SolutionService { tsService.remove(tenantId, tenantId, queries).get(); } List attrKeys = descriptor.getTenantAttributeKeys(); - if (tsKeys != null && !tsKeys.isEmpty()) { + if (attrKeys != null && !attrKeys.isEmpty()) { attributesService.removeAll(tenantId, tenantId, AttributeScope.SERVER_SCOPE, attrKeys).get(); } } catch (Exception e) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bfbb211c5c..97f14f7969 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -2167,3 +2167,6 @@ mqtt: jitter_factor: "${TB_MQTT_CLIENT_RETRANSMISSION_JITTER_FACTOR:0.15}" iot-hub: base-url: "${IOT_HUB_BASE_URL:https://iot-hub.thingsboard.io}" # IoT Hub base URL for fetching published items, resources, etc. + connect-timeout-sec: "${IOT_HUB_CONNECT_TIMEOUT_SEC:5}" # timeout in seconds to connect to IoT Hub server. + read-timeout-sec: "${IOT_HUB_READ_TIMEOUT_SEC:10}" # timeout in seconds to read from IoT Hub server. + max-file-data-size-bytes: "${IOT_HUB_MAX_FILE_DATA_SIZE_BYTES:104857600}" # maximum size in bytes of a file-data payload fetched from IoT Hub. Requests advertising a larger Content-Length, or streams that exceed this size, are rejected to avoid out-of-memory issues on the platform side. 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 2ad7d1d01a..22692f6ee4 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.iot_hub; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.page.PageData; @@ -23,10 +24,13 @@ import org.thingsboard.server.dao.Dao; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; public interface IotHubInstalledItemDao extends Dao { + Optional findByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId); + PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink); List findInstalledItemIdsByTenantId(TenantId tenantId); @@ -37,4 +41,6 @@ public interface IotHubInstalledItemDao extends Dao { void deleteByTenantId(TenantId tenantId); + boolean deleteByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId); + } 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 c98f03d104..5bb857ca2c 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 @@ -43,7 +43,7 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { @Override public IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id) { - return iotHubInstalledItemDao.findById(tenantId, id.getId()); + return iotHubInstalledItemDao.findByTenantIdAndId(tenantId, id).orElse(null); } @Override @@ -68,7 +68,7 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { @Override public void deleteById(TenantId tenantId, IotHubInstalledItemId id) { - iotHubInstalledItemDao.removeById(tenantId, id.getId()); + iotHubInstalledItemDao.deleteByTenantIdAndId(tenantId, id); } @Override 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 69f93cd5e7..8f06beaa39 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 @@ -25,10 +25,14 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.UUID; interface IotHubInstalledItemRepository extends JpaRepository { + Optional findByTenantIdAndId(UUID tenantId, UUID id); + @Query("SELECT DISTINCT item.itemId FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId") List findInstalledItemIdsByTenantId(@Param("tenantId") UUID tenantId); @@ -64,4 +68,9 @@ interface IotHubInstalledItemRepository extends JpaRepository ids); + } 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 b0db650ae0..9d61b13209 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 @@ -21,6 +21,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.page.PageData; @@ -35,6 +36,8 @@ import org.thingsboard.server.dao.util.SqlDao; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.UUID; @SqlDao @@ -44,6 +47,11 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao findByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId) { + return repository.findByTenantIdAndId(tenantId.getId(), installedItemId.getId()).map(DaoUtil::getData); + } + @Override public PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink) { return DaoUtil.toPageData(repository.findByTenantId( @@ -80,6 +88,11 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao 0; + } + private static PageRequest toPageRequest(PageLink pageLink) { Sort sort; SortOrder sortOrder = pageLink.getSortOrder(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index e7939ea32e..0045a1042e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -39,6 +39,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -83,7 +84,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Ship `/iot-hub/{itemId}` (latest published version of an item) and `/iot-hub/version/{itemVersionId}` (specific version snapshot, warning gate if unpublished) deep links that resolve a version, navigate to the type-specific browse page, and open the existing detail dialog. - -**Architecture:** A router-reachable `TbIotHubItemResolverComponent` owns resolution: fetch by itemId, (optionally) gate on a blocking warning dialog, then `router.navigate` to `/iot-hub/{typeSegment(type)}` carrying the version in `history.state`. The target type-page consumes the state once and opens the existing `TbIotHubItemDetailDialogComponent` via `IotHubActionsService`, with a new `preview` flag that adds an "Unpublished preview" badge. Zero ThingsBoard backend changes — install flows reuse existing versionId endpoints. - -**Tech Stack:** Angular 20, Angular Material dialogs, NgRx (for toast dispatch), RxJS. TypeScript strict mode. - -**Note on testing:** `ui-ngx` has no frontend test runner wired up (no `npm test` script, no karma/jest config). Each task is verified by `npm run lint` + `npm run build:prod` passing and, for flow-level tasks, a manual dev-server smoke test documented in the final task. - -**Prerequisite (external):** The two new IoT Hub endpoints (`GET /api/items/{itemId}/published` and `GET /api/items/{itemId}/latest`) must be deployed on the target IoT Hub instance for end-to-end testing. Until they land, local verification can be done against a mocked IoT Hub or against the published endpoint only. See the spec (`docs/superpowers/specs/2026-04-22-iot-hub-item-deep-link-design.md`) for the full IoT Hub-side contract. - ---- - -## File structure - -**Create:** -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts` — UUID check, type→route-segment mapping, `isPublished` predicate -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts` — route-reachable controller -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.ts` — warning dialog logic -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.html` — warning dialog template -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.scss` — warning dialog styles - -**Modify:** -- `ui-ngx/src/app/core/http/iot-hub-api.service.ts` — add `getPublishedVersion`, `getLatestVersion` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts` — add `preview` field on data + component -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html` — preview badge markup -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss` — preview badge styles -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts` — propagate `preview` in `openItemDetail` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts` — declare + export warning dialog -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts` — two new child routes (placed last) -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts` — declare resolver component -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts` — `maybeOpenDeepLinkedItem` handoff -- `ui-ngx/src/assets/locale/locale.constant-en_US.json` — 9 new i18n keys under the existing `"iot-hub"` block - ---- - -### Task 1: Add i18n keys - -Add all new English strings up front so later tasks can reference them freely. Other locales follow the existing project convention (en_US only for new iot-hub keys; translators backfill later). - -**Files:** -- Modify: `ui-ngx/src/assets/locale/locale.constant-en_US.json` - -- [ ] **Step 1: Add keys inside the existing `"iot-hub"` block** - -Open `ui-ngx/src/assets/locale/locale.constant-en_US.json`, find the `"iot-hub"` block (starts at the line matching `"iot-hub": {`), and append the following nine keys to it (place them immediately before the closing `}` of the `"iot-hub"` object, keeping JSON valid — i.e. add a trailing comma to the previous key): - -```json - "item-detail": "IoT Hub item", - "item-preview": "IoT Hub item preview", - "unpublished-warning-title": "Unpublished content", - "unpublished-warning-text": "This is a preview of unpublished content. It has not been reviewed by IoT Hub. Installing unverified content can introduce security and stability risks — only continue if you trust the creator.", - "unpublished-warning-confirm": "I understand the risk, continue", - "unpublished-preview": "Unpublished preview", - "deep-link-invalid-id": "Invalid IoT Hub item link.", - "deep-link-not-found": "This IoT Hub item doesn't exist or was removed.", - "deep-link-fetch-failed": "Couldn't load IoT Hub item. Please try again." -``` - -- [ ] **Step 2: Validate JSON** - -Run: -```bash -cd ui-ngx && node -e "JSON.parse(require('fs').readFileSync('src/assets/locale/locale.constant-en_US.json', 'utf8')); console.log('OK')" -``` -Expected output: `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/assets/locale/locale.constant-en_US.json -git commit -m "feat(iot-hub): add i18n keys for item deep-link and unpublished warning" -``` - ---- - -### Task 2: Deep-link utility module - -A tiny, pure-TS helper file. No Angular deps. Exported functions are used by the resolver and (for `isPublished`) by the detail dialog. - -**Files:** -- Create: `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts` - -- [ ] **Step 1: Create the file** - -Create `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts` with: - -```ts -/// -/// 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. -/// - -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; -import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; - -const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -export function isUUID(s: string | null | undefined): s is string { - return !!s && UUID_RE.test(s); -} - -export function typeSegment(t: ItemType): string | undefined { - switch (t) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboards'; - case ItemType.SOLUTION_TEMPLATE: return 'solution-templates'; - case ItemType.CALCULATED_FIELD: return 'calculated-fields'; - case ItemType.RULE_CHAIN: return 'rule-chains'; - case ItemType.DEVICE: return 'devices'; - default: return undefined; - } -} - -export function isPublished(v: MpItemVersionView): boolean { - return !!v.publishedTime && v.publishedTime > 0; -} - -export interface DeepLinkOpenItem { - version: MpItemVersionView; - preview: boolean; -} -``` - -- [ ] **Step 2: Verify TypeScript compiles** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -20 -``` -Expected: build succeeds. If `ng build` is too slow for iteration, use `npx tsc --noEmit -p tsconfig.json 2>&1 | grep iot-hub-deep-link || echo 'no type errors in utils file'`. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts -git commit -m "feat(iot-hub): add deep-link utility helpers (isUUID, typeSegment, isPublished)" -``` - ---- - -### Task 3: IoT Hub API service — two new methods - -Add `getPublishedVersion` and `getLatestVersion` to `IotHubApiService`. Both hit IoT Hub cross-origin endpoints (same `baseUrl` as existing `/api/versions/published`). - -**Files:** -- Modify: `ui-ngx/src/app/core/http/iot-hub-api.service.ts` - -- [ ] **Step 1: Add the two methods** - -Open `ui-ngx/src/app/core/http/iot-hub-api.service.ts`. Insert the following two methods immediately after `getVersionInfo` (which ends near line 100): - -```ts - public getPublishedVersion(itemId: string, config?: IotHubRequestConfig): Observable { - return this.http.get( - `${this.baseUrl}/api/items/${itemId}/published`, - { params: this.buildParams(config) } - ); - } - - public getLatestVersion(itemId: string, config?: IotHubRequestConfig): Observable { - return this.http.get( - `${this.baseUrl}/api/items/${itemId}/latest`, - { params: this.buildParams(config) } - ); - } -``` - -Use the exact `Edit` tool call: `old_string` should be the line ` public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable {` (and enough context around it), and `new_string` should prepend the two new methods above it. - -- [ ] **Step 2: Verify build** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -15 -``` -Expected: build succeeds. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/core/http/iot-hub-api.service.ts -git commit -m "feat(iot-hub): add getPublishedVersion and getLatestVersion API methods" -``` - ---- - -### Task 4: Unpublished warning dialog component - -New dialog styled after `TbIotHubDeleteDialogComponent`: title, description, item summary, Cancel + danger-accented confirm button. Returns `boolean` from `afterClosed`. - -**Files:** -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.ts` -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.html` -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.scss` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts` - -- [ ] **Step 1: Create the component .ts** - -Create `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.ts`: - -```ts -/// -/// 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. -/// - -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Router } from '@angular/router'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; - -export interface IotHubUnpublishedWarningDialogData { - item: MpItemVersionView; -} - -@Component({ - selector: 'tb-iot-hub-unpublished-warning-dialog', - standalone: false, - templateUrl: './iot-hub-unpublished-warning-dialog.component.html', - styleUrls: ['./iot-hub-unpublished-warning-dialog.component.scss'] -}) -export class TbIotHubUnpublishedWarningDialogComponent extends DialogComponent { - - constructor( - protected store: Store, - protected router: Router, - protected dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: IotHubUnpublishedWarningDialogData - ) { - super(store, router, dialogRef); - } - - confirm(): void { - this.dialogRef.close(true); - } - - cancel(): void { - this.dialogRef.close(false); - } -} -``` - -- [ ] **Step 2: Create the component .html** - -Create `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.html`: - -```html - -
-
- warning -

{{ 'iot-hub.unpublished-warning-title' | translate }}

-
-

{{ 'iot-hub.unpublished-warning-text' | translate }}

-
- {{ data.item.name }} - v {{ data.item.version }} -
-
-
- - -
-``` - -- [ ] **Step 3: Create the component .scss** - -Create `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.scss`: - -```scss -/** - * 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. - */ - -.tb-iot-hub-warning-content { - display: flex; - flex-direction: column; - gap: 16px; - padding: 24px; - max-width: 480px; -} - -.tb-iot-hub-warning-title-row { - display: flex; - align-items: center; - gap: 12px; -} - -.tb-iot-hub-warning-icon { - color: #d32f2f; - font-size: 28px; - width: 28px; - height: 28px; -} - -.tb-iot-hub-warning-title { - font-size: 20px; - font-weight: 600; - line-height: 24px; - letter-spacing: 0.1px; - color: rgba(0, 0, 0, 0.87); - margin: 0; -} - -.tb-iot-hub-warning-text { - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - color: rgba(0, 0, 0, 0.75); - margin: 0; -} - -.tb-iot-hub-warning-item { - display: flex; - align-items: baseline; - gap: 8px; - padding: 12px 16px; - background: rgba(211, 47, 47, 0.08); - border-left: 3px solid #d32f2f; - border-radius: 2px; - - .tb-iot-hub-warning-item-name { - font-weight: 600; - color: rgba(0, 0, 0, 0.87); - } - - .tb-iot-hub-warning-item-version { - font-size: 13px; - color: rgba(0, 0, 0, 0.54); - } -} - -.tb-iot-hub-warning-actions { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 8px; - padding: 8px; -} -``` - -- [ ] **Step 4: Register in `IotHubComponentsModule`** - -Open `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts`. - -Add the import line after the existing `TbIotHubDeleteDialogComponent` import: - -```ts -import { TbIotHubUnpublishedWarningDialogComponent } from './iot-hub-unpublished-warning-dialog.component'; -``` - -In the `declarations` array, add `TbIotHubUnpublishedWarningDialogComponent` after `TbIotHubDeleteDialogComponent`. - -In the `exports` array, add `TbIotHubUnpublishedWarningDialogComponent` after `TbIotHubDeleteDialogComponent`. - -- [ ] **Step 5: Verify build** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -15 -``` -Expected: build succeeds. - -- [ ] **Step 6: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.ts \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.html \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.scss \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts -git commit -m "feat(iot-hub): add unpublished content warning dialog" -``` - ---- - -### Task 5: Detail dialog `preview` flag + badge, actions service signature - -Add `preview?: boolean` to `IotHubItemDetailDialogData`, store on the component, render an "Unpublished preview" badge in the meta-bar next to the version chip. Extend `IotHubActionsService.openItemDetail` to forward the flag. - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts` - -- [ ] **Step 1: Extend `IotHubItemDetailDialogData` + component field** - -In `iot-hub-item-detail-dialog.component.ts`, find the `IotHubItemDetailDialogData` interface (around line 43) and add the optional `preview` field: - -```ts -export interface IotHubItemDetailDialogData { - item: MpItemVersionView; - installedItem?: IotHubInstalledItem; - installedItemsCount?: number; - mode?: IotHubItemDetailDialogMode; - showCreator?: boolean; - preview?: boolean; -} -``` - -In the component class, add a new public field below `showCreator`: - -```ts - preview: boolean; -``` - -In the constructor body (below `this.showCreator = data.showCreator !== false;`), add: - -```ts - this.preview = data.preview === true; -``` - -- [ ] **Step 2: Add badge markup in template** - -In `iot-hub-item-detail-dialog.component.html`, find the block starting with `` that contains the version icon (around line 39, the `update` mat-icon). Immediately **after** the closing `` of that version group (just before the `@if (item.publishedTime)` block), insert: - -```html - @if (preview) { - - - warning - {{ 'iot-hub.unpublished-preview' | translate }} - - } -``` - -- [ ] **Step 3: Style the badge** - -Append to `iot-hub-item-detail-dialog.component.scss`: - -```scss -.dlg-subtitle-group.tb-unpublished-preview-badge { - color: #d32f2f; - font-weight: 600; - - .dlg-subtitle-icon { - color: #d32f2f; - } -} -``` - -- [ ] **Step 4: Extend `IotHubActionsService.openItemDetail`** - -In `iot-hub-actions.service.ts`, replace the `openItemDetail` method signature and body with: - -```ts - openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, installedItemsCount?: number, - mode?: IotHubItemDetailDialogMode, showCreator?: boolean, preview?: boolean): Observable { - return this.dialog.open(TbIotHubItemDetailDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - autoFocus: false, - data: { item, installedItem, installedItemsCount, mode, showCreator, preview } as IotHubItemDetailDialogData - }).afterClosed(); - } -``` - -Existing callers pass 5 or fewer args and remain valid — the new parameter is optional. - -- [ ] **Step 5: Verify build** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -15 -``` -Expected: build succeeds. - -- [ ] **Step 6: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts -git commit -m "feat(iot-hub): add unpublished preview badge to item detail dialog" -``` - ---- - -### Task 6: Resolver component - -The heart of the feature. Fetches the version, gates on warning for unpublished preview, redirects to the type-page with router state. - -**Files:** -- Create: `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts` - -- [ ] **Step 1: Create the component** - -Create `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts`: - -```ts -/// -/// 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. -/// - -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { MatDialog } from '@angular/material/dialog'; -import { Store } from '@ngrx/store'; -import { TranslateService } from '@ngx-translate/core'; -import { AppState } from '@core/core.state'; -import { ActionNotificationShow } from '@core/notification/notification.actions'; -import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { - DeepLinkOpenItem, - isPublished, - isUUID, - typeSegment -} from './iot-hub-deep-link.utils'; -import { - IotHubUnpublishedWarningDialogData, - TbIotHubUnpublishedWarningDialogComponent -} from '@home/components/iot-hub/iot-hub-unpublished-warning-dialog.component'; - -@Component({ - selector: 'tb-iot-hub-item-resolver', - standalone: false, - template: '' -}) -export class TbIotHubItemResolverComponent implements OnInit { - - constructor( - private route: ActivatedRoute, - private router: Router, - private dialog: MatDialog, - private store: Store, - private translate: TranslateService, - private iotHubApi: IotHubApiService - ) {} - - ngOnInit(): void { - const itemId = this.route.snapshot.paramMap.get('itemId'); - const preview = this.route.snapshot.data['preview'] === true; - - if (!isUUID(itemId)) { - this.failTo('iot-hub.deep-link-invalid-id'); - return; - } - - const fetch$ = preview - ? this.iotHubApi.getLatestVersion(itemId, { ignoreErrors: true }) - : this.iotHubApi.getPublishedVersion(itemId, { ignoreErrors: true }); - - fetch$.subscribe({ - next: v => this.handleResolved(v, preview), - error: err => { - const key = err?.status === 404 - ? 'iot-hub.deep-link-not-found' - : 'iot-hub.deep-link-fetch-failed'; - this.failTo(key); - } - }); - } - - private handleResolved(version: MpItemVersionView, preview: boolean): void { - const segment = typeSegment(version.type); - if (!segment) { - this.failTo('iot-hub.deep-link-fetch-failed'); - return; - } - - const showWarning = preview && !isPublished(version); - - if (showWarning) { - this.dialog.open< - TbIotHubUnpublishedWarningDialogComponent, - IotHubUnpublishedWarningDialogData, - boolean - >(TbIotHubUnpublishedWarningDialogComponent, { - panelClass: ['tb-dialog'], - disableClose: true, - autoFocus: false, - data: { item: version } - }).afterClosed().subscribe(confirmed => { - if (confirmed) { - this.openOnTypePage(version, segment, true); - } else { - this.router.navigate(['/iot-hub'], { replaceUrl: true }); - } - }); - } else { - this.openOnTypePage(version, segment, false); - } - } - - private openOnTypePage(version: MpItemVersionView, segment: string, preview: boolean): void { - const openItem: DeepLinkOpenItem = { version, preview }; - this.router.navigate(['/iot-hub', segment], { - state: { openItem }, - replaceUrl: true - }); - } - - private failTo(messageKey: string): void { - this.store.dispatch(new ActionNotificationShow({ - message: this.translate.instant(messageKey), - type: 'error', - duration: 5000 - })); - this.router.navigate(['/iot-hub'], { replaceUrl: true }); - } -} -``` - -- [ ] **Step 2: Verify TypeScript compiles (the module wiring comes in Task 7)** - -The component is not yet declared in any module, so a full `ng build` will fail. Run a type-check only on this file: - -```bash -cd ui-ngx && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "iot-hub-item-resolver|iot-hub-deep-link" || echo "no type errors in new files" -``` -Expected: `no type errors in new files`. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts -git commit -m "feat(iot-hub): add item resolver component for deep links" -``` - ---- - -### Task 7: Routing + module registration - -Register the resolver component in `IotHubModule` and add the two new child routes to `iot-hub-routing.module.ts`. Reserved words must be matched first, so the `:itemId` wildcard routes go **last** in the children array. - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts` -- Modify: `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts` - -- [ ] **Step 1: Declare resolver in `IotHubModule`** - -In `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts`, add the import: - -```ts -import { TbIotHubItemResolverComponent } from './iot-hub-item-resolver.component'; -``` - -In the `declarations` array, add `TbIotHubItemResolverComponent` after `TbIotHubSearchPageComponent`. - -- [ ] **Step 2: Add routes** - -In `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts`, add the import: - -```ts -import { TbIotHubItemResolverComponent } from './iot-hub-item-resolver.component'; -``` - -In the `children` array of the `/iot-hub` route, **immediately before** the closing `]` (after the existing `creator/:creatorId` entry), insert: - -```ts - { - path: ':itemId', - component: TbIotHubItemResolverComponent, - data: { - auth: [Authority.TENANT_ADMIN], - title: 'iot-hub.item-detail' - } - }, - { - path: ':itemId/preview', - component: TbIotHubItemResolverComponent, - data: { - auth: [Authority.TENANT_ADMIN], - title: 'iot-hub.item-preview', - preview: true - } - } -``` - -Make sure there is a comma after the preceding `creator/:creatorId` entry. - -- [ ] **Step 3: Verify build** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -15 -``` -Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts \ - ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts -git commit -m "feat(iot-hub): wire resolver component into routing and module" -``` - ---- - -### Task 8: Type-page handoff - -`TbIotHubItemsPageComponent` consumes `history.state.openItem` on init: resolves installed state, opens the detail dialog with the `preview` flag, then clears the state entry so refresh does not re-open. - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts` - -- [ ] **Step 1: Extend imports + constructor** - -Open `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts`. - -Replace the import block (top of file, currently lines 17-20) with: - -```ts -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; -import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; -import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; -import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { PageLink } from '@shared/models/page/page-link'; -import { DeepLinkOpenItem } from './iot-hub-deep-link.utils'; -``` - -Replace the constructor with: - -```ts - constructor( - private route: ActivatedRoute, - private router: Router, - private iotHubApiService: IotHubApiService, - private iotHubActions: IotHubActionsService - ) {} -``` - -- [ ] **Step 2: Extend `ngOnInit` and add handoff method** - -Replace the existing `ngOnInit` body with: - -```ts - ngOnInit(): void { - const itemType = this.route.snapshot.data['itemType'] as string; - this.config = PAGE_CONFIGS[itemType]; - this.loadInstalledCount(); - this.maybeOpenDeepLinkedItem(); - } -``` - -Add these two new methods anywhere in the class (e.g. after `loadInstalledCount`): - -```ts - private maybeOpenDeepLinkedItem(): void { - const openItem = history.state?.openItem as DeepLinkOpenItem | undefined; - if (!openItem || openItem.version.type !== this.config.type) { - return; - } - history.replaceState({ ...history.state, openItem: undefined }, ''); - this.resolveInstalledItem(openItem.version).subscribe(installed => { - this.iotHubActions.openItemDetail( - openItem.version, - installed ?? undefined, - installed ? 1 : 0, - 'default', - true, - openItem.preview - ).subscribe(); - }); - } - - private resolveInstalledItem(version: MpItemVersionView): Observable { - return this.iotHubApiService - .getInstalledItems(new PageLink(1), undefined, version.itemId, { ignoreLoading: true }) - .pipe(map(page => page.data[0] ?? null)); - } -``` - -- [ ] **Step 3: Verify build** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -15 -``` -Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts -git commit -m "feat(iot-hub): open deep-linked item from history state on type-page init" -``` - ---- - -### Task 9: Lint, build, and manual smoke test - -Final verification gate. Runs the lint and production build, then walks each user-facing flow in a dev server to confirm behavior. - -**Files:** none (verification only) - -- [ ] **Step 1: Lint** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng lint 2>&1 | tail -30 -``` -Expected: no new errors introduced by files created/modified in tasks 1–8. Warnings in unrelated files are acceptable. - -- [ ] **Step 2: Production build** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production 2>&1 | tail -20 -``` -Expected: build completes without errors. - -- [ ] **Step 3: Start dev server** - -Run: -```bash -cd ui-ngx && node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 -``` -Dev server should be reachable at `http://localhost:4200`. Log in as a TENANT_ADMIN user. (A running ThingsBoard backend at the default proxy target is required per `proxy.conf.js`.) - -- [ ] **Step 4: Manual smoke test — published deep link** - -Pick an itemId from `/iot-hub/widgets` (the card's click handler logs it, or inspect the dialog URL via browser dev tools; alternatively, query `iotHubBaseUrl/api/versions/published` and copy an `itemId`). - -Navigate to `http://localhost:4200/iot-hub/{that-itemId}`. Verify: -- URL bar ends at `/iot-hub/widgets` (or the correct type page for the item) after resolution -- Detail dialog opens with the item's info, no "Unpublished preview" badge, and Install/Update actions behave normally -- Closing the dialog leaves the user on the type-page - -Navigate to `http://localhost:4200/iot-hub/00000000-0000-0000-0000-000000000000` (a valid-shaped but non-existent UUID). Verify: -- URL bar ends at `/iot-hub` -- Red toast: "This IoT Hub item doesn't exist or was removed." - -Navigate to `http://localhost:4200/iot-hub/not-a-uuid`. Verify: -- URL bar ends at `/iot-hub` -- Red toast: "Invalid IoT Hub item link." - -- [ ] **Step 5: Manual smoke test — preview deep link (requires IoT Hub `/latest` endpoint)** - -**If the IoT Hub-side endpoints are not yet deployed**, skip this step and record that end-to-end preview verification is deferred until the IoT Hub PR lands. The per-URL flow is exercised in Step 4; the preview URL hitting a non-existent endpoint will trigger the "fetch failed" toast, which is the correct fallback behavior. - -**If the endpoints are available**, navigate to `http://localhost:4200/iot-hub/{unpublished-itemId}/preview`. Verify: -- Warning dialog opens immediately with the item's name/version, red warning icon, and two buttons -- "Cancel" → closes dialog, lands on `/iot-hub` (home) -- Revisit the URL, click "I understand the risk, continue" → URL advances to `/iot-hub/{typePage}`, detail dialog opens with the red "Unpublished preview" badge in the meta bar -- Install button still works end-to-end (calls the TB install endpoint, which proxies to IoT Hub using the unpublished versionId) - -Navigate to `http://localhost:4200/iot-hub/{published-only-itemId}/preview`. Verify: -- No warning shown (preview fell back to the published version) -- Detail dialog opens with **no** preview badge -- Behavior matches the regular published URL - -- [ ] **Step 6: Commit the final verification (if any lint/build fixes were needed)** - -If the smoke test uncovered issues that required fixes, stage and commit them with a message like `fix(iot-hub): address smoke-test findings for deep-link flow`. Otherwise, this step is a no-op — all previous commits already capture the work. - ---- - -## IoT Hub-side changes required (recap) - -One new endpoint plus behavior + CORS contracts on the existing by-versionId family: - -1. **New endpoint** `GET /api/items/{itemId}/published` — latest PUBLISHED version as `MpItemVersionView`; `404` if none. Anonymous cross-origin. Powers the `/iot-hub/{itemId}` URL. -2. **`GET /api/versions/{versionId}`** must return the requested version regardless of state (PUBLISHED / DRAFT / PENDING_REVIEW / …). Anonymous cross-origin; versionId UUID is the soft-secret gate. Powers the `/iot-hub/version/{itemVersionId}` URL. -3. **`MpItemVersionView`** must allow the frontend to tell published from unpublished. Either `publishedTime` must be falsy (`0`/`null`) for non-published versions, or add an explicit `state` field. Frontend's `isPublished()` uses `publishedTime > 0` today. -4. **Related by-versionId endpoints** must also serve unpublished versions (required for install-from-deep-link): - - `GET /api/versions/{versionId}/readme` - - `GET /api/versions/{versionId}/fileData` - - `POST /api/versions/{versionId}/install` -5. **Install counter policy** for unpublished versions — recommended: skip counting. -6. **CORS** on `/api/items/{itemId}/published` and the `/api/versions/{versionId}/...` family must permit cross-origin GET from any origin. - -These live in the IoT Hub repository, not ThingsBoard CE. diff --git a/docs/superpowers/plans/2026-04-27-install-method-sync.md b/docs/superpowers/plans/2026-04-27-install-method-sync.md deleted file mode 100644 index 717ae910bb..0000000000 --- a/docs/superpowers/plans/2026-04-27-install-method-sync.md +++ /dev/null @@ -1,685 +0,0 @@ -# Install-Method Sync with IoT Hub Marketplace — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Sync CE's `InstallMethod` enum + labels with the IoT Hub marketplace's expanded 50-value allow-list, and add a "PE only" gate in the device install dialog so users selecting a PE-only integration get a clear message instead of a silently-broken wizard. - -**Architecture:** Frontend-only. CE has no backend install-method allow-list (`DeviceInstalledItemDescriptor.selectedInstallMethod` is a free-form `String`), so all work lives in `ui-ngx`. The `GATEWAY_CONNECTOR` step type already handles connector config generically (line 761 of `device-install-dialog.component.ts`), so the 12 new `GATEWAY_*` install methods need no new install logic — only enum + label entries. The 26 new `INTEGRATION_*` methods are PE-only on CE: today CE silently skips `CONVERTER`/`INTEGRATION` steps (line 553), running an empty-progress wizard — this plan replaces that silent skip with an explicit "PE only — upgrade required" panel shown right after connectivity selection. - -**Tech Stack:** Angular 20, TypeScript, Material Design (existing `TbDeviceInstallDialogComponent`). - ---- - -## Source of Truth - -The marketplace allow-list lives at: -`/home/ashvayka/git/iot-hub/dao/src/main/java/org/thingsboard/iothub/dao/service/impl/ItemDataServiceImpl.java`, constant `VALID_INSTALL_METHODS`. - -CE's enum must be a strict subset of the marketplace's allow-list, including every entry. Diverging causes "marketplace accepts the package, CE wizard rejects it / shows raw constant" failures. - -PE/CE applicability — extracted from `/home/ashvayka/git/iot-hub/device-library-contribution.md`: - -| Group | Constants | CE-supported install behavior | -|-------|-----------|-------------------------------| -| Direct (5) | `DIRECT_HTTP`, `DIRECT_MQTT`, `DIRECT_COAP`, `DIRECT_LWM2M`, `DIRECT_SNMP` | Yes — runs through `DEVICE` + standard steps | -| Gateway (15) | `GATEWAY_MQTT`, `GATEWAY_MODBUS`, `GATEWAY_OPCUA`, `GATEWAY_BACNET`, `GATEWAY_BLE`, `GATEWAY_CAN`, `GATEWAY_FTP`, `GATEWAY_KNX`, `GATEWAY_OCPP`, `GATEWAY_ODBC`, `GATEWAY_REQUEST`, `GATEWAY_REST`, `GATEWAY_SNMP`, `GATEWAY_SOCKET`, `GATEWAY_XMPP` | Yes — generic `GATEWAY_CONNECTOR` step writes connector config to gateway shared attributes regardless of connector type | -| ChirpStack CE (1) | `CHIRPSTACK` | Yes | -| Integrations (29) | `INTEGRATION_*` (all values listed in step 4 of `VALID_INSTALL_METHODS`) | **No — PE only**. Show "PE only" panel, do not start wizard | - ---- - -## File Structure - -| Action | File | Responsibility | -|--------|------|----------------| -| Modify | `ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts` | Extend `InstallMethod` enum to 50 values; extend `installMethodLabels` to 50 labels; add new `peOnlyInstallMethods` set | -| Modify | `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts` | Add `peOnlySelected` getter; route `confirmConnectivity()` to a new "PE only" view-state instead of `startWizard()` when user picks a PE-only method | -| Modify | `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html` | Add a new conditional panel that renders when `peOnlySelected` is true: title, body copy, "Learn about ThingsBoard PE" link, Close button | -| Modify | `ui-ngx/src/assets/locale/locale.constant-en_US.json` | Add three translation keys for the PE-only panel: `iot-hub.device-install-pe-only-title`, `iot-hub.device-install-pe-only-message`, `iot-hub.device-install-pe-only-learn-more` | - ---- - -## Task 1: Sync `InstallMethod` enum to all 50 values - -**Files:** -- Modify: `ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts` - -- [ ] **Step 1: Replace the `InstallMethod` enum block** - -Find (lines 17–30): -```typescript -export enum InstallMethod { - DIRECT_HTTP = 'DIRECT_HTTP', - DIRECT_MQTT = 'DIRECT_MQTT', - DIRECT_COAP = 'DIRECT_COAP', - DIRECT_LWM2M = 'DIRECT_LWM2M', - DIRECT_SNMP = 'DIRECT_SNMP', - GATEWAY_MQTT = 'GATEWAY_MQTT', - GATEWAY_MODBUS = 'GATEWAY_MODBUS', - GATEWAY_OPCUA = 'GATEWAY_OPCUA', - CHIRPSTACK = 'CHIRPSTACK', - INTEGRATION_CHIRPSTACK = 'INTEGRATION_CHIRPSTACK', - INTEGRATION_TTN = 'INTEGRATION_TTN', - INTEGRATION_LORIOT = 'INTEGRATION_LORIOT' -} -``` - -Replace with (preserve grouping comments — they mirror the marketplace validator's grouping and make drift visible at review time): -```typescript -export enum InstallMethod { - // Direct device-to-platform transports - DIRECT_HTTP = 'DIRECT_HTTP', - DIRECT_MQTT = 'DIRECT_MQTT', - DIRECT_COAP = 'DIRECT_COAP', - DIRECT_LWM2M = 'DIRECT_LWM2M', - DIRECT_SNMP = 'DIRECT_SNMP', - // ThingsBoard IoT Gateway connectors - GATEWAY_MQTT = 'GATEWAY_MQTT', - GATEWAY_MODBUS = 'GATEWAY_MODBUS', - GATEWAY_OPCUA = 'GATEWAY_OPCUA', - GATEWAY_BACNET = 'GATEWAY_BACNET', - GATEWAY_BLE = 'GATEWAY_BLE', - GATEWAY_CAN = 'GATEWAY_CAN', - GATEWAY_FTP = 'GATEWAY_FTP', - GATEWAY_KNX = 'GATEWAY_KNX', - GATEWAY_OCPP = 'GATEWAY_OCPP', - GATEWAY_ODBC = 'GATEWAY_ODBC', - GATEWAY_REQUEST = 'GATEWAY_REQUEST', - GATEWAY_REST = 'GATEWAY_REST', - GATEWAY_SNMP = 'GATEWAY_SNMP', - GATEWAY_SOCKET = 'GATEWAY_SOCKET', - GATEWAY_XMPP = 'GATEWAY_XMPP', - // ChirpStack (CE-compatible LoRaWAN integration) - CHIRPSTACK = 'CHIRPSTACK', - // ThingsBoard PE integrations (CE shows "PE only" gate) - INTEGRATION_APACHE_PULSAR = 'INTEGRATION_APACHE_PULSAR', - INTEGRATION_AWS_IOT = 'INTEGRATION_AWS_IOT', - INTEGRATION_AWS_KINESIS = 'INTEGRATION_AWS_KINESIS', - INTEGRATION_AWS_SQS = 'INTEGRATION_AWS_SQS', - INTEGRATION_AZURE_EVENT_HUB = 'INTEGRATION_AZURE_EVENT_HUB', - INTEGRATION_AZURE_IOT_HUB = 'INTEGRATION_AZURE_IOT_HUB', - INTEGRATION_AZURE_SERVICE_BUS = 'INTEGRATION_AZURE_SERVICE_BUS', - INTEGRATION_CHIRPSTACK = 'INTEGRATION_CHIRPSTACK', - INTEGRATION_COAP = 'INTEGRATION_COAP', - INTEGRATION_CUSTOM = 'INTEGRATION_CUSTOM', - INTEGRATION_HTTP = 'INTEGRATION_HTTP', - INTEGRATION_IOT_CREATORS = 'INTEGRATION_IOT_CREATORS', - INTEGRATION_KAFKA = 'INTEGRATION_KAFKA', - INTEGRATION_KPN_THINGS = 'INTEGRATION_KPN_THINGS', - INTEGRATION_LORIOT = 'INTEGRATION_LORIOT', - INTEGRATION_MQTT = 'INTEGRATION_MQTT', - INTEGRATION_OPC_UA = 'INTEGRATION_OPC_UA', - INTEGRATION_PARTICLE = 'INTEGRATION_PARTICLE', - INTEGRATION_PUB_SUB = 'INTEGRATION_PUB_SUB', - INTEGRATION_RABBITMQ = 'INTEGRATION_RABBITMQ', - INTEGRATION_REMOTE = 'INTEGRATION_REMOTE', - INTEGRATION_SIGFOX = 'INTEGRATION_SIGFOX', - INTEGRATION_TCP = 'INTEGRATION_TCP', - INTEGRATION_THINGPARK = 'INTEGRATION_THINGPARK', - INTEGRATION_THINGPARK_ENTERPRISE = 'INTEGRATION_THINGPARK_ENTERPRISE', - INTEGRATION_TTI = 'INTEGRATION_TTI', - INTEGRATION_TTN = 'INTEGRATION_TTN', - INTEGRATION_TUYA = 'INTEGRATION_TUYA', - INTEGRATION_UDP = 'INTEGRATION_UDP' -} -``` - -- [ ] **Step 2: Verify enum count is 50** - -Run: -```bash -grep -c '^ [A-Z][A-Z_]* = ' /home/ashvayka/git/ce/ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts -``` -Expected: `50` - -- [ ] **Step 3: Verify CE values are a subset of marketplace `VALID_INSTALL_METHODS`** - -Run: -```bash -diff <(grep -oP "(?<=^ )[A-Z][A-Z_]+(?= = )" /home/ashvayka/git/ce/ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts | sort -u) \ - <(grep -oP '"[A-Z][A-Z_]+"' /home/ashvayka/git/iot-hub/dao/src/main/java/org/thingsboard/iothub/dao/service/impl/ItemDataServiceImpl.java | sed 's/^"//; s/"$//' | head -50 | sort -u) -``` -Expected: no output (sets identical). - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts -git commit -m "feat(iot-hub): expand InstallMethod enum to marketplace's 50-value allow-list" -``` - ---- - -## Task 2: Sync `installMethodLabels` map - -**Files:** -- Modify: `ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts` - -- [ ] **Step 1: Replace the `installMethodLabels` map** - -Find (lines 32–47, after Task 1 the line numbers shift — locate by the `export const installMethodLabels = new Map(` declaration): -```typescript -export const installMethodLabels = new Map( - [ - [InstallMethod.DIRECT_HTTP, 'HTTP'], - [InstallMethod.DIRECT_MQTT, 'MQTT'], - [InstallMethod.DIRECT_COAP, 'CoAP'], - [InstallMethod.DIRECT_LWM2M, 'LwM2M'], - [InstallMethod.DIRECT_SNMP, 'SNMP'], - [InstallMethod.GATEWAY_MQTT, 'MQTT Gateway'], - [InstallMethod.GATEWAY_MODBUS, 'Modbus Gateway'], - [InstallMethod.GATEWAY_OPCUA, 'OPC-UA Gateway'], - [InstallMethod.CHIRPSTACK, 'ChirpStack'], - [InstallMethod.INTEGRATION_CHIRPSTACK, 'ChirpStack (PE)'], - [InstallMethod.INTEGRATION_TTN, 'The Things Stack'], - [InstallMethod.INTEGRATION_LORIOT, 'LORIOT'] - ] -); -``` - -Replace with (labels mirror the brief's table and the "Description" column of the marketplace contributor doc; gateway labels keep the existing "X Gateway" pattern; integration labels keep the brand name without a "(PE)" suffix because the dialog's PE-only panel already conveys that): -```typescript -export const installMethodLabels = new Map( - [ - // Direct - [InstallMethod.DIRECT_HTTP, 'HTTP'], - [InstallMethod.DIRECT_MQTT, 'MQTT'], - [InstallMethod.DIRECT_COAP, 'CoAP'], - [InstallMethod.DIRECT_LWM2M, 'LwM2M'], - [InstallMethod.DIRECT_SNMP, 'SNMP'], - // Gateway connectors - [InstallMethod.GATEWAY_MQTT, 'MQTT Gateway'], - [InstallMethod.GATEWAY_MODBUS, 'Modbus Gateway'], - [InstallMethod.GATEWAY_OPCUA, 'OPC-UA Gateway'], - [InstallMethod.GATEWAY_BACNET, 'BACnet Gateway'], - [InstallMethod.GATEWAY_BLE, 'BLE Gateway'], - [InstallMethod.GATEWAY_CAN, 'CAN Gateway'], - [InstallMethod.GATEWAY_FTP, 'FTP Gateway'], - [InstallMethod.GATEWAY_KNX, 'KNX Gateway'], - [InstallMethod.GATEWAY_OCPP, 'OCPP Gateway'], - [InstallMethod.GATEWAY_ODBC, 'ODBC Gateway'], - [InstallMethod.GATEWAY_REQUEST, 'Request Gateway'], - [InstallMethod.GATEWAY_REST, 'REST Gateway'], - [InstallMethod.GATEWAY_SNMP, 'SNMP Gateway'], - [InstallMethod.GATEWAY_SOCKET, 'Socket Gateway'], - [InstallMethod.GATEWAY_XMPP, 'XMPP Gateway'], - // ChirpStack - [InstallMethod.CHIRPSTACK, 'ChirpStack'], - // PE integrations - [InstallMethod.INTEGRATION_APACHE_PULSAR, 'Apache Pulsar'], - [InstallMethod.INTEGRATION_AWS_IOT, 'AWS IoT'], - [InstallMethod.INTEGRATION_AWS_KINESIS, 'AWS Kinesis'], - [InstallMethod.INTEGRATION_AWS_SQS, 'AWS SQS'], - [InstallMethod.INTEGRATION_AZURE_EVENT_HUB, 'Azure Event Hub'], - [InstallMethod.INTEGRATION_AZURE_IOT_HUB, 'Azure IoT Hub'], - [InstallMethod.INTEGRATION_AZURE_SERVICE_BUS, 'Azure Service Bus'], - [InstallMethod.INTEGRATION_CHIRPSTACK, 'ChirpStack (Integration)'], - [InstallMethod.INTEGRATION_COAP, 'CoAP Integration'], - [InstallMethod.INTEGRATION_CUSTOM, 'Custom Integration'], - [InstallMethod.INTEGRATION_HTTP, 'HTTP Integration'], - [InstallMethod.INTEGRATION_IOT_CREATORS, 'IoT Creators'], - [InstallMethod.INTEGRATION_KAFKA, 'Apache Kafka'], - [InstallMethod.INTEGRATION_KPN_THINGS, 'KPN Things'], - [InstallMethod.INTEGRATION_LORIOT, 'LORIOT'], - [InstallMethod.INTEGRATION_MQTT, 'MQTT Integration'], - [InstallMethod.INTEGRATION_OPC_UA, 'OPC-UA Integration'], - [InstallMethod.INTEGRATION_PARTICLE, 'Particle'], - [InstallMethod.INTEGRATION_PUB_SUB, 'Google Pub/Sub'], - [InstallMethod.INTEGRATION_RABBITMQ, 'RabbitMQ'], - [InstallMethod.INTEGRATION_REMOTE, 'Remote Integration'], - [InstallMethod.INTEGRATION_SIGFOX, 'Sigfox'], - [InstallMethod.INTEGRATION_TCP, 'TCP Integration'], - [InstallMethod.INTEGRATION_THINGPARK, 'ThingPark Wireless'], - [InstallMethod.INTEGRATION_THINGPARK_ENTERPRISE, 'ThingPark Enterprise'], - [InstallMethod.INTEGRATION_TTI, 'The Things Industries'], - [InstallMethod.INTEGRATION_TTN, 'The Things Stack'], - [InstallMethod.INTEGRATION_TUYA, 'Tuya'], - [InstallMethod.INTEGRATION_UDP, 'UDP Integration'] - ] -); -``` - -- [ ] **Step 2: Verify label count matches enum count** - -Run: -```bash -node -e " -const src = require('fs').readFileSync('/home/ashvayka/git/ce/ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts', 'utf8'); -const enumMatches = (src.match(/^ [A-Z][A-Z_]* = '/gm) || []).length; -const labelMatches = (src.match(/\[InstallMethod\./g) || []).length; -console.log('enum:', enumMatches, 'labels:', labelMatches); -process.exit(enumMatches === 50 && labelMatches === 50 ? 0 : 1); -" -``` -Expected: `enum: 50 labels: 50` and exit code 0. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts -git commit -m "feat(iot-hub): add human-readable labels for all 50 install methods" -``` - ---- - -## Task 3: Add `peOnlyInstallMethods` set - -**Files:** -- Modify: `ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts` - -- [ ] **Step 1: Append the PE-only set after `installMethodLabels`** - -After the `installMethodLabels` declaration (and the closing `);`), insert a new exported set. Use the actual enum members so a typo causes a compile error: - -```typescript - -export const peOnlyInstallMethods: ReadonlySet = new Set([ - InstallMethod.INTEGRATION_APACHE_PULSAR, - InstallMethod.INTEGRATION_AWS_IOT, - InstallMethod.INTEGRATION_AWS_KINESIS, - InstallMethod.INTEGRATION_AWS_SQS, - InstallMethod.INTEGRATION_AZURE_EVENT_HUB, - InstallMethod.INTEGRATION_AZURE_IOT_HUB, - InstallMethod.INTEGRATION_AZURE_SERVICE_BUS, - InstallMethod.INTEGRATION_CHIRPSTACK, - InstallMethod.INTEGRATION_COAP, - InstallMethod.INTEGRATION_CUSTOM, - InstallMethod.INTEGRATION_HTTP, - InstallMethod.INTEGRATION_IOT_CREATORS, - InstallMethod.INTEGRATION_KAFKA, - InstallMethod.INTEGRATION_KPN_THINGS, - InstallMethod.INTEGRATION_LORIOT, - InstallMethod.INTEGRATION_MQTT, - InstallMethod.INTEGRATION_OPC_UA, - InstallMethod.INTEGRATION_PARTICLE, - InstallMethod.INTEGRATION_PUB_SUB, - InstallMethod.INTEGRATION_RABBITMQ, - InstallMethod.INTEGRATION_REMOTE, - InstallMethod.INTEGRATION_SIGFOX, - InstallMethod.INTEGRATION_TCP, - InstallMethod.INTEGRATION_THINGPARK, - InstallMethod.INTEGRATION_THINGPARK_ENTERPRISE, - InstallMethod.INTEGRATION_TTI, - InstallMethod.INTEGRATION_TTN, - InstallMethod.INTEGRATION_TUYA, - InstallMethod.INTEGRATION_UDP -]); -``` - -Note: this set has 29 entries (every `INTEGRATION_*`). `CHIRPSTACK` (no `INTEGRATION_` prefix) is NOT in this set — it's the CE-compatible LoRaWAN install method. - -- [ ] **Step 2: Verify TypeScript compiles** - -Run: -```bash -cd /home/ashvayka/git/ce/ui-ngx && npx tsc --noEmit -p tsconfig.app.json 2>&1 | tail -10 -``` -Expected: no errors mentioning `device-package.models.ts`. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts -git commit -m "feat(iot-hub): add peOnlyInstallMethods set covering 29 PE-only integrations" -``` - ---- - -## Task 4: Add `peOnlySelected` state to install dialog component - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts` - -- [ ] **Step 1: Import `peOnlyInstallMethods`** - -Locate the existing import block (lines 36–47) that destructures from `@shared/models/iot-hub/device-package.models`: - -```typescript -import { - installMethodLabels as INSTALL_METHOD_LABELS, - DeviceInstallStep, - DevicePackageInfo, - ENTITY_STEP_TYPES, - EntityStepOutput, - EntityStepProgress, - FormFieldDefinition, - FormFieldType, - InstallStepType, - stepTypeAliasMap -} from '@shared/models/iot-hub/device-package.models'; -``` - -Replace the destructure list to add `peOnlyInstallMethods`: - -```typescript -import { - installMethodLabels as INSTALL_METHOD_LABELS, - peOnlyInstallMethods, - DeviceInstallStep, - DevicePackageInfo, - ENTITY_STEP_TYPES, - EntityStepOutput, - EntityStepProgress, - FormFieldDefinition, - FormFieldType, - InstallStepType, - stepTypeAliasMap -} from '@shared/models/iot-hub/device-package.models'; -``` - -- [ ] **Step 2: Add `peOnlySelected` view-state flag and getter** - -Locate the connectivity-state block in the class (around lines 92–96): - -```typescript - // Connectivity - showConnectivitySelector = false; - availableInstallMethods: string[] = []; - selectedInstallMethod: string | null = null; - installMethodLabels = INSTALL_METHOD_LABELS; -``` - -Replace with: - -```typescript - // Connectivity - showConnectivitySelector = false; - showPeOnlyPanel = false; - availableInstallMethods: string[] = []; - selectedInstallMethod: string | null = null; - installMethodLabels = INSTALL_METHOD_LABELS; - - get isSelectedPeOnly(): boolean { - return this.selectedInstallMethod !== null && peOnlyInstallMethods.has(this.selectedInstallMethod); - } -``` - -- [ ] **Step 3: Route auto-selected single install method to PE-only panel when applicable** - -Locate the `ngOnInit` branching block (around lines 172–178): - -```typescript - } else if (this.availableInstallMethods.length === 1) { - this.selectedInstallMethod = this.availableInstallMethods[0]; - this.showConnectivitySelector = false; - this.startWizard(); - } else { - this.showConnectivitySelector = true; - } -``` - -Replace with: - -```typescript - } else if (this.availableInstallMethods.length === 1) { - this.selectedInstallMethod = this.availableInstallMethods[0]; - this.showConnectivitySelector = false; - if (this.isSelectedPeOnly) { - this.showPeOnlyPanel = true; - } else { - this.startWizard(); - } - } else { - this.showConnectivitySelector = true; - } -``` - -- [ ] **Step 4: Route `confirmConnectivity()` to PE-only panel when user picks a PE-only method** - -Locate `confirmConnectivity()` at line 200: - -```typescript - confirmConnectivity(): void { - if (!this.selectedInstallMethod) { - return; - } - this.startWizard(); - } -``` - -Replace with: - -```typescript - confirmConnectivity(): void { - if (!this.selectedInstallMethod) { - return; - } - if (this.isSelectedPeOnly) { - this.showPeOnlyPanel = true; - this.cdr.detectChanges(); - return; - } - this.startWizard(); - } -``` - -The connectivity selector branch is already hidden once `showPeOnlyPanel` flips true — see the template change in Task 5 — so no separate `showConnectivitySelector` flip is needed. - -- [ ] **Step 5: Verify build** - -Run: -```bash -cd /home/ashvayka/git/ce/ui-ngx && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E 'device-install-dialog|device-package' | head -10 -``` -Expected: no output (file compiles). - -- [ ] **Step 6: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts -git commit -m "feat(iot-hub): gate PE-only install methods with dedicated panel state" -``` - ---- - -## Task 5: Render the PE-only panel in the install dialog template - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html` - -- [ ] **Step 1: Insert the PE-only panel after the connectivity selector block** - -Locate the connectivity selector block (lines 213–238 — starts with `@if (!wizardStarted) {` and contains ``). The structure is: - -```html - @if (!wizardStarted) { - -
- ... -
- - ... - - - } @else if (reviewMode) { -``` - -The `@if (!wizardStarted)` branch must be tightened so the connectivity selector only renders when `!showPeOnlyPanel`, and a new branch must render the PE-only panel. - -Replace the `@if (!wizardStarted) { ... }` outer block (the entire connectivity-selector branch including the `` content and the closing ``) with: - -```html - @if (!wizardStarted && !showPeOnlyPanel) { - -
-
-

{{ 'iot-hub.device-install-select-connectivity' | translate }}

-
- @for (ct of availableInstallMethods; track ct) { - - } -
-
-
- - - - - - } @else if (showPeOnlyPanel) { - -
-
- workspace_premium -

- {{ 'iot-hub.device-install-pe-only-title' | translate }} -

-

- {{ 'iot-hub.device-install-pe-only-message' | translate:{ method: installMethodLabels.get(selectedInstallMethod) || selectedInstallMethod } }} -

- - {{ 'iot-hub.device-install-pe-only-learn-more' | translate }} - -
-
- - - - - } @else if (reviewMode) { -``` - -(The `@else if (reviewMode) {` line was already there — just leave it as the start of the next branch.) - -- [ ] **Step 2: Verify Angular template compiles** - -Run: -```bash -cd /home/ashvayka/git/ce/ui-ngx && npx ng build --configuration=production 2>&1 | tail -8 -``` -Expected: ends with `Application bundle generation complete.` and no template errors. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html -git commit -m "feat(iot-hub): render PE-only panel when user selects a PE-only install method" -``` - ---- - -## Task 6: Add translation keys for the PE-only panel - -**Files:** -- Modify: `ui-ngx/src/assets/locale/locale.constant-en_US.json` - -- [ ] **Step 1: Locate the `iot-hub` section's `device-install-*` keys** - -Run: -```bash -grep -n 'device-install-select-connectivity\|device-install-title' /home/ashvayka/git/ce/ui-ngx/src/assets/locale/locale.constant-en_US.json -``` -This identifies the lines where the existing `device-install-*` keys live so the new keys can be added alongside them. - -- [ ] **Step 2: Add the three new keys** - -Add these three key/value pairs immediately after the existing `device-install-select-connectivity` key, preserving the surrounding JSON structure (commas, indentation): - -```json -"device-install-pe-only-title": "ThingsBoard PE required", -"device-install-pe-only-message": "The {{method}} install method requires ThingsBoard Professional Edition. Upgrade your installation to use this device package.", -"device-install-pe-only-learn-more": "Learn about ThingsBoard PE", -``` - -`{{method}}` is the MessageFormat parameter passed from the template (`installMethodLabels.get(selectedInstallMethod)`). - -- [ ] **Step 3: Verify JSON is valid** - -Run: -```bash -node -e "JSON.parse(require('fs').readFileSync('/home/ashvayka/git/ce/ui-ngx/src/assets/locale/locale.constant-en_US.json','utf8')); console.log('OK');" -``` -Expected: `OK`. - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/assets/locale/locale.constant-en_US.json -git commit -m "feat(iot-hub): add translation keys for PE-only install panel" -``` - ---- - -## Task 7: Manual smoke verification - -**Files:** none (verification only) - -- [ ] **Step 1: Production build sanity check** - -Run: -```bash -cd /home/ashvayka/git/ce/ui-ngx && npx ng build --configuration=production 2>&1 | tail -3 -``` -Expected: ends with `Application bundle generation complete.` - -- [ ] **Step 2: Verify CE enum is a strict superset of the previous 12-value enum** - -Run: -```bash -for c in DIRECT_HTTP DIRECT_MQTT DIRECT_COAP DIRECT_LWM2M DIRECT_SNMP \ - GATEWAY_MQTT GATEWAY_MODBUS GATEWAY_OPCUA \ - CHIRPSTACK INTEGRATION_CHIRPSTACK INTEGRATION_TTN INTEGRATION_LORIOT; do - grep -q "${c} = '${c}'" /home/ashvayka/git/ce/ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts \ - && echo "OK: $c" || echo "MISSING: $c" -done -``` -Expected: 12 lines starting with `OK:` and zero `MISSING:` lines. - -- [ ] **Step 3: Verify all 50 marketplace constants are present** - -Run: -```bash -for c in DIRECT_HTTP DIRECT_MQTT DIRECT_COAP DIRECT_LWM2M DIRECT_SNMP \ - GATEWAY_MQTT GATEWAY_MODBUS GATEWAY_OPCUA \ - GATEWAY_BACNET GATEWAY_BLE GATEWAY_CAN GATEWAY_FTP GATEWAY_KNX \ - GATEWAY_OCPP GATEWAY_ODBC GATEWAY_REQUEST GATEWAY_REST \ - GATEWAY_SNMP GATEWAY_SOCKET GATEWAY_XMPP \ - CHIRPSTACK \ - INTEGRATION_APACHE_PULSAR INTEGRATION_AWS_IOT INTEGRATION_AWS_KINESIS \ - INTEGRATION_AWS_SQS INTEGRATION_AZURE_EVENT_HUB INTEGRATION_AZURE_IOT_HUB \ - INTEGRATION_AZURE_SERVICE_BUS INTEGRATION_CHIRPSTACK INTEGRATION_COAP \ - INTEGRATION_CUSTOM INTEGRATION_HTTP INTEGRATION_IOT_CREATORS \ - INTEGRATION_KAFKA INTEGRATION_KPN_THINGS INTEGRATION_LORIOT \ - INTEGRATION_MQTT INTEGRATION_OPC_UA INTEGRATION_PARTICLE \ - INTEGRATION_PUB_SUB INTEGRATION_RABBITMQ INTEGRATION_REMOTE \ - INTEGRATION_SIGFOX INTEGRATION_TCP INTEGRATION_THINGPARK \ - INTEGRATION_THINGPARK_ENTERPRISE INTEGRATION_TTI INTEGRATION_TTN \ - INTEGRATION_TUYA INTEGRATION_UDP; do - grep -q "${c} = '${c}'" /home/ashvayka/git/ce/ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts \ - && grep -q "InstallMethod.${c}, '" /home/ashvayka/git/ce/ui-ngx/src/app/shared/models/iot-hub/device-package.models.ts \ - || echo "INCOMPLETE: $c" -done -``` -Expected: no output (every constant has both an enum entry and a label). - -- [ ] **Step 4: Browser smoke test (manual)** - -1. Start dev server: `cd /home/ashvayka/git/ce/ui-ngx && npm start` -2. Log in as a TENANT_ADMIN. Open IoT Hub → Browse. -3. Pick any device package that lists multiple install methods. Open install dialog. -4. **Direct/Gateway/CHIRPSTACK methods:** label renders correctly (no raw constant), Next button starts the wizard normally — no regression. -5. **PE-only method (e.g., `INTEGRATION_AWS_IOT` if the package supports it; otherwise temporarily edit any local test package's `device-info.json` to add a PE-only method):** Next button shows the PE-only panel, the title says "ThingsBoard PE required", the body interpolates the method label (e.g., "The AWS IoT install method requires…"), the "Learn about ThingsBoard PE" link opens `thingsboard.io/products/thingsboard-pe/` in a new tab, the Close button dismisses the dialog. -6. **Auto-selected single PE-only method:** if a package lists only one method and it's PE-only, the dialog should open directly on the PE-only panel (no connectivity selector). - -If any check fails, mark this task incomplete and address before merging. - ---- - -## Out of scope - -- **No backend allow-list to update.** CE's `DeviceInstalledItemDescriptor.selectedInstallMethod` is a free-form `String` field; the marketplace validator at `ItemDataServiceImpl.VALID_INSTALL_METHODS` is the single source of truth. -- **No new install behavior for the 12 new gateway connectors.** The existing `GATEWAY_CONNECTOR` step (`device-install-dialog.component.ts:761`) writes any connector config object to the gateway's `active_connectors` shared attribute regardless of connector type. Connector-specific schemas (BACnet, BLE, KNX, etc.) are creator concerns, not wizard concerns — the creator's connector JSON config is opaque to the wizard. -- **No connector-specific `${gateway.*}` variable additions.** Existing gateway outputs (`${gateway.id}`, `${gateway.name}`, `${gateway.token}`, `${gateway.dockerComposeUrl}`) are sufficient. Per-connector values like ports or hostnames are already supplied via `SHOW_FORM` fields and resolved through the standard variable mechanism. -- **No CE PE→install conversion.** PE integrations stay PE-only; CE shows the gate. -- **No automated unit test parity.** `ui-ngx` has no spec runner configured (no `*.spec.ts` files outside `node_modules`). The bash verifications in Task 7 act as the parity check. diff --git a/docs/superpowers/plans/2026-05-06-iot-hub-item-link.md b/docs/superpowers/plans/2026-05-06-iot-hub-item-link.md deleted file mode 100644 index b3fb150346..0000000000 --- a/docs/superpowers/plans/2026-05-06-iot-hub-item-link.md +++ /dev/null @@ -1,974 +0,0 @@ -# IoT Hub `${item-link:uuid}` Markdown Component — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a `${item-link:}` markdown placeholder that renders an IoT Hub marketplace item card (thumbnail + name + creator) inline in readmes and install instructions, opening the linked item in a new tab. - -**Architecture:** A pure-string utility rewrites `${item-link:}` to a `` Angular component tag before the markdown reaches `tb-markdown`. The component is declared in a small `IotHubItemLinkModule` that each `tb-markdown` instance receives as `additionalCompileModules`. The component owns its own fetch (via `IotHubApiService.getPublishedVersion`), state (`loading` / `loaded` / `unavailable`), and click target (`/iot-hub/{itemId}` with `target="_blank"`). - -**Tech Stack:** Angular 20 (TypeScript, Material), `tb-markdown` (NgModule-based dynamic compilation), `IotHubApiService` (existing), Tailwind for utility classes, component-scoped SCSS. - -**Spec:** `docs/superpowers/specs/2026-05-06-iot-hub-item-link.md` - -**File structure:** - -New files: -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts` — `replaceItemLinkPlaceholders` + regex -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link.module.ts` - -Modified files: -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts` — import `IotHubItemLinkModule` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts` — call utility in `loadReadme()`, expose compile modules -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html` — bind `[additionalCompileModules]` on readme `` -- `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts` — add `^item-link:(uuid)$` branch in `resolveVariables()`, expose compile modules -- `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html` — bind `[additionalCompileModules]` -- `ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts` — pre-process `details`, expose compile modules -- `ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html` — bind `[additionalCompileModules]` -- `ui-ngx/src/assets/locale/locale.constant-en_US.json` — add `iot-hub.item-link-unavailable` - ---- - -## Task 1: Create placeholder utility - -**Files:** -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts` - -- [ ] **Step 1: Create the utility file** - -Create `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts`: - -```typescript -/// -/// 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. -/// - -export const ITEM_LINK_PLACEHOLDER_REGEX = - /\$\{item-link:([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\}/g; - -export const ITEM_LINK_KEY_REGEX = - /^item-link:([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/; - -export function itemLinkCardTag(itemId: string): string { - return ``; -} - -export function replaceItemLinkPlaceholders(markdown: string): string { - if (!markdown) { - return markdown; - } - return markdown.replace(ITEM_LINK_PLACEHOLDER_REGEX, (_match, uuid) => itemLinkCardTag(uuid)); -} -``` - -- [ ] **Step 2: Sanity-check the regex** - -Write a temporary script `/tmp/check-item-link-regex.js` with this exact content: - -```javascript -const re = /\$\{item-link:([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\}/g; -const cases = [ - ['${item-link:11111111-2222-3333-4444-555555555555}', 'valid'], - ['${item-link:not-a-uuid}', 'invalid'], - ['Two: ${item-link:11111111-2222-3333-4444-555555555555} and ${item-link:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}', 'two valid'], - ['```\n${item-link:11111111-2222-3333-4444-555555555555}\n```', 'inside code fence (still replaced — documented behavior)'] -]; -for (const [input, label] of cases) { - console.log(label + ':', input.replace(re, (_m, u) => '')); -} -``` - -Run it: -```bash -node /tmp/check-item-link-regex.js -``` - -Expected output: -``` -valid: -invalid: ${item-link:not-a-uuid} -two valid: Two: and -inside code fence (still replaced — documented behavior): ``` - -``` -``` - -Then delete the script: `rm /tmp/check-item-link-regex.js`. - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts -git commit -m "feat(iot-hub): add item-link placeholder utility for markdown" -``` - ---- - -## Task 2: Create item-link card component (skeleton + loaded states, image-thumbnail items) - -**Files:** -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts` -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html` -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss` - -- [ ] **Step 1: Create the directory** - -```bash -mkdir -p ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card -``` - -- [ ] **Step 2: Create `iot-hub-item-link-card.component.ts`** - -```typescript -/// -/// 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. -/// - -import { Component, Input, OnInit } from '@angular/core'; -import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; - -type CardState = 'loading' | 'loaded' | 'unavailable'; - -const COMPACT_TYPES: ReadonlySet = new Set([ - ItemType.CALCULATED_FIELD, - ItemType.ALARM_RULE, - ItemType.RULE_CHAIN -]); - -@Component({ - selector: 'tb-iot-hub-item-link-card', - standalone: false, - templateUrl: './iot-hub-item-link-card.component.html', - styleUrls: ['./iot-hub-item-link-card.component.scss'] -}) -export class TbIotHubItemLinkCardComponent implements OnInit { - - @Input() itemId!: string; - - state: CardState = 'loading'; - item: MpItemVersionView | null = null; - - constructor(private iotHubApiService: IotHubApiService) {} - - ngOnInit(): void { - if (!this.itemId) { - this.state = 'unavailable'; - return; - } - this.iotHubApiService - .getPublishedVersion(this.itemId, { ignoreErrors: true, ignoreLoading: true }) - .subscribe({ - next: item => { - this.item = item; - this.state = item ? 'loaded' : 'unavailable'; - }, - error: () => { - this.state = 'unavailable'; - } - }); - } - - isCompact(): boolean { - return !!this.item && COMPACT_TYPES.has(this.item.type); - } - - getImageUrl(): string | null { - return this.item?.image - ? this.iotHubApiService.resolveResourceUrl(this.item.image) - : null; - } - - getCompactIcon(): string { - if (!this.item) { - return 'category'; - } - if (this.item.icon) { - return this.item.icon; - } - switch (this.item.type) { - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'account_tree'; - default: return 'category'; - } - } - - getTypeIcon(): string { - if (!this.item) { - return 'category'; - } - switch (this.item.type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'account_tree'; - case ItemType.DEVICE: return 'memory'; - default: return 'category'; - } - } - - getCompactColor(): string { - return this.item?.color || '#048ad3'; - } - - getHref(): string { - return `/iot-hub/${this.itemId}`; - } -} -``` - -- [ ] **Step 3: Create `iot-hub-item-link-card.component.html`** - -```html - -@switch (state) { - @case ('loading') { - - } - @case ('loaded') { -
- @if (isCompact()) { - - } @else { - - } - - - } - @case ('unavailable') { - - } -} -``` - -- [ ] **Step 4: Create `iot-hub-item-link-card.component.scss`** - -```scss -/** - * 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. - */ - -:host { - display: block; - margin: 12px 0; -} - -.tb-iot-hub-item-link-card { - display: inline-flex; - align-items: center; - gap: 12px; - width: 320px; - max-width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 8px; - background: #fff; - text-decoration: none; - color: inherit; - transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease; - - &:hover:not(.tb-iot-hub-item-link-card--unavailable):not(.tb-iot-hub-item-link-card--skeleton) { - background: rgba(4, 138, 211, 0.04); - border-color: rgba(4, 138, 211, 0.32); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); - } - - &--unavailable { - opacity: 0.6; - cursor: default; - } - - &--skeleton { - cursor: default; - } -} - -.tb-iot-hub-item-link-thumb { - flex: 0 0 48px; - width: 48px; - height: 48px; - border-radius: 6px; - background: #f4f6f8; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - - img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - } - - &--compact { - color: #fff; - } - - &--unavailable { - color: rgba(0, 0, 0, 0.45); - } -} - -.tb-iot-hub-item-link-thumb-icon { - font-size: 26px; - width: 26px; - height: 26px; - line-height: 26px; - color: #fff; -} - -.tb-iot-hub-item-link-thumb-fallback { - font-size: 26px; - width: 26px; - height: 26px; - color: rgba(0, 0, 0, 0.45); -} - -.tb-iot-hub-item-link-text { - display: flex; - flex-direction: column; - min-width: 0; - gap: 2px; -} - -.tb-iot-hub-item-link-name { - font-size: 14px; - font-weight: 500; - line-height: 1.3; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.tb-iot-hub-item-link-author { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 12px; - color: rgba(0, 0, 0, 0.6); - line-height: 1.3; - - mat-icon { - font-size: 14px; - width: 14px; - height: 14px; - } -} - -.tb-iot-hub-item-link-skeleton-block, -.tb-iot-hub-item-link-skeleton-line { - background: linear-gradient( - 90deg, - rgba(0, 0, 0, 0.06) 0%, - rgba(0, 0, 0, 0.12) 50%, - rgba(0, 0, 0, 0.06) 100% - ); - background-size: 200% 100%; - animation: tb-iot-hub-item-link-shimmer 1.4s ease-in-out infinite; - border-radius: 4px; -} - -.tb-iot-hub-item-link-skeleton-line { - display: block; - height: 12px; - width: 180px; - margin: 4px 0; - - &--short { - width: 96px; - } -} - -@keyframes tb-iot-hub-item-link-shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} -``` - -- [ ] **Step 5: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card -git commit -m "feat(iot-hub): add TbIotHubItemLinkCardComponent for markdown item links" -``` - ---- - -## Task 3: Create the wrapper module and register it - -**Files:** -- Create: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link.module.ts` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts` - -- [ ] **Step 1: Create `iot-hub-item-link.module.ts`** - -```typescript -/// -/// 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. -/// - -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '@shared/shared.module'; -import { TbIotHubItemLinkCardComponent } from './iot-hub-item-link-card.component'; - -@NgModule({ - declarations: [TbIotHubItemLinkCardComponent], - imports: [CommonModule, SharedModule], - exports: [TbIotHubItemLinkCardComponent] -}) -export class IotHubItemLinkModule {} -``` - -- [ ] **Step 2: Register module in `IotHubComponentsModule`** - -Modify `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts` — add the import and add `IotHubItemLinkModule` to the `imports` array. - -After the existing imports near line 32, add: - -```typescript -import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link.module'; -``` - -Then update the `imports` array of the `@NgModule` decorator from: - -```typescript - imports: [ - CommonModule, - SharedModule - ], -``` - -to: - -```typescript - imports: [ - CommonModule, - SharedModule, - IotHubItemLinkModule - ], -``` - -- [ ] **Step 3: Build to verify the module wires up** - -```bash -cd ui-ngx && npx ng build --configuration=development 2>&1 | tail -40 -``` - -Expected: build succeeds (warnings allowed; no errors mentioning `TbIotHubItemLinkCardComponent` or `IotHubItemLinkModule`). - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link.module.ts \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts -git commit -m "feat(iot-hub): wrap item-link card in IotHubItemLinkModule" -``` - ---- - -## Task 4: Add translation key - -**Files:** -- Modify: `ui-ngx/src/assets/locale/locale.constant-en_US.json` - -- [ ] **Step 1: Add the translation key** - -Open `ui-ngx/src/assets/locale/locale.constant-en_US.json`. Locate the `"iot-hub": { ... }` block that begins around line 3701 (the one that opens with `"iot-hub": "IoT Hub",`). Add a new entry next to similar one-liner keys (e.g., right after `"installed-from-iot-hub": "Installed from IoT Hub",` near line 3719): - -```json - "item-link-unavailable": "Item unavailable", -``` - -Make sure the surrounding commas are correct — the new line ends with a comma, and the line above also ends with a comma. - -- [ ] **Step 2: Verify JSON is valid** - -```bash -node -e "JSON.parse(require('fs').readFileSync('ui-ngx/src/assets/locale/locale.constant-en_US.json', 'utf8')); console.log('OK');" -``` -Expected output: `OK` - -- [ ] **Step 3: Commit** - -```bash -git add ui-ngx/src/assets/locale/locale.constant-en_US.json -git commit -m "feat(iot-hub): add item-link-unavailable translation key" -``` - ---- - -## Task 5: Wire into item detail dialog (readme) - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html` - -- [ ] **Step 1: Update the .ts file to pre-process readme and expose compile modules** - -In `iot-hub-item-detail-dialog.component.ts`: - -Add to the imports near the existing `resolveDocLinkPlaceholders` import: - -```typescript -import { replaceItemLinkPlaceholders } from './iot-hub-markdown.utils'; -import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link.module'; -import { Type } from '@angular/core'; -``` - -(`Type` may already be imported via Angular core; merge into the existing `@angular/core` import if so.) - -Inside the class, near the other readonly fields (after `readonly ItemType = ItemType;`), add: - -```typescript - readonly itemLinkCompileModules: Type[] = [IotHubItemLinkModule]; -``` - -Update the existing `loadReadme()` method body (currently at line 307-312): - -```typescript - private loadReadme(): void { - const versionId = this.item.id as string; - this.iotHubApiService.getVersionReadme(versionId, { ignoreLoading: true }).subscribe( - content => this.readmeContent = replaceItemLinkPlaceholders( - this.resolveDocLinks(this.prefixResourceUrls(content || '')) - ) - ); - } -``` - -- [ ] **Step 2: Update the template to pass compile modules** - -In `iot-hub-item-detail-dialog.component.html` at line 302, change: - -```html - -``` - -to: - -```html - -``` - -Leave the changelog `` (line 306) unchanged — changelog is out of scope per the spec. - -- [ ] **Step 3: Build** - -```bash -cd ui-ngx && npx ng build --configuration=development 2>&1 | tail -20 -``` - -Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts \ - ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html -git commit -m "feat(iot-hub): render \${item-link} cards in item readme" -``` - ---- - -## Task 6: Wire into device install dialog (instructions) - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts` -- Modify: `ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html` - -- [ ] **Step 1: Add imports and compile-modules field** - -In `device-install-dialog.component.ts`, add to the imports: - -```typescript -import { IotHubItemLinkModule } from '../iot-hub-item-link-card/iot-hub-item-link.module'; -import { ITEM_LINK_KEY_REGEX, itemLinkCardTag } from '../iot-hub-markdown.utils'; -import { Type } from '@angular/core'; -``` - -(Merge `Type` into the existing `@angular/core` import line.) - -Inside the class, alongside other readonly fields, add: - -```typescript - readonly itemLinkCompileModules: Type[] = [IotHubItemLinkModule]; -``` - -- [ ] **Step 2: Add `^item-link:(uuid)$` branch inside `resolveVariables`** - -In `resolveVariables()` (currently at line 427-477 in the same file), add a new clause **immediately after** the `gateway.downloadButton` clause (around line 444-446) and before the callout matcher. - -Change this section: - -```typescript - // Special action placeholders - if (key === 'gateway.downloadButton') { - return '⬇ Download docker-compose.yml'; - } - // Callout boxes: ${note(...)}, ${warn(...)}, ${error(...)} -``` - -to: - -```typescript - // Special action placeholders - if (key === 'gateway.downloadButton') { - return '⬇ Download docker-compose.yml'; - } - // IoT Hub item link card: ${item-link:} - const itemLinkMatch = key.match(ITEM_LINK_KEY_REGEX); - if (itemLinkMatch) { - return itemLinkCardTag(itemLinkMatch[1]); - } - // Callout boxes: ${note(...)}, ${warn(...)}, ${error(...)} -``` - -- [ ] **Step 3: Update the template** - -In `device-install-dialog.component.html` at line 26-28, change: - -```html - - -``` - -to: - -```html - - -``` - -- [ ] **Step 4: Build** - -```bash -cd ui-ngx && npx ng build --configuration=development 2>&1 | tail -20 -``` - -Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts \ - ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html -git commit -m "feat(iot-hub): render \${item-link} cards in device install instructions" -``` - ---- - -## Task 7: Wire into solution install dialog (instructions) - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts` -- Modify: `ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html` - -- [ ] **Step 1: Update the .ts file** - -Replace the contents of `ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts` with: - -```typescript -/// -/// 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. -/// - -import { Component, Inject, Type } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Router } from '@angular/router'; -import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; -import { - IotHubItemLinkModule -} from '@home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link.module'; -import { - replaceItemLinkPlaceholders -} from '@home/components/iot-hub/iot-hub-markdown.utils'; - -export interface SolutionInstallDialogData { - descriptor: SolutionTemplateInstalledItemDescriptor; - instructions?: boolean; -} - -@Component({ - selector: 'tb-solution-install-dialog', - templateUrl: './solution-install-dialog.component.html', - styleUrls: ['./solution-install-dialog.component.scss'], - standalone: false -}) -export class SolutionInstallDialogComponent { - - details: string; - dashboardId: string | null; - instructions: boolean; - - readonly itemLinkCompileModules: Type[] = [IotHubItemLinkModule]; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: SolutionInstallDialogData, - private dialogRef: MatDialogRef, - private router: Router - ) { - this.details = replaceItemLinkPlaceholders(data.descriptor.details || ''); - this.dashboardId = data.descriptor.dashboardId?.id || null; - this.instructions = !!data.instructions; - } - - gotoMainDashboard(): void { - if (this.dashboardId) { - this.dialogRef.close(); - this.router.navigateByUrl(`/dashboards/${this.dashboardId}`); - } - } - - close(): void { - this.dialogRef.close(); - } -} -``` - -- [ ] **Step 2: Update the template** - -In `solution-install-dialog.component.html` at line 29, change: - -```html - -``` - -to: - -```html - -``` - -- [ ] **Step 3: Build** - -```bash -cd ui-ngx && npx ng build --configuration=development 2>&1 | tail -20 -``` - -Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts \ - ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html -git commit -m "feat(iot-hub): render \${item-link} cards in solution install instructions" -``` - ---- - -## Task 8: Manual visual QA - -**No file changes — verify the feature end-to-end in a browser.** - -- [ ] **Step 1: Start the dev server** - -```bash -cd ui-ngx && npm start -``` - -Wait for `Compiled successfully` and `Application bundle generation complete`. - -Open `http://localhost:4200` in a browser and log in as a tenant administrator. - -- [ ] **Step 2: Verify item readme rendering** - -In a separate terminal, query the configured IoT Hub for an item ID you can use as a test target: - -```bash -# Use the same baseUrl that your TB instance uses (typically https://iot-hub.thingsboard.io). -# Pick any published item you can find via the marketplace UI's network requests in DevTools, -# e.g. by opening the IoT Hub home page and copying an itemId from a response. -echo "Pick a known itemId from network responses in the IoT Hub UI" -``` - -Then, for the TEST PASS: -1. Open IoT Hub in the running app, find an item with a non-empty readme (e.g., a Solution Template), open the detail dialog. -2. In DevTools, intercept the readme response by editing it (Network → block + replay, or temporarily edit `readmeContent` via the Angular DevTools), and inject `${item-link:}` somewhere in the readme markdown. - - Easier alternative: temporarily hardcode a test placeholder in `loadReadme()` by appending `'\n\n${item-link:}\n'` to the fetched content, just for this QA pass. Revert before committing. -3. Reload the dialog. Expected: - - Skeleton card appears briefly with shimmer. - - Card resolves to: 48-px square thumbnail (image or colored compact icon), item name, person icon + creator name. - - Hover shows light blue tint and subtle shadow. - - Click opens `/iot-hub/` in a **new tab**, which lands on the item type page with the detail dialog open. - - Middle-click also opens new tab; ctrl-click does the same. - -Revert any hardcoded test data before continuing. - -- [ ] **Step 3: Verify unavailable state** - -Repeat Step 2 but inject a placeholder with a UUID that does not exist (e.g., `${item-link:00000000-0000-0000-0000-000000000000}`). Expected: -- Card briefly shows skeleton. -- Resolves to disabled card: dim opacity, `link_off` icon in the thumbnail slot, "Item unavailable" label. -- Card is not clickable (no ``, no hover blue). - -- [ ] **Step 4: Verify invalid placeholder is left untouched** - -Inject `${item-link:not-a-uuid}` into the readme. Expected: the rendered markdown shows the literal text `${item-link:not-a-uuid}` (no card, no error). This proves the regex is strict. - -- [ ] **Step 5: Verify device install instructions** - -Open any IoT Hub Device item, click Install, and walk to a step whose markdown is dynamically rendered. Inject `${item-link:}` into one of the markdown templates served by the device package (or temporarily prepend it to `step.markdown` in `device-install-dialog.component.ts` near the existing `resolveVariables` call). Expected: same skeleton → loaded card behavior; click opens `/iot-hub/` in a new tab. - -Revert temporary edits. - -- [ ] **Step 6: Verify solution install instructions** - -Install (or reopen the install instructions for) a Solution Template that has a `details` markdown. Inject `${item-link:}` into `details` (temporarily, in the constructor or via a known solution template whose details you control). Expected: same skeleton → loaded behavior; click opens `/iot-hub/` in a new tab. - -Revert temporary edits. - -- [ ] **Step 7: Confirm no regressions in existing markdown** - -In each of the three render sites, verify after the QA edits are reverted: -- Existing `${gateway.downloadButton}` placeholder still renders correctly in the device install instructions (unchanged behavior). -- Existing callout boxes (`${note(...)}`, `${warn(...)}`, `${error(...)}`) still render in the device install instructions. -- Existing `prefixResourceUrls` and `resolveDocLinks` continue to resolve image URLs and doc-link placeholders in readmes. -- Changelog tab in the item detail dialog still renders without an item-link card (it does not bind `additionalCompileModules`). - -- [ ] **Step 8: Stop the dev server** - -`Ctrl-C` in the terminal running `npm start`. - -- [ ] **Step 9: Final commit (if any QA-driven fix-ups were made)** - -If QA surfaced any issues that required code changes, commit them with a focused message. Otherwise this step is a no-op. - -```bash -git status -# If clean, no commit needed. -``` - ---- - -## Self-review checklist (for the implementing agent) - -Before declaring complete, verify: - -1. **Spec coverage** — every decision in `docs/superpowers/specs/2026-05-06-iot-hub-item-link.md` has a corresponding task above. -2. **No leftover test scaffolding** — temporary hardcoded `${item-link:...}` placeholders used for QA are reverted. -3. **License headers** — every new `.ts` file uses `///` style, every new `.html` uses ``, every new `.scss` uses `/** */`. -4. **Build clean** — `npx ng build --configuration=development` finishes without errors. -5. **Three sites work, fourth doesn't** — readme, device install, solution install all render the card; changelog tab does **not** (by design — out of scope per spec). diff --git a/docs/superpowers/specs/2026-04-02-gateway-support-design.md b/docs/superpowers/specs/2026-04-02-gateway-support-design.md deleted file mode 100644 index db94c4b1af..0000000000 --- a/docs/superpowers/specs/2026-04-02-gateway-support-design.md +++ /dev/null @@ -1,267 +0,0 @@ -# Gateway Support in Device Install Framework — Design Spec - -## Goal - -Extend the device install wizard to support gateway provisioning: creating a gateway device, configuring one or more connectors via shared attributes, and providing the gateway launch command (docker-compose download URL) in post-install instructions. - -## New Step Types - -### GATEWAY - -Creates a gateway device — a device with `additionalInfo: { gateway: true }`. - -- **Template:** device JSON (same format as DEVICE step), must include `"additionalInfo": {"gateway": true}` -- **After creation:** fetches device credentials (same as DEVICE) -- **Find-or-create:** no — always creates new (same as DEVICE) -- **Output variables:** - - `${gateway.id}` — device UUID - - `${gateway.name}` — device name - - `${gateway.token}` — access token - - `${gateway.dockerComposeUrl}` — `/api/device-connectivity/gateway-launch/${gateway.id}/docker-compose/download` - -### GATEWAY_CONNECTOR - -Configures a connector on a previously created gateway by saving connector config as shared attributes. - -- **Template:** connector config JSON — the object with `name`, `type`, `configurationJson`, `logLevel`, etc. -- **Behavior:** - 1. Fetch current `active_connectors` shared attribute from the gateway (may be empty/missing — default to `[]`) - 2. Append connector name to the array - 3. Save updated `active_connectors` as shared attribute on the gateway - 4. Save `{connectorName}: connectorConfig` as shared attribute on the gateway -- **Target entity:** uses `${gateway.id}` from the preceding GATEWAY step -- **Output variables:** `${gatewayConnector.name}` — the connector name from the template -- **Multiple steps:** each GATEWAY_CONNECTOR step appends to `active_connectors`. Two connectors → two steps → `active_connectors = ["Modbus Connector", "MQTT Connector"]` - -## Extension: Optional Attributes on Entity Steps - -All entity creation steps (DEVICE, GATEWAY, DEVICE_PROFILE, DASHBOARD, RULE_CHAIN) gain two optional fields in the step definition: - -```json -{ - "type": "GATEWAY", - "name": "${deviceName}", - "template": "gateway.json", - "serverAttributes": "server-attributes.json", - "sharedAttributes": "shared-attributes.json" -} -``` - -- `serverAttributes` — optional, path to JSON file in ZIP. After entity creation, file is read, variables resolved, and saved as `SERVER_SCOPE` attributes on the created entity. -- `sharedAttributes` — optional, path to JSON file in ZIP. Same, saved as `SHARED_SCOPE`. - -Both files contain a flat JSON object of key-value pairs: -```json -{ - "firmwareVersion": "1.2.3", - "configUrl": "${http.host}:${http.port}/config" -} -``` - -## Changes to Existing Models - -### DeviceInstallStep interface (device-package.models.ts) - -Add fields: -```typescript -export interface DeviceInstallStep { - type: InstallStepType; - name: string; - file?: string; - template?: string; - serverAttributes?: string; // NEW - sharedAttributes?: string; // NEW -} -``` - -### InstallStepType enum - -Add: -```typescript -GATEWAY = 'GATEWAY', -GATEWAY_CONNECTOR = 'GATEWAY_CONNECTOR' -``` - -### ENTITY_STEP_TYPES set - -Add `GATEWAY` and `GATEWAY_CONNECTOR`. - -### stepTypeAliasMap - -Add: -```typescript -GATEWAY: 'gateway', -GATEWAY_CONNECTOR: 'gatewayConnector' -``` - -## Variable Resolution Updates - -### New named entity outputs - -| Step type | Variables | -|-----------|-----------| -| GATEWAY | `${gateway.id}`, `${gateway.name}`, `${gateway.token}`, `${gateway.dockerComposeUrl}` | -| GATEWAY_CONNECTOR | `${gatewayConnector.name}` | - -### EntityStepOutput interface - -Add optional `dockerComposeUrl` field: -```typescript -export interface EntityStepOutput { - id: string; - name: string; - token?: string; - dockerComposeUrl?: string; // NEW -} -``` - -## Frontend Implementation (createEntity) - -### GATEWAY case - -Same as DEVICE: -1. Save device via `deviceService.saveDevice(template, {ignoreErrors: true})` -2. Fetch credentials via `deviceService.getDeviceCredentials(id)` -3. Return output with `dockerComposeUrl` computed from the device ID - -```typescript -case InstallStepType.GATEWAY: { - const result = await firstValueFrom(this.deviceService.saveDevice(template, {ignoreErrors: true})); - const creds = await firstValueFrom(this.deviceService.getDeviceCredentials(result.id.id, false, {ignoreErrors: true})); - return { - id: result.id.id, - name: result.name, - token: creds.credentialsId, - dockerComposeUrl: `/api/device-connectivity/gateway-launch/${result.id.id}/docker-compose/download` - }; -} -``` - -### GATEWAY_CONNECTOR case - -1. Read and resolve the connector template from ZIP -2. Extract `name` from the connector config -3. Fetch current `active_connectors` from gateway's shared attributes (or default to `[]`) -4. Append connector name -5. Save both attributes to gateway via `attributeService.saveEntityAttributes()` - -```typescript -case InstallStepType.GATEWAY_CONNECTOR: { - const gatewayOutput = this.entityOutputs.get('gateway'); - if (!gatewayOutput) throw new Error('GATEWAY step must precede GATEWAY_CONNECTOR'); - const gatewayEntityId = { entityType: 'DEVICE', id: gatewayOutput.id }; - - // Fetch current active_connectors - const attrs = await firstValueFrom(this.attributeService.getEntityAttributes( - gatewayEntityId, AttributeScope.SHARED_SCOPE, ['active_connectors'], {ignoreErrors: true} - )); - const activeConnectors: string[] = attrs.find(a => a.key === 'active_connectors')?.value || []; - - // Add this connector - const connectorName = template.name; - if (!activeConnectors.includes(connectorName)) { - activeConnectors.push(connectorName); - } - - // Save attributes - await firstValueFrom(this.attributeService.saveEntityAttributes( - gatewayEntityId, AttributeScope.SHARED_SCOPE, - [ - { key: 'active_connectors', value: activeConnectors }, - { key: connectorName, value: template } - ], - {ignoreErrors: true} - )); - - return { id: gatewayOutput.id, name: connectorName }; -} -``` - -### Attribute saving after any entity step - -After `createEntity()` returns, check if the step has `serverAttributes` or `sharedAttributes`. If so, read the file, resolve variables, and save: - -```typescript -if (step.serverAttributes) { - const attrsJson = JSON.parse(this.resolveVariables(this.zipFiles.get(step.serverAttributes))); - const attrs = Object.entries(attrsJson).map(([key, value]) => ({ key, value })); - await firstValueFrom(this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, attrs, {ignoreErrors: true})); -} -if (step.sharedAttributes) { - const attrsJson = JSON.parse(this.resolveVariables(this.zipFiles.get(step.sharedAttributes))); - const attrs = Object.entries(attrsJson).map(([key, value]) => ({ key, value })); - await firstValueFrom(this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, attrs, {ignoreErrors: true})); -} -``` - -## Translation Keys - -Add: -``` -"iot-hub.device-install-step-type-GATEWAY": "Gateway", -"iot-hub.device-install-step-type-GATEWAY_CONNECTOR": "Gateway Connector" -``` - -## Example Gateway Package - -``` -modbus-sensor.zip/ -├── device-info.json -├── prerequisites.md -├── form.json -├── gateway.json -├── modbus-connector.json -├── dashboard.json -└── post-install.md -``` - -`device-info.json`: -```json -{ - "name": "Modbus Sensor", - "vendor": "Example", - "hardwareType": "SENSOR", - "connectivityTypes": ["GATEWAY_MODBUS"], - "installSteps": { - "GATEWAY_MODBUS": [ - {"type": "SHOW_INSTRUCTION", "name": "Prerequisites", "file": "prerequisites.md"}, - {"type": "SHOW_FORM", "name": "Configuration", "file": "form.json"}, - {"type": "GATEWAY", "name": "${deviceName} Gateway", "template": "gateway.json"}, - {"type": "GATEWAY_CONNECTOR", "name": "Modbus Connector", "template": "modbus-connector.json"}, - {"type": "DASHBOARD", "name": "Modbus Monitor", "template": "dashboard.json"}, - {"type": "SHOW_INSTRUCTION", "name": "Launch Gateway", "file": "post-install.md"} - ] - } -} -``` - -`gateway.json`: -```json -{ - "name": "${deviceName} Gateway", - "type": "Gateway", - "additionalInfo": {"gateway": true} -} -``` - -`post-install.md`: -```markdown -## Launch Gateway - -1. [Download docker-compose.yml](${gateway.dockerComposeUrl}) -2. Place the file in a directory and run: - -\`\`\`bash -docker compose up -\`\`\` - -The gateway will connect to ThingsBoard at `${mqtt.host}:${mqtt.port}` using access token `${gateway.token}`. -``` - -## No Backend Changes - -All new logic is frontend-only: -- GATEWAY uses existing `saveDevice` API (same as DEVICE) -- GATEWAY_CONNECTOR uses existing `saveEntityAttributes` API -- Attribute saving uses existing `saveEntityAttributes` API -- `dockerComposeUrl` is a constructed URL string, not a new endpoint diff --git a/docs/superpowers/specs/2026-04-02-transport-variables-design.md b/docs/superpowers/specs/2026-04-02-transport-variables-design.md deleted file mode 100644 index e1cfe4bbc4..0000000000 --- a/docs/superpowers/specs/2026-04-02-transport-variables-design.md +++ /dev/null @@ -1,72 +0,0 @@ -# Transport Variables in Device Install Templates — Design Spec - -## Goal - -Add transport host/port variables to the device install wizard's template resolution so that post-install instructions and entity templates can reference the platform's configured transport endpoints (e.g., `${mqtt.host}`, `${coap.port}`). - -## Variables - -All 6 transport protocols are supported: - -| Variable | Example value | Source | -|----------|--------------|--------| -| `${http.host}` | `demo.thingsboard.io` | Admin settings `connectivity.http.host` | -| `${http.port}` | `8080` | Admin settings `connectivity.http.port` | -| `${https.host}` | `demo.thingsboard.io` | Admin settings `connectivity.https.host` | -| `${https.port}` | `443` | Admin settings `connectivity.https.port` | -| `${mqtt.host}` | `demo.thingsboard.io` | Admin settings `connectivity.mqtt.host` | -| `${mqtt.port}` | `1883` | Admin settings `connectivity.mqtt.port` | -| `${mqtts.host}` | `demo.thingsboard.io` | Admin settings `connectivity.mqtts.host` | -| `${mqtts.port}` | `8883` | Admin settings `connectivity.mqtts.port` | -| `${coap.host}` | `demo.thingsboard.io` | Admin settings `connectivity.coap.host` | -| `${coap.port}` | `5683` | Admin settings `connectivity.coap.port` | -| `${coaps.host}` | `demo.thingsboard.io` | Admin settings `connectivity.coaps.host` | -| `${coaps.port}` | `5684` | Admin settings `connectivity.coaps.port` | - -These variables are available in all template and instruction files, alongside form values and entity outputs. - -## Data Source - -Frontend fetches from existing admin settings API: -``` -GET /api/admin/settings/connectivity -``` - -Returns `DeviceConnectivitySettings` — a `Record` where each entry has `{ enabled, host, port }`. - -The `AdminService.getAdminSettings('connectivity')` method and `DeviceConnectivitySettings` model already exist in the codebase. - -## Resolution Priority - -The `resolveVariables()` function checks sources in this order: -1. Form field values (`formValues[key]`) — e.g., `${deviceName}` -2. Transport connectivity (`transportVars[key]`) — e.g., `${mqtt.host}` -3. Named entity outputs (`entityOutputs[alias].prop`) — e.g., `${device.id}` - -Transport variables use dot notation (`mqtt.host`) which currently falls through to entity output resolution. By adding a transport lookup before entity outputs, we ensure `${mqtt.host}` resolves to the transport config rather than looking for a non-existent entity alias called `mqtt`. - -## Changes - -**Single file:** `ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts` - -1. Inject `AdminService` (from `@core/http/admin.service`) -2. In `ngOnInit`, after ZIP parsing, fetch connectivity settings and flatten to `Record`: - ``` - { 'http.host': '...', 'http.port': '8080', 'mqtt.host': '...', 'mqtt.port': '1883', ... } - ``` -3. In `resolveVariables()`, check the transport map for dot-notation keys before falling through to entity outputs - -**No backend changes. No new models. No new endpoints.** - -## Example Usage in Templates - -Post-install instruction (`post-install.md`): -```markdown -#define THINGSBOARD_SERVER "${mqtt.host}" -#define THINGSBOARD_PORT ${mqtt.port} -``` - -Integration template (`integration.json`): -```json -{"baseUrl": "${http.host}:${http.port}"} -``` diff --git a/docs/superpowers/specs/2026-04-09-provisioning-conflict-resolution.md b/docs/superpowers/specs/2026-04-09-provisioning-conflict-resolution.md deleted file mode 100644 index 25251e7e3f..0000000000 --- a/docs/superpowers/specs/2026-04-09-provisioning-conflict-resolution.md +++ /dev/null @@ -1,102 +0,0 @@ -# Provisioning Conflict Resolution — Design Spec - -## Goal - -When an entity with the same name already exists during device package provisioning, show a conflict resolution UI instead of an error. The user chooses how to proceed (use existing, overwrite, or create copy) and provisioning continues. - -## Conflict Detection - -Before creating each entity, pre-check by name. If an entity with the same name exists, show the conflict UI instead of attempting creation. - -| Entity type | Pre-check | Conflict options | -|-------------|-----------|-----------------| -| DEVICE_PROFILE | `findDeviceProfileByName` | Use existing / Overwrite | -| RULE_CHAIN | `findRuleChainByName` | Use existing / Overwrite | -| DEVICE | `findDeviceByName` (new) | Use existing / Overwrite | -| GATEWAY | `findDeviceByName` (new, same API) | Use existing / Overwrite | -| DASHBOARD | `findDashboardByName` (new) | Overwrite / Create copy | -| GATEWAY_CONNECTOR | no pre-check | — | - -## New Status: `conflict` - -```typescript -export type EntityStepStatus = 'pending' | 'running' | 'success' | 'error' | 'conflict'; -``` - -`EntityStepProgress` gains: -```typescript -existingEntity?: EntityStepOutput; // the found entity, for resolution -conflictType?: 'use-or-overwrite' | 'overwrite-or-copy'; // which buttons to show -``` - -## Resolution Actions - -**Use existing**: take the existing entity's ID/name/url, store as output, continue to next step. No API call. - -**Overwrite**: fetch existing entity by ID, merge template data (keeping the ID), save. For rule chains, also save metadata. Store result as output. - -**Create copy**: create new entity with the template as-is (TB allows duplicate dashboard names). Store result as output. - -## UI - -Conflict row styling: -- Amber/yellow background (not red — it's a decision, not an error) -- Warning icon (amber `⚠`) -- Description: "Device profile with this name already exists" -- Two buttons on the right - -For `use-or-overwrite`: -``` -⚠ Device Profile — Modbus TH Sensor - Entity with this name already exists - [Use existing] [Overwrite] -``` - -For `overwrite-or-copy`: -``` -⚠ Dashboard — Modbus TH Monitor - Dashboard with this name already exists - [Overwrite] [Create copy] -``` - -Button styling: -- Use existing / Create copy: outlined primary (safe option) -- Overwrite: outlined warning/amber (destructive) - -## Provisioning Step Flow - -1. For each entity step: - a. Set status = 'running' - b. Pre-check: search for existing entity by name - c. If found: set status = 'conflict', store existing entity, pause (return from loop) - d. If not found: create entity → success → continue - e. If creation fails: set status = 'error' → pause (existing behavior) -2. When user clicks a conflict resolution button: - a. Execute resolution (use existing / overwrite / create copy) - b. Set status = 'success' - c. Resume `runEntitySteps` from the next step - -## Read-Only Review Dialog - -No changes needed — provisioning step shows all entities as success with "Done" label. No conflict UI in review mode. - -## Changes - -### Models (`device-package.models.ts`) -- Add `'conflict'` to `EntityStepStatus` -- Add `existingEntity?: EntityStepOutput` and `conflictType?: string` to `EntityStepProgress` - -### Dialog Component (`device-install-dialog.component.ts`) -- Add `findDeviceByName` and `findDashboardByName` methods -- Refactor `createEntity` to pre-check before creating -- Add `resolveConflict(ep, resolution)` method -- Resume loop after conflict resolution - -### Dialog Template (`device-install-dialog.component.html`) -- Add conflict row rendering with amber styling and resolution buttons - -### Dialog Styles (`device-install-dialog.component.scss`) -- Add `.tb-progress-conflict` amber styles - -### Translations -- Add conflict-related translation keys diff --git a/docs/superpowers/specs/2026-04-22-iot-hub-item-deep-link-design.md b/docs/superpowers/specs/2026-04-22-iot-hub-item-deep-link-design.md deleted file mode 100644 index fda5f3c451..0000000000 --- a/docs/superpowers/specs/2026-04-22-iot-hub-item-deep-link-design.md +++ /dev/null @@ -1,289 +0,0 @@ -# IoT Hub item deep link — design - -**Status:** approved -**Date:** 2026-04-22 -**Author:** Andrii Shvaika -**Scope:** ThingsBoard CE frontend + IoT Hub backend contract (external) - -## Summary - -Allow two shareable deep-link URL shapes: -- `http:///iot-hub/{itemId}` — opens the detail view for the latest published version of the item. -- `http:///iot-hub/version/{itemVersionId}` — opens a specific version snapshot. Published versions open directly; unpublished versions are gated behind a security warning. - -The first URL is the canonical "share this marketplace item" link. The second targets creator review workflows — stable snapshots of an exact draft. - -## Goals - -- Two shareable, bookmarkable deep link shapes with distinct semantics: - - `/iot-hub/{itemId}` — always latest published version of the item (404 if none published). - - `/iot-hub/version/{itemVersionId}` — specific version; warning gate when unpublished, then "Unpublished preview" badge in the detail dialog. -- Zero backend changes in ThingsBoard. Install/update flows reuse the existing versionId-based pipeline. - -## Non-goals - -- Authenticated access to unpublished content. Authorization is "public-by-link": whoever has the UUID can fetch the content. -- Full standalone page for item detail. The detail view stays as an Angular Material dialog; the deep link navigates to the type-specific browse page (`/iot-hub/widgets`, `/iot-hub/dashboards`, etc.) and opens the dialog over it. -- Install-specific semantics for unpublished versions. Install reuses the normal flow; only the IoT Hub-side install counter policy may differ (see IoT Hub-side changes). -- A "latest including drafts" shorthand shape (`/iot-hub/{itemId}/preview`). If creators want to preview a specific draft, they use the version URL with the exact versionId. - -## User flows - -### Published link: `/iot-hub/{itemId}` - -1. User pastes or clicks `/iot-hub/{itemId}`. -2. Angular mounts `TbIotHubItemResolverComponent`. -3. Resolver calls `iotHubApiService.getPublishedVersion(itemId)` → IoT Hub `GET /api/items/{itemId}/published` (new endpoint). -4. On success, resolver navigates to `/iot-hub/{typeSegment(item.type)}` with router state `{ openItem: { version, preview: false } }` and `replaceUrl: true`. Detail dialog opens with no badge. -5. `TbIotHubItemsPageComponent.ngOnInit` consumes `history.state.openItem`, resolves installed state, and calls `IotHubActionsService.openItemDetail(...)`. -6. On 404 (no published version exists) or other error, resolver shows a toast and redirects to `/iot-hub`. - -### Version link: `/iot-hub/version/{itemVersionId}` - -1. User pastes or clicks `/iot-hub/version/{itemVersionId}`. -2. Angular mounts `TbIotHubItemResolverComponent`. -3. Resolver calls `iotHubApiService.getVersionInfo(itemVersionId)` → IoT Hub `GET /api/versions/{versionId}` (existing endpoint). -4. If the returned version is published → resolver navigates directly to `/iot-hub/{typeSegment(item.type)}` with router state `{ openItem: { version, preview: false } }` and `replaceUrl: true`. Detail dialog opens with no badge. -5. If the returned version is unpublished → resolver opens `TbIotHubUnpublishedWarningDialogComponent` (`disableClose: true`). - - Cancel → `router.navigate(['/iot-hub'])`. - - "I understand the risk, continue" → navigates to `/iot-hub/{typeSegment(item.type)}` with `{ openItem: { version, preview: true } }`; detail dialog opens with the "Unpublished preview" badge. -6. Install / Update / Remove / Open-entity actions behave as usual — all operate on the versionId already fetched, so install-from-version works end-to-end. -7. On 404 or other error, resolver shows a toast and redirects to `/iot-hub`. - -## Angular routing - -Two routes added to `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts`, placed after the existing named child routes (`widgets`, `dashboards`, `solution-templates`, `calculated-fields`, `rule-chains`, `devices`, `search`, `installed`, `creator/:creatorId`). The literal `version/:itemVersionId` must come **before** the `:itemId` wildcard so the router matches the literal first: - -```ts -{ path: 'version/:itemVersionId', component: TbIotHubItemResolverComponent, - data: { auth: [Authority.TENANT_ADMIN], title: 'iot-hub.item-detail' } }, -{ path: ':itemId', component: TbIotHubItemResolverComponent, - data: { auth: [Authority.TENANT_ADMIN], title: 'iot-hub.item-detail' } }, -``` - -The resolver branches on which param is present — `paramMap.get('itemVersionId')` signals the version-URL flow; `paramMap.get('itemId')` signals the published-URL flow. A UUID-shape check runs inside the resolver (not as a `UrlMatcher`) so an invalid id produces a friendly toast instead of a generic not-found page. - -## Components - -### `TbIotHubItemResolverComponent` (new) - -Location: `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts`. - -- Standalone: `false`. Declared in `IotHubModule`. -- Template: empty (`template: ''`). The component renders nothing; it is a router-reachable controller. -- `ngOnInit`: - 1. Read `itemVersionId` and `itemId` from route params. Set `byVersion = itemVersionId != null`; pick the relevant id accordingly. - 2. Reject non-UUID id → `iot-hub.deep-link-invalid-id` toast + redirect to `/iot-hub`. - 3. Dispatch to `getVersionInfo(id)` (byVersion) or `getPublishedVersion(id)` (published), both with `{ ignoreErrors: true }`. - 4. On error, map HTTP status to `iot-hub.deep-link-not-found` (404) or `iot-hub.deep-link-fetch-failed` (other) and redirect. - 5. On success, call `handleResolved(version, byVersion)`: - - `byVersion` + version unpublished → open warning dialog; confirm routes to type-page with state (`preview: true`); cancel routes to `/iot-hub`. - - Otherwise → route directly to type-page with state (`preview: false`). -- All navigations use `replaceUrl: true` so the resolver URL does not pollute browser history. -- The published URL (`/iot-hub/{itemId}`) cannot surface unpublished content — `getPublishedVersion` only returns PUBLISHED versions — so the warning branch is unreachable for that flow. The `byVersion` gate in `handleResolved` makes this explicit. - -### `iot-hub-deep-link.utils.ts` (new) - -Shared helpers: - -```ts -export const isUUID = (s: string | null): s is string => !!s && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s); - -export function typeSegment(t: ItemType): string | undefined { - switch (t) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboards'; - case ItemType.SOLUTION_TEMPLATE: return 'solution-templates'; - case ItemType.CALCULATED_FIELD: return 'calculated-fields'; - case ItemType.RULE_CHAIN: return 'rule-chains'; - case ItemType.DEVICE: return 'devices'; - default: return undefined; - } -} - -export function isPublished(v: MpItemVersionView): boolean { - return !!v.publishedTime && v.publishedTime > 0; -} -``` - -A `typeSegment` returning `undefined` (future `ItemType` values) surfaces as `iot-hub.deep-link-fetch-failed` in the resolver. - -### `TbIotHubUnpublishedWarningDialogComponent` (new) - -Location: `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.{ts,html,scss}`. Declared in `IotHubComponentsModule`. - -- Title: translated `iot-hub.unpublished-warning-title` ("Unpublished content") with a red `warning` Material icon. -- Body: `iot-hub.unpublished-warning-text` paragraph ("This is a preview of unpublished content. It has not been reviewed by IoT Hub. Installing unverified content can introduce security and stability risks — only continue if you trust the creator."). -- Secondary line: `{item.name} • v {item.version}` so the user sees what they are acknowledging. -- Buttons: - - `Cancel` — returns `false`. - - `iot-hub.unpublished-warning-confirm` ("I understand the risk, continue") — returns `true`, styled with the project's danger accent. -- Dialog config: `panelClass: ['tb-dialog']`, `disableClose: true`, `autoFocus: false`. -- `MAT_DIALOG_DATA` payload: `{ item: MpItemVersionView }`. - -Structural sibling of `TbIotHubDeleteDialogComponent`. - -### `TbIotHubItemDetailDialogComponent` (modified) - -- `IotHubItemDetailDialogData` gains optional `preview?: boolean`. -- Component stores `this.preview = data.preview === true` and exposes it to the template. -- Template adds a preview badge next to the existing version chip in the sticky meta bar: - - ```html -
- warning - {{ 'iot-hub.unpublished-preview' | translate }} -
- ``` - -- No other template or behavior changes. Install / Update / Remove / Open-entity actions are untouched. -- SCSS adds `.tb-unpublished-preview-badge` (red-on-light-red, matching the warning dialog accent). - -### `IotHubActionsService` (modified) - -```ts -openItemDetail( - item: MpItemVersionView, - installedItem?: IotHubInstalledItem, - installedItemsCount?: number, - mode?: IotHubItemDetailDialogMode, - showCreator?: boolean, - preview?: boolean -): Observable -``` - -`preview` is forwarded into `IotHubItemDetailDialogData`. All existing callers ignore the new parameter (undefined → non-preview). - -### `TbIotHubItemsPageComponent` (modified) - -`ngOnInit` is extended to consume `history.state.openItem` exactly once per navigation: - -```ts -private maybeOpenDeepLinkedItem(): void { - const openItem = history.state?.openItem as - { version: MpItemVersionView; preview?: boolean } | undefined; - if (!openItem || openItem.version.type !== this.config.type) return; - - history.replaceState({ ...history.state, openItem: undefined }, ''); - - this.resolveInstalledItem(openItem.version).subscribe(installed => { - this.iotHubActions.openItemDetail( - openItem.version, - installed, - installed ? 1 : 0, - 'default', - true, - openItem.preview - ).subscribe(result => this.handleDetailResult(result)); - }); -} - -private resolveInstalledItem(v: MpItemVersionView): Observable { - return this.iotHubApiService - .getInstalledItems(new PageLink(1), undefined, v.itemId) - .pipe(map(page => page.data[0] ?? null)); -} -``` - -`handleDetailResult` delegates to the same installed-count refresh logic already used by card clicks. The `history.replaceState` call clears `openItem` so a page refresh does not re-open the dialog from stale state. - -## API contract - -### `IotHubApiService` — one new method - -```ts -public getPublishedVersion(itemId: string, config?: IotHubRequestConfig): Observable { - return this.http.get( - `${this.baseUrl}/api/items/${itemId}/published`, - { params: this.buildParams(config) } - ); -} -``` - -The version URL reuses the existing `getVersionInfo(versionId, config)` which calls `GET /api/versions/{versionId}`. Both accept `{ ignoreErrors: true }` so the resolver can handle failures inline rather than surfacing the global interceptor toast. - -### ThingsBoard backend - -Unchanged. `IotHubController.installVersion` and `IotHubController.updateInstalledItem` already operate on versionIds; both deep-link flows funnel into them without modification. - -## i18n - -Add to `ui-ngx/src/assets/locale/locale.constant-en_US.json` (and mirror into other locales): - -- `iot-hub.item-detail` — "IoT Hub item" -- `iot-hub.unpublished-warning-title` — "Unpublished content" -- `iot-hub.unpublished-warning-text` — "This is a preview of unpublished content. It has not been reviewed by IoT Hub. Installing unverified content can introduce security and stability risks — only continue if you trust the creator." -- `iot-hub.unpublished-warning-confirm` — "I understand the risk, continue" -- `iot-hub.unpublished-preview` — "Unpublished preview" -- `iot-hub.deep-link-invalid-id` — "Invalid IoT Hub item link." -- `iot-hub.deep-link-not-found` — "This IoT Hub item doesn't exist or was removed." -- `iot-hub.deep-link-fetch-failed` — "Couldn't load IoT Hub item. Please try again." - -## Edge cases - -- **Invalid UUID shape** for either `itemId` or `itemVersionId` → `iot-hub.deep-link-invalid-id` toast + redirect to `/iot-hub`. -- **404 from IoT Hub**: - - Published URL: item has no published version, or item doesn't exist. Toast `iot-hub.deep-link-not-found` + redirect. - - Version URL: version doesn't exist or was removed. Same toast + redirect. -- **Network / 5xx error** → `iot-hub.deep-link-fetch-failed` toast + redirect. -- **Version URL resolves to a published version** → no warning, no badge. Serves as a stable snapshot link to that version. -- **Unsupported `ItemType`** (future value not in `typeSegment`) → treated as `iot-hub.deep-link-fetch-failed`. -- **User hits Browser Back from the warning dialog** → dialog destroys with the resolver component; no zombie dialog. -- **User lacks `TENANT_ADMIN`** → the `/iot-hub` parent route guard blocks; no additional guard needed. -- **Deep link for an already-installed item** → detail dialog shows its usual "Installed / Update / Open entity" actions against the resolved versionId. Both URL shapes produce identical behavior once the dialog is open. -- **Refresh after deep link has been resolved** → URL is now `/iot-hub/{typePage}`; `history.state.openItem` is cleared; user sees the type-page with no dialog (expected). - -## Testing - -- `TbIotHubItemResolverComponent` unit tests with mocked `IotHubApiService` and `Router`: - - published URL happy path (no warning, no badge) - - version URL happy path, version is published (no warning, no badge) - - version URL happy path, version is unpublished (warning → confirm → dialog with badge) - - version URL, warning cancel → redirect to `/iot-hub` - - invalid UUID (either param) - - 404 (both URL shapes) - - 5xx / network error - - unsupported `ItemType` -- `isPublished()` and `typeSegment()` unit tests. -- `TbIotHubUnpublishedWarningDialogComponent` component tests: renders item name/version; Cancel returns `false`; Confirm returns `true`. -- `TbIotHubItemsPageComponent.maybeOpenDeepLinkedItem` integration test with seeded `history.state`: asserts dialog opens, state is cleared, type mismatch is ignored. -- No ThingsBoard backend tests (no backend changes). - -## Files touched - -New: -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts` -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.ts` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.html` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-unpublished-warning-dialog.component.scss` - -Modified: -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts` -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts` -- `ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss` -- `ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts` -- `ui-ngx/src/app/core/http/iot-hub-api.service.ts` -- `ui-ngx/src/assets/locale/locale.constant-*.json` - -## IoT Hub-side changes required - -These live in the IoT Hub repository, not ThingsBoard CE. One new endpoint plus behavior/CORS contracts on the existing by-versionId family. - -1. **New endpoint** `GET /api/items/{itemId}/published` - - Returns `MpItemVersionView` for the latest version of the item that is in the PUBLISHED state. - - `404` when the item has no published version or doesn't exist. - - Anonymous cross-origin access (same CORS policy as `/api/versions/published`). - - Powers the `/iot-hub/{itemId}` deep link. -2. **Behavior contract on existing `GET /api/versions/{versionId}`**: must return the requested version regardless of its state (PUBLISHED, DRAFT, PENDING_REVIEW, …). This powers the `/iot-hub/version/{itemVersionId}` deep link. Anonymous cross-origin; the versionId UUID itself is the soft-secret gate. -3. **`MpItemVersionView` response for unpublished versions** must allow the frontend to tell published from unpublished. Either `publishedTime` must be falsy (`null` / `0`) for non-published versions, or an explicit `state` field must be added. Pick one; the frontend uses `isPublished(v)` based on `publishedTime` today. -4. **Related by-versionId endpoints must also serve unpublished versions** (required by the install flow proxied through TB): - - `GET /api/versions/{versionId}/readme` - - `GET /api/versions/{versionId}/fileData` - - `POST /api/versions/{versionId}/install` -5. **Install counter policy**: decide whether `POST /api/versions/{versionId}/install` against an unpublished version increments counters. Recommended: skip, to avoid inflating published install metrics with creator self-tests. -6. **CORS**: ensure `/api/items/{itemId}/published` and the full `/api/versions/{versionId}/...` family permit cross-origin GET from any origin. diff --git a/docs/superpowers/specs/2026-05-06-iot-hub-item-link.md b/docs/superpowers/specs/2026-05-06-iot-hub-item-link.md deleted file mode 100644 index 38f7fe7895..0000000000 --- a/docs/superpowers/specs/2026-05-06-iot-hub-item-link.md +++ /dev/null @@ -1,145 +0,0 @@ -# IoT Hub `${item-link:uuid}` Markdown Component — Design - -**Date:** 2026-05-06 -**Status:** Approved -**Branch:** `feature/iot-hub` - -## Summary - -Add an `${item-link:}` markdown placeholder that renders a small card -(thumbnail + name + creator) for any IoT Hub marketplace item, modeled on -the cards already shown in the home-page search popup. Used by IoT Hub -content authors to cross-link items from readme and install instructions. - -## Decisions - -| Question | Decision | -|---|---| -| UUID identifies | Marketplace **item ID** (stable across versions) | -| Render scope | Item readme + device install instructions + solution install instructions | -| Click behavior | Plain anchor → `/iot-hub/{itemId}` with `target="_blank"` | -| Syntax | `${item-link:}` — ID only, type derived from API response | -| Unavailable item handling | Render disabled card labeled "Item unavailable" | -| Resolution strategy | Render skeleton, fetch async, swap when ready (per card) | - -## Architecture - -Three building blocks, scoped tightly: - -1. **`replaceItemLinkPlaceholders(markdown: string): string`** — pure - string transform. Rewrites `${item-link:}` to - ``. -2. **`TbIotHubItemLinkCardComponent`** — Angular component that owns - fetch, state (`loading` / `loaded` / `unavailable`), and visual. -3. **`IotHubItemLinkModule`** — declares the component; passed as - `additionalCompileModules` on every `tb-markdown` instance that needs it. - -`tb-markdown` already supports compile-time injection of additional -modules; placeholder rewriting + module registration is all that is -needed to make the component render inside markdown. - -## Placeholder syntax - -- Format: `${item-link:}` -- Regex: strict 36-char UUID match - (`/\$\{item-link:([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\}/g`) -- Non-UUID payloads (typos) are left as-is in rendered output, so the - author sees the issue during preview. -- Placeholders inside fenced code blocks are still replaced — matches - the existing `prefixResourceUrls` / `resolveDocLinks` precedent in the - same files. - -## Integration points - -| File | Hook | -|---|---| -| `iot-hub-item-detail-dialog.component.ts` `loadReadme()` | Add as a step in the existing pipeline next to `prefixResourceUrls` and `resolveDocLinks` | -| `device-install-dialog.component.ts` `resolveVariables()` | Add an `^item-link:(uuid)$` branch alongside `gateway.downloadButton` and the callout matchers | -| `solution-install-dialog.component.ts` constructor | New step before assigning `this.details` | - -All three call the same backing helper. Each `tb-markdown` instance -in those templates gets `[additionalCompileModules]="[IotHubItemLinkModule]"`. - -## Component spec - -**Location:** -`ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/` - -**Inputs:** -- `@Input() itemId: string` - -**Lifecycle:** -- `ngOnInit()` calls - `iotHubApiService.getPublishedVersion(itemId, { ignoreErrors: true, ignoreLoading: true })`. -- Success → `state = 'loaded'`, store item. -- Error / 404 / network failure → `state = 'unavailable'`. - -**Template — three states:** - -- **loading:** skeleton card matching the loaded layout (gray thumb + - two text shimmer lines). -- **loaded:** anchor to `/iot-hub/{itemId}` (`target="_blank"`, - `rel="noopener noreferrer"`), thumbnail slot + name + creator row. -- **unavailable:** non-clickable card, 60 % opacity, `link_off` icon - in the thumbnail slot, label "Item unavailable" (translated). - -**Thumbnail rules** (mirror the search popup at -`iot-hub-home.component.html:103-115`): -- Compact types (`CALCULATED_FIELD`, `ALARM_RULE`, `RULE_CHAIN`): - colored square + `getCompactIcon()` + item color. -- Other types: image via `iotHubApiService.resolveResourceUrl(item.image)`, - fallback to a `mat-icon` of the type when no image is present. -- The same branching exists in `iot-hub-home.component.ts` (search popup) - and `iot-hub-item-detail-dialog.component.ts`. The new card duplicates - it locally — extracting a shared helper is out of scope for this PR. - -**Styling:** -- Component-scoped SCSS. -- Fixed width ~320 px, block-level (each card on its own line in markdown). -- Subtle CSS-keyframe shimmer for the skeleton state. -- Hover: matches search-popup card hover. - -**Click:** -- Plain anchor — middle-click / ctrl-click / "open in new tab" all work - natively. Existing route `/iot-hub/:itemId` - (`TbIotHubItemResolverComponent`) handles resolution, the - unpublished-version warning, and opening the detail dialog on the - type page. - -## Translations - -Add the following keys (and propagate to other locale files): - -| Key | English | -|---|---| -| `iot-hub.item-link-unavailable` | "Item unavailable" | - -## Out of scope - -- Changelog rendering (explicitly excluded). -- IoT Hub creator-side authoring helpers (placeholder is plain text in - raw markdown; nothing required on the IoT Hub backend). -- Batch endpoint for resolving multiple items in one request — N parallel - requests is fine for the expected 0–5 references per page. -- A non-block (inline) variant of the card. -- Hover preview / tooltip / type chip on the card itself. - -## Testing - -- Component unit tests: state transitions (loading → loaded, loading - → unavailable), thumbnail logic for compact vs. non-compact types, - fallback when image missing. -- Regex unit test for `replaceItemLinkPlaceholders` covering: valid - UUID, invalid UUID (left untouched), placeholder inside fenced code - (still replaced — documented behavior), multiple placeholders in one - document. -- Manual visual QA in all three render sites. - -## Risks / open items - -- `tb-markdown` recompiles on every `data` change; if a parent toggles - the markdown rapidly, the card re-fetches. Acceptable: readmes / - instructions don't churn during normal viewing. -- `getPublishedVersion` returns the **current** published version. If a - newer published version changes the name/thumbnail, links update - automatically — that is the documented behavior of the ID-only syntax. From f4aaeff6d9790ea2dae2042e280081cd77577b22 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 12:20:45 +0300 Subject: [PATCH 15/37] chore(iot-hub): swap "Add from IoT Hub" action icon from store to hub Align the four entity table "Add from IoT Hub" descriptors (devices, rule chains, calculated fields, widget types) with the new hub icon already used by the IoT Hub menu and breadcrumb. Drops the old `store` glyph that no longer matches the rest of the IoT Hub UI. --- .../calculated-fields/calculated-fields-table-config.ts | 2 +- .../modules/home/pages/device/devices-table-config.resolver.ts | 2 +- .../home/pages/rulechain/rulechains-table-config.resolver.ts | 2 +- .../home/pages/widget/widget-types-table-config.resolver.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index af4bff4ddd..c7f7bc5b74 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -110,7 +110,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, onAction: () => this.addCalculatedFieldFromIotHub() } diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index f711b24d8f..72d0a08dd6 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -421,7 +421,7 @@ export class DevicesTableConfigResolver { }, { name: this.translate.instant('iot-hub.add-from-iot-hub'), - icon: 'store', + icon: 'hub', isEnabled: () => true, onAction: (_$event) => this.addDeviceFromIotHub() }, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts index 43a7c91b53..e631eeaf15 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts @@ -174,7 +174,7 @@ export class RuleChainsTableConfigResolver { }, { name: this.translate.instant('iot-hub.add-from-iot-hub'), - icon: 'store', + icon: 'hub', isEnabled: () => true, onAction: (_$event) => this.addRuleChainFromIotHub() } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts index dfca3ed83c..cb8b436166 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts @@ -157,7 +157,7 @@ export class WidgetTypesTableConfigResolver { this.config.addActionDescriptors.push( { name: this.translate.instant('iot-hub.add-from-iot-hub'), - icon: 'store', + icon: 'hub', isEnabled: () => true, onAction: (_$event) => this.addWidgetFromIotHub() } From 5f231fdb78674970e51708fc7c0ce7f93bbd5a12 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 12 Jun 2026 11:47:03 +0200 Subject: [PATCH 16/37] Moved customized widgets to Deprecated --- .../data/json/system/widget_types/air_quality_index_card.json | 2 +- .../widget_types/air_quality_index_card_with_background.json | 2 +- .../json/system/widget_types/air_quality_index_chart_card.json | 2 +- .../air_quality_index_chart_card_with_background.json | 2 +- .../json/system/widget_types/carbon_monoxide__co__card.json | 2 +- .../widget_types/carbon_monoxide__co__card_with_background.json | 2 +- .../system/widget_types/carbon_monoxide__co__chart_card.json | 2 +- .../carbon_monoxide__co__chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/co2_card.json | 2 +- .../data/json/system/widget_types/co2_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/co2_chart_card.json | 2 +- .../system/widget_types/co2_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/efficiency_card.json | 2 +- .../system/widget_types/efficiency_card_with_background.json | 2 +- .../data/json/system/widget_types/efficiency_chart_card.json | 2 +- .../widget_types/efficiency_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/efficiency_progress_bar.json | 2 +- .../widget_types/efficiency_progress_bar_with_background.json | 2 +- .../data/json/system/widget_types/efficiency_range_chart.json | 2 +- .../widget_types/efficiency_range_chart_with_background.json | 2 +- .../main/data/json/system/widget_types/flooding_level_card.json | 2 +- .../widget_types/flooding_level_card_with_background.json | 2 +- .../json/system/widget_types/flooding_level_chart_card.json | 2 +- .../widget_types/flooding_level_chart_card_with_background.json | 2 +- .../json/system/widget_types/flooding_level_progress_bar.json | 2 +- .../flooding_level_progress_bar_with_background.json | 2 +- .../src/main/data/json/system/widget_types/flow_rate_card.json | 2 +- .../system/widget_types/flow_rate_card_with_background.json | 2 +- .../data/json/system/widget_types/flow_rate_chart_card.json | 2 +- .../widget_types/flow_rate_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/flow_rate_gauge.json | 2 +- .../data/json/system/widget_types/flow_rate_progress_bar.json | 2 +- .../widget_types/flow_rate_progress_bar_with_background.json | 2 +- .../data/json/system/widget_types/flow_rate_range_chart.json | 2 +- .../widget_types/flow_rate_range_chart_with_background.json | 2 +- .../main/data/json/system/widget_types/fluid_pressure_card.json | 2 +- .../widget_types/fluid_pressure_card_with_background.json | 2 +- .../json/system/widget_types/fluid_pressure_chart_card.json | 2 +- .../widget_types/fluid_pressure_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/fluid_pressure_gauge.json | 2 +- .../json/system/widget_types/fluid_pressure_progress_bar.json | 2 +- .../fluid_pressure_progress_bar_with_background.json | 2 +- .../json/system/widget_types/fluid_pressure_range_chart.json | 2 +- .../fluid_pressure_range_chart_with_background.json | 2 +- .../data/json/system/widget_types/ground_temperature_card.json | 2 +- .../widget_types/ground_temperature_card_with_background.json | 2 +- .../json/system/widget_types/ground_temperature_chart_card.json | 2 +- .../ground_temperature_chart_card_with_background.json | 2 +- .../system/widget_types/horizontal_air_quality_index_card.json | 2 +- .../horizontal_air_quality_index_card_with_background.json | 2 +- .../widget_types/horizontal_carbon_monoxide__co__card.json | 2 +- .../horizontal_carbon_monoxide__co__card_with_background.json | 2 +- .../main/data/json/system/widget_types/horizontal_co2_card.json | 2 +- .../widget_types/horizontal_co2_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_efficiency_card.json | 2 +- .../horizontal_efficiency_card_with_background.json | 2 +- .../system/widget_types/horizontal_flooding_level_card.json | 2 +- .../horizontal_flooding_level_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_flow_rate_card.json | 2 +- .../widget_types/horizontal_flow_rate_card_with_background.json | 2 +- .../system/widget_types/horizontal_fluid_pressure_card.json | 2 +- .../horizontal_fluid_pressure_card_with_background.json | 2 +- .../system/widget_types/horizontal_ground_temperature_card.json | 2 +- .../horizontal_ground_temperature_card_with_background.json | 2 +- .../data/json/system/widget_types/horizontal_humidity_card.json | 2 +- .../widget_types/horizontal_humidity_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_illuminance_card.json | 2 +- .../horizontal_illuminance_card_with_background.json | 2 +- .../horizontal_individual_allergy_index__iai__card.json | 2 +- ...tal_individual_allergy_index__iai__card_with_background.json | 2 +- .../json/system/widget_types/horizontal_leaf_wetness_card.json | 2 +- .../horizontal_leaf_wetness_card_with_background.json | 2 +- .../widget_types/horizontal_nitrogen_dioxide__no2__card.json | 2 +- .../horizontal_nitrogen_dioxide__no2__card_with_background.json | 2 +- .../json/system/widget_types/horizontal_noise_level_card.json | 2 +- .../horizontal_noise_level_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_ozone__o3__card.json | 2 +- .../horizontal_ozone__o3__card_with_background.json | 2 +- .../data/json/system/widget_types/horizontal_pm10_card.json | 2 +- .../widget_types/horizontal_pm10_card_with_background.json | 2 +- .../data/json/system/widget_types/horizontal_pm2_5_card.json | 2 +- .../widget_types/horizontal_pm2_5_card_with_background.json | 2 +- .../system/widget_types/horizontal_power_consumption_card.json | 2 +- .../horizontal_power_consumption_card_with_background.json | 2 +- .../data/json/system/widget_types/horizontal_pressure_card.json | 2 +- .../widget_types/horizontal_pressure_card_with_background.json | 2 +- .../system/widget_types/horizontal_pump_vibration_card.json | 2 +- .../horizontal_pump_vibration_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_radon_level_card.json | 2 +- .../horizontal_radon_level_card_with_background.json | 2 +- .../data/json/system/widget_types/horizontal_rainfall_card.json | 2 +- .../widget_types/horizontal_rainfall_card_with_background.json | 2 +- .../system/widget_types/horizontal_rotational_speed_card.json | 2 +- .../horizontal_rotational_speed_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_snow_depth_card.json | 2 +- .../horizontal_snow_depth_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_soil_moisture_card.json | 2 +- .../horizontal_soil_moisture_card_with_background.json | 2 +- .../system/widget_types/horizontal_solar_radiation_card.json | 2 +- .../horizontal_solar_radiation_card_with_background.json | 2 +- .../widget_types/horizontal_sulfur_dioxide__so2__card.json | 2 +- .../horizontal_sulfur_dioxide__so2__card_with_background.json | 2 +- .../json/system/widget_types/horizontal_temperature_card.json | 2 +- .../horizontal_temperature_card_with_background.json | 2 +- .../data/json/system/widget_types/horizontal_uv_index_card.json | 2 +- .../widget_types/horizontal_uv_index_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_vibration_card.json | 2 +- .../widget_types/horizontal_vibration_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_visibility_card.json | 2 +- .../horizontal_visibility_card_with_background.json | 2 +- .../horizontal_volatile_organic_compounds_card.json | 2 +- ...izontal_volatile_organic_compounds_card_with_background.json | 2 +- .../json/system/widget_types/horizontal_wind_speed_card.json | 2 +- .../horizontal_wind_speed_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/humidity_card.json | 2 +- .../json/system/widget_types/humidity_card_with_background.json | 2 +- .../main/data/json/system/widget_types/humidity_chart_card.json | 2 +- .../widget_types/humidity_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/humidity_progress_bar.json | 2 +- .../widget_types/humidity_progress_bar_with_background.json | 2 +- .../main/data/json/system/widget_types/illuminance_card.json | 2 +- .../system/widget_types/illuminance_card_with_background.json | 2 +- .../data/json/system/widget_types/illuminance_chart_card.json | 2 +- .../widget_types/illuminance_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/illuminance_progress_bar.json | 2 +- .../widget_types/illuminance_progress_bar_with_background.json | 2 +- .../widget_types/individual_allergy_index__iai__card.json | 2 +- .../individual_allergy_index__iai__card_with_background.json | 2 +- .../widget_types/individual_allergy_index__iai__chart_card.json | 2 +- ...dividual_allergy_index__iai__chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/indoor_co2_card.json | 2 +- .../system/widget_types/indoor_co2_card_with_background.json | 2 +- .../data/json/system/widget_types/indoor_co2_chart_card.json | 2 +- .../widget_types/indoor_co2_chart_card_with_background.json | 2 +- .../json/system/widget_types/indoor_horizontal_co2_card.json | 2 +- .../indoor_horizontal_co2_card_with_background.json | 2 +- .../system/widget_types/indoor_horizontal_humidity_card.json | 2 +- .../indoor_horizontal_humidity_card_with_background.json | 2 +- .../system/widget_types/indoor_horizontal_illuminance_card.json | 2 +- .../indoor_horizontal_illuminance_card_with_background.json | 2 +- .../json/system/widget_types/indoor_horizontal_pm10_card.json | 2 +- .../indoor_horizontal_pm10_card_with_background.json | 2 +- .../json/system/widget_types/indoor_horizontal_pm2_5_card.json | 2 +- .../indoor_horizontal_pm2_5_card_with_background.json | 2 +- .../system/widget_types/indoor_horizontal_temperature_card.json | 2 +- .../indoor_horizontal_temperature_card_with_background.json | 2 +- .../data/json/system/widget_types/indoor_humidity_card.json | 2 +- .../widget_types/indoor_humidity_card_with_background.json | 2 +- .../json/system/widget_types/indoor_humidity_chart_card.json | 2 +- .../indoor_humidity_chart_card_with_background.json | 2 +- .../json/system/widget_types/indoor_humidity_progress_bar.json | 2 +- .../indoor_humidity_progress_bar_with_background.json | 2 +- .../data/json/system/widget_types/indoor_illuminance_card.json | 2 +- .../widget_types/indoor_illuminance_card_with_background.json | 2 +- .../json/system/widget_types/indoor_illuminance_chart_card.json | 2 +- .../indoor_illuminance_chart_card_with_background.json | 2 +- .../system/widget_types/indoor_illuminance_progress_bar.json | 2 +- .../indoor_illuminance_progress_bar_with_background.json | 2 +- .../main/data/json/system/widget_types/indoor_pm10_card.json | 2 +- .../system/widget_types/indoor_pm10_card_with_background.json | 2 +- .../data/json/system/widget_types/indoor_pm10_chart_card.json | 2 +- .../widget_types/indoor_pm10_chart_card_with_background.json | 2 +- .../main/data/json/system/widget_types/indoor_pm2_5_card.json | 2 +- .../system/widget_types/indoor_pm2_5_card_with_background.json | 2 +- .../data/json/system/widget_types/indoor_pm2_5_chart_card.json | 2 +- .../widget_types/indoor_pm2_5_chart_card_with_background.json | 2 +- .../json/system/widget_types/indoor_simple_co2_chart_card.json | 2 +- .../indoor_simple_co2_chart_card_with_background.json | 2 +- .../system/widget_types/indoor_simple_humidity_chart_card.json | 2 +- .../indoor_simple_humidity_chart_card_with_background.json | 2 +- .../widget_types/indoor_simple_illuminance_chart_card.json | 2 +- .../indoor_simple_illuminance_chart_card_with_background.json | 2 +- .../json/system/widget_types/indoor_simple_pm10_chart_card.json | 2 +- .../indoor_simple_pm10_chart_card_with_background.json | 2 +- .../system/widget_types/indoor_simple_pm2_5_chart_card.json | 2 +- .../indoor_simple_pm2_5_chart_card_with_background.json | 2 +- .../widget_types/indoor_simple_temperature_chart_card.json | 2 +- .../indoor_simple_temperature_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/indoor_temperature_card.json | 2 +- .../widget_types/indoor_temperature_card_with_background.json | 2 +- .../json/system/widget_types/indoor_temperature_chart_card.json | 2 +- .../indoor_temperature_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/indoor_temperature_gauge.json | 2 +- .../system/widget_types/indoor_temperature_progress_bar.json | 2 +- .../indoor_temperature_progress_bar_with_background.json | 2 +- .../system/widget_types/indoor_temperature_range_chart.json | 2 +- .../indoor_temperature_range_chart_with_background.json | 2 +- .../main/data/json/system/widget_types/leaf_wetness_card.json | 2 +- .../system/widget_types/leaf_wetness_card_with_background.json | 2 +- .../data/json/system/widget_types/leaf_wetness_chart_card.json | 2 +- .../widget_types/leaf_wetness_chart_card_with_background.json | 2 +- .../json/system/widget_types/leaf_wetness_progress_bar.json | 2 +- .../widget_types/leaf_wetness_progress_bar_with_background.json | 2 +- .../json/system/widget_types/nitrogen_dioxide__no2__card.json | 2 +- .../nitrogen_dioxide__no2__card_with_background.json | 2 +- .../system/widget_types/nitrogen_dioxide__no2__chart_card.json | 2 +- .../nitrogen_dioxide__no2__chart_card_with_background.json | 2 +- .../main/data/json/system/widget_types/noise_level_card.json | 2 +- .../system/widget_types/noise_level_card_with_background.json | 2 +- .../data/json/system/widget_types/noise_level_chart_card.json | 2 +- .../widget_types/noise_level_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/ozone__o3__card.json | 2 +- .../system/widget_types/ozone__o3__card_with_background.json | 2 +- .../data/json/system/widget_types/ozone__o3__chart_card.json | 2 +- .../widget_types/ozone__o3__chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/pm10_card.json | 2 +- .../json/system/widget_types/pm10_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/pm10_chart_card.json | 2 +- .../system/widget_types/pm10_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/pm2_5_card.json | 2 +- .../json/system/widget_types/pm2_5_card_with_background.json | 2 +- .../main/data/json/system/widget_types/pm2_5_chart_card.json | 2 +- .../system/widget_types/pm2_5_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/power_consumption_card.json | 2 +- .../widget_types/power_consumption_card_with_background.json | 2 +- .../json/system/widget_types/power_consumption_chart_card.json | 2 +- .../power_consumption_chart_card_with_background.json | 2 +- .../json/system/widget_types/power_consumption_range_chart.json | 2 +- .../power_consumption_range_chart_with_background.json | 2 +- .../src/main/data/json/system/widget_types/pressure_card.json | 2 +- .../json/system/widget_types/pressure_card_with_background.json | 2 +- .../main/data/json/system/widget_types/pressure_chart_card.json | 2 +- .../widget_types/pressure_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/pressure_progress_bar.json | 2 +- .../widget_types/pressure_progress_bar_with_background.json | 2 +- .../main/data/json/system/widget_types/pump_vibration_card.json | 2 +- .../widget_types/pump_vibration_card_with_background.json | 2 +- .../json/system/widget_types/pump_vibration_chart_card.json | 2 +- .../widget_types/pump_vibration_chart_card_with_background.json | 2 +- .../json/system/widget_types/pump_vibration_range_chart.json | 2 +- .../pump_vibration_range_chart_with_background.json | 2 +- .../main/data/json/system/widget_types/radon_level_card.json | 2 +- .../system/widget_types/radon_level_card_with_background.json | 2 +- .../data/json/system/widget_types/radon_level_chart_card.json | 2 +- .../widget_types/radon_level_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/rainfall_card.json | 2 +- .../json/system/widget_types/rainfall_card_with_background.json | 2 +- .../main/data/json/system/widget_types/rainfall_chart_card.json | 2 +- .../widget_types/rainfall_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/rotational_speed_card.json | 2 +- .../widget_types/rotational_speed_card_with_background.json | 2 +- .../json/system/widget_types/rotational_speed_chart_card.json | 2 +- .../rotational_speed_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/rotational_speed_gauge.json | 2 +- .../json/system/widget_types/rotational_speed_progress_bar.json | 2 +- .../rotational_speed_progress_bar_with_background.json | 2 +- .../json/system/widget_types/rotational_speed_range_chart.json | 2 +- .../rotational_speed_range_chart_with_background.json | 2 +- .../widget_types/simple_air_quality_index_chart_card.json | 2 +- .../simple_air_quality_index_chart_card_with_background.json | 2 +- .../widget_types/simple_carbon_monoxide__co__chart_card.json | 2 +- .../simple_carbon_monoxide__co__chart_card_with_background.json | 2 +- .../data/json/system/widget_types/simple_co2_chart_card.json | 2 +- .../widget_types/simple_co2_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_efficiency_chart_card.json | 2 +- .../simple_efficiency_chart_card_with_background.json | 2 +- .../system/widget_types/simple_flooding_level_chart_card.json | 2 +- .../simple_flooding_level_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_flow_rate_chart_card.json | 2 +- .../simple_flow_rate_chart_card_with_background.json | 2 +- .../system/widget_types/simple_fluid_pressure_chart_card.json | 2 +- .../simple_fluid_pressure_chart_card_with_background.json | 2 +- .../widget_types/simple_ground_temperature_chart_card.json | 2 +- .../simple_ground_temperature_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_humidity_chart_card.json | 2 +- .../simple_humidity_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_illuminance_chart_card.json | 2 +- .../simple_illuminance_chart_card_with_background.json | 2 +- .../simple_individual_allergy_index__iai__chart_card.json | 2 +- ...dividual_allergy_index__iai__chart_card_with_background.json | 2 +- .../system/widget_types/simple_leaf_wetness_chart_card.json | 2 +- .../simple_leaf_wetness_chart_card_with_background.json | 2 +- .../widget_types/simple_nitrogen_dioxide__no2__chart_card.json | 2 +- ...imple_nitrogen_dioxide__no2__chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_noise_level_chart_card.json | 2 +- .../simple_noise_level_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_ozone__o3__chart_card.json | 2 +- .../simple_ozone__o3__chart_card_with_background.json | 2 +- .../data/json/system/widget_types/simple_pm10_chart_card.json | 2 +- .../widget_types/simple_pm10_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/simple_pm2_5_chart_card.json | 2 +- .../widget_types/simple_pm2_5_chart_card_with_background.json | 2 +- .../widget_types/simple_power_consumption_chart_card.json | 2 +- .../simple_power_consumption_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_pressure_chart_card.json | 2 +- .../simple_pressure_chart_card_with_background.json | 2 +- .../system/widget_types/simple_pump_vibration_chart_card.json | 2 +- .../simple_pump_vibration_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_radon_level_chart_card.json | 2 +- .../simple_radon_level_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_rainfall_chart_card.json | 2 +- .../simple_rainfall_chart_card_with_background.json | 2 +- .../system/widget_types/simple_rotational_speed_chart_card.json | 2 +- .../simple_rotational_speed_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_snow_depth_chart_card.json | 2 +- .../simple_snow_depth_chart_card_with_background.json | 2 +- .../system/widget_types/simple_soil_moisture_chart_card.json | 2 +- .../simple_soil_moisture_chart_card_with_background.json | 2 +- .../system/widget_types/simple_solar_radiation_chart_card.json | 2 +- .../simple_solar_radiation_chart_card_with_background.json | 2 +- .../widget_types/simple_sulfur_dioxide__so2__chart_card.json | 2 +- .../simple_sulfur_dioxide__so2__chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_temperature_chart_card.json | 2 +- .../simple_temperature_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_uv_index_chart_card.json | 2 +- .../simple_uv_index_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_vibration_chart_card.json | 2 +- .../simple_vibration_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_visibility_chart_card.json | 2 +- .../simple_visibility_chart_card_with_background.json | 2 +- .../simple_volatile_organic_compounds_chart_card.json | 2 +- ...e_volatile_organic_compounds_chart_card_with_background.json | 2 +- .../json/system/widget_types/simple_wind_speed_chart_card.json | 2 +- .../simple_wind_speed_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/snow_depth_card.json | 2 +- .../system/widget_types/snow_depth_card_with_background.json | 2 +- .../data/json/system/widget_types/snow_depth_chart_card.json | 2 +- .../widget_types/snow_depth_chart_card_with_background.json | 2 +- .../main/data/json/system/widget_types/soil_moisture_card.json | 2 +- .../system/widget_types/soil_moisture_card_with_background.json | 2 +- .../data/json/system/widget_types/soil_moisture_chart_card.json | 2 +- .../widget_types/soil_moisture_chart_card_with_background.json | 2 +- .../json/system/widget_types/soil_moisture_progress_bar.json | 2 +- .../soil_moisture_progress_bar_with_background.json | 2 +- .../data/json/system/widget_types/solar_radiation_card.json | 2 +- .../widget_types/solar_radiation_card_with_background.json | 2 +- .../json/system/widget_types/solar_radiation_chart_card.json | 2 +- .../solar_radiation_chart_card_with_background.json | 2 +- .../json/system/widget_types/sulfur_dioxide__so2__card.json | 2 +- .../widget_types/sulfur_dioxide__so2__card_with_background.json | 2 +- .../system/widget_types/sulfur_dioxide__so2__chart_card.json | 2 +- .../sulfur_dioxide__so2__chart_card_with_background.json | 2 +- .../main/data/json/system/widget_types/temperature_card.json | 2 +- .../system/widget_types/temperature_card_with_background.json | 2 +- .../data/json/system/widget_types/temperature_chart_card.json | 2 +- .../widget_types/temperature_chart_card_with_background.json | 2 +- .../main/data/json/system/widget_types/temperature_gauge.json | 2 +- .../data/json/system/widget_types/temperature_range_chart.json | 2 +- .../widget_types/temperature_range_chart_with_background.json | 2 +- .../src/main/data/json/system/widget_types/uv_index_card.json | 2 +- .../json/system/widget_types/uv_index_card_with_background.json | 2 +- .../main/data/json/system/widget_types/uv_index_chart_card.json | 2 +- .../widget_types/uv_index_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/vibration_card.json | 2 +- .../system/widget_types/vibration_card_with_background.json | 2 +- .../data/json/system/widget_types/vibration_chart_card.json | 2 +- .../widget_types/vibration_chart_card_with_background.json | 2 +- .../src/main/data/json/system/widget_types/visibility_card.json | 2 +- .../system/widget_types/visibility_card_with_background.json | 2 +- .../data/json/system/widget_types/visibility_chart_card.json | 2 +- .../widget_types/visibility_chart_card_with_background.json | 2 +- .../system/widget_types/volatile_organic_compounds_card.json | 2 +- .../volatile_organic_compounds_card_with_background.json | 2 +- .../widget_types/volatile_organic_compounds_chart_card.json | 2 +- .../volatile_organic_compounds_chart_card_with_background.json | 2 +- .../data/json/system/widget_types/wind_speed_and_direction.json | 2 +- .../widget_types/wind_speed_and_direction_with_background.json | 2 +- .../src/main/data/json/system/widget_types/wind_speed_card.json | 2 +- .../system/widget_types/wind_speed_card_with_background.json | 2 +- .../data/json/system/widget_types/wind_speed_chart_card.json | 2 +- .../widget_types/wind_speed_chart_card_with_background.json | 2 +- 361 files changed, 361 insertions(+), 361 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_card.json b/application/src/main/data/json/system/widget_types/air_quality_index_card.json index 9dc9a99194..11690f4387 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_card.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_card.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_card", "name": "Air quality index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_card_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json b/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json index 5bf555d92c..27b8f82118 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_card_with_background", "name": "Air quality index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_card_with_background_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json index 58ba002255..95058ecf29 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_chart_card", "name": "Air quality index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_chart_card_system_widget_image.png", "description": "Displays air quality index data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json index 43ddb147ac..50bf9e06ec 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_chart_card_with_background", "name": "Air quality index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_chart_card_with_background_system_widget_image.png", "description": "Displays air quality index data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json index eb2b8fde5f..d3632989e7 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_card", "name": "Carbon monoxide (CO) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-card.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json index 3c53033b15..ceef3f3ae6 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_card_with_background", "name": "Carbon monoxide (CO) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json index f1ac79ea11..dd712fcf75 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_chart_card", "name": "Carbon monoxide (CO) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-and-chart-card.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json index cddc48b6e4..d3c334f7b2 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_chart_card_with_background", "name": "Carbon monoxide (CO) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-chart-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_card.json b/application/src/main/data/json/system/widget_types/co2_card.json index 3edb6cb025..e7072077da 100644 --- a/application/src/main/data/json/system/widget_types/co2_card.json +++ b/application/src/main/data/json/system/widget_types/co2_card.json @@ -1,7 +1,7 @@ { "fqn": "co2_card", "name": "CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_card_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_card_with_background.json b/application/src/main/data/json/system/widget_types/co2_card_with_background.json index 8f7936e919..71670a437c 100644 --- a/application/src/main/data/json/system/widget_types/co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "co2_card_with_background", "name": "CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_card_with_background_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_chart_card.json b/application/src/main/data/json/system/widget_types/co2_chart_card.json index 70d06ffe37..4aff9d8f84 100644 --- a/application/src/main/data/json/system/widget_types/co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "co2_chart_card", "name": "CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_chart_card_system_widget_image.png", "description": "Displays a CO2 level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json index 3de7fadf01..4f0b248af9 100644 --- a/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "co2_chart_card_with_background", "name": "CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_chart_card_with_background_system_widget_image.png", "description": "Displays a CO2 level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_card.json b/application/src/main/data/json/system/widget_types/efficiency_card.json index 3ee0c4ea87..77f403ffed 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_card.json +++ b/application/src/main/data/json/system/widget_types/efficiency_card.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_card", "name": "Efficiency card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_card.svg", "description": "Displays the latest efficiency telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json index 31712b3b81..4fccc48fe2 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_card_with_background", "name": "Efficiency card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_card_with_background.svg", "description": "Displays the latest efficiency telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_chart_card.json b/application/src/main/data/json/system/widget_types/efficiency_chart_card.json index 298eddb21c..6a546bfd5e 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_chart_card.json +++ b/application/src/main/data/json/system/widget_types/efficiency_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_chart_card", "name": "Efficiency chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_chart_card.svg", "description": "Displays efficiency data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json index bd3b02b991..98391712de 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_chart_card_with_background", "name": "Efficiency chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_chart_card_with_background.svg", "description": "Displays efficiency data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json b/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json index e216dcd9be..594041df15 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_progress_bar", "name": "Efficiency progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_progress_bar.svg", "description": "Displays efficiency reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json index e52de65f59..fc727df8ac 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_progress_bar_with_background", "name": "Efficiency progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_progress_bar_with_background.svg", "description": "Displays efficiency reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_range_chart.json b/application/src/main/data/json/system/widget_types/efficiency_range_chart.json index ce08ee7c5b..f6451ec2bc 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_range_chart.json +++ b/application/src/main/data/json/system/widget_types/efficiency_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_range_chart", "name": "Efficiency range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_range_chart.svg", "description": "Displays changes to efficiency data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json index df6e41242c..5cf9870734 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_range_chart_with_background", "name": "Efficiency range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_range_chart_with_background.svg", "description": "Displays changes to efficiency data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_card.json b/application/src/main/data/json/system/widget_types/flooding_level_card.json index 9d9215cf0e..d86045112b 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_card.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_card.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_card", "name": "Flooding level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_card_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json index 20fa345a25..16d1b9f213 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_card_with_background", "name": "Flooding level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_card_with_background_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json b/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json index 5592656dc4..5317ceac0a 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_chart_card", "name": "Flooding level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_chart_card_system_widget_image.png", "description": "Displays flooding level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json index 5b7dfda3da..21cc83c7a1 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_chart_card_with_background", "name": "Flooding level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_chart_card_with_background_system_widget_image.png", "description": "Displays a flooding level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json index b081f8aa99..8ffc0eabbd 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_progress_bar", "name": "Flooding level progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_progress_bar_system_widget_image.png", "description": "Displays flooding level reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json index 2048b17063..b908112abe 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_progress_bar_with_background", "name": "Flooding level progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_progress_bar_with_background_system_widget_image.png", "description": "Displays flooding level reading as a horizontal progress bar with the background image. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_card.json b/application/src/main/data/json/system/widget_types/flow_rate_card.json index bfe1c34f15..8ffb240570 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_card.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_card.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_card", "name": "Flow rate card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_card.svg", "description": "Displays the latest flow rate telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json index cf127d5bf7..68e4888388 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_card_with_background", "name": "Flow rate card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_card_with_background.svg", "description": "Displays the latest flow rate telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json b/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json index df5c63b14e..4beefb2187 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_chart_card", "name": "Flow rate chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_chart_card.svg", "description": "Displays flow rate data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json index 0ffb5a7122..5a8ad8064e 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_chart_card_with_background", "name": "Flow rate chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_chart_card_with_background.svg", "description": "Displays flow rate data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_gauge.json b/application/src/main/data/json/system/widget_types/flow_rate_gauge.json index 83176686ac..1f1ac5769e 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_gauge.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_gauge.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_gauge", "name": "Flow rate gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_gauge.svg", "description": "Flow rate gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json index 8e63ab04e8..70b6c98d68 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_progress_bar", "name": "Flow rate progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_progress_bar_(1).svg", "description": "Displays flow rate reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json index d7ee1333e3..8ce5c56e10 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_progress_bar_with_background", "name": "Flow rate progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_progress_bar_with_background.svg", "description": "Displays flow rate reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json b/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json index c19becc363..90b936ad95 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_range_chart", "name": "Flow rate range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_range_chart.svg", "description": "Displays changes to flow rate data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json index f5b7d437b2..7383538dee 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_range_chart_with_background", "name": "Flow rate range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_range_chart_with_background.svg", "description": "Displays changes to flow rate data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_card.json b/application/src/main/data/json/system/widget_types/fluid_pressure_card.json index 2c881da205..5d93544cac 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_card", "name": "Pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card.svg", "description": "Displays the latest fluid pressure telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json index d059d9b026..668e83a251 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_card_with_background", "name": "Pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card_with_background.svg", "description": "Displays the latest fluid pressure telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json index ee27a6b376..c6de6acc35 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_chart_card", "name": "Pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card.svg", "description": "Displays fluid pressure data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json index e9e9fede9a..122b553773 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_chart_card_with_background", "name": "Pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card_with_background.svg", "description": "Displays fluid pressure data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json b/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json index 98318d30a4..4efbbb4104 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_gauge", "name": "Pressure gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_gauge.svg", "description": "Fluid pressure gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json index 0a4e80dd60..b8d00d058a 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_progress_bar", "name": "Pressure progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_progress_bar_(1).svg", "description": "Displays fluid pressure reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json index 8000ff00af..63a5ba0560 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_progress_bar_with_background", "name": "Pressure progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/progress_bar_with_background.svg", "description": "Displays fluid pressure reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json index fd948984c9..2f4c231e92 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_range_chart", "name": "Pressure range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_range_chart_(1).svg", "description": "Displays changes to fluid pressure data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json index a8e3c8df2c..efb0aea62e 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_range_chart_with_background", "name": "Pressure range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_range_chart_with_background.svg", "description": "Displays changes to fluid pressure data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_card.json b/application/src/main/data/json/system/widget_types/ground_temperature_card.json index 77c2c0ccfb..b5f63e8999 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_card", "name": "Ground temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_card_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json index 3263548bb2..85ec331c3b 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_card_with_background", "name": "Ground temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json index 949e48f9de..bee76a8789 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_chart_card", "name": "Ground temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_chart_card_system_widget_image.png", "description": "Displays a ground temperature data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json index 06bf06bd98..acef304a20 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_chart_card_with_background", "name": "Ground temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays a ground temperature data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json index 8190e9fcaf..3edfcd5f0d 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_air_quality_card", "name": "Horizontal air quality index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_air_quality_index_card_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json index 2bf3dcbe23..4067793511 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_air_quality_card_with_background", "name": "Horizontal air quality index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_air_quality_index_card_with_background_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json index 02482fb5c9..12ec735556 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_carbon_monoxide_co_card", "name": "Horizontal carbon monoxide (CO) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-card-horizontal.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json index 38391e23d6..bc47432672 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_carbon_monoxide_co_card_with_background", "name": "Horizontal carbon monoxide (CO) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-horizontal-value-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_co2_card.json b/application/src/main/data/json/system/widget_types/horizontal_co2_card.json index e6c8e1eb6d..26096b638d 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_co2_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_co2_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_co2_card", "name": "Horizontal CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_co2_card_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json index 34a4e1aba4..5e634879fe 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_co2_card_with_background", "name": "Horizontal CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_co2_card_with_background_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json index a955d68869..7f2b9ec764 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_efficiency_card", "name": "Horizontal efficiency card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_efficiency_card.svg", "description": "Displays the latest efficiency telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json index 78f8863965..3b6368de9e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_efficiency_card_with_background", "name": "Horizontal efficiency card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_efficiency_card_with_background.svg", "description": "Displays the latest efficiency telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json index 333d983be9..b66bdeee76 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flooding_level_card", "name": "Horizontal flooding level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flooding_level_card_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json index 7c6794b4f7..883bc4b617 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flooding_level_card_with_background", "name": "Horizontal flooding level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flooding_level_card_with_background_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json index 15aca34b02..78e31e13c0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flow_rate_card", "name": "Horizontal flow rate card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flow_rate_card.svg", "description": "Displays the latest flow rate telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json index 4f6de487cd..aac479c826 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flow_rate_card_with_background", "name": "Horizontal flow rate card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flow_rate_card_with_background.svg", "description": "Displays the latest flow rate telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json index b9432be3ca..d7b5896e2d 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_fluid_pressure_card", "name": "Horizontal pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card.svg", "description": "Displays the latest fluid pressure telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json index 5598b8ed1c..4c23275ccb 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_fluid_pressure_card_with_background", "name": "Horizontal pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card_with_background.svg", "description": "Displays the latest fluid pressure telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json index 6b0f064707..fbde48637c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ground_temperature_card", "name": "Horizontal ground temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_ground_temperature_card_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json index bb03947926..0ef886521f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ground_temperature_card_with_background", "name": "Horizontal ground temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_ground_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json b/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json index fa576d2ee0..cb6fe5e5ef 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_humidity_card", "name": "Horizontal humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_humidity_card_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json index 450d4046f3..8abeddf248 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_humidity_card_with_background", "name": "Horizontal humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_humidity_card_with_background_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json index 456268a8fa..b985882e79 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_illuminance_card", "name": "Horizontal illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_illuminance_card_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json index a3ff36784f..1cf61643b7 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_illuminance_card_with_background", "name": "Horizontal illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json index 8cd5159aab..22724cee64 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_individual_allergy_index_iai_card", "name": "Horizontal individual allergy index (IAI) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card-horizontal.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json index 8cc3664ad4..1e9d4d0cd1 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_individual_allergy_index_iai_card_with_background", "name": "Horizontal individual allergy index (IAI) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card-horizontal-with-background.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json index f31a0a7bb3..8badf0af91 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_leaf_wetness_card", "name": "Horizontal leaf wetness card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_leaf_wetness_card_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json index 637c6182cc..1d663cc004 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_leaf_wetness_card_with_background", "name": "Horizontal leaf wetness card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_leaf_wetness_card_with_background_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json index ce820784bd..f4898088a8 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_nitrogen_dioxide_no2_card", "name": "Horizontal nitrogen dioxide (NO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-card-horizontal.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json index d36eb56e95..668b865160 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_nitrogen_dioxide_no2_card_with_background", "name": "Horizontal nitrogen dioxide (NO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-horizontal-value-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json index a6a5cc1004..c124103ec9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_noise_level_card", "name": "Horizontal noise level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_noise_level_card_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json index 02d041e1d6..f8cd8498ab 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_noise_level_card_with_background", "name": "Horizontal noise level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_noise_level_card_with_background_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json index 61a63dd7b6..b6a04e0e3f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ozone_o3_card", "name": "Horizontal ozone (O3) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card-horizontal.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json index 0a0886ca7b..14a0bebdfb 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ozone_o3_card_with_background", "name": "Horizontal ozone (O3) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card-horizontal-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json b/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json index 5f9eb7d645..263c451c0a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm10_card", "name": "Horizontal PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm10_card_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json index ca3b39eac9..4a38b9685b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm10_card_with_background", "name": "Horizontal PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm10_card_with_background_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json index 617031cc7e..97fa858610 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm2_5_card", "name": "Horizontal PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm2_5_card_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json index f85406bd75..c50422d915 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm2_5_card_with_background", "name": "Horizontal PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json index b7d93c0cbc..b8d213bd17 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_power_consumption_card", "name": "Horizontal power consumption card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_power_consumption_card.svg", "description": "Displays the latest power consumption telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json index d74452d787..a7f40e0d8a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_power_consumption_card_with_background", "name": "Horizontal power consumption card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_power_consumption_card_with_background.svg", "description": "Displays the latest power consumption telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json b/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json index 64ab997988..cdbdde4f54 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pressure_card", "name": "Horizontal pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json index 0b4d9dbf35..9a70aa0a70 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pressure_card_with_background", "name": "Horizontal pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card_with_background_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json index de91fdde12..03b9dc4a07 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pump_vibration_card", "name": "Horizontal vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card.svg", "description": "Displays the latest vibration telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json index c787de5a2c..97eaf6e5f6 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pump_vibration_card_with_background", "name": "Horizontal vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card_with_background.svg", "description": "Displays the latest vibration telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json index e5edbd240c..46d233d5b2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_radon_level_card", "name": "Horizontal radon level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_radon_level_card_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json index f15bce1cb9..a76fea03ad 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_radon_level_card_with_background", "name": "Horizontal radon level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_radon_level_card_with_background_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json index 22a7cf3675..8be07dd505 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rainfall_card", "name": "Horizontal rainfall card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rainfall_card_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json index 19f0333ac8..9283ed79d0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rainfall_card_with_background", "name": "Horizontal rainfall card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rainfall_card_with_background_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json index 6e041e0fd8..d229baa2f4 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rotational_speed_card", "name": "Horizontal rotational speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rotational_speed_card.svg", "description": "Displays the latest rotational speed telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json index 90cc5dbb51..63f8b4c7e3 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rotational_speed_card_with_background", "name": "Horizontal rotational speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rotational_speed_card_with_background.svg", "description": "Displays the latest rotational speed telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json index d94b6c218a..8eced60303 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_snow_depth_card", "name": "Horizontal snow depth card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_snow_depth_card_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json index da6e3f16cb..6a5f14f880 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_snow_depth_card_with_background", "name": "Horizontal snow depth card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_snow_depth_card_with_background_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json index 7c8094006c..5f4d34bf96 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_soil_moisture_card", "name": "Horizontal soil moisture card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_soil_moisture_card_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json index 970acb6bc6..22a6736053 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_soil_moisture_card_with_background", "name": "Horizontal soil moisture card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_soil_moisture_card_with_background_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json index c9b3cbfa93..3cda84d737 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_solar_radiation_card", "name": "Horizontal solar radiation card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_solar_radiation_card_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json index 7220911d6d..41238026ff 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_solar_radiation_card_with_background", "name": "Horizontal solar radiation card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_solar_radiation_card_with_background_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json index ad8d850963..1376a9568a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_sulfur_dioxide_so2_card", "name": "Horizontal sulfur dioxide (SO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-card-horizontal.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json index f8c2601a28..e037518ed2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_sulfur_dioxide_so2_card_with_background", "name": "Horizontal sulfur dioxide (SO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-horizontal-value-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json b/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json index d38d5412bd..7d9fcb7a84 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_temperature_card", "name": "Horizontal temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_temperature_card_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json index 6ed35afab5..392eb560cf 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_temperature_card_with_background", "name": "Horizontal temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json index 57eed149c4..63abd84c1e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_uv_index_card", "name": "Horizontal UV Index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_uv_index_card_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json index 2a542e7c62..7f27b72bd0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_uv_index_card_with_background", "name": "Horizontal UV Index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_uv_index_card_with_background_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json b/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json index df72fa560f..cd932353ec 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_vibration_card", "name": "Horizontal vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json index c7137ba291..7aa076dfca 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_vibration_card_with_background", "name": "Horizontal vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card_with_background_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json b/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json index f36ba0adca..4b8c4d4388 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_visibility_card", "name": "Horizontal visibility card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_visibility_card_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json index e278d5c9a6..a30af605a5 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_visibility_card_with_background", "name": "Horizontal visibility card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_visibility_card_with_background_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json index cccfe7c18b..67ea3e5493 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_volatile_organic_compounds_card", "name": "Horizontal volatile organic compounds card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_volatile_organic_compounds_card_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json index a97f139a51..3bc9184668 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_volatile_organic_compounds_card_with_background", "name": "Horizontal volatile organic compounds card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_volatile_organic_compounds_card_with_background_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json index 9883066d90..8840fd39f7 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_wind_speed_card", "name": "Horizontal wind speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_wind_speed_card_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json index 0b6700ebc8..d5f073b5cf 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_wind_speed_card_with_background", "name": "Horizontal wind speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_wind_speed_card_with_background_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_card.json b/application/src/main/data/json/system/widget_types/humidity_card.json index 28b7c88e7c..7bfcfbfb83 100644 --- a/application/src/main/data/json/system/widget_types/humidity_card.json +++ b/application/src/main/data/json/system/widget_types/humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "humidity_card", "name": "Humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_card_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/humidity_card_with_background.json index 2f33b5ec33..1a95ef1ae4 100644 --- a/application/src/main/data/json/system/widget_types/humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "humidity_card_with_background", "name": "Humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_card_with_background_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_chart_card.json b/application/src/main/data/json/system/widget_types/humidity_chart_card.json index dad80f9797..2bbfa4c92b 100644 --- a/application/src/main/data/json/system/widget_types/humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "humidity_chart_card", "name": "Humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_chart_card_system_widget_image.png", "description": "Displays a humidity data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json index be13ef3285..a625836f1c 100644 --- a/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "humidity_chart_card_with_background", "name": "Humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_chart_card_with_background_system_widget_image.png", "description": "Displays a humidity data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_progress_bar.json b/application/src/main/data/json/system/widget_types/humidity_progress_bar.json index b8d80a2040..4f867635fe 100644 --- a/application/src/main/data/json/system/widget_types/humidity_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/humidity_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "humidity_progress_bar", "name": "Humidity progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_progress_bar_system_widget_image.png", "description": "Displays humidity reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json index 4d3ddc7d21..da3c2e4984 100644 --- a/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "humidity_progress_bar_with_background", "name": "Humidity progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_progress_bar_with_background_system_widget_image.png", "description": "Displays humidity reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_card.json b/application/src/main/data/json/system/widget_types/illuminance_card.json index 6e24dcaa4a..5194b4fb54 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_card", "name": "Illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_card_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json index 0c990bb1b9..fd61f306dc 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_card_with_background", "name": "Illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/illuminance_chart_card.json index f664cc0d27..68d4da8c02 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_chart_card", "name": "Illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_chart_card_system_widget_image.png", "description": "Displays a illuminance data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json index 7849cad5e5..1e3e0a6545 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_chart_card_with_background", "name": "Illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays a illuminance data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json b/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json index de3b03cf95..f85547d66a 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_progress_bar", "name": "Illuminance progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_progress_bar_system_widget_image.png", "description": "Displays illuminance reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json index 181208cb24..941617becb 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_progress_bar_with_background", "name": "Illuminance progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_progress_bar_with_background_system_widget_image.png", "description": "Displays illuminance reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json index a578136d2b..b2736d88b7 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_card", "name": "Individual allergy index (IAI) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json index 7f6cfe39a5..76dd5bd73a 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_card_with_background", "name": "Individual allergy index (IAI) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card-with-background.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json index 13d72efffe..1face8a5e2 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_iai_chart_card", "name": "Individual allergy index (IAI) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-chart-card.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json index 2fcbe2bed8..39aa442b03 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_iai_chart_card_with_background", "name": "Individual allergy index (IAI) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-chart-card-with-background.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_card.json b/application/src/main/data/json/system/widget_types/indoor_co2_card.json index 1133460238..a066aa2a12 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_card", "name": "Indoor CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_card_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json index e1ddcb3b6b..25019aef1d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_card_with_background", "name": "Indoor CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_card_with_background_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json index ba1df65908..f4776e2601 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_chart_card", "name": "Indoor CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_chart_card_system_widget_image.png", "description": "Displays a indoor CO2 level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json index c4abd4a0b6..a4adcfbe14 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_chart_card_with_background", "name": "Indoor CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor CO2 level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json index b453998ddb..6234b2554a 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_co2_card", "name": "Indoor horizontal CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_co2_card_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json index 4ca050fd88..eee2ed64a6 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_co2_card_with_background", "name": "Indoor horizontal CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_co2_card_with_background_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json index 0c62fdfe5b..918084de45 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_humidity_card", "name": "Indoor horizontal humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_humidity_card_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json index 04f8faefcb..8f274993e5 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_humidity_card_with_background", "name": "Indoor horizontal humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_humidity_card_with_background_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json index b921411cd8..b3776fcedd 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_illuminance_card", "name": "Indoor horizontal illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_illuminance_card_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json index d81be63a9c..c68c2775cb 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_illuminance_card_with_background", "name": "Indoor horizontal illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json index 5d315d0e01..d32018de8e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm10_card", "name": "Indoor horizontal PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm10_card_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json index ea3833494c..18318d08b6 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm10_card_with_background", "name": "Indoor horizontal PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm10_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json index 5ae0d04108..502882696b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm2_5_card", "name": "Indoor horizontal PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm2_5_card_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json index 6e47de67a8..a691db2695 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm2_5_card_with_background", "name": "Indoor horizontal PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json index 4b5f76317c..47eb89501a 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_temperature_card", "name": "Indoor horizontal temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_temperature_card_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json index 1b1231720f..a49bd7e292 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_temperature_card_with_background", "name": "Indoor horizontal temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_card.json b/application/src/main/data/json/system/widget_types/indoor_humidity_card.json index 1310fcf9e0..84b45af23d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_card", "name": "Indoor humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_card_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json index 998d0cbe8b..4ebffddbdc 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_card_with_background", "name": "Indoor humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_card_with_background_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json index 267cc65a5f..b3f0909bc0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_chart_card", "name": "Indoor humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_chart_card_system_widget_image.png", "description": "Displays a indoor humidity data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json index 82c9bbeaf6..2517b1b1d0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_chart_card_with_background", "name": "Indoor humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor humidity data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json index bc02349832..1f5e04ed77 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_progress_bar", "name": "Indoor humidity progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_progress_bar_system_widget_image.png", "description": "Displays indoor humidity reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json index 8f3f721ec5..1cb3a1dd52 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_progress_bar_with_background", "name": "Indoor humidity progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_progress_bar_with_background_system_widget_image.png", "description": "Displays indoor humidity reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json index 73732e8f4b..648007f40e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_card", "name": "Indoor illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_card_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json index b6f143b9f8..e34daef9a7 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_card_with_background", "name": "Indoor illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json index 9ea21c112d..bad4adb97d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_chart_card", "name": "Indoor illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_chart_card_system_widget_image.png", "description": "Displays a indoor illuminance data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json index 6badbfbfa0..5df2e86c84 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_chart_card_with_background", "name": "Indoor illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor illuminance data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json index d58ec4f73d..027e771121 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_progress_bar", "name": "Indoor illuminance progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_progress_bar_system_widget_image.png", "description": "Displays indoor illuminance reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json index 676428dbcd..7cb00cb4e6 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_progress_bar_with_background", "name": "Indoor illuminance progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_progress_bar_with_background_system_widget_image.png", "description": "Displays indoor illuminance reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_card.json b/application/src/main/data/json/system/widget_types/indoor_pm10_card.json index 1a58d29bd4..d042fe5e69 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_card", "name": "Indoor PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_card_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json index 14f1445ebc..eead478319 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_card_with_background", "name": "Indoor PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json index b04b7063cf..2f88f05151 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_chart_card", "name": "Indoor PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_chart_card_system_widget_image.png", "description": "Displays a indoor fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json index 9eb69e6a26..d29a9d09d4 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_chart_card_with_background", "name": "Indoor PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json index 990b7caecc..74044c2cbf 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_card", "name": "Indoor PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_card_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json index ef1fde22b2..43039d7a1f 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_card_with_background", "name": "Indoor PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json index 82d4e3af9a..d2507c6942 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_chart_card", "name": "Indoor PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_chart_card_system_widget_image.png", "description": "Displays a indoor fine particulate matter (PM2.5) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json index 9ee11386e6..32b78f75a6 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_chart_card_with_background", "name": "Indoor PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor fine particulate matter (PM2.5) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json index 0a442fa4e0..8d783e8a3b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_co2_chart_card", "name": "Indoor simple CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_co2_chart_card_system_widget_image.png", "description": "Displays historical indoor CO2 level values as a simplified chart. Optionally may display the corresponding latest indoor CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json index 537f95b673..9c0f5f4f82 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_co2_chart_card_with_background", "name": "Indoor simple CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_co2_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor CO2 level values as a simplified chart with background. Optionally may display the corresponding latest indoor CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json index eb278e4c0f..85d089175e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_humidity_chart_card", "name": "Indoor simple humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_humidity_chart_card_system_widget_image.png", "description": "Displays historical indoor humidity values as a simplified chart. Optionally may display the corresponding latest indoor humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json index eda7d8919d..56089749a1 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_humidity_chart_card_with_background", "name": "Indoor simple humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_humidity_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor humidity values as a simplified chart with background. Optionally may display the corresponding latest indoor humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json index 4362cdf807..f6ee30f395 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_illuminance_chart_card", "name": "Indoor simple illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_illuminance_chart_card_system_widget_image.png", "description": "Displays historical indoor illuminance values as a simplified chart. Optionally may display the corresponding latest indoor illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json index 5652a3693f..3f873c6c20 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_illuminance_chart_card_with_background", "name": "Indoor simple illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor illuminance values as a simplified chart with background. Optionally may display the corresponding latest indoor illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json index 995181bff6..76092d54c6 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm10_chart_card", "name": "Indoor simple PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm10_chart_card_system_widget_image.png", "description": "Displays historical indoor fine and coarse particulate matter (PM10) values as a simplified chart. Optionally may display the corresponding latest indoor PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json index 26b4c74ca6..7ce70c7db5 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm10_chart_card_with_background", "name": "Indoor simple PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm10_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor fine and coarse particulate matter (PM10) values as a simplified chart with background. Optionally may display the corresponding latest indoor PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json index 550bb8d03e..f949e1a067 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm2_5_chart_card", "name": "Indoor simple PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm2_5_chart_card_system_widget_image.png", "description": "Displays historical indoor fine particulate matter (PM2.5) values as a simplified chart. Optionally may display the corresponding latest indoor PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json index 368e31b0f6..8cf7d64949 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm2_5_chart_card_with_background", "name": "Indoor simple PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor fine particulate matter (PM2.5) values as a simplified chart with background. Optionally may display the corresponding latest indoor PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json index 3b118b8994..4f90b1ffa0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_temperature_chart_card", "name": "Indoor simple temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_temperature_chart_card_system_widget_image.png", "description": "Displays historical indoor temperature values as a simplified chart. Optionally may display the corresponding latest indoor temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json index e62cbba7fd..016efeda0d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_temperature_chart_card_with_background", "name": "Indoor simple temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor temperature values as a simplified chart with background. Optionally may display the corresponding latest indoor temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_card.json b/application/src/main/data/json/system/widget_types/indoor_temperature_card.json index 5f55ace191..5faf1891d7 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_card", "name": "Indoor temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_card_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json index 23466d55ec..6e33a75f37 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_card_with_background", "name": "Indoor temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json index c24cb6e5eb..750842be0e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_chart_card", "name": "Indoor temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_chart_card_system_widget_image.png", "description": "Displays a indoor temperature data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json index 96e606d3b7..69d97ebbf3 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_chart_card_with_background", "name": "Indoor temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor temperature data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json b/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json index a935befa6a..429a3b1c9e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_gauge", "name": "Indoor temperature gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_gauge.svg", "description": "Indoor temperatue gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json index ae5196ce89..01c7174de8 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_progress_bar", "name": "Indoor temperature progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_progress_bar_system_widget_image.png", "description": "Displays indoor temperature reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json index e2b6443efe..9f954dbc3b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_progress_bar_with_background", "name": "Indoor temperature progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_progress_bar_with_background_system_widget_image.png", "description": "Displays indoor temperature reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json index 4dcf322cb7..84cd1c9c7e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_range_chart", "name": "Indoor temperature range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_range_chart.svg", "description": "Displays changes to indoor temperature data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json index e8a0752aeb..07477b9c5e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_range_chart_with_background", "name": "Indoor temperature range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_range_chart_with_background.svg", "description": "Displays changes to indoor temperature data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_card.json b/application/src/main/data/json/system/widget_types/leaf_wetness_card.json index 65109597fd..5ed2af2184 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_card.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_card.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_card", "name": "Leaf wetness card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_card_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json index 3258de6b32..aeb98e9119 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_card_with_background", "name": "Leaf wetness card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_card_with_background_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json index 235a468a74..a6efbe6cac 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_chart_card", "name": "Leaf wetness chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_chart_card_system_widget_image.png", "description": "Displays a leaf wetness data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json index 3c9f15b0b2..e62ad0cc5f 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_chart_card_with_background", "name": "Leaf wetness chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_chart_card_with_background_system_widget_image.png", "description": "Displays a leaf wetness data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json index fe3919e6bf..55692ea464 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_progress_bar", "name": "Leaf wetness progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_progress_bar_system_widget_image.png", "description": "Displays leaf wetness reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json index 271dca2485..cd854fc08c 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_progress_bar_with_background", "name": "Leaf wetness progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_progress_bar_with_background_system_widget_image.png", "description": "Displays leaf wetness reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json index 95605deae6..11c90052d6 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_card", "name": "Nitrogen dioxide (NO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-card.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json index 7fee7b991a..ba6b1d630c 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_card_with_background", "name": "Nitrogen dioxide (NO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json index 39def6e43b..b4089e045c 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_chart_card", "name": "Nitrogen dioxide (NO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-chart-card.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json index e48517f3bb..362ff731bb 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_chart_card_with_background", "name": "Nitrogen dioxide (NO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-chart-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_card.json b/application/src/main/data/json/system/widget_types/noise_level_card.json index 4b214b63a0..6cb7dbbfba 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_card.json +++ b/application/src/main/data/json/system/widget_types/noise_level_card.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_card", "name": "Noise level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_card_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json b/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json index 564d3d665e..7ac2c36c8b 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_card_with_background", "name": "Noise level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_card_with_background_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_chart_card.json b/application/src/main/data/json/system/widget_types/noise_level_chart_card.json index 00da830302..bfd06d8760 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/noise_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_chart_card", "name": "Noise level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_chart_card_system_widget_image.png", "description": "Displays a noise level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json index 032e34c994..dd617a1764 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_chart_card_with_background", "name": "Noise level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_chart_card_with_background_system_widget_image.png", "description": "Displays a noise level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__card.json b/application/src/main/data/json/system/widget_types/ozone__o3__card.json index 77f12bc288..e5e98b0ef7 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__card.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__card.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_card", "name": "Ozone (O3) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json b/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json index 02145f1e0a..36568288eb 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_card_with_background", "name": "Ozone (O3) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json index c4a3e47767..cac726d689 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_chart_card", "name": "Ozone (O3) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-chart-card.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json index 6037c4454c..e7b46fa7ee 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_chart_card_with_background", "name": "Ozone (O3) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-chart-card-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_card.json b/application/src/main/data/json/system/widget_types/pm10_card.json index 3412808d60..d953d7db67 100644 --- a/application/src/main/data/json/system/widget_types/pm10_card.json +++ b/application/src/main/data/json/system/widget_types/pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "pm10_card", "name": "PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_card_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/pm10_card_with_background.json index fbb8c7251e..fe069ceba8 100644 --- a/application/src/main/data/json/system/widget_types/pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm10_card_with_background", "name": "PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_card_with_background_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_chart_card.json b/application/src/main/data/json/system/widget_types/pm10_chart_card.json index 5014d27f7c..e4b30f0a91 100644 --- a/application/src/main/data/json/system/widget_types/pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pm10_chart_card", "name": "PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_chart_card_system_widget_image.png", "description": "Displays a fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json index 8f2ba1d14e..23ae1523e1 100644 --- a/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm10_chart_card_with_background", "name": "PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_chart_card_with_background_system_widget_image.png", "description": "Displays a fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_card.json b/application/src/main/data/json/system/widget_types/pm2_5_card.json index 0770923327..b281cf43df 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_card", "name": "PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_card_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json index e82a840d27..12f572163f 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_card_with_background", "name": "PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json index eff5191922..b23e40011e 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_chart_card", "name": "PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_chart_card_system_widget_image.png", "description": "Displays a fine particulate matter (PM2.5) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json index cdfac469eb..3a6303d581 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_chart_card_with_background", "name": "PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays a fine particulate matter (PM2.5) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_card.json b/application/src/main/data/json/system/widget_types/power_consumption_card.json index e99cc76bb4..e743d32d35 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_card.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_card.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_card", "name": "Power consumption card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_card.svg", "description": "Displays the latest power consumption telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json index 2f08a16a5b..c95eec0e17 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_card_with_background", "name": "Power consumption card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_card_with_background.svg", "description": "Displays the latest power consumption telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json b/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json index 1009371fe6..13106bdf7e 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_chart_card", "name": "Power consumption chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_chart_card.svg", "description": "Displays power consumption data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json index c006f600fc..5df45ff182 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_chart_card_with_background", "name": "Power consumption chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_chart_card_with_background.svg", "description": "Displays power consumption data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json b/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json index bc295c570f..878f0c5083 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_range_chart", "name": "Power consumption range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_range_chart.svg", "description": "Displays changes to power consumption data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json index 3ebcbb96c9..347938d831 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_range_chart_with_background", "name": "Power consumption range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_range_chart_with_background.svg", "description": "Displays changes to power consumption data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_card.json b/application/src/main/data/json/system/widget_types/pressure_card.json index 9138ab6391..3d5afbd1d9 100644 --- a/application/src/main/data/json/system/widget_types/pressure_card.json +++ b/application/src/main/data/json/system/widget_types/pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "pressure_card", "name": "Pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/pressure_card_with_background.json index d87db84e8a..fa82b7790c 100644 --- a/application/src/main/data/json/system/widget_types/pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pressure_card_with_background", "name": "Pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card_with_background_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_chart_card.json b/application/src/main/data/json/system/widget_types/pressure_chart_card.json index e0ea0e9895..0ec733b6de 100644 --- a/application/src/main/data/json/system/widget_types/pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pressure_chart_card", "name": "Pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card_system_widget_image.png", "description": "Displays a pressure data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json index 58c5859931..d5aefcba58 100644 --- a/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pressure_chart_card_with_background", "name": "Pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card_with_background_system_widget_image.png", "description": "Displays a pressure data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_progress_bar.json b/application/src/main/data/json/system/widget_types/pressure_progress_bar.json index 93d19780ae..652b18c60b 100644 --- a/application/src/main/data/json/system/widget_types/pressure_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/pressure_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "pressure_progress_bar", "name": "Pressure progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_progress_bar_system_widget_image.png", "description": "Displays pressure reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json index e5189912d2..6ed924e65d 100644 --- a/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pressure_progress_bar_with_background", "name": "Pressure progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_progress_bar_with_background_system_widget_image.png", "description": "Displays pressure reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_card.json b/application/src/main/data/json/system/widget_types/pump_vibration_card.json index 414cfaeb36..9ec50e2b1d 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_card", "name": "Vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card.svg", "description": "Displays the latest vibration telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json index c3e90f61a3..3b16aafca2 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_card_with_background", "name": "Vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card_with_background.svg", "description": "Displays the latest vibration telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json index acbe3524e0..c887086f5a 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_chart_card", "name": "Vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card.svg", "description": "Displays vibration data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json index 46c249f200..78813ec690 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_chart_card_with_background", "name": "Vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card_with_background.svg", "description": "Displays vibrarion data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json index 561130c14e..735484b0eb 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_range_chart", "name": "Vibration range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_range_chart.svg", "description": "Displays changes to vibration data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json index 27ba578048..86667b8b5a 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_range_chart_with_background", "name": "Vibration range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_range_chart_with_background.svg", "description": "Displays changes to vibration data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_card.json b/application/src/main/data/json/system/widget_types/radon_level_card.json index 8c8acc6a88..804e24d0d3 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_card.json +++ b/application/src/main/data/json/system/widget_types/radon_level_card.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_card", "name": "Radon level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_card_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json b/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json index 9f5b6f79ad..8573966948 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_card_with_background", "name": "Radon level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_card_with_background_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_chart_card.json b/application/src/main/data/json/system/widget_types/radon_level_chart_card.json index d76f5d2d9f..7a3a2a4c74 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/radon_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_chart_card", "name": "Radon level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_chart_card_system_widget_image.png", "description": "Displays a radon level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json index aded058cd6..807cf86888 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_chart_card_with_background", "name": "Radon level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_chart_card_with_background_system_widget_image.png", "description": "Displays a radon level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_card.json b/application/src/main/data/json/system/widget_types/rainfall_card.json index 04ce14806b..a6e83db315 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_card.json +++ b/application/src/main/data/json/system/widget_types/rainfall_card.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_card", "name": "Rainfall card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_card_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json b/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json index 81ede92ad1..7d3aec5759 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_card_with_background", "name": "Rainfall card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_card_with_background_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_chart_card.json b/application/src/main/data/json/system/widget_types/rainfall_chart_card.json index b5bd51ad00..9aa17966d6 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_chart_card.json +++ b/application/src/main/data/json/system/widget_types/rainfall_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_chart_card", "name": "Rainfall chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_chart_card_system_widget_image.png", "description": "Displays a rainfall data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json index f25ba166f7..d70028dff0 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_chart_card_with_background", "name": "Rainfall chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_chart_card_with_background_system_widget_image.png", "description": "Displays a rainfall data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_card.json b/application/src/main/data/json/system/widget_types/rotational_speed_card.json index 681d53c66d..05dd27a0bd 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_card.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_card", "name": "Rotational speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_card.svg", "description": "Displays the latest rotational speed telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json index efaa958688..c48ad0c699 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_card_with_background", "name": "Rotational speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_card_with_background.svg", "description": "Displays the latest rotational speed telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json index d5c48138f2..13781dcc3b 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_chart_card", "name": "Rotational speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_chart_card.svg", "description": "Displays rotational speed data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json index 1e2a0c2397..b99138d7eb 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_chart_card_with_background", "name": "Rotational speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_chart_card_with_background.svg", "description": "Displays rotational speed data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json b/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json index f42d801090..6cc99263e8 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_gauge", "name": "Rotational speed gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_gauge.svg", "description": "Rotational speed gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json index 7288b70478..ec57b21f9a 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_progress_bar", "name": "Rotational speed progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_progress_bar.svg", "description": "Displays rotational speed reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json index c8d2f0e188..90aa6db6b6 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_progress_bar_with_background", "name": "Rotational speed progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_progress_bar_with_background.svg", "description": "Displays rotational speed reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json index bcbeb8c9a8..02b7279d0b 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_range_chart", "name": "Rotational speed range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_range_chart.svg", "description": "Displays changes to rotational speed data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json index 537314bd0d..c4db34c71f 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_range_chart_with_background", "name": "Rotational speed range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_range_chart_with_background.svg", "description": "Displays changes to rotational speed data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json index 812aa8694b..7145a8be5d 100644 --- a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_air_quality_chart_card", "name": "Simple air quality index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_air_quality_index_chart_card_system_widget_image.png", "description": "Displays historical air quality index values as a simplified chart. Optionally may display the corresponding latest air quality index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json index 1c22c060f2..1d0df6e671 100644 --- a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_air_quality_chart_card_with_background", "name": "Simple air quality index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_air_quality_index_chart_card_with_background_system_widget_image.png", "description": "Displays historical air quality index values as a simplified chart with background. Optionally may display the corresponding latest air quality index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json index d494338fee..838e7d79ab 100644 --- a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_carbon_monoxide_co_chart_card", "name": "Simple carbon monoxide (CO) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-simple-value-and-chart-card.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json index 66364f8d06..d64b6cd6aa 100644 --- a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_carbon_monoxide_co_chart_card_with_background", "name": "Simple carbon monoxide (CO) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-simple-chart-value-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json b/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json index 9e4be18356..f90c813214 100644 --- a/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_co2_chart_card", "name": "Simple CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_co2_chart_card_system_widget_image.png", "description": "Displays historical CO2 level values as a simplified chart. Optionally may display the corresponding latest CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json index d6ef0088b4..6c87c52552 100644 --- a/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_co2_chart_card_with_background", "name": "Simple CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_co2_chart_card_with_background_system_widget_image.png", "description": "Displays historical CO2 level values as a simplified chart with background. Optionally may display the corresponding latest CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json index d957a82a58..03d8d320f6 100644 --- a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_efficiency_chart_card", "name": "Simple efficiency chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_efficiency_chart_card.svg", "description": "Displays historical efficiency values as a simplified chart. Optionally may display the corresponding efficiency value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json index 9fe02a7725..f0b6f95cf1 100644 --- a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_efficiency_chart_card_with_background", "name": "Simple efficiency chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_efficiency_chart_card_with_background.svg", "description": "Displays historical efficiency values as a simplified chart with background. Optionally may display the corresponding efficiency value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json index 55a4da3aba..a00bf0a1bd 100644 --- a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_flooding_level_chart_card", "name": "Simple flooding level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flooding_level_chart_card_system_widget_image.png", "description": "Displays historical flooding level values as a simplified chart. Optionally may display the corresponding latest flooding level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json index dab9a38433..d1a6af6c4c 100644 --- a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_flooding_level_chart_card_with_background", "name": "Simple flooding level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flooding_level_chart_card_with_background_system_widget_image.png", "description": "Displays historical flooding level values as a simplified chart with background. Optionally may display the corresponding latest flooding level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json index 05fd114507..455972e0b4 100644 --- a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_flow_rate_chart_card", "name": "Simple flow rate chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flow_rate_chart_card_(1).svg", "description": "Displays historical flow rate values as a simplified chart. Optionally may display the corresponding latest flow rate value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json index bea92879b0..d1bd9e166a 100644 --- a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_flow_rate_chart_card_with_background", "name": "Simple flow rate chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flow_rate_chart_card_with_background.svg", "description": "Displays historical flow rate values as a simplified chart with background. Optionally may display the corresponding latest flow rate value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json index 57417c5986..cdb1c3653f 100644 --- a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_fluid_pressure_chart_card", "name": "Simple pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_(1).svg", "description": "Displays historical fluid pressure values as a simplified chart. Optionally may display the corresponding latest fluid pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json index e7b8f22933..240b66d43a 100644 --- a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_fluid_pressure_chart_card_with_background", "name": "Simple pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_with_background.svg", "description": "Displays historical fluid pressure values as a simplified chart with background. Optionally may display the corresponding latest fluid pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json index e57b06fdd1..717d72d4b9 100644 --- a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_ground_temperature_chart_card", "name": "Simple ground temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_ground_temperature_chart_card_system_widget_image.png", "description": "Displays historical ground temperature values as a simplified chart. Optionally may display the corresponding latest ground temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json index 81f971a8ba..3fb22c1596 100644 --- a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_ground_temperature_chart_card_with_background", "name": "Simple ground temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_ground_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays historical ground temperature values as a simplified chart with background. Optionally may display the corresponding latest ground temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json index e45f5f63c5..0c2807920b 100644 --- a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_humidity_chart_card", "name": "Simple humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_humidity_chart_card_system_widget_image.png", "description": "Displays historical humidity values as a simplified chart. Optionally may display the corresponding latest humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json index 484c9c1a08..94820fa8be 100644 --- a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_humidity_chart_card_with_background", "name": "Simple humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_humidity_chart_card_with_background_system_widget_image.png", "description": "Displays historical humidity values as a simplified chart with background. Optionally may display the corresponding latest humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json index 43c8a79361..5e2ca4f008 100644 --- a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_illuminance_chart_card", "name": "Simple illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_illuminance_chart_card_system_widget_image.png", "description": "Displays historical illuminance values as a simplified chart. Optionally may display the corresponding latest illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json index 373fac46dc..0495640507 100644 --- a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_illuminance_chart_card_with_background", "name": "Simple illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays historical illuminance values as a simplified chart with background. Optionally may display the corresponding latest illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json index 81764c775f..018de4a068 100644 --- a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_individual_allergy_index_iai_chart_card", "name": "Simple individual allergy index (IAI) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple-IAI-value-and-chart-card.svg", "description": "Displays the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals as a simplified chart. Optionally may display the corresponding latest concentration of allergens value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json index 283ff4a8e0..8a24d9446e 100644 --- a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_individual_allergy_index_iai_chart_card_with_background2", "name": "Simple individual allergy index (IAI) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-simple-chart-value-card-with-background.svg", "description": "Displays the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals as a simplified chart. Optionally may display the corresponding latest concentration of allergens value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json index 2205278c9f..c598469ff0 100644 --- a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_leaf_wetness_chart_card", "name": "Simple leaf wetness chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_leaf_wetness_chart_card_system_widget_image.png", "description": "Displays historical leaf wetness values as a simplified chart. Optionally may display the corresponding latest leaf wetness value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json index d71fc4a5bb..cc47bced80 100644 --- a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_leaf_wetness_chart_card_with_background", "name": "Simple leaf wetness chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_leaf_wetness_chart_card_with_background_system_widget_image.png", "description": "Displays historical leaf wetness values as a simplified chart with background. Optionally may display the corresponding latest leaf wetness value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json index 0a1c1d8657..5ed427bbaf 100644 --- a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_nitrogen_dioxide_no2_chart_card", "name": "Simple nitrogen dioxide (NO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple-NO2-value-and-chart-card.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json index 892238dc9b..43fe8dbe83 100644 --- a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_nitrogen_dioxide_no2_chart_card_with_background", "name": "Simple nitrogen dioxide (NO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-simple-chart-value-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json index d33eed776f..81bb1bbff2 100644 --- a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_noise_level_chart_card", "name": "Simple noise level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_noise_level_chart_card_system_widget_image.png", "description": "Displays historical noise level values as a simplified chart. Optionally may display the corresponding latest noise level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json index 225d30b14c..76236956fb 100644 --- a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_noise_level_chart_card_with_background", "name": "Simple noise level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_noise_level_chart_card_with_background_system_widget_image.png", "description": "Displays historical noise level values as a simplified chart with background. Optionally may display the corresponding latest noise level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json index b2c9eb5e97..cd7510ddd3 100644 --- a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_ozone_o3_chart_card", "name": "Simple ozone (O3) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple-ozone-value-and-chart-card.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json index 88995bb4f5..6601f78b1a 100644 --- a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_ozone_o3_chart_card_with_background", "name": "Simple ozone (O3) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-chart-value-card-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json index 6e7d5c1369..691f6f633c 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm10_chart_card", "name": "Simple PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm10_chart_card_system_widget_image.png", "description": "Displays historical fine and coarse particulate matter (PM10) values as a simplified chart. Optionally may display the corresponding latest PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json index a0b3d74e54..d8fd1d528a 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm10_chart_card_with_background", "name": "Simple PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm10_chart_card_with_background_system_widget_image.png", "description": "Displays historical fine and coarse particulate matter (PM10) values as a simplified chart with background. Optionally may display the corresponding latest PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json index 25502227f3..5f1e083dea 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm2_5_chart_card", "name": "Simple PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm2_5_chart_card_system_widget_image.png", "description": "Displays historical fine particulate matter (PM2.5) values as a simplified chart. Optionally may display the corresponding latest PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json index ccdf3ba0a1..98f2a91d22 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm2_5_chart_card_with_background", "name": "Simple PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays historical fine particulate matter (PM2.5) values as a simplified chart with background. Optionally may display the corresponding latest PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json index befea52bb3..f123980aa0 100644 --- a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_power_consumption_chart_card", "name": "Simple power consumption chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_power_consumption_chart_card_(1).svg", "description": "Displays historical power consumption values as a simplified chart. Optionally may display the corresponding latest power consumption value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json index f0bb8ba9e9..ac9221538e 100644 --- a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_power_consumption_chart_card_with_background", "name": "Simple power consumption chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_power_consumption_chart_card_with_background.svg", "description": "Displays historical power consumption values as a simplified chart with background. Optionally may display the corresponding latest power consumption value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json index dcc4b11510..ea07d4defe 100644 --- a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pressure_chart_card", "name": "Simple pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_system_widget_image.png", "description": "Displays historical pressure values as a simplified chart. Optionally may display the corresponding latest pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json index 0c177e6364..6360e69120 100644 --- a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pressure_chart_card_with_background", "name": "Simple pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_with_background_system_widget_image.png", "description": "Displays historical pressure values as a simplified chart with background. Optionally may display the corresponding latest pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json index a84a49e2de..7a9d8b90a1 100644 --- a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pump_vibration_chart_card", "name": "Simple vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_(1).svg", "description": "Displays historical vibration values as a simplified chart. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json index 5cb064c807..dc30f0f5ba 100644 --- a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pump_vibration_chart_card_with_background", "name": "Simple vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_with_background.svg", "description": "Displays historical vibration values as a simplified chart with background. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json index 27faab79ac..446e1ecac4 100644 --- a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_radon_level_chart_card", "name": "Simple radon level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_radon_level_chart_card_system_widget_image.png", "description": "Displays historical radon level values as a simplified chart. Optionally may display the corresponding latest radon level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json index 84e90026ad..a105267cc9 100644 --- a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_radon_level_chart_card_with_background", "name": "Simple radon level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_radon_level_chart_card_with_background_system_widget_image.png", "description": "Displays historical radon level values as a simplified chart with background. Optionally may display the corresponding latest radon level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json index ddbf9c66ee..b195796912 100644 --- a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_rainfall_chart_card", "name": "Simple rainfall chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rainfall_chart_card_system_widget_image.png", "description": "Displays historical rainfall values as a simplified chart. Optionally may display the corresponding latest rainfall value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json index c5f82d49b9..90c6c65228 100644 --- a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_rainfall_chart_card_with_background", "name": "Simple rainfall chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rainfall_chart_card_with_background_system_widget_image.png", "description": "Displays historical rainfall values as a simplified chart with background. Optionally may display the corresponding latest rainfall value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json index 9e39e6ad0a..12677ae61c 100644 --- a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_rotational_speed_chart_card", "name": "Simple rotational speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rotational_speed_chart_card.svg", "description": "Displays historical rotational speed values as a simplified chart. Optionally may display the corresponding latest rotational speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json index 2f338515c5..16f77d316c 100644 --- a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_rotational_speed_chart_card_with_background", "name": "Simple rotational speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rotational_speed_chart_card_with_background.svg", "description": "Displays historical rotational speed values as a simplified chart with background. Optionally may display the corresponding latest rotational speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json index e23671c563..35f841e85e 100644 --- a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_snow_depth_chart_card", "name": "Simple snow depth chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_snow_depth_chart_card_system_widget_image.png", "description": "Displays historical snow depth values as a simplified chart. Optionally may display the corresponding latest snow depth value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json index 48010dbcc9..92680d4f65 100644 --- a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_snow_depth_chart_card_with_background", "name": "Simple snow depth chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_snow_depth_chart_card_with_background_system_widget_image.png", "description": "Displays historical snow depth values as a simplified chart with background. Optionally may display the corresponding latest snow depth value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json index 3f9e4450aa..32401d128a 100644 --- a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_soil_moisture_chart_card", "name": "Simple soil moisture chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_soil_moisture_chart_card_system_widget_image.png", "description": "Displays historical soil moisture values as a simplified chart. Optionally may display the corresponding latest soil moisture value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json index 916c7d7d37..619fa49dcc 100644 --- a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_soil_moisture_chart_card_with_background", "name": "Simple soil moisture chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_soil_moisture_chart_card_with_background_system_widget_image.png", "description": "Displays historical soil moisture values as a simplified chart with background. Optionally may display the corresponding latest soil moisture value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json index e3ebfac43a..a02997a7a6 100644 --- a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_solar_radiation_chart_card", "name": "Simple solar radiation chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_solar_radiation_chart_card_system_widget_image.png", "description": "Displays historical solar radiation values as a simplified chart. Optionally may display the corresponding latest solar radiation value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json index f3fc53349f..b33497ba6b 100644 --- a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_solar_radiation_chart_card_with_background", "name": "Simple solar radiation chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_solar_radiation_chart_card_with_background_system_widget_image.png", "description": "Displays historical solar radiation values as a simplified chart with background. Optionally may display the corresponding latest solar radiation value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json index 0a91102cdc..3d4c2c6381 100644 --- a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_sulfur_dioxide_so2_chart_card", "name": "Simple sulfur dioxide (SO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-simple-value-and-chart-card.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json index 2a016e3072..1d76efc260 100644 --- a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_sulfur_dioxide_so2_chart_card_with_background", "name": "Simple sulfur dioxide (SO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-simple-chart-value-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json index bac4ec201e..dd99a6593d 100644 --- a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_temperature_chart_card", "name": "Simple temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_temperature_chart_card_system_widget_image.png", "description": "Displays historical temperature values as a simplified chart. Optionally may display the corresponding latest temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json index 50145be107..ad5df7c8fa 100644 --- a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_temperature_chart_card_with_background", "name": "Simple temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays historical temperature values as a simplified chart with background. Optionally may display the corresponding latest temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json index d10f55833b..23e4aefaf2 100644 --- a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_uv_index_chart_card", "name": "Simple UV Index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_uv_index_chart_card_system_widget_image.png", "description": "Displays historical UV index values as a simplified chart. Optionally may display the corresponding latest UV index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json index a1ba49e7bc..bc93c8267a 100644 --- a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_uv_index_chart_card_with_background", "name": "Simple UV Index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_uv_index_chart_card_with_background_system_widget_image.png", "description": "Displays historical UV index values as a simplified chart with background. Optionally may display the corresponding latest UV index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json index a045af5ef6..a2fd2087d0 100644 --- a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_vibration_chart_card", "name": "Simple vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_system_widget_image.png", "description": "Displays historical vibration values as a simplified chart. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json index 9e39eed359..5eb472b4b7 100644 --- a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_vibration_chart_card_with_background", "name": "Simple vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_with_background_system_widget_image.png", "description": "Displays historical vibration values as a simplified chart with background. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json index 7f098c8d96..4391a693ab 100644 --- a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_visibility_chart_card", "name": "Simple visibility chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_visibility_chart_card_system_widget_image.png", "description": "Displays historical visibility values as a simplified chart. Optionally may display the corresponding latest visibility value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json index 6deff18090..e4a609c9c0 100644 --- a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_visibility_chart_card_with_background", "name": "Simple visibility chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_visibility_chart_card_with_background_system_widget_image.png", "description": "Displays historical visibility values as a simplified chart with background. Optionally may display the corresponding latest visibility value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json index d176526dcb..69b0fef3be 100644 --- a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_volatile_organic_compounds_chart_card", "name": "Simple volatile organic compounds chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_volatile_organic_compounds_chart_card_system_widget_image.png", "description": "Displays historical volatile organic compounds (VOCs) values as a simplified chart. Optionally may display the corresponding latest VOCs value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json index 1eba6b3fd0..0055ce2466 100644 --- a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_volatile_organic_compounds_chart_card_with_background", "name": "Simple volatile organic compounds chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_volatile_organic_compounds_chart_card_with_background_system_widget_image.png", "description": "Displays historical volatile organic compounds (VOCs) values as a simplified chart with background. Optionally may display the corresponding latest VOCs value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json index 6660f67d78..d23269162c 100644 --- a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_wind_speed_chart_card", "name": "Simple wind speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_wind_speed_chart_card_system_widget_image.png", "description": "Displays historical wind speed values as a simplified chart. Optionally may display the corresponding latest wind speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json index fadebc2925..beab2caeca 100644 --- a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_wind_speed_chart_card_with_background", "name": "Simple wind speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_wind_speed_chart_card_with_background_system_widget_image.png", "description": "Displays historical wind speed values as a simplified chart with background. Optionally may display the corresponding latest wind speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_card.json b/application/src/main/data/json/system/widget_types/snow_depth_card.json index 1d8f56a2fb..a866de773e 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_card.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_card.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_card", "name": "Snow depth card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_card_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json b/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json index c9f1b12488..8ac7ea2e29 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_card_with_background", "name": "Snow depth card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_card_with_background_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json b/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json index decfe00951..403579060a 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_chart_card", "name": "Snow depth chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_chart_card_system_widget_image.png", "description": "Displays a snow depth data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json index f0b2951731..e96d5a26d8 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_chart_card_with_background", "name": "Snow depth chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_chart_card_with_background_system_widget_image.png", "description": "Displays a snow depth data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_card.json b/application/src/main/data/json/system/widget_types/soil_moisture_card.json index 03df119344..bfedfec7cb 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_card.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_card.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_card", "name": "Soil moisture card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_card_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json index a66fbc1532..444f51deb0 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_card_with_background", "name": "Soil moisture card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_card_with_background_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json index 986100613a..c0bc989c21 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_chart_card", "name": "Soil moisture chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_chart_card_system_widget_image.png", "description": "Displays a soil moisture data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json index acce7054b3..554e236cfc 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_chart_card_with_background", "name": "Soil moisture chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_chart_card_with_background_system_widget_image.png", "description": "Displays a soil moisture data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json index efd88de86c..952fd6b242 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_progress_bar", "name": "Soil moisture progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_progress_bar_system_widget_image.png", "description": "Displays soil moisture reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json index da1ec3617a..c274dc7c9c 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_progress_bar_with_background", "name": "Soil moisture progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_progress_bar_with_background_system_widget_image.png", "description": "Displays soil moisture reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_card.json b/application/src/main/data/json/system/widget_types/solar_radiation_card.json index 286668be56..14dbb2173f 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_card.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_card.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_card", "name": "Solar radiation card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_card_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json b/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json index 7264f59fe2..4dd421f3d1 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_card_with_background", "name": "Solar radiation card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_card_with_background_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json index a96b0f8c89..9d4c0c3003 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_chart_card", "name": "Solar radiation chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_chart_card_system_widget_image.png", "description": "Displays a solar radiation data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json index a0dac85a70..1fb1360ddb 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_chart_card_with_background", "name": "Solar radiation chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_chart_card_with_background_system_widget_image.png", "description": "Displays a solar radiation data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json index 25b950df3e..f1b605e322 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_card", "name": "Sulfur dioxide (SO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-card.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json index a097776152..6a266e6486 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_card_with_background", "name": "Sulfur dioxide (SO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json index 597c454242..d47d15c23b 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_chart_card", "name": "Sulfur dioxide (SO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-and-chart-card.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json index 3934f87dc0..7ba6e5ba62 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_chart_card_with_background", "name": "Sulfur dioxide (SO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-chart-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_card.json b/application/src/main/data/json/system/widget_types/temperature_card.json index 1308512c13..a851c8bd0d 100644 --- a/application/src/main/data/json/system/widget_types/temperature_card.json +++ b/application/src/main/data/json/system/widget_types/temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "temperature_card", "name": "Temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_card_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/temperature_card_with_background.json index f711b715d5..bd95a50b8c 100644 --- a/application/src/main/data/json/system/widget_types/temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "temperature_card_with_background", "name": "Temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_card_with_background_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_chart_card.json b/application/src/main/data/json/system/widget_types/temperature_chart_card.json index 689de25c3f..49495ec4ec 100644 --- a/application/src/main/data/json/system/widget_types/temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "temperature_chart_card", "name": "Temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_chart_card_system_widget_image.png", "description": "Displays a temperature data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json index 582d22c0f6..eedef9011e 100644 --- a/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "temperature_chart_card_with_background", "name": "Temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_chart_card_with_background_system_widget_image.png", "description": "Displays a temperature data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_gauge.json b/application/src/main/data/json/system/widget_types/temperature_gauge.json index 0db4b047f0..4dda4ee0b0 100644 --- a/application/src/main/data/json/system/widget_types/temperature_gauge.json +++ b/application/src/main/data/json/system/widget_types/temperature_gauge.json @@ -1,7 +1,7 @@ { "fqn": "temperature_gauge", "name": "Temperature gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/Temperature_gauge.svg", "description": "Temperature gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_range_chart.json b/application/src/main/data/json/system/widget_types/temperature_range_chart.json index 2810381c51..1d34f69066 100644 --- a/application/src/main/data/json/system/widget_types/temperature_range_chart.json +++ b/application/src/main/data/json/system/widget_types/temperature_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "temperature_range_chart", "name": "Temperature range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_range_chart.svg", "description": "Displays changes to temperature data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json index e4e49fe12c..96f913a1b0 100644 --- a/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "temperature_range_chart_with_background", "name": "Temperature range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_range_chart_with_background.svg", "description": "Displays changes to temperature data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_card.json b/application/src/main/data/json/system/widget_types/uv_index_card.json index 797948b1cb..305771860e 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_card.json +++ b/application/src/main/data/json/system/widget_types/uv_index_card.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_card", "name": "UV Index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_card_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json b/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json index 4b3f255401..e515e0f13f 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_card_with_background", "name": "UV Index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_card_with_background_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_chart_card.json b/application/src/main/data/json/system/widget_types/uv_index_chart_card.json index 393f7359a4..9a00d0a809 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/uv_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_chart_card", "name": "UV Index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_chart_card_system_widget_image.png", "description": "Displays a UV index data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json index 78a7b899be..9c19f15e50 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_chart_card_with_background", "name": "UV Index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_chart_card_with_background_system_widget_image.png", "description": "Displays a UV index data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_card.json b/application/src/main/data/json/system/widget_types/vibration_card.json index 033da084fb..5976833c8a 100644 --- a/application/src/main/data/json/system/widget_types/vibration_card.json +++ b/application/src/main/data/json/system/widget_types/vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "vibration_card", "name": "Vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/vibration_card_with_background.json index f078b93194..7aa3439472 100644 --- a/application/src/main/data/json/system/widget_types/vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "vibration_card_with_background", "name": "Vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card_with_background_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_chart_card.json b/application/src/main/data/json/system/widget_types/vibration_chart_card.json index c7dcf7a6ed..75d31e3954 100644 --- a/application/src/main/data/json/system/widget_types/vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "vibration_chart_card", "name": "Vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card_system_widget_image.png", "description": "Displays a vibration data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json index 443d1b7a33..bdf5418806 100644 --- a/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "vibration_chart_card_with_background", "name": "Vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card_with_background_system_widget_image.png", "description": "Displays a vibration data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_card.json b/application/src/main/data/json/system/widget_types/visibility_card.json index c640c702e9..aca104e0bd 100644 --- a/application/src/main/data/json/system/widget_types/visibility_card.json +++ b/application/src/main/data/json/system/widget_types/visibility_card.json @@ -1,7 +1,7 @@ { "fqn": "visibility_card", "name": "Visibility card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_card_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_card_with_background.json b/application/src/main/data/json/system/widget_types/visibility_card_with_background.json index 02e12105b8..0100aa7f7c 100644 --- a/application/src/main/data/json/system/widget_types/visibility_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/visibility_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "visibility_card_with_background", "name": "Visibility card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_card_with_background_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_chart_card.json b/application/src/main/data/json/system/widget_types/visibility_chart_card.json index fe69562505..5659ba9f8c 100644 --- a/application/src/main/data/json/system/widget_types/visibility_chart_card.json +++ b/application/src/main/data/json/system/widget_types/visibility_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "visibility_chart_card", "name": "Visibility chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_chart_card_system_widget_image.png", "description": "Displays a visibility data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json index 52454f7947..00daf878de 100644 --- a/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "visibility_chart_card_with_background", "name": "Visibility chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_chart_card_with_background_system_widget_image.png", "description": "Displays a visibility data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json index 517ecaf1ce..6678ec814b 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_card", "name": "Volatile organic compounds card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_card_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json index fb5bf45da1..4a8dce094a 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_card_with_background", "name": "Volatile organic compounds card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_card_with_background_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json index b146908138..ab35fe4aa1 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_chart_card", "name": "Volatile organic compounds chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_chart_card_system_widget_image.png", "description": "Displays a volatile organic compounds (VOCs) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json index 0e1e1890f6..d2bf98bf09 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_chart_card_with_background", "name": "Volatile organic compounds chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_chart_card_with_background_system_widget_image.png", "description": "Displays a volatile organic compounds (VOCs) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json b/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json index 78c4fdabf0..65835b597d 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_and_direction", "name": "Wind speed and direction", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_and_direction_system_widget_image.png", "description": "Displays the latest values of the wind speed and direction.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json index f96a21781f..7d0c1e07f7 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_and_direction_with_background", "name": "Wind speed and direction with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_and_direction_with_background_system_widget_image.png", "description": "Displays the latest values of the wind speed and direction with background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_card.json b/application/src/main/data/json/system/widget_types/wind_speed_card.json index 0727d4a905..071fefdaab 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_card.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_card", "name": "Wind speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_card_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json index 1fdc2e2ec3..1113dfe606 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_card_with_background", "name": "Wind speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_card_with_background_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json b/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json index 1b057c932d..c793dd86ad 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_chart_card", "name": "Wind speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_chart_card_system_widget_image.png", "description": "Displays a wind speed data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json index 7c5b7ff83d..f8bf3db950 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_chart_card_with_background", "name": "Wind speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_chart_card_with_background_system_widget_image.png", "description": "Displays a wind speed data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { From bed6caa8bf9b262a3d639133d2a3ad64a415ddb5 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 13:57:04 +0300 Subject: [PATCH 17/37] feat(iot-hub): add "Add from IoT Hub" action to alarm rules table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inject IotHubActionsService into AlarmRulesTableConfig (and the surrounding alarm-rules table component + AlarmRulesTableConfig resolver) and register a third header action descriptor — "Add from IoT Hub" (icon: hub) — that calls iotHubActions.addItem(ItemType.ALARM_RULE, { entityId }) and refreshes the table on a successful install. - Extend TbIotHubAddItemDialogComponent so the same TbIotHubSelectCfEntityDialogComponent prompt is reused when an ALARM_RULE install is launched without a preselected entityId, not just for CALCULATED_FIELD installs. --- .../alarm-rules/alarm-rules-table-config.ts | 17 +++++++++++++++++ .../alarm-rules/alarm-rules-table.component.ts | 5 ++++- .../iot-hub-add-item-dialog.component.ts | 2 +- .../home/pages/alarm/alarm-routing.module.ts | 3 +++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index f5043bdcee..9e95a55938 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -70,6 +70,8 @@ import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.comp import { Router } from '@angular/router'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; +import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; export type AlarmRuleTableEntity = CalculatedFieldAlarmRule | CalculatedFieldAlarmRuleInfo; @@ -97,6 +99,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig true, onAction: () => this.importCalculatedField() + }, + { + name: this.translate.instant('iot-hub.add-from-iot-hub'), + icon: 'hub', + isEnabled: () => true, + onAction: () => this.addAlarmRuleFromIotHub() } ]; @@ -333,6 +342,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig { + if (result?.descriptor) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file') .pipe( diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index ce0aecb598..a882868728 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -37,6 +37,7 @@ import { DatePipe } from '@angular/common'; import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; import { UtilsService } from "@core/services/utils.service"; import { ActivatedRoute, Router } from "@angular/router"; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; @Component({ selector: 'tb-alarm-rules-table', @@ -71,7 +72,8 @@ export class AlarmRulesTableComponent { private utilsService: UtilsService, private destroyRef: DestroyRef, private route: ActivatedRoute, - private router: Router + private router: Router, + private iotHubActions: IotHubActionsService ) { this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { @@ -91,6 +93,7 @@ export class AlarmRulesTableComponent { this.entityDebugSettingsService, this.utilsService, this.router, + this.iotHubActions, this.pageMode, ); this.cd.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts index 3920a19f1b..0111fa0abe 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts @@ -82,7 +82,7 @@ export class TbIotHubAddItemDialogComponent extends DialogComponent( TbIotHubSelectCfEntityDialogComponent, { panelClass: ['tb-dialog'], diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts index 75dd645b40..00432627b4 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts @@ -36,6 +36,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { UtilsService } from '@core/services/utils.service'; import { AlarmRulesTableConfig } from '@home/components/alarm-rules/alarm-rules-table-config'; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; export const AlarmRulesTableConfigResolver: ResolveFn = (_route: ActivatedRouteSnapshot, @@ -50,6 +51,7 @@ export const AlarmRulesTableConfigResolver: ResolveFn = entityDebugSettingsService = inject(EntityDebugSettingsService), utilsService = inject(UtilsService), router = inject(Router), + iotHubActions = inject(IotHubActionsService), ) => { return new AlarmRulesTableConfig( alarmRulesService, @@ -66,6 +68,7 @@ export const AlarmRulesTableConfigResolver: ResolveFn = entityDebugSettingsService, utilsService, router, + iotHubActions, true, ); }; From 3c0d1c2197b02cf9b7557e8ce7d1336a00ec051e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 12 Jun 2026 15:39:05 +0200 Subject: [PATCH 18/37] Removed empty widget bundles --- .../system/widget_bundles/air_quality.json | 111 ---------- .../widget_bundles/indoor_environment.json | 94 --------- .../widget_bundles/industial_widgets.json | 84 -------- .../widget_bundles/outdoor_environment.json | 190 ------------------ 4 files changed, 479 deletions(-) delete mode 100644 application/src/main/data/json/system/widget_bundles/air_quality.json delete mode 100644 application/src/main/data/json/system/widget_bundles/indoor_environment.json delete mode 100644 application/src/main/data/json/system/widget_bundles/industial_widgets.json delete mode 100644 application/src/main/data/json/system/widget_bundles/outdoor_environment.json 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,iVBORw0KGgoAAAANSUhEUgAAAMkAAAChCAMAAABav2sAAAABcVBMVEUAAAAAAADf39/09PTw8PD9/f3AwMDy8vL39/fy8vL19fXw8PDw8PD////39/fz8/MjTMf6+vrj4+P29vbx8fHn5+fHx8dYWFg/pxra2tp0dHSQkJDt7e3h4eFmZmbV1dWsrKz/pgBaedW5ubmCgoKf04z/3aDdNVGenp71xc3T09PCwsL64ubI0vHOzs4+Ys7wqLTIyMirq6uRpePYGDjk6fisvOr/9N91j9zri5v/6b9KSkr/+u8wV8u33qniUmnn9OODmt//78//46/z+vHW3fXgQ13/sSDnb4L/vkXx9PufsOa7u7v/2JD/04D/zW//x2DaJkXy9Pv98fPc3NxYszi6x+6esefPz8//6L/umqiHyHA9PT3/rBD309rP6ca2trYsLCzb79TLy8ur2ZrpfY91wFpLrCiDm99MbdHzt8HD47jtmaiTzX7aJkQxV8rtmqj/2Y+JiYmTzn7lYHZUVFQ8PDz/tzBohNnNzc2Pj4/UlCD8AAAADXRSTlMAByDvv/Eqr8W577Cvv6N3egAACkpJREFUeNrsms9v0zAUgBEgceHhxNiOnNhJ10RVlGqp0qpd1a2HtqqqwdjQDoNJTIIbnBBI/PzreQ5ts0FBK91GOvVbE7t2V/ub/ZzW2Z07d+/fe3RtPH549w5yI63cvQfXyj1s5GZaeQDXzH0UuZFWHsM1cw9FbqSVR3DNPEKRG2hlY7IxmXNbTFqD9jWaxCwIMWGeF0BOJYAZERREq5u87TfeH8MMIQS9SpPwTei9YQDUDxwQIWMVV7AKZaJCARwIQiYgxApwVjd51xpY2zCFVV7FAaPADIAZwIRSoIz5DJP86TImwotDMxrUqTgQx7GLJo7YpU4zQhPmiCiibsWLxBWYNFpjNJnjUOpEses5nsM+xw71feFEzcgPXgUOVu6GkecvFSeVCoAx8Tzq4+8yDzvtBy51sa1g12sKv+n53tWYPL1oIowJNT/YZOxHTQfCeBefGBMXPi9nInZ3cxMX8C18fOddEbke/WkCThTCGxo4UXAdY8KcMPxp8iZ0hF8RDrjeK+oLP3xFXZRrlnTtalQH503MnBYsAPPjCAbUZJgIgFIamBACwcpqYlnWCSykuVbXk5aFNI5hKZY3IVrj2bbBxkRh7ueDED7N29Ikq5g0LMPTeYvwK6oowo6A/jcTiV2VicrkAQ90CjqVGp9rqQIJGZeJraR8KckKJmMrpwtT6gBa2ZK8VqBsxbXNM641r2MRqNeEaHyBxEQRSZYx4djvTIKUkhOeAOdEa805PiTolzIBLH3JYQWTJ1bOoDCxk9c6ldjhlKR1ldQlT1+TNMvykkzhC9J6RrIDlZUsTr5aSL8NUw64TvHI6gQymR4kso4T4jWpp1rnbjgmWh6kPMt42UyedC3rXbEKc25zDhxDEQgndQWE2FyBwqI8iFR+qDQDXra1C2nDn1CwmNxrKZNiYbLzlGC6nt9PuOSJUpxwzBFC8BRwpYlS62eiucQ1imuCAtLAucQivnYmhCiOvVfERhWjQRRmuA7WzuQP8PWbXXM2OxIbk43JxmRjAr+wMdmY3DKTvT087RhMZu/0dG89TU6PzmoAvS3DPsAzTCY9WIj9+DGeqQdBDAErmcneBHuOJqOto1qtNoKdrcmwd3a2eFSYpDZQ5oHrsaZXMpOd571naIJDcQoIpiOAQzwtgn9PbCbAA0+ENBblMkFqxuRo67Q2wpF4vjU0Jc9gEckrFxABtAKMBeU02d9CJjtosmOC5hAWQlmZI/6nyehoCB+29guTK127jlstgE51xklxE68zrW/0+9XO8VWY5Ey2LmFCALHNcUmT4xdVy6qafe8ZJzORQTe/OTHuWzmN9hWYnA7BTDE4msfJcAeQvfyYY0sSaM0J15JfMk5OsJu5SXs756llTTvc6AL0B1jRtxov2mOsqF6BydkZwM7ZBJ8cTsP+WW2vNuz1Rr3RcFjsZnASSIRznpDLmbSs6ot35/rYtbrz+0Uo8RRLpm44aNsrm2CMTI6+YAavL8/3zRUSjo6eQa3X26nVcHTmEDshPFCEEHXJOBk/AagWJm3LGk+zT61+HyfUE2vqhpnBSiaj/Z7x2Z/s1/ILzJfJoZlPh7Ve7XA03BuNVo34CybdIgvjQQs1t+e3Wra3x9f1CbIHhqs0edKwOnCRDk6qtfksXJh0rAbcDpOG1b0dJtjr9gKTk+VMNNFKK0D+n0n19yGB8TziW63O5Uy41JwD8t9M2gsnUr+oHVzOREt8AHAN/0rAijxJljfpWo2F106rk9cue2VU8K/ErutGIZ2pLG3SzrtcUKzMVveF+XQ2uKGIDyI8sdB1xT/OroH1/hgW0EYVBEVWMFnwlYNh0SKoP8s0nQpdyqTT6hTJIk4G3e6LbVjJxHUoXEC4XuSIX+UoDc6/UODAlOybFnVpVIEC5sXm7DA4T+QhF41p2Uw8iod37m9N5yFRUInLud9lk4IgIsi3TwEx0I8xmfLxgMyhn8hC/rvJeTwxDeZwOiBzfFbMLVr+PUjmzpW8PEIKRARTaFTa3VTmuq5fAaQiYEbTp3CBaFbn0NKa5DSNiwt/hjmQEzdLucOtOQCfr1POXzsp3IrBgz/A/6sJ5wDFXoKAvyJyGCzG/s8mGi5yu+4EbUw2JhuTWRsq//9sk+Rok7UBEtskiL02Jhx4XaUqBc6llEEiX2Yqk5BKDkkqM635uphoLusqUQnIVGI+SV5KniqQAYc0SOpaq3UxOYeGX1FyjeLkPOsd8QUbkx/t3G9r2kAAx3HcZtdu/DYWjzu5lDOQhCQzD4L4RATNAxGfivWJDmXt1sG6lsH2/tklzLZZmfSW5NzGfQpXEfH8kvgX7kyJKfl1lqeoWUuGaJnlGWr2UoZomKX+vSpePJEhemZ58rz1qjZPn+UhGmYxjP9Co0badgySjo6bqM/JkbZ9iY6aqFXzSNe+RCeo2bGmfYkaqFuzoeGznZYSmJJHMyWK/s8SasHBLYcAnuUQgtKKJQQQBEDHhUWJ7RA4XgclFUtsCgYqg6hruXBJx2UW9TglnXI1xRLCbdsTAIgAp6LTsym3BKEuSimWMP4ODMzhHvccbjE6pHJWNxQWyiiWgIbwXEhyBjCAhQ5AmEtRUvGY5CU5TjkYtR2HCIFyiiUQtsdIlsQ5hrDkfHBIT1RdMrR3JZaw5TGhAl4P5RRLCIcgyLzvIBTCJczjRPCys+Qle5GSJ9ff8ypsUZTy95SUZUoUmRIFpkSRKVFgShSZEgWmRFWxJPXPgZHv+x9QmcOUrNsjYLm8vFyjMgcpGS2zkvYVqqW/ZLIcyZJJ+5s/QpX0l1yvIUvST5frT9fAyN+mqID+EpmQZiUZvz3B9dWHEaqgv2TdzvjpFhjJksm5f56iAvpLJmmatj9Mtu2ryeUSMmdbzYux/hIpP7v8dnt5DmA7+eajPL0lRZN0dwEVqL6E2ExwgqJDfFpZbADECyBKIiAfizbfsRenQIehSH/JZjwfIBn3IyTBx2CFZJ6NBVGAfSwOyaYo0F/yOZnKkkiWjFf4MsV4gM8BgC/RIMFiIEdZkgxi3EdcK/8nGLOI+HkZBfpLkD12QJYE2WPGVI59AMH0azCI+7Ov8yjqz2Y3C9xxmSN6ALwQlhAucqGN+8tGD18S7EoSeYTifozZTF6L6Qp3GAGGFijHfT3BhlwwB7niO+Pv+a0/8ErpmCRIgqxkIEvyG93KCzpsKIMesuzwQcmnPSXLVuu18l9rf8n3GT6O8XGG1TgrWWE2jvsbTAfRTbwIPuMWt7DXg5LU/730zR/A/pI4CII4G+eb/HkSzOO4P58HcXQznY9xy/bwCIf99hvfG4NkAciza3fdDmUhHueu5LR7ikqcAfKuLi6U3uODBHkJikJB8AjFki5cVMEKcXHqnp11Wyol8SIfoe5hiVtRSX5HsuTitFBSAw0l3ZZrvVU4JiXU/Dxpdc/w9i1y5pc7RaZEgSlRZEoUmBJFpkSBKVFkShSYEmUNPesZG03UraFpPeMxanaiYaWhppU02lYaHp2gPs1jGbLPv7Xi7AcB+P0LpI8HcAAAAABJRU5ErkJggg==", - "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,iVBORw0KGgoAAAANSUhEUgAAAMkAAAChCAMAAABav2sAAAABa1BMVEUAAAAAAADf39/v7+/19fXw8PD9/f3y8vLAwMAAAAD5+fn39/f////39/fz8/MjTMf5+fnj4+OQkJDHx8esrKzb29t0dHTn5+f29vbx8fH9/f3f8Mqv2XuCgoKAwyzV1dVYWFjU1NTk6fju7u65ubnIycjh4eH4+fjOzs78/PxmZmb7+/s+Ys7/pgDy9PvI0vGenp4wV8utvOrCwsL/vEDg4OCRpeP/9N/c3Nw/Ys7l6vrv9+V1j9yxwfCDmt+dnZ3W3fW6x+6su+rI5aP/zXD3+/Kcr+n/6b+7u7vP6K+/4ZVaedX/78/P6a9KSkqQykb/sSD/qxDy9f3L1vX/+u9mg91aeNVMbdH/5K//x2A8PDzY4Pekt+6RpuP/0oCg0mG+zPPp6en/2JCYzlNLS0v/+e/r6+umpqb/tzDs8OuLoul3k+Xn9NjX7L3/3aDA4Za33Yio1m7/wk8vLy8gICDn9NfY7L3/15CIxzpPyZjhAAAADHRSTlMACyC+76/x7yoI78V/Zk47AAAKpUlEQVR42uybW2/TMBiGJxjazTsnduKYXERpjkSl564dSNXYEGKTNqFN4jCQQAKBgBsuAIm/z+ekXelYUena0Y4+7WLHdWc/sj8n9bq1tRs3N9jccG/dWCN0K+ubc+N23sqNDcyVjRu5yDrmyjq1cgtz5iaJXEkrLubMhja5jTmzvrbGMGeYNtnEnNlcmaxMhszbpFavXQ+TsGJUzlRsIRq4EPcSJjEIN4bvYwCnMtsXcGdostesGw/Rx33Q4K7NuRX7sWtZMbjNfYtOURa+y3kcg55/aSK+ewC4A9u2XHrYvm7BtR1q5ZVPBTMyaVXC+2cm1BhKcclyvLLXcOwSL/ll4fkNz3ZsYdl2KbYbnvV3Ju6TxjcX4K+cku1wh7+yXlllSzhkUuLveTm2Z2ZSb46YvBLUZ0tQW1zY70VDWLysPXITOJ7w/9Kk5DxxizHJTUqghyXKEJbOemU+M5Pa6JgIZ2DilVxPkINVKjXsslUuk4nlee7fmdBbLPRNHjhPBiaUJZMnFh1mZdLs0ZhchMOvYu0S9qxMuoZhdHERPq7CxMaMTEJDs3sNric7hmYHBQHAwEwJ0wQ9MjCmKMsyeuZlxLQmWUbvZ/oXMLNDB53RWRWh31xiElOb9EZMMkQH6YFqI1MySZK2OpBZckDlZpKgE8s0SLIpTZiChEwClcgslSaJZR3VyWTQSSWC1E8ZAtVW6dQmdUPTQkEi/SRIZAKZybafHARJmrWz3KSNpC39IEumNEEQKCWlNFMmlWSgrEolQefoZCqgIr+t2OwjnqkLyny2uHEChNflDlJzJSYMEQAzz+mMDvflNDlAm6UskhGylJmRUmmimGJpunQm7SRQFPRKQke8lKpDP5TKpTNRqYrSiEAqTakUIy9mqiBZOpMxqOi6mCxhxAMrk5XJyuS/N9nfOv708ilwtKV5tLwmX6rHj15Wt4DT6qflNtmqbheHd9V9/BnXQ2yBuwtq8pRE8LV6hDdkM5Y77A58V8BxYD3gi2miXfb17KIpVj0ZN7k+J5/ByUR4LkQ8ucluq1VHQa3V2qvhjHor7JfvtVq7MzGhwah+2gYeVR+9O66+wYUk5QSAC9fVyaQm4aFBVPZAvB5mi9OmUZQ38xfCWZhs7787OX4KzYeTE1yMNU3EV4zKTrdiNGtAyzB63Rdn22E14zF2KsXW0vOdZ1QnnIEJ9HjsD+Iff8A0EdFxUhPqfQiETeo1Xhg9AIeDTaTQqONxM6/SLf7y8uzSJi+38gl2iq2XAI5PgNPTcaEfpWZgRj6b0KRnPAe0xA6NAXUdeGxUUHBY6TV3KCmqoEsDd0kTGo43R/vHJ9s0HO+2H1VJ5+jH0zEinQ6TEVI5oUktrBVz7DHqNDzFKKEg7D3vAhhsK+12u5c2+fC1Wq2enALbx5TZ2qaS0y9jZxdjACYdk+EcG5hQUsPoVvJDFMwm4o+KQTjaP+qXzPAaTyHQxS8m4XmThb6DDA3NQOQ5ltjkvgZErWK8qI2Y4BImkamYYiCuymTIi/41b7cw2TtngmZ/Wd7t9SaIeMbyTS7iyk16fRG9Cu/li+2L8xWaw3QiEz8COpga33IxpDOpyevhpfvQOMyDZgcj0HR7rZOm0fubfeHpRUTseY7NUWBOaEJzqXk/J9TTq/Ks+fv9VZfqHN6nF2tXEfHcK3wcx4Zm8ruVASFQr1D6PPy9ki5vdmuY2sTmv5WIRjxWpMDyHGvqz4zh7sXdrdXDy6zCfKRPhFW2uP+gca6WIMouhri24y/WZ0YHEB7OcD0BjRjVK7kL+Tl+8xc+fqSDLN3rnz5wZD/nvN0cIqj0Au5igcakhBwh+hMLA3gZw7xY/L0V20YBL/F8Yg2JG+jjlhZ2l0g4juNxEM4vAeLwc7UG5x5fRJOUoYA7woLtYzxuf62KxULu3KkOEA3W0bLAn/AdoXEwDvYvTfwUkOaE36l0uWZ8JfnP4+SMa7pXvzJZmVzCJAXyL6fB1BkzoLwyTSjZAVheuCwmEipRSRRAMkkkUaaC4AAqI6dASl+m0ZKY+EoGKkgDpCl1O2unqcpSBcUOEAUy8FPJlsTkF1iE86RyeeJkhKWO+CErk//BZANzxtUm65gzt9fWbmLO3CKRK2jlOv237NqNWy6bGxs3cxHdyu3NubFOraxY8bOdu31tGgjgOL4hlM39ruQuMU2ujUlIqjV5UyhCaSl7USo40EGHb9zAyWSCD6j4gH++l05Bsx6X25pym/1At3TrMr67XtJu690Ku9u12V3bagLCzl4D9bmzU4TcRa3uLlJ2GqhVY2ddaxbcQc321rOawNYu6tbYXstju61t1G5TUtmmRNPtLNlH6odhHwtOCNhBir91U1xBqcRmjOJa1CUuzfrMTn3bocUKAvAzPyiuIE1BxTuHObiCUglLw+xi3QP/Ytc2bJra1NHoU5U4/SyIENHICRgTJQhcKq64xboEUZg5LFhJSd9xWVDsTuzYDvbDCJGd2eICTfIS+jzNMlHiUtelzAYNaRS6iNyApW9YP6DOSkpY6jPbjlhKXQYnWpQwvGR9aJKX4CvdDy5KnCyy4UdiUN64/WJdgoDt21m2khK7uIh92/tBZAduGAXPRUkgstSUJXLUxXUZchT2Ka7LkJIV2JRo+r9LWkigweCSwYMeNBhc0otvS0li3ZZ7l6ZNiSbdEs5zy39sEYtzVGRkiSUSct57S0QQITkqMbCkaeUoxL0mBO4RjgqMK7EsjhJOPKgZVsKJhWU8CyqGlVgcS/E8h4JRJfyx7Db0/LFqVIwq8fwWlkvdb+eKFJNKLAsy1HVxc0q4/AY2AwNXpJRKZp+AD50pMBaXttiQqKFEftpgfcUNlpS0O/eBs3HnY7tzCLyaddqQWHlJ7kGC2VjgHqTKJcJ9jF/h2QFw+HvjO2btMXTpl3gcywUUC+VjtHbJpx/P2p/H0xnU9Euks+ToFMJ8JLtNpZLZIaaLktkZDqZ4+vTZwccP06fQoF1S/nmP3p88ETkvJqVxwxLyEhx8ORsXJfgsNoqS4k0tJdL5Pn/x7gTApFRCcpTISxZmpQ0t+iVJs2nh38+O3j9ZVpLnxeqUy5lwPul146RUcvru6MVodKmEW90mJEwoieP00mSenMyPRxAx/xAlCSRMKGk2W+CDAdQGg1bVe9e0o2X6UCWvOON5fA9q90IA1Up+3tdy6ClVPnbhsvml3rxyybitZfxIhV+x5Hhyenx0NCnNE4tXLiFJa0iGuD7ikYSIXV11TI4wmZyOnhyXSqqPyWCAbhzj+pJhHA+7w/iKZ0bMMRodz09KY6Jxju/Gifj+WIHhMO7GhLZkJfIpwGxI6JQkQw8JxQqQhCbUG2o8FsaCH9iKWgkTzieFP7OZZhRSBJAzp2QxU0LXh4zi6a9JJZbvMMjlHgfkDCoBcQNIqQ7BMKmEv6YcEjnym/NbIiB/3YJE61z15UaV4LUHCdKCglkl0ue3FuFQMK2EN3OOstzLoWRaCZATXvqAR1CBeSXFuFiLgRkU26TqH+dMLAF4TojX7BOBV+0ws2ShFzahweCSVmxBg8Elmv9KZHCJpk2Jpk2Jhk2Jpk2JhvWVrGU1ga3dBuq2u6Y1C/ZQszu36dWya1pNYKfOsW/siRC5G/Sq8u0t4ReWmHw+zwnmiQAAAABJRU5ErkJggg==", - "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 From 2f118df5fdff4b9fe66c993bac2ca348f74f5deb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 16:50:52 +0300 Subject: [PATCH 19/37] refactor(iot-hub): unify install/update parse-failure messages Route all parse-failure throws through a single parseFailure(action, itemType[, section], cause) helper so the user-facing wording lives in one place and update paths share the template with install paths. --- .../service/iot_hub/DefaultIotHubService.java | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) 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 26d208dbf6..9c75a85232 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 @@ -81,6 +81,17 @@ import java.util.UUID; @Slf4j public class DefaultIotHubService implements IotHubService { + private static final String ACTION_INSTALL = "install"; + private static final String ACTION_UPDATE = "update"; + private static final String SECTION_PACKAGE_DATA = "package data"; + private static final String SECTION_RULE_CHAIN_METADATA = "rule chain metadata"; + + private static final String ITEM_TYPE_WIDGET = "widget"; + private static final String ITEM_TYPE_DASHBOARD = "dashboard"; + private static final String ITEM_TYPE_CALCULATED_FIELD = "calculated field"; + private static final String ITEM_TYPE_RULE_CHAIN = "rule chain"; + private static final String ITEM_TYPE_DEVICE_PROFILE = "device profile"; + private final IotHubRestClient iotHubRestClient; private final TbWidgetTypeService tbWidgetTypeService; private final TbDashboardService tbDashboardService; @@ -155,7 +166,7 @@ public class DefaultIotHubService implements IotHubService { try { widgetTypeDetails = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true); } catch (Exception e) { - throw new Exception("Failed to parse widget data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_WIDGET, e); } widgetTypeDetails.setId(null); widgetTypeDetails.setTenantId(tenantId); @@ -171,7 +182,7 @@ public class DefaultIotHubService implements IotHubService { try { dashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true); } catch (Exception e) { - throw new Exception("Failed to parse dashboard data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_DASHBOARD, e); } dashboard.setId(null); dashboard.setTenantId(tenantId); @@ -187,7 +198,7 @@ public class DefaultIotHubService implements IotHubService { try { calculatedField = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); } catch (Exception e) { - throw new Exception("Failed to parse calculated field data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_CALCULATED_FIELD, e); } calculatedField.setId(null); calculatedField.setTenantId(tenantId); @@ -211,14 +222,14 @@ public class DefaultIotHubService implements IotHubService { try { ruleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true); } catch (Exception e) { - throw new Exception("Failed to parse rule chain: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_RULE_CHAIN, e); } RuleChainMetaData metadata; try { metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true); } catch (Exception e) { - throw new Exception("Failed to parse rule chain metadata: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_RULE_CHAIN, SECTION_RULE_CHAIN_METADATA, e); } ruleChain.setId(null); @@ -281,7 +292,7 @@ public class DefaultIotHubService implements IotHubService { try { deviceProfile = JacksonUtil.fromString(new String(fileData), DeviceProfile.class, true); } catch (Exception e) { - throw new Exception("Failed to parse device profile data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_DEVICE_PROFILE, e); } deviceProfile.setId(null); deviceProfile.setTenantId(tenantId); @@ -405,7 +416,7 @@ public class DefaultIotHubService implements IotHubService { try { newWidgetType = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true); } catch (Exception e) { - throw new Exception("Failed to parse widget data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_WIDGET, e); } WidgetTypeDetails existing = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId()); if (existing == null) { @@ -421,7 +432,7 @@ public class DefaultIotHubService implements IotHubService { try { newDashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true); } catch (Exception e) { - throw new Exception("Failed to parse dashboard data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_DASHBOARD, e); } Dashboard existing = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId()); if (existing == null) { @@ -437,7 +448,7 @@ public class DefaultIotHubService implements IotHubService { try { newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); } catch (Exception e) { - throw new Exception("Failed to parse calculated field data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_CALCULATED_FIELD, e); } CalculatedField existing = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); if (existing == null) { @@ -455,7 +466,7 @@ public class DefaultIotHubService implements IotHubService { try { metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true); } catch (Exception e) { - throw new Exception("Failed to parse rule chain metadata: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_RULE_CHAIN, SECTION_RULE_CHAIN_METADATA, e); } RuleChain existing = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId()); if (existing == null) { @@ -465,7 +476,7 @@ public class DefaultIotHubService implements IotHubService { try { newRuleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true); } catch (Exception e) { - throw new Exception("Failed to parse rule chain: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_RULE_CHAIN, e); } existing.setName(newRuleChain.getName()); RuleChain savedRuleChain = ruleChainService.saveRuleChain(existing); @@ -673,4 +684,13 @@ public class DefaultIotHubService implements IotHubService { iotHubInstalledItemService.deleteById(tenantId, installedItemId); log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); } + + private static Exception parseFailure(String action, String itemTypeName, Exception cause) { + return parseFailure(action, itemTypeName, SECTION_PACKAGE_DATA, cause); + } + + private static Exception parseFailure(String action, String itemTypeName, String section, Exception cause) { + return new Exception("Unable to " + action + " this " + itemTypeName + " — the " + section + " could not be parsed. " + + "The package may be corrupted or in an unexpected format; please contact the creator to have it re-published.", cause); + } } From 6f540c3715bb4ea904fa41a1227861c6c9011dfc Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 16:54:08 +0300 Subject: [PATCH 20/37] refactor(iot-hub): route alarm rule install/update parse failures through parseFailure helper --- .../server/service/iot_hub/DefaultIotHubService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 c81062eef0..bbe2ffcdae 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 @@ -93,6 +93,7 @@ public class DefaultIotHubService implements IotHubService { private static final String ITEM_TYPE_WIDGET = "widget"; private static final String ITEM_TYPE_DASHBOARD = "dashboard"; private static final String ITEM_TYPE_CALCULATED_FIELD = "calculated field"; + private static final String ITEM_TYPE_ALARM_RULE = "alarm rule"; private static final String ITEM_TYPE_RULE_CHAIN = "rule chain"; private static final String ITEM_TYPE_DEVICE_PROFILE = "device profile"; @@ -225,7 +226,7 @@ public class DefaultIotHubService implements IotHubService { try { alarmRule = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); } catch (Exception e) { - throw new Exception("Failed to parse alarm rule data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_ALARM_RULE, e); } if (alarmRule.getType() != CalculatedFieldType.ALARM) { throw new Exception("Expected ALARM-type calculated field for ALARM_RULE install; got: " + alarmRule.getType()); @@ -496,7 +497,7 @@ public class DefaultIotHubService implements IotHubService { try { newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); } catch (Exception e) { - throw new Exception("Failed to parse alarm rule data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_ALARM_RULE, e); } if (newCf.getType() != CalculatedFieldType.ALARM) { throw new Exception("Expected ALARM-type calculated field for ALARM_RULE update; got: " + newCf.getType()); From c7153a5f567cdaab79134e5ca0d1fff32cbea2bb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 17:22:16 +0300 Subject: [PATCH 21/37] fix(iot-hub): preserve typed search text when picking an autocomplete result Drop the (optionSelected) handler and route navigation through the option's (click) instead. Setting [value]="searchText" makes Material write the typed string back through ngModel (a no-op) rather than the MpItemVersionView object, so the input keeps the user's query after the detail dialog closes and searchText.trim() in seeAllResults() stays safe. --- .../home/pages/iot-hub/iot-hub-home.component.html | 3 +-- .../modules/home/pages/iot-hub/iot-hub-home.component.ts | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html index f9f7285c3b..71506d090e 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html @@ -78,7 +78,6 @@ @if (searchLoading) { @@ -99,7 +98,7 @@ @for (group of searchResultGroups; track group.type) { @for (item of group.items; track item.id) { - + @if (isCompactType(item.type)) {
{{ getCompactIcon(item) }} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index c8fff1ea7e..87f8eee0b2 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -17,7 +17,6 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { Router } from '@angular/router'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; -import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { BreakpointObserver } from '@angular/cdk/layout'; import { forkJoin, Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; @@ -235,13 +234,6 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { return typeof value === 'string' ? value : this.searchText || ''; }; - onSearchOptionSelected(event: MatAutocompleteSelectedEvent): void { - const item = event.option.value as MpItemVersionView; - this.searchText = ''; - this.searchAutoTrigger?.closePanel(); - this.openItemDetail(item); - } - clearSearch(): void { this.searchText = ''; this.searchSubject.next(''); From 7002697f69b989b5ae9ede5178c01572d66bc354 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 18:35:07 +0300 Subject: [PATCH 22/37] fix(iot-hub): append /preview suffix on card image URL when missing --- .../home/components/iot-hub/iot-hub-item-card.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index bdf50acddf..2d9c13d8d7 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts @@ -62,8 +62,8 @@ export class TbIotHubItemCardComponent { return null; } const [path, query] = this.item.image.split('?'); - const original = path.endsWith('/preview') ? path.slice(0, -'/preview'.length) : path; - const resolved = query ? `${original}?${query}` : original; + const withPreview = path.endsWith('/preview') ? path : `${path}/preview`; + const resolved = query ? `${withPreview}?${query}` : withPreview; return this.iotHubApiService.resolveResourceUrl(resolved); } From 71182fea387733d26ce0a3b9650e2b4df56e5588 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 18:35:13 +0300 Subject: [PATCH 23/37] refactor(iot-hub): fetch device package zip inside install dialog Move the zip fetch from IotHubActionsService into TbDeviceInstallDialogComponent.ngOnInit so the dialog opens immediately and shows its own loading spinner instead of blocking the caller. Drops the now-unused fetchZipData helper and zipData field from DeviceInstallDialogData. --- .../device-install-dialog.component.html | 4 ++-- .../device-install-dialog.component.ts | 7 ++++-- .../iot-hub/iot-hub-actions.service.ts | 22 +++++-------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html index 733ed0a906..402381ad67 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html @@ -164,8 +164,8 @@ @if (loading) { -
- +
+
} @else {
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 53dc3ac523..e4df929fd2 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 @@ -53,10 +53,10 @@ import { InstallStepType, stepTypeAliasMap } from '@shared/models/iot-hub/device-package.models'; +import { mergeMap } from 'rxjs/operators'; export interface DeviceInstallDialogData { item: MpItemVersionView; - zipData: ArrayBuffer; reviewMode?: boolean; selectedInstallMethod?: string; installState?: Record; @@ -138,8 +138,11 @@ export class TbDeviceInstallDialogComponent extends DialogComponent { try { + const zipData = await firstValueFrom(this.iotHubApiService.getVersionFileData(this.data.item.id, { ignoreLoading: true }).pipe( + mergeMap((blob: Blob) => blob.arrayBuffer()) + )); const JSZip = (await import('jszip')).default; - const zip = await JSZip.loadAsync(this.data.zipData); + const zip = await JSZip.loadAsync(zipData); const imageExtensions = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg']); for (const [path, entry] of Object.entries(zip.files) as [string, any][]) { if (!entry.dir) { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts index 4fd1565c07..bae080a464 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts @@ -129,34 +129,24 @@ export class IotHubActionsService { } installDevice(item: MpItemVersionView): Observable { - return this.fetchZipData(item).pipe( - mergeMap((zipData: ArrayBuffer) => this.openDeviceInstallDialog(item, zipData)) - ); + return this.openDeviceInstallDialog(item); } reviewDevice(item: MpItemVersionView, deviceDescriptor: DeviceInstalledItemDescriptor): Observable { - return this.fetchZipData(item).pipe( - mergeMap((zipData: ArrayBuffer) => this.openDeviceInstallDialog(item, zipData, { + return this.openDeviceInstallDialog(item, { reviewMode: true, selectedInstallMethod: deviceDescriptor.selectedInstallMethod, installState: deviceDescriptor.installState - })) - ); - } - - private fetchZipData(item: MpItemVersionView): Observable { - return this.iotHubApiService.getVersionFileData(item.id as string, { ignoreLoading: true }).pipe( - mergeMap((blob: Blob) => blob.arrayBuffer()) - ); + }); } - private openDeviceInstallDialog(item: MpItemVersionView, zipData: ArrayBuffer, + private openDeviceInstallDialog(item: MpItemVersionView, options?: { reviewMode?: boolean; selectedInstallMethod?: string; installState?: any }): Observable { - return this.dialog.open(TbDeviceInstallDialogComponent, { + return this.dialog.open(TbDeviceInstallDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog-lt-md'], disableClose: true, autoFocus: false, - data: { item, zipData, ...options } as DeviceInstallDialogData + data: { item, ...options } }).afterClosed(); } } From 1afd2f273b58e5e91ad9e041e32c04ae1d9a8e55 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 18:35:19 +0300 Subject: [PATCH 24/37] style(iot-hub): detail dialog mobile close button + meta-grid overflow polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the detail dialog close button into desktop (inline in header) and mobile (absolute, top-right) variants. Switch meta-grid first column to minmax(0, …) so the creator section can shrink, and truncate the creator name with a tooltip. Add 8px right margin to the card type label. --- .../iot-hub/iot-hub-item-card.component.scss | 1 + .../iot-hub-item-detail-dialog.component.html | 10 ++++--- .../iot-hub-item-detail-dialog.component.scss | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss index d46124d91e..71f3ea051d 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss @@ -285,6 +285,7 @@ font-weight: 500; color: rgba(0, 0, 0, 0.54); text-transform: uppercase; + margin-right: 8px; tb-icon { font-size: 12px; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index 3827599242..e42f1521aa 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -69,12 +69,16 @@ } } -
+ +
@@ -163,7 +167,7 @@
@if (showCreator) { -
+
{{ 'iot-hub.creator' | translate }}
verified }
- {{ item.creatorDisplayName }} + {{ item.creatorDisplayName }}
} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss index 0622ce4af5..5b7a182a01 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss @@ -54,6 +54,21 @@ } +.dlg-close-btn-desktop { + @media #{$mat-lt-sm} { + display: none; + } +} + +.dlg-close-btn-mobile { + position: absolute; + top: 16px; + right: 12px; + @media #{$mat-gt-xs} { + display: none; + } +} + // Header icon for CF/RC — Design: 64px, rounded-8, colored bg, white icon 36px .dlg-header-icon { width: 64px; @@ -250,21 +265,25 @@ display: grid; gap: 24px; padding: 16px 24px 12px; - grid-template-columns: max-content minmax(0, max-content) minmax(auto, max-content) minmax(auto, 1fr) max-content; + grid-template-columns: minmax(0, max-content) minmax(0, max-content) minmax(auto, max-content) minmax(auto, 1fr) max-content; &.dlg-meta-4-col { - grid-template-columns: max-content minmax(auto, max-content) minmax(auto, 1fr) max-content; + grid-template-columns: minmax(0, max-content) minmax(auto, max-content) minmax(auto, 1fr) max-content; } .dlg-meta-details { display: none; } + .dlg-meta-creator-section { + min-width: 0; + } + @media #{$mat-lt-md} { - grid-template-columns: max-content max-content max-content; + grid-template-columns: minmax(0, 1fr) max-content max-content; &.dlg-meta-4-col { - grid-template-columns: max-content max-content max-content; + grid-template-columns: minmax(0, 1fr) max-content max-content; } .dlg-meta-details-section { From 402732377abdb11dc7f5144142454bb2d6689b8f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 19:57:54 +0300 Subject: [PATCH 25/37] refactor(iot-hub): use blob object URLs for device package zip images Switch from base64 data URLs to URL.createObjectURL for images extracted from the device package zip. Revokes them in ngOnDestroy to avoid leaking memory, and avoids embedding multi-megabyte data URLs in the DOM. --- .../device-install-dialog.component.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) 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 e4df929fd2..c94bb477f3 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 @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Inject, OnInit, Type, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { @@ -40,9 +40,6 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import { generateSecret } from '@core/utils'; import { - installMethodIcons as INSTALL_METHOD_ICONS, - installMethodLabels as INSTALL_METHOD_LABELS, - peOnlyInstallMethods, DeviceInstallStep, DevicePackageInfo, ENTITY_STEP_TYPES, @@ -50,7 +47,10 @@ import { EntityStepProgress, FormFieldDefinition, FormFieldType, + installMethodIcons as INSTALL_METHOD_ICONS, + installMethodLabels as INSTALL_METHOD_LABELS, InstallStepType, + peOnlyInstallMethods, stepTypeAliasMap } from '@shared/models/iot-hub/device-package.models'; import { mergeMap } from 'rxjs/operators'; @@ -89,7 +89,7 @@ const DEFAULT_RANDOM_SIZE = 20; templateUrl: './device-install-dialog.component.html', styleUrls: ['./device-install-dialog.component.scss'] }) -export class TbDeviceInstallDialogComponent extends DialogComponent implements OnInit { +export class TbDeviceInstallDialogComponent extends DialogComponent implements OnInit, OnDestroy { @ViewChild('installStepper', {static: false}) stepper: MatStepper; @@ -148,9 +148,9 @@ export class TbDeviceInstallDialogComponent extends DialogComponent Date: Fri, 12 Jun 2026 19:57:57 +0300 Subject: [PATCH 26/37] style(iot-hub): add bottom margin to markdown doc-link buttons so they wrap cleanly --- .../modules/home/components/iot-hub/iot-hub-markdown.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts index 6685a898f5..6db27ce352 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts @@ -54,7 +54,7 @@ export function resolveDocLinkPlaceholders( function buildDocLinkButton(url: string, text: string, icon: string): string { const safeUrl = escapeHtmlAttr(url); const safeText = escapeHtml(text); - return `
` + + return `` + `${icon}${safeText}`; } From 772a9cda7c833c4e31c4395551c573c3c95bd988 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Jun 2026 19:58:01 +0300 Subject: [PATCH 27/37] style(photoswipe): drop hard-coded image size caps and align zoom button with close button Remove the 90vw/78vh caps so the image scales naturally inside the viewport, and apply the same 40x40 sizing to the zoom button as the close button. --- .../app/shared/directives/photoswipe-gallery.directive.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/directives/photoswipe-gallery.directive.ts b/ui-ngx/src/app/shared/directives/photoswipe-gallery.directive.ts index f4bd39e72b..22eb3c76dd 100644 --- a/ui-ngx/src/app/shared/directives/photoswipe-gallery.directive.ts +++ b/ui-ngx/src/app/shared/directives/photoswipe-gallery.directive.ts @@ -62,8 +62,6 @@ const PHOTO_GALLERY_STYLE = '\n' + '.pswp__item img.pswp__img {\n' + ' display: block;\n' + - ' max-width: 90vw;\n' + - ' max-height: 78vh;\n' + ' object-fit: contain;\n' + ' border-radius: 4px; \n' + ' box-shadow: 0 20px 60px #00000080;\n' + @@ -90,14 +88,14 @@ const PHOTO_GALLERY_STYLE = ' outline: none;\n' + '}\n' + '\n' + - '.pswp__button.pswp__button--close {\n' + + '.pswp__button.pswp__button--close, .pswp__button.pswp__button--zoom {\n' + ' width: 40px;\n' + ' height: 40px;\n' + ' margin-top: 16px;\n' + ' margin-right: 16px;\n' + '}\n' + '\n' + - '.pswp__button.pswp__button--close .pswp__icn {\n' + + '.pswp__button.pswp__button--close .pswp__icn, .pswp__button.pswp__button--zoom .pswp__icn {\n' + ' width: 24px;\n' + ' height: 24px;\n' + ' top: 7px;\n' + From d74496b44a1243b0e9fe0ff85c2efe76100a3138 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 15 Jun 2026 11:12:26 +0300 Subject: [PATCH 28/37] fix(iot-hub): include webp in device package image extensions --- .../device-install-dialog/device-install-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c94bb477f3..ffb979689e 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 Date: Mon, 15 Jun 2026 13:18:06 +0300 Subject: [PATCH 29/37] fix alreadInstalledItems to searchByTenantAndItemId instead of only tenantId --- .../service/iot_hub/DefaultIotHubService.java | 56 +++++++++++++++--- .../iot_hub/DefaultIotHubServiceTest.java | 58 +++++++++++++++---- .../dao/iot_hub/IotHubInstalledItemDao.java | 3 + .../iot_hub/IotHubInstalledItemService.java | 3 + .../IotHubInstalledItemServiceImpl.java | 6 ++ .../IotHubInstalledItemRepository.java | 6 ++ .../iot_hub/JpaIotHubInstalledItemDao.java | 6 ++ 7 files changed, 119 insertions(+), 19 deletions(-) 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 201e48001e..4ac2cd4244 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 @@ -713,7 +713,24 @@ public class DefaultIotHubService implements IotHubService { throw new IllegalArgumentException("Marketplace version not found: " + versionId); } - Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)); + // 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, @@ -721,10 +738,6 @@ public class DefaultIotHubService implements IotHubService { // level deep, so there is no need to walk a related item's own related items. LinkedHashMap entries = new LinkedHashMap<>(); - // 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); if (related != null && related.isArray()) { for (JsonNode relatedNode : related) { String relatedItemId = relatedNode.asText(); @@ -816,9 +829,22 @@ public class DefaultIotHubService implements IotHubService { } // 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 against the current state - // so we never install the same item twice. - Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)); + // 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<>(); @@ -847,6 +873,9 @@ public class DefaultIotHubService implements IotHubService { 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(); } @@ -895,6 +924,17 @@ public class DefaultIotHubService implements IotHubService { 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(String itemId, Set alreadyInstalledItemIds) { if (itemId == null) { return false; 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 index 9631129a81..d505509e4a 100644 --- 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 @@ -103,7 +103,7 @@ class DefaultIotHubServiceTest { String versionId = UUID.randomUUID().toString(); String itemId = UUID.randomUUID().toString(); when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(version(versionId, itemId, "DASHBOARD", "Root", "1.0")); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); InstallPlan plan = service.resolveInstallPlan(user, versionId); @@ -125,7 +125,7 @@ class DefaultIotHubServiceTest { when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)) .thenReturn(version(UUID.randomUUID().toString(), relatedItemId, "WIDGET", "Dep", "2.0")); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); InstallPlan plan = service.resolveInstallPlan(user, versionId); @@ -146,7 +146,7 @@ class DefaultIotHubServiceTest { root.putArray("relatedItems").add(relatedItemId); when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenReturn(null); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); InstallPlan plan = service.resolveInstallPlan(user, versionId); @@ -166,7 +166,7 @@ class DefaultIotHubServiceTest { root.putArray("relatedItems").add(relatedItemId); when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenThrow(new RuntimeException("boom")); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); InstallPlan plan = service.resolveInstallPlan(user, versionId); @@ -181,7 +181,7 @@ class DefaultIotHubServiceTest { UUID rootItemId = UUID.randomUUID(); when(iotHubRestClient.getVersionInfo(versionId)) .thenReturn(version(versionId, rootItemId.toString(), "DASHBOARD", "Root", "1.0")); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of(rootItemId)); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(rootItemId)); InstallPlan plan = service.resolveInstallPlan(user, versionId); @@ -204,7 +204,7 @@ class DefaultIotHubServiceTest { assertThat(result.isSuccess()).isFalse(); assertThat(result.getErrorMessage()).contains("empty"); - verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantId(any()); + verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); } @Test @@ -213,7 +213,6 @@ class DefaultIotHubServiceTest { InstallPlanEntry missing = new InstallPlanEntry(); missing.setItemId(UUID.randomUUID().toString()); missing.setStatus(InstallPlanEntry.Status.MISSING); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(missing)), null, null); @@ -221,6 +220,8 @@ class DefaultIotHubServiceTest { 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 @@ -233,7 +234,7 @@ class DefaultIotHubServiceTest { 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.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of(itemId)); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(itemId)); InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(entry)), null, null); @@ -243,6 +244,41 @@ class DefaultIotHubServiceTest { 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(); @@ -253,7 +289,7 @@ class DefaultIotHubServiceTest { InstallPlanEntry dep = willInstall(depVersionId, false); InstallPlanEntry root = willInstall(rootVersionId, true); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); IotHubInstalledItem rootItem = installedItem(UUID.randomUUID()); DashboardInstalledItemDescriptor rootDescriptor = new DashboardInstalledItemDescriptor(); @@ -287,7 +323,7 @@ class DefaultIotHubServiceTest { InstallPlanEntry dep2 = willInstall(dep2VersionId, false); InstallPlanEntry root = willInstall(rootVersionId, true); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + 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()); @@ -316,7 +352,7 @@ class DefaultIotHubServiceTest { InstallPlanEntry dep = willInstall(depVersionId, false); InstallPlanEntry root = willInstall(rootVersionId, true); - when(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)).thenReturn(List.of()); + 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()); 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); From 8cbac06ffc7b1e7ef133e63f9420586462487226 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 15 Jun 2026 17:27:30 +0300 Subject: [PATCH 30/37] fix(iot-hub): skip already-installed check for non-widget/solution-template root entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root entries other than WIDGET or SOLUTION_TEMPLATE should always be installed even when a prior version is already installed — only widgets and solution templates are deduplicated by item id at install-plan resolution time. --- .../service/iot_hub/DefaultIotHubService.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 21e654baba..fbd7e4160b 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 @@ -814,7 +814,7 @@ public class DefaultIotHubService implements IotHubService { entry.setType(optText(versionInfo, FIELD_TYPE)); entry.setVersion(optText(versionInfo, FIELD_VERSION)); entry.setRoot(root); - entry.setStatus(isAlreadyInstalled(itemId, alreadyInstalledItemIds) + entry.setStatus(isAlreadyInstalled(entry, alreadyInstalledItemIds) ? InstallPlanEntry.Status.ALREADY_INSTALLED : InstallPlanEntry.Status.WILL_INSTALL); entries.put(itemId, entry); @@ -872,7 +872,7 @@ public class DefaultIotHubService implements IotHubService { } case ALREADY_INSTALLED -> resultEntries.add(resultEntry); case WILL_INSTALL -> { - if (isAlreadyInstalled(entry.getItemId(), alreadyInstalledItemIds)) { + 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); @@ -946,12 +946,15 @@ public class DefaultIotHubService implements IotHubService { } } - private static boolean isAlreadyInstalled(String itemId, Set alreadyInstalledItemIds) { - if (itemId == null) { + 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(itemId)); + return alreadyInstalledItemIds.contains(UUID.fromString(entry.getItemId())); } catch (IllegalArgumentException ex) { return false; } From 456ef7e7361dc0ba478437dcad43fdcec705ec84 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 15 Jun 2026 18:56:28 +0300 Subject: [PATCH 31/37] feat(iot-hub): add YouTube link to creator profile Add youtubeUrl to CreatorView and render it alongside the other creator social links. Reindents the link block so it nests correctly inside the surrounding flex column. --- .../iot-hub-creator-profile.component.html | 66 ++++++++++--------- .../models/iot-hub/iot-hub-creator.models.ts | 1 + 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html index e36dd5210c..f599fcc743 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html @@ -61,36 +61,42 @@

{{ creator.description }}

}
- @if (creator.website) { - - language - {{ getWebsiteLabel() }} - - } - @if (creator.contactEmail) { - - mdi:email-outline - {{ creator.contactEmail }} - - } - @if (creator.githubUrl) { - - mdi:github - GitHub - - } - @if (creator.linkedinUrl) { - - mdi:linkedin - LinkedIn - - } - @if (creator.twitterUrl) { - - mdi:twitter - X - - } + @if (creator.website) { + + language + {{ getWebsiteLabel() }} + + } + @if (creator.contactEmail) { + + mdi:email-outline + {{ creator.contactEmail }} + + } + @if (creator.githubUrl) { + + mdi:github + GitHub + + } + @if (creator.linkedinUrl) { + + mdi:linkedin + LinkedIn + + } + @if (creator.twitterUrl) { + + mdi:twitter + X + + } + @if (creator.youtubeUrl) { + + mdi:youtube + YouTube + + }
diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-creator.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-creator.models.ts index 0f9c791f3d..3cf365fbb5 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-creator.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-creator.models.ts @@ -25,6 +25,7 @@ export interface CreatorView { githubUrl: string; linkedinUrl: string; twitterUrl: string; + youtubeUrl: string; publishedCount: number; verified: boolean; } From 93b50bdf96eb9647e3b9aaaea171496951660a43 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 12:06:36 +0300 Subject: [PATCH 32/37] fix(solutions): bound solution-template zip extraction to mitigate zip-bomb attacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stream entries through a fixed buffer and abort with IOException when the archive crosses any of three configurable thresholds: total uncompressed bytes, per-entry uncompressed bytes, or entry count. Defaults — 200 MiB total, 50 MiB per entry, 10 000 entries — and env-var overrides IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES / IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES / IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT are exposed under iot-hub in thingsboard.yml. --- .../solutions/DefaultSolutionService.java | 36 +++++++++++++++++-- .../src/main/resources/thingsboard.yml | 3 ++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index 9d13f15b96..53a9d503b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -142,6 +142,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -179,6 +180,15 @@ public class DefaultSolutionService implements SolutionService { @Value("${ui.solution_templates.docs_base_url:https://thingsboard.io/docs}") private String docsBaseUrl; + @Value("${iot-hub.max-uncompressed-archive-bytes:209715200}") + private long maxUncompressedArchiveBytes; + + @Value("${iot-hub.max-uncompressed-entry-bytes:52428800}") + private long maxUncompressedEntryBytes; + + @Value("${iot-hub.max-archive-entry-count:10000}") + private int maxArchiveEntryCount; + private final RuleChainService ruleChainService; private final TbRuleChainService tbRuleChainService; private final DeviceProfileService deviceProfileService; @@ -1550,10 +1560,18 @@ public class DefaultSolutionService implements SolutionService { return Collections.emptyList(); } - private static void extractZip(byte[] zipData, Path destDir) throws IOException { + private static final int EXTRACT_BUFFER_SIZE = 8 * 1024; + + private void extractZip(byte[] zipData, Path destDir) throws IOException { + long totalBytes = 0; + int entryCount = 0; + byte[] buf = new byte[EXTRACT_BUFFER_SIZE]; try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { + if (++entryCount > maxArchiveEntryCount) { + throw new IOException("Solution template archive exceeds max entry count: " + maxArchiveEntryCount); + } Path entryPath = destDir.resolve(entry.getName()).normalize(); if (!entryPath.startsWith(destDir)) { throw new IOException("ZIP entry outside of target directory: " + entry.getName()); @@ -1562,7 +1580,21 @@ public class DefaultSolutionService implements SolutionService { Files.createDirectories(entryPath); } else { Files.createDirectories(entryPath.getParent()); - Files.write(entryPath, zis.readAllBytes()); + try (OutputStream out = Files.newOutputStream(entryPath)) { + long entryBytes = 0; + int n; + while ((n = zis.read(buf)) > 0) { + entryBytes += n; + totalBytes += n; + if (entryBytes > maxUncompressedEntryBytes) { + throw new IOException("Solution template entry exceeds max uncompressed size: " + entry.getName()); + } + if (totalBytes > maxUncompressedArchiveBytes) { + throw new IOException("Solution template archive exceeds max uncompressed size: " + maxUncompressedArchiveBytes + " bytes"); + } + out.write(buf, 0, n); + } + } } zis.closeEntry(); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 12c6685684..fbc6ada7b8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -2172,3 +2172,6 @@ iot-hub: connect-timeout-sec: "${IOT_HUB_CONNECT_TIMEOUT_SEC:5}" # timeout in seconds to connect to IoT Hub server. read-timeout-sec: "${IOT_HUB_READ_TIMEOUT_SEC:10}" # timeout in seconds to read from IoT Hub server. max-file-data-size-bytes: "${IOT_HUB_MAX_FILE_DATA_SIZE_BYTES:104857600}" # maximum size in bytes of a file-data payload fetched from IoT Hub. Requests advertising a larger Content-Length, or streams that exceed this size, are rejected to avoid out-of-memory issues on the platform side. + max-uncompressed-archive-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES:209715200}" # maximum cumulative uncompressed size in bytes for a solution template archive. Extraction aborts past this threshold to mitigate zip-bomb attacks. + max-uncompressed-entry-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES:52428800}" # maximum uncompressed size in bytes for any single entry inside a solution template archive. + max-archive-entry-count: "${IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT:10000}" # maximum number of entries allowed inside a solution template archive. From 62c1963507f450ad1430a28eafde5d68b34e90be Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 12:15:34 +0300 Subject: [PATCH 33/37] fix(solutions): clamp solution-template installTimeoutMs to a configurable max MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-install Thread.sleep was driven by installTimeoutMs from the uploaded solution.json with no upper bound — a malicious or misconfigured template could pin an HTTP worker thread indefinitely. Cap it at a configurable max (default 60s) exposed as iot-hub.max-install-timeout-ms with an IOT_HUB_MAX_INSTALL_TIMEOUT_MS env override. --- .../server/service/solutions/DefaultSolutionService.java | 5 ++++- application/src/main/resources/thingsboard.yml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index 53a9d503b6..71020ae47e 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -189,6 +189,9 @@ public class DefaultSolutionService implements SolutionService { @Value("${iot-hub.max-archive-entry-count:10000}") private int maxArchiveEntryCount; + @Value("${iot-hub.max-install-timeout-ms:60000}") + private long maxInstallTimeoutMs; + private final RuleChainService ruleChainService; private final TbRuleChainService tbRuleChainService; private final DeviceProfileService deviceProfileService; @@ -400,7 +403,7 @@ public class DefaultSolutionService implements SolutionService { List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); if (ruleChainDefs.stream().anyMatch(r -> StringUtils.isNotEmpty(r.getUpdate()))) { - long timeout = loadInstallTimeoutMs(ctx.getTempDir()); + long timeout = Math.min(loadInstallTimeoutMs(ctx.getTempDir()), maxInstallTimeoutMs); if (timeout > 0) { Thread.sleep(timeout); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index fbc6ada7b8..f678076c57 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -2175,3 +2175,4 @@ iot-hub: max-uncompressed-archive-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES:209715200}" # maximum cumulative uncompressed size in bytes for a solution template archive. Extraction aborts past this threshold to mitigate zip-bomb attacks. max-uncompressed-entry-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES:52428800}" # maximum uncompressed size in bytes for any single entry inside a solution template archive. max-archive-entry-count: "${IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT:10000}" # maximum number of entries allowed inside a solution template archive. + max-install-timeout-ms: "${IOT_HUB_MAX_INSTALL_TIMEOUT_MS:60000}" # upper clamp in milliseconds for the post-install wait declared in a solution template's solution.json. Prevents a malicious or misconfigured template from pinning an HTTP worker thread. From 77c74d7ccd70db15089dbbdf56c2d31ced76b54c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 12:43:33 +0300 Subject: [PATCH 34/37] fix(iot-hub): compensate orphaned entities when installed-item tracking save fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If iotHubInstalledItemService.save throws after the local entity was already created, the entity was orphaned — visible in the tenant but invisible to IoT Hub update/delete. Extracted the per-type entity-deletion dispatch from deleteInstalledItem into a deleteEntityForDescriptor helper and call it from doInstallVersion and registerDeviceInstall on tracking-save failure. registerDeviceInstall now also wraps reportVersionInstalled best-effort to match doInstallVersion. --- .../service/iot_hub/DefaultIotHubService.java | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) 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 fbd7e4160b..e88d812f5b 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 @@ -178,7 +178,22 @@ public class DefaultIotHubService implements IotHubService { installedItem.setItemType(itemType); installedItem.setVersion(version); installedItem.setDescriptor(descriptor); - installedItem = iotHubInstalledItemService.save(tenantId, installedItem); + try { + installedItem = iotHubInstalledItemService.save(tenantId, installedItem); + } catch (Exception e) { + // Tracking save failed after the entity was already created locally — compensate by + // deleting the orphaned entity so the user is not left with an installed item that + // IoT Hub update/delete flows cannot see. + log.error("[{}] Failed to save IoT Hub installed-item tracking row for {} ({}); rolling back created entity", + tenantId, itemName, itemType, e); + try { + deleteEntityForDescriptor(user, tenantId, descriptor, null); + } catch (Exception rb) { + log.error("[{}] Compensating delete after failed tracking save threw — entity may remain orphaned", + tenantId, rb); + } + throw e; + } try { iotHubRestClient.reportVersionInstalled(versionId); @@ -620,9 +635,26 @@ public class DefaultIotHubService implements IotHubService { installedItem.setItemType("DEVICE"); installedItem.setVersion(version); installedItem.setDescriptor(descriptor); - iotHubInstalledItemService.save(tenantId, installedItem); + try { + iotHubInstalledItemService.save(tenantId, installedItem); + } catch (Exception e) { + log.error("[{}] Failed to save IoT Hub installed-item tracking row for device package {} (version {}); rolling back created entities", + tenantId, itemName, version, e); + try { + deleteEntityForDescriptor(user, tenantId, descriptor, null); + } catch (Exception rb) { + log.error("[{}] Compensating delete after failed tracking save threw — device package entities may remain orphaned", + tenantId, rb); + } + throw e; + } - iotHubRestClient.reportVersionInstalled(versionId); + 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("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); return InstallItemVersionResult.success(descriptor); @@ -677,7 +709,20 @@ public class DefaultIotHubService implements IotHubService { throw new IllegalArgumentException("Installed item not found"); } - IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); + deleteEntityForDescriptor(user, tenantId, installedItem.getDescriptor(), installedItemId); + + iotHubInstalledItemService.deleteById(tenantId, installedItemId); + log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); + } + + /** + * Per-type dispatcher that removes the local entities a marketplace install created. Shared by + * {@link #deleteInstalledItem} and by the tracking-row compensation path in + * {@link #doInstallVersion} so a failed tracking save does not leave the entity orphaned. + */ + private void deleteEntityForDescriptor(SecurityUser user, TenantId tenantId, + IotHubInstalledItemDescriptor descriptor, + IotHubInstalledItemId installedItemId) { if (descriptor instanceof WidgetInstalledItemDescriptor wd) { WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, wd.getWidgetTypeId()); if (widgetType != null) { @@ -709,9 +754,6 @@ public class DefaultIotHubService implements IotHubService { log.error("[{}] Failed to delete solution for installed item {}", tenantId, installedItemId, e); } } - - iotHubInstalledItemService.deleteById(tenantId, installedItemId); - log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); } @Override From 729cdfc046057777b9a3449009fcf3645a305a27 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 13:10:41 +0300 Subject: [PATCH 35/37] test: align DeviceConnectivityUtilTest with new DockerComposeParams signature --- .../server/dao/util/DeviceConnectivityUtilTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java index 2c6fee2fcf..e53a76f09f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java @@ -20,6 +20,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.device.DockerComposeParams; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -115,8 +116,9 @@ class DeviceConnectivityUtilTest { } private static String renderCompose(DeviceCredentials credentials) throws Exception { + DockerComposeParams params = new DockerComposeParams(true, "tb-gateway", true, true, true, true); var resource = DeviceConnectivityUtil.getGatewayDockerComposeFile( - "host.docker.internal", "3.8-stable", credentials); + "host.docker.internal", "3.8-stable", credentials, params); try (var in = resource.getInputStream()) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } From a4e4f5615863f544acaf008fc0993f2b56574ebc Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 13:10:41 +0300 Subject: [PATCH 36/37] fix(iot-hub): show connection-method selector step even when a single method is available --- .../device-install-dialog/device-install-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ffb979689e..96c5f9ed72 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 @@ -565,7 +565,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent 1 && !this.selectedInstallMethod) { + if (!this.reviewMode && this.availableInstallMethods.length > 0 && !this.selectedInstallMethod) { // First step: connection method selector. Remaining steps are // appended via appendInstallSteps() once a method is confirmed. this.wizardSteps.push({ From 5fa837fb0309a939a4b80925183fbee0486c5e1d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 16 Jun 2026 13:44:59 +0300 Subject: [PATCH 37/37] fix(iot-hub): restrict installed-items list sort property to a server-side allow-list Unvalidated sortProperty on GET /api/iot-hub/installedItems was passed straight into Spring Data's Sort.by, so a client could trigger a 500 PropertyReferenceException by naming an unknown entity field. Add ALLOWED_SORT_PROPERTIES on IotHubInstalledItemEntity (createdTime, itemName, itemType, version) and call validatePageLink in the service so the failure is a 400 with a clear message. --- .../dao/iot_hub/IotHubInstalledItemServiceImpl.java | 4 ++++ .../server/dao/model/sql/IotHubInstalledItemEntity.java | 8 ++++++++ 2 files changed, 12 insertions(+) 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 d89c21f548..7e86bcb0b9 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 @@ -23,12 +23,15 @@ import org.thingsboard.server.common.data.id.TenantId; 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 org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + @Service @RequiredArgsConstructor @Slf4j @@ -49,6 +52,7 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { @Override public PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink) { + validatePageLink(pageLink, IotHubInstalledItemEntity.ALLOWED_SORT_PROPERTIES); return iotHubInstalledItemDao.findByTenantId(tenantId, itemTypes, itemId, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java index e0cd2f1c17..118a175a36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java @@ -31,7 +31,11 @@ import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.UUID; @Getter @@ -41,6 +45,10 @@ import java.util.UUID; @Table(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TABLE_NAME) public class IotHubInstalledItemEntity extends BaseSqlEntity { + public static final Set ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet( + new LinkedHashSet<>(List.of("createdTime", "itemName", "itemType", "version")) + ); + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN, nullable = false, columnDefinition = "UUID") private UUID tenantId;