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.
18 KiB
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 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 classesJdeScoping.DataAccess- Connection testing viaIDbConnectionFactory
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:
-
Source Section
- Connection name dropdown (references ConnectionStrings)
- Query text area with syntax highlighting
- Parameters grid (name/value pairs)
-
Schedules Section
- Mass/Daily/Hourly sub-panels
- Each has: Enabled checkbox, IntervalMinutes input, PrePurge flag, ReIndex flag
-
Destination Section
- Table name input
- Match columns list (add/remove)
- Exclude from update columns list (add/remove)
File Discovery
Auto-discovery with manual fallback:
- Check
JDESCOPING_CONFIG_PATHenvironment variable - Look in same directory as ConfigManager executable
- Look in
../JdeScoping.Host/relative to executable - Look in
~/.jdescoping/(Unix) or%LOCALAPPDATA%\JdeScoping\(Windows) - 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
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
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
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
ConfigFileServiceloads valid config filesConfigFileServicethrows on malformed JSON with helpful messageConfigFileServicecreates backup before savingBackupServicecreates timestamped backupsBackupServicerotates old backups (keeps last 10)BackupServicerestores backup to target pathAutoDiscoveryServicefinds config in expected locationsAutoDiscoveryServicereturns null when not foundDiffServicegenerates 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
IFileSystemabstraction for all file operations (enables testing without real filesystem)IDbConnectionFactorymocked for connection testingILogger<T>verified for expected log calls using Moq
Example Test
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:
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
ReactiveCommandand state snapshots
JSON Handling
Use System.Text.Json with these options:
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.