# 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 _logger; public async Task SaveAsync(string path, ConfigModel config) { using var scope = _logger.BeginScope(new Dictionary { ["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 LoadAsync(string folderPath, CancellationToken ct = default); Task SaveAsync(string folderPath, ConfigModel config, CancellationToken ct = default); } public interface IAutoDiscoveryService { Task FindConfigFolderAsync(CancellationToken ct = default); } public interface IBackupService { Task CreateBackupAsync(string filePath, CancellationToken ct = default); Task> 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 TestConnectionAsync(string connectionString, CancellationToken ct = default); } public interface IFileSystem { Task ReadAllTextAsync(string path, CancellationToken ct = default); Task WriteAllTextAsync(string path, string content, CancellationToken ct = default); bool FileExists(string path); bool DirectoryExists(string path); Task 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` verified for expected log calls using Moq ### Example Test ```csharp public class BackupServiceTests { private readonly Mock _fileSystem; private readonly Mock> _logger; private readonly BackupService _sut; public BackupServiceTests() { _fileSystem = new Mock(); _logger = new Mock>(); _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(), 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(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); // ViewModels services.AddTransient(); Services = services.BuildServiceProvider(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainWindow { DataContext = Services.GetRequiredService() }; } 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.