fix(admin): resolve Low code-review findings (Admin-010,011,012)
- Admin-010: vendor Bootstrap 5.3.3 (CSS + JS bundle + maps + provenance README) under wwwroot/lib/bootstrap and reference local paths from App.razor — Admin no longer pulls Bootstrap from jsDelivr. - Admin-011: swap FleetStatusPoller's three plain dictionaries for ConcurrentDictionary so ResetCache can't race a poll tick. - Admin-012: drop the EquipmentId column from EquipmentCsvImporter (per admin-ui.md — equipment id is system-derived from EquipmentUuid); EquipmentImportBatchService and the textarea placeholder updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,14 +6,16 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>OtOpcUa Admin</title>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"/>
|
||||
@* Admin-010: Bootstrap 5 is vendored under wwwroot/lib/bootstrap/ per admin-ui.md
|
||||
"Tech Stack" — no public-CDN dependency so air-gapped fleet deployments work. *@
|
||||
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="theme.css"/>
|
||||
<link rel="stylesheet" href="app.css"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
<body>
|
||||
<Routes/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<div class="mt-3">
|
||||
<label class="form-label">CSV content (paste or uploaded)</label>
|
||||
<textarea class="form-control mono" rows="8" @bind="_csvText"
|
||||
placeholder="# OtOpcUaCsv v1 ZTag,MachineCode,SAPID,EquipmentId,…"/>
|
||||
placeholder="# OtOpcUaCsv v1 ZTag,MachineCode,SAPID,EquipmentUuid,Name,…"/>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="ParseAsync" disabled="@_busy">Parse</button>
|
||||
|
||||
Binary file not shown.
@@ -15,8 +15,13 @@ namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
/// columns must all be present. The version bump handshake lets future shapes parse
|
||||
/// without ambiguity — v2 files go through a different parser variant.</para>
|
||||
///
|
||||
/// <para><b>Required columns</b> per decision #117: ZTag, MachineCode, SAPID,
|
||||
/// EquipmentId, EquipmentUuid, Name, UnsAreaName, UnsLineName.</para>
|
||||
/// <para><b>Required columns</b> per decision #117 + admin-ui.md "Equipment CSV import"
|
||||
/// (Admin-012 — revised after adversarial review finding #4): ZTag, MachineCode, SAPID,
|
||||
/// EquipmentUuid, Name, UnsAreaName, UnsLineName. <b>No <c>EquipmentId</c> column</b> —
|
||||
/// it is system-derived as <c>'EQ-' + first 12 hex chars of EquipmentUuid</c> via
|
||||
/// <see cref="ZB.MOM.WW.OtOpcUa.Configuration.Validation.DraftValidator.DeriveEquipmentId"/>
|
||||
/// and is never accepted from the CSV: operator-supplied values would mint duplicate
|
||||
/// equipment identity on typos.</para>
|
||||
///
|
||||
/// <para><b>Optional columns</b> per decision #139: Manufacturer, Model, SerialNumber,
|
||||
/// HardwareRevision, SoftwareRevision, YearOfConstruction, AssetLocation,
|
||||
@@ -32,7 +37,8 @@ public static class EquipmentCsvImporter
|
||||
|
||||
public static IReadOnlyList<string> RequiredColumns { get; } = new[]
|
||||
{
|
||||
"ZTag", "MachineCode", "SAPID", "EquipmentId", "EquipmentUuid",
|
||||
// Admin-012: no EquipmentId — derived from EquipmentUuid at finalise time.
|
||||
"ZTag", "MachineCode", "SAPID", "EquipmentUuid",
|
||||
"Name", "UnsAreaName", "UnsLineName",
|
||||
};
|
||||
|
||||
@@ -136,7 +142,6 @@ public static class EquipmentCsvImporter
|
||||
ZTag = cells[colIndex["ZTag"]],
|
||||
MachineCode = cells[colIndex["MachineCode"]],
|
||||
SAPID = cells[colIndex["SAPID"]],
|
||||
EquipmentId = cells[colIndex["EquipmentId"]],
|
||||
EquipmentUuid = cells[colIndex["EquipmentUuid"]],
|
||||
Name = cells[colIndex["Name"]],
|
||||
UnsAreaName = cells[colIndex["UnsAreaName"]],
|
||||
@@ -157,7 +162,6 @@ public static class EquipmentCsvImporter
|
||||
"ZTag" => row.ZTag,
|
||||
"MachineCode" => row.MachineCode,
|
||||
"SAPID" => row.SAPID,
|
||||
"EquipmentId" => row.EquipmentId,
|
||||
"EquipmentUuid" => row.EquipmentUuid,
|
||||
"Name" => row.Name,
|
||||
"UnsAreaName" => row.UnsAreaName,
|
||||
@@ -225,11 +229,11 @@ public static class EquipmentCsvImporter
|
||||
/// <summary>One parsed equipment row with required + optional fields.</summary>
|
||||
public sealed class EquipmentCsvRow
|
||||
{
|
||||
// Required (decision #117)
|
||||
// Required (decision #117). Admin-012: no EquipmentId here — derived from
|
||||
// EquipmentUuid at finalise time so a CSV typo cannot create a duplicate identity.
|
||||
public required string ZTag { get; init; }
|
||||
public required string MachineCode { get; init; }
|
||||
public required string SAPID { get; init; }
|
||||
public required string EquipmentId { get; init; }
|
||||
public required string EquipmentUuid { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string UnsAreaName { get; init; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
|
||||
@@ -65,6 +66,14 @@ public sealed class EquipmentImportBatchService(OtOpcUaConfigDbContext db)
|
||||
|
||||
foreach (var row in acceptedRows)
|
||||
{
|
||||
// Admin-012: EquipmentId is not on the CSV row — derive it from EquipmentUuid
|
||||
// so the staging row carries the canonical 'EQ-' + first-12-hex form. Rows
|
||||
// with an unparseable UUID get an empty placeholder; the same finalise path
|
||||
// that re-derives the UUID (FinaliseBatchAsync) will overwrite it.
|
||||
var derivedEquipmentId = Guid.TryParse(row.EquipmentUuid, out var parsedUuid)
|
||||
? DraftValidator.DeriveEquipmentId(parsedUuid)
|
||||
: string.Empty;
|
||||
|
||||
db.EquipmentImportRows.Add(new EquipmentImportRow
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
@@ -73,7 +82,7 @@ public sealed class EquipmentImportBatchService(OtOpcUaConfigDbContext db)
|
||||
ZTag = row.ZTag,
|
||||
MachineCode = row.MachineCode,
|
||||
SAPID = row.SAPID,
|
||||
EquipmentId = row.EquipmentId,
|
||||
EquipmentId = derivedEquipmentId,
|
||||
EquipmentUuid = row.EquipmentUuid,
|
||||
Name = row.Name,
|
||||
UnsAreaName = row.UnsAreaName,
|
||||
@@ -178,11 +187,16 @@ public sealed class EquipmentImportBatchService(OtOpcUaConfigDbContext db)
|
||||
{
|
||||
var equipmentUuid = Guid.TryParse(row.EquipmentUuid, out var u) ? u : Guid.NewGuid();
|
||||
|
||||
// Admin-012: EquipmentId is always derived from the UUID that actually lands in
|
||||
// the Equipment row. Using the staging row's pre-derived value would diverge
|
||||
// when the staged UUID was blank and we just generated a fresh one above.
|
||||
var derivedEquipmentId = DraftValidator.DeriveEquipmentId(equipmentUuid);
|
||||
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentRowId = Guid.NewGuid(),
|
||||
GenerationId = generationId,
|
||||
EquipmentId = row.EquipmentId,
|
||||
EquipmentId = derivedEquipmentId,
|
||||
EquipmentUuid = equipmentUuid,
|
||||
DriverInstanceId = driverInstanceIdForRows,
|
||||
UnsLineId = unsLineIdForRows,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bootstrap 5.3.3 — vendored copy
|
||||
|
||||
Per `docs/v2/admin-ui.md` "Tech Stack" the Admin UI vendors Bootstrap 5 here so
|
||||
the app has no public-CDN dependency — air-gapped fleet deployments must work
|
||||
without internet egress.
|
||||
|
||||
| File | Source |
|
||||
|------|--------|
|
||||
| `css/bootstrap.min.css` | https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css |
|
||||
| `css/bootstrap.min.css.map` | https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css.map |
|
||||
| `js/bootstrap.bundle.min.js` | https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js |
|
||||
| `js/bootstrap.bundle.min.js.map` | https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js.map |
|
||||
|
||||
Bootstrap is MIT-licensed (https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||
To upgrade, re-download all four files at the matching version and bump this
|
||||
table.
|
||||
6
src/Server/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/lib/bootstrap/css/bootstrap.min.css
vendored
Normal file
6
src/Server/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/lib/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
src/Server/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
7
src/Server/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user