diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor
index c5e29aa0..16b39acc 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor
@@ -16,6 +16,9 @@
+
+
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor
index fc0a46ff..3489cc2c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor
@@ -145,6 +145,7 @@ else
}
Add alias (browse Galaxy)
Add tag
+ Convert relay virtual-tags…
@if (!string.IsNullOrWhiteSpace(_tagError))
{
@@ -196,6 +197,76 @@ else
}
+ @if (!string.IsNullOrWhiteSpace(_convertMessage))
+ {
+
@_convertMessage
+ }
+
+ @if (_convertPreview is not null)
+ {
+
+
+ Convert relay virtual-tags to aliases
+ { _convertPreview = null; })">Close
+
+
+
Will convert (@_convertPreview.Converted.Count)
+ @if (_convertPreview.Converted.Count == 0)
+ {
+
No relay virtual-tags to convert.
+ }
+ else
+ {
+
+ Virtual tag Full name Data type
+
+ @foreach (var c in _convertPreview.Converted)
+ {
+
+ @c.VirtualTagName
+ @c.FullName
+ @c.DataType
+
+ }
+
+
+ }
+
+
Skipped (@_convertPreview.Skipped.Count)
+ @if (_convertPreview.Skipped.Count == 0)
+ {
+
Nothing skipped.
+ }
+ else
+ {
+
+ Virtual tag Reason
+
+ @foreach (var s in _convertPreview.Skipped)
+ {
+
+ @s.VirtualTagName
+ @s.Reason
+
+ }
+
+
+ }
+
+
+ @if (_convertPreview.Converted.Count > 0)
+ {
+
+ @if (_convertBusy) { }
+ Apply
+
+ }
+ { _convertPreview = null; })" disabled="@_convertBusy">Cancel
+
+
+
+ }
+
@@ -331,6 +402,12 @@ else
private bool _aliasModalIsNew;
private AliasTagEditDto? _aliasModalExisting;
+ // --- Relay→alias converter (per-equipment). _convertPreview holds the dry-run result while the
+ // inline preview panel is open; null = panel closed. _convertMessage is a brief post-apply summary. ---
+ private RelayConversionResult? _convertPreview;
+ private bool _convertBusy;
+ private string? _convertMessage;
+
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
private IReadOnlyList? _vtags;
private string? _vtagError;
@@ -440,6 +517,36 @@ else
await ReloadTagsAsync();
}
+ // --- Relay→alias converter handlers (per-equipment, scoped to EquipmentId). Dry-run first to fill
+ // the inline preview panel; Apply mutates the draft then reloads the tags + virtual-tags lists so
+ // the converted vtags vanish and the new aliases appear. ---
+
+ private async Task PreviewConvertRelaysAsync()
+ {
+ _convertMessage = null;
+ _convertBusy = true;
+ try
+ {
+ _convertPreview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: true);
+ }
+ finally { _convertBusy = false; }
+ }
+
+ private async Task ApplyConvertRelaysAsync()
+ {
+ _convertBusy = true;
+ try
+ {
+ var r = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: false);
+ _convertPreview = null;
+ _convertMessage = $"Converted {r.Converted.Count}, skipped {r.Skipped.Count}.";
+ // The converted virtual tags become aliases, so both lists change.
+ await ReloadTagsAsync();
+ await ReloadVirtualTagsAsync();
+ }
+ finally { _convertBusy = false; }
+ }
+
// --- Virtual Tags tab handlers ---
private async Task ReloadVirtualTagsAsync()
@@ -542,6 +649,9 @@ else
_tags = null;
_vtags = null;
_alarms = null;
+ // Drop any open relay-conversion preview/summary so it can't leak across equipment changes.
+ _convertPreview = null;
+ _convertMessage = null;
if (!IsNew)
{
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/RelayAliasConvert.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/RelayAliasConvert.razor
new file mode 100644
index 00000000..ae769a8e
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/RelayAliasConvert.razor
@@ -0,0 +1,154 @@
+@page "/uns/convert-relays"
+@* Fleet-wide relay→alias converter. Previews (dry-run) and applies the conversion of pure-relay
+ virtual tags into Galaxy alias tags across every equipment. Edits the draft config only; the
+ operator publishes normally afterward. FleetAdmin-gated (mirrors RoleGrants.razor). *@
+@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "FleetAdmin")]
+@rendermode RenderMode.InteractiveServer
+@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
+@inject IUnsTreeService Svc
+
+Convert relays
+
+
+
Convert relay virtual-tags to aliases
+
Back to UNS
+
+
+
+ Sweeps every equipment for pure-relay virtual tags (a script body that simply returns another
+ tag's value) and rewrites each one as a Galaxy alias tag. This edits the draft configuration
+ only — publish afterward to take effect. Run a preview first to review what will change.
+
+
+
+
+ @if (_busy) { }
+ Preview
+
+ @if (_applied)
+ {
+ Conversion applied. Publish to take effect.
+ }
+
+
+@if (!string.IsNullOrWhiteSpace(_error))
+{
+ @_error
+}
+
+@if (_preview is not null)
+{
+
+ Will convert (@_preview.Converted.Count)
+ @if (_preview.Converted.Count == 0)
+ {
+ No relay virtual-tags to convert.
+ }
+ else
+ {
+
+
+ Equipment Virtual tag Full name Data type
+
+ @foreach (var c in _preview.Converted)
+ {
+
+ @c.EquipmentId
+ @c.VirtualTagName
+ @c.FullName
+ @c.DataType
+
+ }
+
+
+
+ }
+
+
+
+ Skipped (@_preview.Skipped.Count)
+ @if (_preview.Skipped.Count == 0)
+ {
+ Nothing skipped.
+ }
+ else
+ {
+
+
+ Equipment Virtual tag Reason
+
+ @foreach (var s in _preview.Skipped)
+ {
+
+ @s.EquipmentId
+ @s.VirtualTagName
+ @s.Reason
+
+ }
+
+
+
+ }
+
+
+ @if (!_preview.Applied && _preview.Converted.Count > 0)
+ {
+
+ @if (!_confirming)
+ {
+ _confirming = true" disabled="@_busy">
+ Apply conversion…
+
+ }
+ else
+ {
+ Convert @_preview.Converted.Count relay virtual-tag(s) to aliases in the draft?
+
+ @if (_busy) { }
+ Yes, apply
+
+ _confirming = false" disabled="@_busy">
+ Cancel
+
+ }
+
+ }
+}
+
+@code {
+ private RelayConversionResult? _preview;
+ private bool _busy;
+ private bool _confirming;
+ private bool _applied;
+ private string? _error;
+
+ private async Task PreviewAsync()
+ {
+ _error = null;
+ _confirming = false;
+ _applied = false;
+ _busy = true;
+ try
+ {
+ _preview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(null, dryRun: true);
+ }
+ catch (Exception ex) { _error = ex.Message; }
+ finally { _busy = false; }
+ }
+
+ private async Task ApplyAsync()
+ {
+ _error = null;
+ _busy = true;
+ try
+ {
+ await Svc.ConvertRelayVirtualTagsToAliasesAsync(null, dryRun: false);
+ _applied = true;
+ _confirming = false;
+ // Re-run the preview so the Converted set is now empty and only remaining skips show.
+ _preview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(null, dryRun: true);
+ }
+ catch (Exception ex) { _error = ex.Message; }
+ finally { _busy = false; }
+ }
+}