Client application (MILTON.Client)
Overview
MILTON.Client is the Blazor WebAssembly frontend for MILTON. It hosts the routed user experience for project management, project configuration, document authoring, and template editing. The application shell lives in MainLayout.razor and ProjectLayout.razor, routed pages live in the Pages/ folder, reusable components live in the Components/ folder, real-time document updates are handled by DocumentStateService.cs, and client-side L1 caching is implemented through the scoped store classes in the StateContainer/ folder.
This client surface supports the backend feature areas documented in Projects Feature, Documents Feature, and Project Configuration Feature.
Folder Structure
graph LR Root["MILTON.Client/"] Root --> Pages["Pages/"] Pages --> Home["Home.razor — /"] Pages --> PD["ProjectDashboard.razor"] Pages --> DL["DocumentsList.razor"] Pages --> DE["DocumentEditor.razor"] Pages --> TL["Templates.razor"] Pages --> CT["CreateTemplate.razor"] Pages --> ET["EditTemplate.razor"] Pages --> PC["ProjectConfig.razor"] Pages --> NP["New-project.razor"] Pages --> NF["NotFound.razor"] Root --> Components["Components/"] Components --> Comp["Component.razor"] Components --> ITD["InstantiateTemplateDialog.razor"] Components --> Docs["Documents/"] Docs --> BR["BlockRenderer.razor"] Docs --> DBV["DefaultBlockView.razor"] Docs --> DPP["DocumentPreviewPanel.razor"] Docs --> RS["RenderSection.razor"] Docs --> RFA["RepoFileAutocomplete.razor"] Docs --> RC["RequirementCard.razor"] Docs --> SC["ScannerCard.razor"] Docs --> SSD["SectionSettingsDrawer.razor"] Docs --> STI["SectionTreeItem.razor"] Docs --> TPP["TemplatePreviewPanel.razor"] Docs --> TSE["TemplateSectionEditor.razor"] Docs --> TCC["TestCaseCard.razor"] Docs --> TV["TraceView.razor"] Root --> Layout["Layout/"] Layout --> ML["MainLayout.razor"] Layout --> PL["ProjectLayout.razor"] Layout --> RM["ReconnectModal.razor"] Layout --> RTL["RedirectToLogin.razor"] Root --> Services["Services/"] Services --> DSS["DocumentStateService.cs"] Root --> State["StateContainer/"] State --> PS["ProjectStore.cs"] State --> DS["DocumentStore.cs"] State --> TS["TemplateStore.cs"]
For detailed component-level documentation, see Components.md, Pages.md, and Infrastructure.md.
Architecture
flowchart TD subgraph Client ["MILTON.Client — Blazor WASM"] Pages["Pages/<br/>Home · ProjectDashboard · DocumentsList<br/>Templates · DocumentEditor · ProjectConfig"] Components["Components/<br/>BlockRenderer · RequirementCard · ScannerCard<br/>TestCaseCard · RenderSection · InstantiateTemplateDialog"] Stores["StateContainer/<br/>ProjectStore · DocumentStore · TemplateStore"] DSS["DocumentStateService"] end API["MILTON API<br/>/api/projects · /api/documents · /api/projects/{id}/..."] Hub["DocumentHub<br/>/hubs/document"] DI["Blazor WASM DI scope<br/>shared across route changes"] Pages -- "HTTP + JSON" --> API API -- "responses" --> Pages Pages -- "reads & writes" --> Components Components -- "bound to data from" --> Stores Stores -- "scoped lifetime" --> DI DSS <-- "SignalR" --> Hub
Technology
- Framework: Blazor WebAssembly on ASP.NET Core 10
- UI Library: MudBlazor 8.15.0
- Real-time Engine: SignalR via
DocumentStateService.cs - Client state model: Scoped dependency-injected in-memory stores registered in
Program.cs
L1 In-Memory Cache
The client’s L1 cache is implemented with scoped services registered in Program.cs:
builder.Services.AddScoped<ProjectStore>();
builder.Services.AddScoped<DocumentStore>();
builder.Services.AddScoped<TemplateStore>();Because these stores are registered with AddScoped, each running browser client gets its own in-memory cache instance. The cache survives navigation between routed pages inside the active app session, but it is reset on a full page reload or in a new browser tab.
Store Catalog
| Store | Implementation file | Cached data | Keying strategy | Primary consumers |
|---|---|---|---|---|
ProjectStore | StateContainer/ProjectStore.cs | List<GetProjectResponse> | Single list for the active client scope | Home.razor, ProjectLayout.razor, ProjectDashboard.razor, ProjectConfig.razor |
DocumentStore | StateContainer/DocumentStore.cs | Dictionary<int, List<DocumentSummaryResponse>> and Dictionary<Guid, DocumentResponse> | Project ID for summary lists, document ID for editor detail payloads | ProjectDashboard.razor, DocumentsList.razor, CreateTemplate.razor, EditTemplate.razor, DocumentEditor.razor, InstantiateTemplateDialog.razor |
TemplateStore | StateContainer/TemplateStore.cs | Dictionary<int, List<TemplateResponse>> | Project ID | ProjectDashboard.razor, Templates.razor, EditTemplate.razor |
Cache Population
Project cache
ProjectStore is populated from GET /api/projects the first time a page needs the project list.
Home.razorloads the projects list ifProjectStore.HasDatais false, then renders directly fromProjectStore.Projects.ProjectLayout.razorreuses the same cached list to resolve the active project title for project-scoped navigation.ProjectDashboard.razorfirst tries to resolve the current project from cache before falling back toGET /api/projects.ProjectConfig.razoralways fetches the project configuration payload fromGET /api/projects/{projectId}/config, but only callsGET /api/projectswhenProjectStoreis empty.
Document cache
DocumentStore holds both project document summaries and document detail payloads.
ProjectDashboard.razor,DocumentsList.razor,CreateTemplate.razor, andEditTemplate.razorpopulateProjectDocuments[projectId]fromGET /api/projects/{projectId}/documentsonly when the project entry is missing.DocumentEditor.razorstores the result ofGET /api/documents/{id}inDocumentDetails[documentId]the first time a document editor is opened.InstantiateTemplateDialog.razorclears the project’s document-summary cache after a successful template instantiation so the next list read will fetch fresh document summaries.
Template cache
TemplateStore holds template lists per project.
ProjectDashboard.razorandTemplates.razorpopulateProjectTemplates[projectId]fromGET /api/projects/{projectId}/templateson the first read for that project.EditTemplate.razorfirst searchesProjectTemplates[projectId]for the target template. If the template is not already present, it fetchesGET /api/documents/templates/{templateId}and appends the result into the cached project list.
Invalidation Rules
The current implementation uses explicit invalidation after successful writes instead of background reconciliation.
| Mutation path | Cache action | Reason |
|---|---|---|
New-project.razor after POST /api/projects | ProjectStore.Clear() | Forces the next project-list read to include the newly created project |
CreateTemplate.razor after POST /api/documents/templates | TemplateStore.ClearForProject(ProjectId) | Forces the next template-list read to include the new template |
EditTemplate.razor after PUT /api/documents/templates/{templateId} | TemplateStore.ClearForProject(ProjectId) | Prevents stale template payloads from being reused |
InstantiateTemplateDialog.razor after POST /api/documents/templates/instantiate | DocumentStore.ClearForProject(ProjectId) | Forces project document summaries to be refetched after document creation |
| Full page reload or new browser tab | New scoped store instances | Resets all in-memory state |
Note:
DocumentStore.ClearForProject(projectId)only removes project document-summary lists. TheDocumentDetailscache inDocumentStorehas no dedicated invalidation method in the current implementation, so detail entries remain in memory for the lifetime of the scoped client instance.
Security and Offline Constraints
Due to strict requirements, all scripts, styles, and web fonts are vendored locally. The frontend must not depend on external CDNs or external runtime APIs because it is designed to support offline and air-gapped environments.
Real-time Sync (SignalR)
Live updates for document processing flow through DocumentStateService, which maintains a SignalR connection to the backend DocumentHub. The hub organizes messaging into two types of groups to scale notifications efficiently:
- Document Groups (
JoinDocumentGroup(Guid documentId)): Pushes granular progress logs, completion, and error events for a specific document. Subscribed to by detail pages likeDocumentEditor.razorviaJoinDocumentAsync. - Project Groups (
JoinProjectGroup(int projectId)): Pushes document summarization updates and completion events across an entire project. Subscribed to by list pages likeProjectDashboard.razorviaJoinProjectAsync.
Preventing Memory Leaks
Because DocumentStateService lives as a scoped service beyond the lifecycle of individual components, all components subscribing to state events must implement IDisposable or IAsyncDisposable to unregister their event handlers. Failing to do so creates memory leaks where destroyed components continue to receive events.
Example usage:
// Subscribe in initialization
protected override async Task OnInitializedAsync()
{
await StateService.StartAsync();
StateService.OnProgress += HandleProgress;
await StateService.JoinDocumentAsync(documentId);
}
// Unsubscribe and leave group to prevent leaks and unnecessary traffic
public async ValueTask DisposeAsync()
{
StateService.OnProgress -= HandleProgress;
await StateService.LeaveDocumentAsync(documentId);
}