Files
jdescopingtool/docs/plans/2026-01-19-config-manager-design.md
T
Joseph Doherty d49330e697 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.
2026-01-20 02:26:26 -05:00

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 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

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

  • 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

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 ReactiveCommand and 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.