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:
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
Reference in New Issue
Block a user