@page "/scripts/new" @page "/scripts/{ScriptId}" @* Script CRUD. SourceHash is computed automatically from SourceCode on save so the integrity check in v2's deployment pipeline doesn't require operator action. *@ @attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.Forms @using Microsoft.EntityFrameworkCore @using System.ComponentModel.DataAnnotations @using System.Security.Cryptography @using System.Text @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @inject IDbContextFactory DbFactory @inject NavigationManager Nav

@(IsNew ? "New script" : "Edit script")

Cancel
@if (!_loaded) {

Loading…

} else if (!IsNew && _existing is null) {
@ScriptId not found.
} else {
Identity
@* ScriptId is system-generated (SC-…) on create, never operator-supplied — shown read-only when editing an existing script, hidden on the New page. *@ @if (!IsNew) {
}
Source
SHA-256 hash is computed automatically on save.
@if (!string.IsNullOrWhiteSpace(_error)) {
@_error
}
Cancel @if (!IsNew) { }
} @code { [Parameter] public string? ScriptId { get; set; } private bool IsNew => string.IsNullOrEmpty(ScriptId); private FormModel _form = new(); private Script? _existing; private bool _loaded; private bool _busy; private string? _error; protected override async Task OnInitializedAsync() { if (!IsNew) { await using var db = await DbFactory.CreateDbContextAsync(); _existing = await db.Scripts.AsNoTracking().FirstOrDefaultAsync(s => s.ScriptId == ScriptId); if (_existing is not null) { _form = new FormModel { ScriptId = _existing.ScriptId, Name = _existing.Name, SourceCode = _existing.SourceCode, RowVersion = _existing.RowVersion, }; } } _loaded = true; } private async Task SubmitAsync() { _busy = true; _error = null; try { var sourceHash = HashSource(_form.SourceCode); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) { // ScriptId is system-generated (mirrors EquipmentId's "EQ-{12 hex}" convention); // Language is always CSharp. db.Scripts.Add(new Script { ScriptId = $"SC-{Guid.NewGuid().ToString("N")[..12]}", Name = _form.Name, Language = "CSharp", SourceCode = _form.SourceCode, SourceHash = sourceHash, }); } else { var entity = await db.Scripts.FirstOrDefaultAsync(s => s.ScriptId == ScriptId); if (entity is null) { _error = "Row no longer exists."; return; } db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; entity.Name = _form.Name; entity.SourceCode = _form.SourceCode; entity.SourceHash = sourceHash; } await db.SaveChangesAsync(); Nav.NavigateTo("/scripts"); } catch (DbUpdateConcurrencyException) { _error = "Another user changed this script while you were editing."; } catch (Exception ex) { _error = ex.Message; } finally { _busy = false; } } private async Task DeleteAsync() { if (IsNew) return; _busy = true; _error = null; try { await using var db = await DbFactory.CreateDbContextAsync(); var entity = await db.Scripts.FirstOrDefaultAsync(s => s.ScriptId == ScriptId); if (entity is null) { Nav.NavigateTo("/scripts"); return; } db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; db.Scripts.Remove(entity); await db.SaveChangesAsync(); Nav.NavigateTo("/scripts"); } catch (DbUpdateConcurrencyException) { _error = "Another user changed this script while you were viewing it."; } catch (Exception ex) { _error = $"Delete failed: {ex.Message}. Likely because virtual tags or scripted alarms still reference this script — remove them first."; } finally { _busy = false; } } private static string HashSource(string source) => Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(source))); private sealed class FormModel { // ScriptId is system-generated, display-only (not user-entered) — no validation. public string ScriptId { get; set; } = ""; [Required] public string Name { get; set; } = ""; [Required] public string SourceCode { get; set; } = ""; public byte[] RowVersion { get; set; } = []; } }