diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260608104706_NullableEquipmentDriverInstanceId.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260608104706_NullableEquipmentDriverInstanceId.cs
index 60bb5a37..bc9e28f4 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260608104706_NullableEquipmentDriverInstanceId.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260608104706_NullableEquipmentDriverInstanceId.cs
@@ -24,6 +24,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
///
protected override void Down(MigrationBuilder migrationBuilder)
{
+ // WARNING: this rollback converts any existing NULL (driver-less) rows to "" for DriverInstanceId.
+ // Only safe when no driver-less equipment exists in the database.
migrationBuilder.AlterColumn(
name: "DriverInstanceId",
table: "Equipment",
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor
index 4076347c..b9b9098c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor
@@ -83,8 +83,15 @@ else
await using var db = await DbFactory.CreateDbContextAsync();
var driversInCluster = db.DriverInstances.AsNoTracking()
.Where(d => d.ClusterId == ClusterId).Select(d => d.DriverInstanceId);
+ // Driver-less equipment (DriverInstanceId == null) has no DriverInstance FK.
+ // Scope it to this cluster via UnsLine → UnsArea.ClusterId instead.
+ var areaIds = db.UnsAreas.AsNoTracking()
+ .Where(a => a.ClusterId == ClusterId).Select(a => a.UnsAreaId);
+ var linesInCluster = db.UnsLines.AsNoTracking()
+ .Where(l => areaIds.Contains(l.UnsAreaId)).Select(l => l.UnsLineId);
_rows = await db.Equipment.AsNoTracking()
- .Where(e => driversInCluster.Contains(e.DriverInstanceId))
+ .Where(e => driversInCluster.Contains(e.DriverInstanceId)
+ || (e.DriverInstanceId == null && linesInCluster.Contains(e.UnsLineId)))
.OrderBy(e => e.Name)
.ToListAsync();
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor
index e5dd0eec..9867aade 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor
@@ -108,6 +108,9 @@
private bool _busy;
private string? _error;
+ // Bulk import requires a DriverInstanceId by design — every CSV row must reference an existing driver.
+ // Driver-less equipment (DriverInstanceId == null) is not supported via bulk import;
+ // create it via the single-add editor (/clusters/{id}/equipment/new) or the SQL loader.
private static readonly string[] RequiredColumns = ["Name", "MachineCode", "UnsLineId", "DriverInstanceId"];
private static readonly string[] OptionalColumns = ["ZTag", "SAPID", "Manufacturer", "Model"];