-}
-else
-{
-
- Namespaces are content (decision #123) — they're served at the OPC UA endpoint and bound
- to driver instances. NamespaceUri must be unique fleet-wide.
-
-
-
-
+ The Namespace entity has been removed. The v3 Raw/UNS split replaces it —
+ raw driver folders and the unified namespace are managed on the /raw and
+ /uns pages, arriving in v3 Batch 2/3. This page is a placeholder so existing
+ links do not 404.
+
+ The Namespace entity has been removed. The v3 Raw/UNS split replaces it —
+ raw driver folders and the unified namespace are managed on the /raw and
+ /uns pages, arriving in v3 Batch 2/3. This editor no longer applies.
+
The remote OPC UA NodeId the driver reads/writes/subscribes against. Use the browse picker on the driver page to find it.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs
index 8e8eea22..e6f03fd9 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs
@@ -140,12 +140,13 @@ public sealed class ScriptTagCatalog(IDbContextFactory d
{
await using var db = await dbFactory.CreateDbContextAsync(ct);
- // Only the resolvable keys are projected, so no UNS join is needed: the equipment-tag key is
- // the FullName from TagConfig, the FolderPath-scoped tag key is FolderPath/Name, and the
- // virtual-tag key is its own Name. Pull just those columns, untracked.
+ // v3: Tag is Raw-only — EquipmentId / FolderPath / DriverInstanceId were retired (the
+ // equipment↔Tag + driver bindings are gone). The resolvable key is the driver FullName carried
+ // in TagConfig; project only the surviving columns. (The Raw/UNS tag catalog is rebuilt in
+ // Batch 2/3.)
var tagRows = await db.Tags
.AsNoTracking()
- .Select(t => new { t.EquipmentId, t.Name, t.FolderPath, t.DataType, t.DriverInstanceId, t.TagConfig })
+ .Select(t => new { t.Name, t.DataType, t.TagConfig })
.ToListAsync(ct);
var vtagRows = await db.VirtualTags
@@ -157,13 +158,10 @@ public sealed class ScriptTagCatalog(IDbContextFactory d
foreach (var t in tagRows)
{
- var path = t.EquipmentId is null
- // FolderPath-scoped tag (EquipmentId null) — subscribes by the MXAccess dot-ref, which
- // is "FolderPath.Name" when a folder is set, else just "Name".
- ? (string.IsNullOrWhiteSpace(t.FolderPath) ? t.Name : $"{t.FolderPath}.{t.Name}")
- // Equipment driver tag — the runtime GetTag key is the driver FullName from TagConfig.
- : ExtractFullNameFromTagConfig(t.TagConfig);
- entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, t.DriverInstanceId));
+ // The runtime GetTag key is the driver FullName from TagConfig; fall back to Name if absent.
+ var full = ExtractFullNameFromTagConfig(t.TagConfig);
+ var path = string.IsNullOrWhiteSpace(full) ? t.Name : full;
+ entries.Add(new ScriptTagInfo(path, "Tag", t.DataType, null));
}
foreach (var v in vtagRows)
@@ -185,7 +183,10 @@ public sealed class ScriptTagCatalog(IDbContextFactory d
///
private static string ExtractFullNameFromTagConfig(string tagConfig)
{
- if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
+ // Best-effort: pull the driver FullName out of the TagConfig JSON blob if present.
+ // Returns "" when absent/unparseable so the caller falls back to the tag Name — never
+ // the raw blob (which would surface as a garbage completion path).
+ if (string.IsNullOrWhiteSpace(tagConfig)) return "";
try
{
using var doc = System.Text.Json.JsonDocument.Parse(tagConfig);
@@ -193,10 +194,10 @@ public sealed class ScriptTagCatalog(IDbContextFactory d
&& doc.RootElement.TryGetProperty("FullName", out var fullName)
&& fullName.ValueKind == System.Text.Json.JsonValueKind.String)
{
- return fullName.GetString() ?? tagConfig;
+ return fullName.GetString() ?? "";
}
}
- catch (System.Text.Json.JsonException) { /* fall through to raw blob */ }
- return tagConfig;
+ catch (System.Text.Json.JsonException) { /* fall through to tag Name */ }
+ return "";
}
}
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 1150a9b0..0a12e7ce 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
@@ -51,14 +51,10 @@ public sealed class UnsTreeService(IDbContextFactory dbF
})
.ToListAsync(ct);
- // Per-equipment driver-tag counts (tags with no equipment are excluded).
- var tagCounts = (await db.Tags
- .AsNoTracking()
- .Where(t => t.EquipmentId != null)
- .GroupBy(t => t.EquipmentId)
- .Select(g => new { EquipmentId = g.Key!, Count = g.Count() })
- .ToListAsync(ct))
- .ToDictionary(x => x.EquipmentId, x => x.Count, StringComparer.Ordinal);
+ // Per-equipment driver-tag counts: the equipment↔Tag binding was retired in v3
+ // (Tag is now Raw-only; authoring moved to the Raw tree in Batch 2/3). No binding
+ // exists to count against, so every equipment reports zero driver tags.
+ var tagCounts = new Dictionary(StringComparer.Ordinal);
// Per-equipment virtual-tag counts (virtual tags with no equipment are excluded).
var vtagCounts = (await db.VirtualTags
@@ -83,14 +79,11 @@ public sealed class UnsTreeService(IDbContextFactory dbF
}
///
- public async Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default)
+ public 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);
+ // The equipment↔Tag binding was retired in v3 (Tag is now Raw-only; authoring moved to
+ // the Raw tree in Batch 2/3). No equipment-bound tags exist to list.
+ return Task.FromResult>(Array.Empty());
}
///
@@ -141,7 +134,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF
e.Name,
e.MachineCode,
e.UnsLineId,
- e.DriverInstanceId,
+ // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains.
+ null,
e.ZTag,
e.SAPID,
e.Manufacturer,
@@ -168,9 +162,10 @@ public sealed class UnsTreeService(IDbContextFactory dbF
.Where(t => t.TagId == tagId)
.Select(t => new TagEditDto(
t.TagId,
- t.EquipmentId!,
+ // Tag is Raw-only in v3 — equipment binding + driver binding retired.
+ string.Empty,
t.Name,
- t.DriverInstanceId,
+ string.Empty,
t.DataType,
t.AccessLevel,
t.WriteIdempotent,
@@ -293,37 +288,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
- // A cluster move must not orphan driver-bound equipment from its driver's
- // cluster. Any equipment under this area that is bound to a driver in a different cluster
- // than the target blocks the move.
- if (newClusterId != entity.ClusterId)
- {
- var lineIds = await db.UnsLines
- .Where(l => l.UnsAreaId == unsAreaId)
- .Select(l => l.UnsLineId)
- .ToListAsync(ct);
-
- var boundEquipment = await db.Equipment
- .Where(eq => lineIds.Contains(eq.UnsLineId) && eq.DriverInstanceId != null)
- .Select(eq => new { eq.EquipmentId, eq.DriverInstanceId })
- .ToListAsync(ct);
-
- foreach (var eq in boundEquipment)
- {
- var driverCluster = await db.DriverInstances
- .Where(d => d.DriverInstanceId == eq.DriverInstanceId)
- .Select(d => d.ClusterId)
- .FirstOrDefaultAsync(ct);
-
- if (driverCluster is not null && driverCluster != newClusterId)
- {
- return new UnsMutationResult(
- false,
- $"Cannot move area to '{newClusterId}': equipment '{eq.EquipmentId}' is bound to a driver in cluster '{driverCluster}' (decision #122). Re-home or unbind it first.");
- }
- }
- }
-
+ // The equipment↔driver binding was retired in v3, so a cluster move can no longer
+ // orphan driver-bound equipment — the old cross-cluster binding guard is gone.
entity.Name = name;
entity.Notes = string.IsNullOrWhiteSpace(notes) ? null : notes;
entity.ClusterId = newClusterId;
@@ -416,40 +382,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
- // A reparent to a different area must not orphan driver-bound equipment
- // from its driver's cluster. Resolve the new area's cluster and check every bound
- // equipment item under this line against it.
- if (newUnsAreaId != entity.UnsAreaId)
- {
- var newAreaCluster = await db.UnsAreas
- .Where(a => a.UnsAreaId == newUnsAreaId)
- .Select(a => (string?)a.ClusterId)
- .FirstOrDefaultAsync(ct);
-
- if (newAreaCluster is not null)
- {
- var boundEquipment = await db.Equipment
- .Where(eq => eq.UnsLineId == unsLineId && eq.DriverInstanceId != null)
- .Select(eq => new { eq.EquipmentId, eq.DriverInstanceId })
- .ToListAsync(ct);
-
- foreach (var eq in boundEquipment)
- {
- var driverCluster = await db.DriverInstances
- .Where(d => d.DriverInstanceId == eq.DriverInstanceId)
- .Select(d => d.ClusterId)
- .FirstOrDefaultAsync(ct);
-
- if (driverCluster is not null && driverCluster != newAreaCluster)
- {
- return new UnsMutationResult(
- false,
- $"Cannot move line to area '{newUnsAreaId}': equipment '{eq.EquipmentId}' is bound to a driver in cluster '{driverCluster}' (decision #122). Re-home or unbind it first.");
- }
- }
- }
- }
-
+ // The equipment↔driver binding was retired in v3, so a line reparent can no longer
+ // orphan driver-bound equipment — the old cross-cluster binding guard is gone.
entity.UnsAreaId = newUnsAreaId;
entity.Name = name;
entity.Notes = string.IsNullOrWhiteSpace(notes) ? null : notes;
@@ -525,7 +459,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF
{
EquipmentId = equipmentId,
EquipmentUuid = uuid,
- DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId,
+ // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains.
UnsLineId = input.UnsLineId,
Name = input.Name,
MachineCode = input.MachineCode,
@@ -596,7 +530,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF
{
EquipmentId = equipmentId,
EquipmentUuid = uuid,
- DriverInstanceId = string.IsNullOrWhiteSpace(row.DriverInstanceId) ? null : row.DriverInstanceId,
+ // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains.
UnsLineId = row.UnsLineId,
Name = row.Name,
MachineCode = row.MachineCode,
@@ -650,7 +584,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF
return guard.Value;
}
- entity.DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId;
+ // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains.
entity.UnsLineId = input.UnsLineId;
entity.Name = input.Name;
entity.MachineCode = input.MachineCode;
@@ -803,10 +737,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF
return "cluster nodes";
}
- if (await db.Namespaces.AnyAsync(ns => ns.ClusterId == clusterId, ct))
- {
- return "namespaces";
- }
+ // The Namespace entity was retired in v3 (Raw/UNS split); no namespaces child-check remains.
if (await db.UnsAreas.AnyAsync(a => a.ClusterId == clusterId, ct))
{
@@ -826,141 +757,40 @@ public sealed class UnsTreeService(IDbContextFactory dbF
$"Delete failed: {blocker} still reference cluster '{clusterId}' — remove them first.";
///
- public async Task> LoadTagDriversForEquipmentAsync(
+ public Task> LoadTagDriversForEquipmentAsync(
string equipmentId,
CancellationToken ct = default)
{
- await using var db = await dbFactory.CreateDbContextAsync(ct);
-
- var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct);
- if (equipmentCluster is null)
- {
- return Array.Empty<(string, string, string, string)>();
- }
-
- // Drivers in the equipment's cluster whose namespace is Equipment-kind.
- // GalaxyMxGateway is an ordinary Equipment-kind driver post-de-split, so it surfaces here
- // alongside the PLC drivers; its DriverConfig feeds the Galaxy live-browse address picker.
- var equipmentNamespaceIds = await db.Namespaces
- .Where(n => n.ClusterId == equipmentCluster && n.Kind == NamespaceKind.Equipment)
- .Select(n => n.NamespaceId)
- .ToListAsync(ct);
-
- var drivers = await db.DriverInstances
- .Where(d => d.ClusterId == equipmentCluster && equipmentNamespaceIds.Contains(d.NamespaceId))
- .OrderBy(d => d.DriverInstanceId)
- .Select(d => new { d.DriverInstanceId, d.Name, d.DriverType, d.DriverConfig })
- .ToListAsync(ct);
-
- return drivers
- .Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name}", d.DriverType, d.DriverConfig))
- .ToList();
+ // The equipment↔driver binding and the Namespace entity were retired in v3 (Raw/UNS split).
+ // Equipment no longer scopes a candidate-driver list; tag/driver authoring moved to the Raw
+ // tree in Batch 2/3. No candidate drivers to offer.
+ return Task.FromResult>(
+ Array.Empty<(string, string, string, string)>());
}
///
- public async Task CreateTagAsync(
+ public Task CreateTagAsync(
string equipmentId,
TagInput input,
CancellationToken ct = default)
{
- await using var db = await dbFactory.CreateDbContextAsync(ct);
-
- if (await db.Tags.AnyAsync(t => t.TagId == input.TagId, ct))
- {
- return new UnsMutationResult(false, $"Tag '{input.TagId}' already exists.");
- }
-
- if (!IsValidJson(input.TagConfig))
- {
- return new UnsMutationResult(false, "TagConfig is not valid JSON.");
- }
-
- if (!await db.Equipment.AnyAsync(e => e.EquipmentId == equipmentId, ct))
- return new UnsMutationResult(false, $"Equipment '{equipmentId}' not found.");
-
- var equipmentCluster = await ResolveEquipmentClusterAsync(db, equipmentId, ct);
- var guard = await CheckTagDriverGuardAsync(db, input.DriverInstanceId, equipmentCluster, ct);
- if (guard is not null)
- {
- return guard.Value;
- }
-
- if (await db.Tags.AnyAsync(t => t.EquipmentId == equipmentId && t.Name == input.Name, ct))
- {
- return new UnsMutationResult(false, $"A tag named '{input.Name}' already exists on this equipment.");
- }
-
- db.Tags.Add(new Tag
- {
- TagId = input.TagId,
- DriverInstanceId = input.DriverInstanceId,
- EquipmentId = equipmentId,
- Name = input.Name,
- FolderPath = null,
- DataType = input.DataType,
- AccessLevel = input.AccessLevel,
- WriteIdempotent = input.WriteIdempotent,
- PollGroupId = string.IsNullOrWhiteSpace(input.PollGroupId) ? null : input.PollGroupId,
- TagConfig = input.TagConfig,
- });
- await db.SaveChangesAsync(ct);
- return new UnsMutationResult(true, null);
+ // Equipment-bound tag authoring was retired in v3: Tag is now Raw-only and the equipment↔Tag
+ // binding no longer exists. Authoring moved to the Raw tree (/raw) in v3 Batch 2.
+ return Task.FromResult(new UnsMutationResult(
+ false, "Tag authoring moved to the Raw tree (/raw) in v3 Batch 2."));
}
///
- public async Task UpdateTagAsync(
+ public Task UpdateTagAsync(
string tagId,
TagInput input,
byte[] rowVersion,
CancellationToken ct = default)
{
- await using var db = await dbFactory.CreateDbContextAsync(ct);
-
- var entity = await db.Tags.FirstOrDefaultAsync(t => t.TagId == tagId, ct);
- if (entity is null)
- {
- return new UnsMutationResult(false, "Row no longer exists.");
- }
-
- db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
-
- if (!IsValidJson(input.TagConfig))
- {
- return new UnsMutationResult(false, "TagConfig is not valid JSON.");
- }
-
- var equipmentCluster = await ResolveEquipmentClusterAsync(db, entity.EquipmentId, ct);
- var guard = await CheckTagDriverGuardAsync(db, input.DriverInstanceId, equipmentCluster, ct);
- if (guard is not null)
- {
- return guard.Value;
- }
-
- if (await db.Tags.AnyAsync(
- t => t.EquipmentId == entity.EquipmentId && t.Name == input.Name && t.TagId != tagId,
- ct))
- {
- return new UnsMutationResult(false, $"A tag named '{input.Name}' already exists on this equipment.");
- }
-
- // EquipmentId and FolderPath (null) are preserved — tree tags are always equipment-bound.
- entity.DriverInstanceId = input.DriverInstanceId;
- entity.Name = input.Name;
- entity.DataType = input.DataType;
- entity.AccessLevel = input.AccessLevel;
- entity.WriteIdempotent = input.WriteIdempotent;
- entity.PollGroupId = string.IsNullOrWhiteSpace(input.PollGroupId) ? null : input.PollGroupId;
- entity.TagConfig = input.TagConfig;
-
- try
- {
- await db.SaveChangesAsync(ct);
- return new UnsMutationResult(true, null);
- }
- catch (DbUpdateConcurrencyException)
- {
- return new UnsMutationResult(false, "Another user changed this tag while you were editing.");
- }
+ // Equipment-bound tag editing was retired in v3: Tag is now Raw-only and the equipment↔Tag
+ // binding no longer exists. Authoring moved to the Raw tree (/raw) in v3 Batch 2.
+ return Task.FromResult(new UnsMutationResult(
+ false, "Tag authoring moved to the Raw tree (/raw) in v3 Batch 2."));
}
///
@@ -1048,9 +878,9 @@ public sealed class UnsTreeService(IDbContextFactory dbF
.Select(s => s.SourceCode).FirstOrDefaultAsync(ct);
if (!EquipmentScriptPaths.ContainsEquipToken(src)) return null;
- var configs = await db.Tags.Where(t => t.EquipmentId == equipmentId)
- .Select(t => t.TagConfig).ToListAsync(ct);
- var fullNames = configs.Select(TagConfigFullName.Extract);
+ // The equipment↔Tag binding was retired in v3 (Tag is Raw-only), so no equipment-bound driver
+ // tags exist to derive an equipment tag base from. The {{equip}} token can't be resolved here.
+ var fullNames = Array.Empty();
if (EquipmentScriptPaths.DeriveEquipmentBase(fullNames) is null)
{
return new UnsMutationResult(false,
@@ -1350,11 +1180,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF
return new UnsMutationResult(false, $"Driver '{driverInstanceId}' not found.");
}
- var ns = await db.Namespaces.FirstOrDefaultAsync(n => n.NamespaceId == driver.NamespaceId, ct);
- if (ns?.Kind != NamespaceKind.Equipment)
- {
- return new UnsMutationResult(false, $"Driver '{driverInstanceId}' is not in an Equipment-kind namespace.");
- }
+ // The Namespace entity + NamespaceKind were retired in v3 (Raw/UNS split); the
+ // driver-in-Equipment-namespace check no longer applies.
if (driver.ClusterId != equipmentCluster)
{