diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs
new file mode 100644
index 00000000..dc589278
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs
@@ -0,0 +1,10 @@
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
+
+/// A tag row for the equipment page's Tags tab table — display columns plus the id used to
+/// open the edit modal. RowVersion is re-read fresh on delete (matching the tree's delete path).
+public sealed record EquipmentTagRow(string TagId, string Name, string DriverInstanceId, string DataType, TagAccessLevel AccessLevel);
+
+/// A virtual-tag row for the equipment page's Virtual Tags tab table.
+public sealed record EquipmentVirtualTagRow(string VirtualTagId, string Name, string DataType, string ScriptId, bool Enabled);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
index 3bff22e0..2758a45f 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
@@ -127,6 +127,28 @@ public interface IUnsTreeService
/// Tag nodes followed by VirtualTag nodes; empty if the equipment has none.
Task> LoadEquipmentChildrenAsync(string equipmentId, CancellationToken ct = default);
+ ///
+ /// Loads the driver tags bound to a single equipment as flat row projections for the equipment
+ /// page's Tags tab table, ordered by Name. Each row carries the display columns plus the
+ /// TagId the table uses to open the edit modal. Reads untracked. Returns an empty list when
+ /// the equipment has no tags.
+ ///
+ /// The equipment whose tags to load.
+ /// A token to cancel the load.
+ /// The equipment's tag rows ordered by Name; empty if it has none.
+ Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
+
+ ///
+ /// Loads the virtual tags scoped to a single equipment as flat row projections for the equipment
+ /// page's Virtual Tags tab table, ordered by Name. Each row carries the display columns plus the
+ /// VirtualTagId the table uses to open the edit modal. Reads untracked. Returns an empty list
+ /// when the equipment has no virtual tags.
+ ///
+ /// The equipment whose virtual tags to load.
+ /// A token to cancel the load.
+ /// The equipment's virtual-tag rows ordered by Name; empty if it has none.
+ Task> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
+
///
/// Loads a single UNS area projected for editing, or null if it no longer exists.
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
index dd21f0b3..335e1249 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
@@ -127,6 +127,28 @@ public sealed class UnsTreeService(IDbContextFactory dbF
return result;
}
+ ///
+ public async Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default)
+ {
+ await using var db = await dbFactory.CreateDbContextAsync(ct);
+ return await db.Tags.AsNoTracking()
+ .Where(t => t.EquipmentId == equipmentId)
+ .OrderBy(t => t.Name)
+ .Select(t => new EquipmentTagRow(t.TagId, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel))
+ .ToListAsync(ct);
+ }
+
+ ///
+ public async Task> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default)
+ {
+ await using var db = await dbFactory.CreateDbContextAsync(ct);
+ return await db.VirtualTags.AsNoTracking()
+ .Where(v => v.EquipmentId == equipmentId)
+ .OrderBy(v => v.Name)
+ .Select(v => new EquipmentVirtualTagRow(v.VirtualTagId, v.Name, v.DataType, v.ScriptId, v.Enabled))
+ .ToListAsync(ct);
+ }
+
///
public async Task LoadAreaAsync(string unsAreaId, CancellationToken ct = default)
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs
new file mode 100644
index 00000000..8a4b4016
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs
@@ -0,0 +1,42 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
+
+[Trait("Category", "Unit")]
+public sealed class UnsTreeServiceEquipmentChildRowsTests
+{
+ private static UnsTreeService SeededService()
+ {
+ var dbName = $"uns-childrows-{Guid.NewGuid():N}";
+ UnsTreeTestDb.SeedNamed(dbName);
+ return new UnsTreeService(UnsTreeTestDb.Factory(dbName));
+ }
+
+ [Fact]
+ public async Task LoadTagsForEquipment_returns_tags_in_name_order_scoped()
+ {
+ var rows = await SeededService().LoadTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
+ rows.Count.ShouldBe(2); // the EquipmentId=null orphan tag is excluded
+ rows[0].TagId.ShouldBe("TAG-2"); // "running" < "speed"
+ rows[0].Name.ShouldBe("running");
+ rows[0].DataType.ShouldBe("Boolean");
+ rows[1].TagId.ShouldBe("TAG-1");
+ rows[1].DataType.ShouldBe("Float");
+ }
+
+ [Fact]
+ public async Task LoadVirtualTagsForEquipment_returns_vtags_in_name_order()
+ {
+ var rows = await SeededService().LoadVirtualTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
+ rows.Count.ShouldBe(1);
+ rows[0].VirtualTagId.ShouldBe("VTAG-1");
+ rows[0].Name.ShouldBe("computed");
+ rows[0].DataType.ShouldBe("Double");
+ }
+
+ [Fact]
+ public async Task LoadTagsForEquipment_empty_for_unknown_equipment()
+ => (await SeededService().LoadTagsForEquipmentAsync("EQ-NONE")).ShouldBeEmpty();
+}