feat: wire SQLite replication between site nodes and fix ConfigurationDatabase tests

Add SiteReplicationActor (runs on every site node) to replicate deployed
configs and store-and-forward buffer operations to the standby peer via
cluster member discovery and fire-and-forget Tell. Wire ReplicationService
handler and pass replication actor to DeploymentManagerActor singleton.

Fix 5 pre-existing ConfigurationDatabase test failures: RowVersion NOT NULL
on SQLite, stale migration name assertion, and seed data count mismatch.
This commit is contained in:
Joseph Doherty
2026-03-18 08:28:02 -04:00
parent f063fb1ca3
commit eb8ead58d2
23 changed files with 707 additions and 33 deletions

View File

@@ -11,15 +11,11 @@
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ISiteRepository SiteRepository
@inject CommunicationService CommunicationService
@inject IJSRuntime JS
@implements IDisposable
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Debug View</h4>
<div class="alert alert-info py-1 px-2 mb-0 small">
Debug view streams are lost on failover. Re-open if connection drops.
</div>
</div>
<h4 class="mb-3">Debug View</h4>
<ToastNotification @ref="_toast" />
@@ -182,6 +178,24 @@
_loading = false;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
var storedSiteId = await JS.InvokeAsync<string>("localStorage.getItem", "debugView.siteId");
var storedInstanceName = await JS.InvokeAsync<string>("localStorage.getItem", "debugView.instanceName");
if (!string.IsNullOrEmpty(storedSiteId) && int.TryParse(storedSiteId, out var siteId)
&& !string.IsNullOrEmpty(storedInstanceName))
{
_selectedSiteId = siteId;
await LoadInstancesForSite();
_selectedInstanceName = storedInstanceName;
StateHasChanged();
await Connect();
}
}
private async Task LoadInstancesForSite()
{
_siteInstances.Clear();
@@ -224,6 +238,12 @@
}
_connected = true;
// Persist selection to localStorage for auto-reconnect on refresh
await JS.InvokeVoidAsync("localStorage.setItem", "debugView.siteId", _selectedSiteId.ToString());
await JS.InvokeVoidAsync("localStorage.setItem", "debugView.instanceName", _selectedInstanceName);
await JS.InvokeVoidAsync("localStorage.setItem", "debugView.siteIdentifier", site.SiteIdentifier);
_toast.ShowSuccess($"Connected to {_selectedInstanceName}");
// Periodic refresh (simulating SignalR push by re-subscribing)
@@ -253,7 +273,7 @@
_connecting = false;
}
private void Disconnect()
private async Task Disconnect()
{
_refreshTimer?.Dispose();
_refreshTimer = null;
@@ -268,6 +288,11 @@
}
}
// Clear persisted selection — user explicitly disconnected
await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.siteId");
await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.instanceName");
await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.siteIdentifier");
_connected = false;
_snapshot = null;
_attributeValues.Clear();