@page "/deployments" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.Commons.Interfaces @using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin @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.ControlPlane.AdminOperations @attribute [Authorize(Roles = "FleetAdmin,ConfigEditor")] @inject IDbContextFactory DbFactory @inject IAdminOperationsClient AdminOps @inject AuthenticationStateProvider AuthState @rendermode InteractiveServer Deployments

Deployments

@if (_drift is not null) { @(_drift.Value ? "Configuration drift" : "In sync") }
@if (_lastMessage is not null) {
@_lastMessage
} @foreach (var d in _deployments) { }
Deployment Revision Status Created by Created (UTC) Sealed (UTC)
@Short(d.DeploymentId) @d.RevisionHash[..12]… @d.Status @d.CreatedBy @d.CreatedAtUtc.ToString("u") @(d.SealedAtUtc?.ToString("u") ?? "—")
@code { private IReadOnlyList _deployments = Array.Empty(); private bool _busy; private bool _lastSuccess; private string? _lastMessage; private bool? _drift; protected override async Task OnInitializedAsync() { await ReloadAsync(); } private async Task ReloadAsync() { await using var db = await DbFactory.CreateDbContextAsync(); _deployments = await db.Deployments .AsNoTracking() .OrderByDescending(d => d.CreatedAtUtc) .Take(50) .ToListAsync(); // Drift: if no sealed deployment yet, no drift to report. Otherwise compare the latest // sealed revision hash to a fresh snapshot of the live-edit state. var latestSealed = _deployments.FirstOrDefault(d => d.Status == DeploymentStatus.Sealed); if (latestSealed is null) { _drift = null; return; } var current = await ConfigComposer.SnapshotAndFlattenAsync(db); _drift = !string.Equals(current.RevisionHash, latestSealed.RevisionHash, StringComparison.Ordinal); } private async Task StartDeploymentAsync() { _busy = true; _lastMessage = null; try { var auth = await AuthState.GetAuthenticationStateAsync(); var createdBy = auth.User.Identity?.Name ?? "(anonymous)"; var result = await AdminOps.StartDeploymentAsync( createdBy: createdBy, ct: CancellationToken.None); _lastSuccess = result.Outcome == StartDeploymentOutcome.Accepted; _lastMessage = result.Outcome switch { StartDeploymentOutcome.Accepted => $"Deployment {Short(result.DeploymentId!.Value.Value)} dispatched (rev {result.RevisionHash!.Value.Value[..12]}…).", StartDeploymentOutcome.AnotherDeploymentInFlight => result.Message ?? "Another deployment is already in flight.", StartDeploymentOutcome.NoChanges => "No changes detected since the last sealed deployment.", _ => result.Message ?? "Deployment rejected.", }; await ReloadAsync(); } catch (Exception ex) { _lastSuccess = false; _lastMessage = $"Deploy failed: {ex.Message}"; } finally { _busy = false; } } private static string Short(Guid id) => id.ToString("N")[..8]; }