docs: add XML documentation and ConfigManager implementation plans

Add comprehensive XML documentation (param/returns tags) across 132 source
files to improve IntelliSense and API discoverability. Include ConfigManager
design documents and implementation plans for phases 1-9.
This commit is contained in:
Joseph Doherty
2026-01-20 02:26:26 -05:00
parent c044337539
commit d49330e697
136 changed files with 9181 additions and 4 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,488 @@
# JdeScoping.ConfigManager Design
## Purpose
A standalone Avalonia desktop application for managing JdeScoping configuration files (`appsettings.json` and `pipelines.json`) through a graphical user interface.
## UI Design Specification
See **[configmanager-ui-design.md](../designs/configmanager-ui-design.md)** for complete visual design specifications including:
- Color system and design tokens
- Typography and spacing scales
- Component specifications (tree view, forms, dialogs, buttons)
- Screen layouts and wireframes
- Animation and interaction patterns
- Accessibility requirements
- Avalonia-specific implementation guidance
## Project Structure
**Location:** `NEW/src/Utils/JdeScoping.ConfigManager/`
**Test Project:** `NEW/tests/JdeScoping.ConfigManager.Tests/`
```
JdeScoping.ConfigManager/
├── Application/ # App startup, DI container setup
├── Models/ # Typed models for config sections
├── Services/ # File I/O, validation, backup, diff
├── ViewModels/ # Tree nodes, form view models
├── Views/ # Avalonia XAML views
├── Converters/ # Value converters for data binding
└── Constants/ # File paths, defaults, magic strings
```
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| Avalonia | 11.2.* | UI framework |
| Avalonia.Desktop | 11.2.* | Desktop platform support |
| Avalonia.Themes.Fluent | 11.2.* | Fluent design theme |
| Avalonia.Controls.DataGrid | 11.2.* | Data grid for lists |
| MessageBox.Avalonia | 3.1.* | Dialog boxes |
| DiffPlex | latest | Diff generation for preview |
| Microsoft.Extensions.Logging | latest | Logging framework |
| Microsoft.Extensions.DependencyInjection | latest | DI container |
| Serilog.Extensions.Logging | latest | File logging sink |
| Serilog.Sinks.File | latest | File logging sink |
**Project References:**
- `JdeScoping.Core` - Reuse existing Options classes
- `JdeScoping.DataAccess` - Connection testing via `IDbConnectionFactory`
## UI Layout
```
┌─────────────────────────────────────────────────────────────┐
│ Menu Bar: File | Edit | Tools | Help │
├─────────────────────────────────────────────────────────────┤
│ Toolbar: [Open Folder] [Save] [Undo] [Redo] [Test Conn] │
├──────────────────┬──────────────────────────────────────────┤
│ │ │
│ Tree View │ Form Panel │
│ ──────────── │ ────────── │
│ 📁 Settings │ [Dynamic form based on selection] │
│ ├─ DataSync │ │
│ ├─ DataAccess │ - Typed input fields │
│ ├─ Auth │ - Validation indicators │
│ └─ ... │ - Help tooltips │
│ 📁 Pipelines │ │
│ ├─ WorkOrder │ │
│ ├─ Lot │ │
│ └─ ... │ │
│ │ │
├──────────────────┴──────────────────────────────────────────┤
│ Status Bar: [File path] [Modified indicator] [Validation] │
└─────────────────────────────────────────────────────────────┘
```
### Tree View Behavior
- Two root nodes: "Settings" and "Pipelines"
- Settings children are config sections (DataSync, Auth, Ldap, etc.)
- Pipelines children are individual pipeline names from `pipelines.json`
- Right-click context menu on Pipelines node: New, Duplicate, Delete
- Icons indicate validation state:
- ✓ Green checkmark: valid
- ⚠ Yellow warning: warnings present
- ✗ Red X: errors present
- Asterisk (*) suffix on node name indicates unsaved changes
### Form Panel Design
**Settings Forms:**
Each settings section displays a typed form based on the corresponding Options class from `JdeScoping.Core`:
- **DataSyncOptions** - Intervals, lookback multiplier, retention days
- **DataAccessOptions** - Connection timeout, command timeout
- **AuthOptions** - Token expiry, cookie settings
- **LdapOptions** - Server URL, search base, group name
- **ConnectionStrings** - Read-only display with masked passwords
Form features:
- Labels with tooltips from XML documentation
- Range validation hints (min/max from attributes)
- Immediate validation feedback (red border + error message)
- Reset button to revert to last saved state
**Pipeline Forms:**
Three collapsible sections:
1. **Source Section**
- Connection name dropdown (references ConnectionStrings)
- Query text area with syntax highlighting
- Parameters grid (name/value pairs)
2. **Schedules Section**
- Mass/Daily/Hourly sub-panels
- Each has: Enabled checkbox, IntervalMinutes input, PrePurge flag, ReIndex flag
3. **Destination Section**
- Table name input
- Match columns list (add/remove)
- Exclude from update columns list (add/remove)
## File Discovery
Auto-discovery with manual fallback:
1. Check `JDESCOPING_CONFIG_PATH` environment variable
2. Look in same directory as ConfigManager executable
3. Look in `../JdeScoping.Host/` relative to executable
4. Look in `~/.jdescoping/` (Unix) or `%LOCALAPPDATA%\JdeScoping\` (Windows)
5. If not found, prompt user to select folder containing config files
Discovery looks for both `appsettings.json` and `pipelines.json` (or `Pipelines/pipelines.json`).
## Validation
### Layer 1: Schema Validation (Real-time)
Runs on every field change:
- Required fields not empty
- Correct data types (numbers parse correctly, booleans valid)
- Enum values within allowed set
- String length constraints
### Layer 2: Business Rules (On demand + before save)
Triggered via "Validate All" button or automatically before save:
- Pipeline names must be unique
- Schedule intervals within bounds (hourly ≥ 15 min, daily ≥ 60 min)
- Connection names in pipelines must reference existing ConnectionStrings
- Match columns should not overlap with exclude columns
### Layer 3: Live Connection Testing (On demand)
- "Test Connection" button in toolbar and on connection forms
- Tests against actual database using `IDbConnectionFactory`
- 10-second timeout with clear feedback
- Shows success message or detailed error
### Validation UI
- Tree nodes show validation icons
- Status bar shows summary: "3 errors, 1 warning"
- Clicking status bar opens validation results panel listing all issues
## Save Workflow
```
User clicks Save
┌─────────────────┐
│ Run validation │──► Errors found? ──► Show errors, abort save
└─────────────────┘
│ No errors
┌─────────────────┐
│ Generate diff │──► Show diff preview dialog
└─────────────────┘
│ User clicks "Save"
┌─────────────────┐
│ Create backup │──► {filename}.{timestamp}.bak
└─────────────────┘
┌─────────────────┐
│ Write new file │──► Show success notification
└─────────────────┘
```
### Backup Management
- Backups stored alongside original files
- Naming: `appsettings.2026-01-19_143022.bak`
- Keep last 10 backups per file, auto-delete older ones
- Tools menu → "View Backups" allows restoring previous versions
## Sensitive Data Handling
Connection strings containing passwords:
- Display with password field masked (••••••••)
- Read-only in this tool
- Tooltip: "Use SecureStoreManager to edit credentials"
- No logging of connection string values
## Logging
### Configuration
```csharp
services.AddLogging(builder =>
{
builder.AddConsole();
builder.AddSerilog(new LoggerConfiguration()
.WriteTo.File("logs/configmanager-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7)
.CreateLogger());
builder.SetMinimumLevel(LogLevel.Information);
});
```
### Structured Logging Pattern
```csharp
public class ConfigFileService
{
private readonly ILogger<ConfigFileService> _logger;
public async Task<SaveResult> SaveAsync(string path, ConfigModel config)
{
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["FilePath"] = path,
["Operation"] = "Save"
});
_logger.LogInformation("Starting config save");
try
{
var backupPath = await CreateBackupAsync(path);
_logger.LogInformation("Backup created at {BackupPath}", backupPath);
await WriteConfigAsync(path, config);
_logger.LogInformation("Config saved successfully");
return SaveResult.Success(backupPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save config");
throw;
}
}
}
```
### What Gets Logged
| Event | Level | Context |
|-------|-------|---------|
| Application startup | Information | Version, runtime |
| Application shutdown | Information | - |
| File opened | Information | File path |
| File saved | Information | File path, backup path |
| Validation run | Information | Error count, warning count |
| Connection test | Information | Connection name, success/failure |
| Connection test failure | Warning | Connection name, error message |
| Unhandled exception | Error | Full stack trace |
## Error Handling
| Error Type | User Experience |
|------------|-----------------|
| File not found | Dialog: "Config file not found. Browse to select location?" |
| File read permission | Dialog: "Cannot read file. Check permissions." |
| JSON parse error | Dialog showing line/column with syntax context |
| File write permission | Dialog: "Cannot save file. Check permissions." with retry option |
| Connection test timeout | Inline message: "Connection timed out after 10 seconds" |
| Connection test failure | Inline message with database error details |
| Unhandled exception | Dialog: "Unexpected error occurred" with copy-to-clipboard option |
## Key Interfaces
```csharp
public interface IConfigFileService
{
Task<ConfigModel> LoadAsync(string folderPath, CancellationToken ct = default);
Task<SaveResult> SaveAsync(string folderPath, ConfigModel config, CancellationToken ct = default);
}
public interface IAutoDiscoveryService
{
Task<string?> FindConfigFolderAsync(CancellationToken ct = default);
}
public interface IBackupService
{
Task<string> CreateBackupAsync(string filePath, CancellationToken ct = default);
Task<IReadOnlyList<BackupInfo>> GetBackupsAsync(string filePath, CancellationToken ct = default);
Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default);
Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default);
}
public interface IDiffService
{
DiffResult GenerateDiff(string original, string modified);
}
public interface IValidationService
{
ValidationResult ValidateSchema(ConfigModel config);
ValidationResult ValidateBusinessRules(ConfigModel config);
Task<ConnectionTestResult> TestConnectionAsync(string connectionString, CancellationToken ct = default);
}
public interface IFileSystem
{
Task<string> ReadAllTextAsync(string path, CancellationToken ct = default);
Task WriteAllTextAsync(string path, string content, CancellationToken ct = default);
bool FileExists(string path);
bool DirectoryExists(string path);
Task<string[]> GetFilesAsync(string path, string pattern, CancellationToken ct = default);
Task CopyFileAsync(string source, string destination, CancellationToken ct = default);
Task DeleteFileAsync(string path, CancellationToken ct = default);
}
```
## Unit Tests
**Test Project:** `JdeScoping.ConfigManager.Tests`
**Framework:** xUnit + Moq + Shouldly
### Test Categories
**1. Model Tests**
- JSON serialization roundtrip preserves all properties
- Deserialization applies default values for missing properties
- Nullable properties handled correctly
**2. Validation Tests**
- Schema validation catches missing required fields
- Schema validation catches type mismatches
- Business rules detect duplicate pipeline names
- Business rules catch invalid interval values (too low/high)
- Business rules detect orphaned connection references
**3. Service Tests**
- `ConfigFileService` loads valid config files
- `ConfigFileService` throws on malformed JSON with helpful message
- `ConfigFileService` creates backup before saving
- `BackupService` creates timestamped backups
- `BackupService` rotates old backups (keeps last 10)
- `BackupService` restores backup to target path
- `AutoDiscoveryService` finds config in expected locations
- `AutoDiscoveryService` returns null when not found
- `DiffService` generates accurate diff output
**4. ViewModel Tests**
- Tree structure built correctly from config model
- Selecting tree node updates form panel
- Property changes mark node as dirty
- Undo reverts last change
- Redo reapplies undone change
- Validation errors propagate to tree node icons
- Save command disabled when validation errors exist
### Mocking Strategy
- `IFileSystem` abstraction for all file operations (enables testing without real filesystem)
- `IDbConnectionFactory` mocked for connection testing
- `ILogger<T>` verified for expected log calls using Moq
### Example Test
```csharp
public class BackupServiceTests
{
private readonly Mock<IFileSystem> _fileSystem;
private readonly Mock<ILogger<BackupService>> _logger;
private readonly BackupService _sut;
public BackupServiceTests()
{
_fileSystem = new Mock<IFileSystem>();
_logger = new Mock<ILogger<BackupService>>();
_sut = new BackupService(_fileSystem.Object, _logger.Object);
}
[Fact]
public async Task CreateBackupAsync_CreatesTimestampedBackup()
{
// Arrange
var sourcePath = "/config/appsettings.json";
_fileSystem.Setup(f => f.FileExists(sourcePath)).Returns(true);
// Act
var backupPath = await _sut.CreateBackupAsync(sourcePath);
// Assert
backupPath.ShouldStartWith("/config/appsettings.");
backupPath.ShouldEndWith(".bak");
_fileSystem.Verify(f => f.CopyFileAsync(sourcePath, backupPath, default), Times.Once);
}
[Fact]
public async Task CleanupOldBackupsAsync_KeepsOnlySpecifiedCount()
{
// Arrange
var filePath = "/config/appsettings.json";
var backups = Enumerable.Range(1, 15)
.Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak")
.ToArray();
_fileSystem.Setup(f => f.GetFilesAsync("/config", "appsettings.*.bak", default))
.ReturnsAsync(backups);
// Act
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10);
// Assert
_fileSystem.Verify(f => f.DeleteFileAsync(It.IsAny<string>(), default), Times.Exactly(5));
}
}
```
## Implementation Notes
### Avalonia-Specific Patterns
**DI Setup in App.axaml.cs:**
```csharp
public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection();
// Logging
services.AddLogging(builder => { /* config */ });
// Services
services.AddSingleton<IFileSystem, FileSystem>();
services.AddSingleton<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddSingleton<IBackupService, BackupService>();
services.AddSingleton<IDiffService, DiffService>();
services.AddSingleton<IValidationService, ValidationService>();
services.AddScoped<IConfigFileService, ConfigFileService>();
// ViewModels
services.AddTransient<MainWindowViewModel>();
Services = services.BuildServiceProvider();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = Services.GetRequiredService<MainWindowViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
}
```
**Tree View with ReactiveUI:**
Consider using `ReactiveUI` for:
- Observable property changes
- Command binding
- Undo/redo via `ReactiveCommand` and state snapshots
### JSON Handling
Use `System.Text.Json` with these options:
```csharp
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
```
Preserve JSON comments and formatting where possible using `JsonNode` for manipulation rather than full deserialization when appropriate.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff