feat: add JoeAppEngine OPC UA nodes, fix DCL auto-reconnect and quality push

- Add JoeAppEngine folder to OPC UA nodes.json (BTCS, AlarmCntsBySeverity, Scheduler/ScanTime)
- Fix DataConnectionActor: capture Self in PreStart for use from non-actor threads,
  preventing Self.Tell failure in Disconnected event handler
- Implement InstanceActor.HandleConnectionQualityChanged to mark attributes Bad on disconnect
- Fix LmxFakeProxy TagMapper to serialize arrays as JSON instead of "System.Int32[]"
- Allow DataType and DataSourceReference updates in TemplateService.UpdateAttributeAsync
- Update test_infra_opcua.md with JoeAppEngine documentation
This commit is contained in:
Joseph Doherty
2026-03-19 13:27:54 -04:00
parent ffdda51990
commit 7740a3bcf9
70 changed files with 2684 additions and 541 deletions
@@ -70,9 +70,11 @@
<td class="small"><TimestampDisplay Value="@msg.LastAttemptTimestamp" /></td>
<td>
<button class="btn btn-outline-success btn-sm py-0 px-1 me-1"
title="Retry message (not yet implemented)">Retry</button>
@onclick="() => RetryMessage(msg)" disabled="@_actionInProgress"
title="Retry message (move back to pending)">Retry</button>
<button class="btn btn-outline-danger btn-sm py-0 px-1"
title="Discard message (not yet implemented)">Discard</button>
@onclick="() => DiscardMessage(msg)" disabled="@_actionInProgress"
title="Permanently discard message">Discard</button>
</td>
</tr>
}
@@ -102,6 +104,7 @@
private bool _searching;
private string? _errorMessage;
private bool _actionInProgress;
private ToastNotification _toast = default!;
private ConfirmDialog _confirmDialog = default!;
@@ -150,4 +153,65 @@
}
_searching = false;
}
private async Task RetryMessage(ParkedMessageEntry msg)
{
_actionInProgress = true;
try
{
var request = new ParkedMessageRetryRequest(
CorrelationId: Guid.NewGuid().ToString("N"),
SiteId: _selectedSiteId,
MessageId: msg.MessageId,
Timestamp: DateTimeOffset.UtcNow);
var response = await CommunicationService.RetryParkedMessageAsync(_selectedSiteId, request);
if (response.Success)
{
_toast.ShowSuccess($"Message {msg.MessageId[..Math.Min(12, msg.MessageId.Length)]} queued for retry.");
await FetchPage();
}
else
{
_toast.ShowError(response.ErrorMessage ?? "Retry failed.");
}
}
catch (Exception ex)
{
_toast.ShowError($"Retry failed: {ex.Message}");
}
_actionInProgress = false;
}
private async Task DiscardMessage(ParkedMessageEntry msg)
{
var confirmed = await _confirmDialog.ShowAsync(
$"Permanently discard message {msg.MessageId[..Math.Min(12, msg.MessageId.Length)]}? This cannot be undone.",
"Discard Parked Message");
if (!confirmed) return;
_actionInProgress = true;
try
{
var request = new ParkedMessageDiscardRequest(
CorrelationId: Guid.NewGuid().ToString("N"),
SiteId: _selectedSiteId,
MessageId: msg.MessageId,
Timestamp: DateTimeOffset.UtcNow);
var response = await CommunicationService.DiscardParkedMessageAsync(_selectedSiteId, request);
if (response.Success)
{
_toast.ShowSuccess($"Message {msg.MessageId[..Math.Min(12, msg.MessageId.Length)]} discarded.");
await FetchPage();
}
else
{
_toast.ShowError(response.ErrorMessage ?? "Discard failed.");
}
}
catch (Exception ex)
{
_toast.ShowError($"Discard failed: {ex.Message}");
}
_actionInProgress = false;
}
}