From d49330e69735ae21a2066b530db9f732848132ca Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 20 Jan 2026 02:26:26 -0500 Subject: [PATCH] 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. --- NEW/JdeScoping.slnx | 2 + .../Controllers/AuthController.cs | 6 + .../FileIOController.ComponentLots.cs | 3 + .../Controllers/FileIOController.Items.cs | 3 + .../FileIOController.PartOperations.cs | 4 + .../FileIOController.WorkOrders.cs | 5 + .../Controllers/FileIOController.cs | 7 + .../Controllers/LookupController.cs | 4 + .../Controllers/PipelineController.cs | 12 + .../Controllers/RefreshStatusController.cs | 4 + .../Controllers/SearchController.cs | 17 + NEW/src/JdeScoping.Api/Hubs/StatusHub.cs | 6 + .../JdeScoping.Api/Mapping/IPipelineMapper.cs | 8 + .../Auth/AuthStateProvider.cs | 8 + .../Auth/IUserStorageService.cs | 1 + .../Auth/UserStorageService.cs | 17 + .../AutocompleteFilterPanelBase.cs | 14 + .../FilterPanels/FileUploadFilterPanelBase.cs | 6 + .../Extensions/ViewModelMappingExtensions.cs | 7 + .../Http/AuthRedirectHandler.cs | 5 + .../Models/SearchCriteriaViewModel.cs | 37 + .../Models/SearchViewModel.cs | 30 + .../Models/ValidCombination.cs | 53 + .../Services/ApiClientBase.cs | 47 + .../Services/AuthApiClient.cs | 25 + .../JdeScoping.Client/Services/AuthService.cs | 14 + .../Services/CryptoService.cs | 5 + .../Services/FileApiClient.cs | 56 + .../Services/HubConnectionService.cs | 27 + .../Services/IAuthService.cs | 2 + .../Services/IRefreshStatusService.cs | 3 + .../Services/ISearchSubmissionService.cs | 4 + .../Services/LookupApiClient.cs | 28 + .../Services/PipelineApiClient.cs | 28 + .../Services/RefreshStatusService.cs | 10 + .../Services/SearchApiClient.cs | 38 + .../Services/SearchSubmissionService.cs | 4 + .../JdeScoping.Core/ApiContracts/ApiRoutes.cs | 24 + .../ApiContracts/IAuthApiClient.cs | 5 + .../ApiContracts/IFileApiClient.cs | 20 + .../ApiContracts/ILookupApiClient.cs | 8 + .../ApiContracts/IPipelineApiClient.cs | 8 + .../ApiContracts/ISearchApiClient.cs | 10 + .../ApiContracts/Results/ValidationError.cs | 2 + .../JdeScoping.Core/Models/Auth/LoginModel.cs | 6 + .../Models/Infrastructure/DataUpdateDto.cs | 14 + .../SearchResults/MisNonMatchSearchResult.cs | 12 + .../Models/SearchResults/MisSearchResult.cs | 19 + .../Models/SearchResults/SearchModel.cs | 10 + .../Models/SearchResults/SearchResult.cs | 91 +- .../ViewModels/ComponentLotViewModel.cs | 9 + .../ViewModels/OperatorViewModel.cs | 13 + .../ViewModels/ViewModelExtensions.cs | 12 + .../Repositories/LotFinderRepository.cs | 3 + .../Services/SearchProcessor.cs | 6 + .../Configuration/DevPipelinesRoot.cs | 3 + .../JdeScoping.DataSync.Dev/DevEtlRegistry.cs | 18 + .../Services/DevEtlPipelineFactory.cs | 6 + .../Sources/ProtobufZstdFileSource.cs | 18 + .../Configuration/PipelinesRoot.cs | 2 + .../Configuration/ScheduleConfig.cs | 12 + .../Etl/EtlServiceCollectionExtensions.cs | 2 + .../Etl/Pipeline/EtlPipeline.cs | 17 + .../Etl/Pipeline/EtlPipelineBuilder.cs | 45 + .../Etl/Scripts/CommonScripts.cs | 28 + .../Etl/Scripts/SqlScriptRunner.cs | 18 + .../Etl/Transformers/DataTransformerBase.cs | 37 + .../Transformers/TransformingDataReader.cs | 187 ++ .../HealthChecks/DataSyncHealthCheck.cs | 1 + .../Options/PipelineOptions.cs | 4 + .../Services/DataUpdateRepository.cs | 2 + .../Services/EtlPipelineFactory.cs | 26 + .../Services/ScheduleChecker.cs | 3 + .../Services/SearchExecutionService.cs | 9 + .../Services/SyncOrchestrator.cs | 5 + .../Services/TableSyncOperation.cs | 5 + .../Telemetry/DataSyncMetrics.cs | 12 + NEW/src/JdeScoping.DataSync/WorkProcessor.cs | 4 + .../Generators/FluentTableWriter.cs | 13 + .../Mapping/ColumnBuilder.cs | 9 + .../Mapping/ExcelClassMap.cs | 5 + .../Mapping/ExcelMapRegistry.cs | 10 + .../Mapping/Maps/FilterEntryMaps.cs | 24 + .../Maps/MisNonMatchSearchResultMap.cs | 4 + .../Mapping/Maps/MisSearchResultMap.cs | 3 + .../Mapping/Maps/SearchResultMap.cs | 3 + .../Parsing/ExcelParserService.cs | 4 + .../Auth/LdapAuthService.cs | 5 + .../Security/RsaKeyService.cs | 3 + .../Security/SecretsMigrator.cs | 7 + .../Security/SecureStoreRsaKeyService.cs | 5 + .../Security/SecureStoreService.cs | 5 + .../Models/ConfigModel.cs | 157 + .../Models/PipelineModel.cs | 88 + .../Models/ScheduleModel.cs | 15 + .../Utils/JdeScoping.ConfigManager/Program.cs | 8 + .../Services/AutoDiscoveryService.cs | 10 + .../Services/BackupService.cs | 30 + .../Services/ConfigFileService.cs | 31 + .../Services/ConfigLoadException.cs | 9 + .../Services/DiffService.cs | 6 + .../Services/FileSystem.cs | 60 + .../Services/IBackupService.cs | 38 + .../Services/IConfigFileService.cs | 27 + .../Services/IDiffService.cs | 36 + .../Services/IFileSystem.cs | 70 + .../Services/IValidationService.cs | 31 + .../Services/ValidationService.cs | 10 + .../ViewModels/AsyncRelayCommand.cs | 21 + .../ViewModels/RelayCommand.cs | 26 + .../ViewModels/TreeNodeViewModel.cs | 40 + .../ViewModels/ViewModelBase.cs | 15 + .../Views/MainWindow.axaml.cs | 3 + .../App.axaml.cs | 5 + .../Application/SecretUseCases.cs | 10 + .../Application/StoreUseCases.cs | 13 + .../Converters/BooleanConverters.cs | 67 + .../JdeScoping.SecureStoreManager/Program.cs | 10 +- .../Services/AvaloniaClipboardService.cs | 4 + .../Services/AvaloniaDialogService.cs | 37 + .../Services/IDialogService.cs | 6 + .../Services/SecureStoreManager.cs | 4 + .../ViewModels/AsyncRelayCommand.cs | 11 + .../ViewModels/DialogViewModels.cs | 94 + .../ViewModels/MainWindowViewModel.cs | 71 + .../ViewModels/RelayCommand.cs | 12 + .../ViewModels/SecretItemViewModel.cs | 6 + .../ViewModels/ViewModelBase.cs | 1 + .../Views/MainWindow.axaml.cs | 3 + .../Views/NewStoreDialog.axaml.cs | 6 + .../Views/OpenStoreDialog.axaml.cs | 6 + .../Views/SecretEditDialog.axaml.cs | 11 + docs/designs/configmanager-ui-design.md | 1013 ++++++ .../plans/2026-01-19-config-manager-design.md | 488 +++ ...2026-01-19-configmanager-implementation.md | 2484 +++++++++++++++ .../2026-01-19-configmanager-phases7-9.md | 2706 +++++++++++++++++ 136 files changed, 9181 insertions(+), 4 deletions(-) create mode 100644 docs/designs/configmanager-ui-design.md create mode 100644 docs/plans/2026-01-19-config-manager-design.md create mode 100644 docs/plans/2026-01-19-configmanager-implementation.md create mode 100644 docs/plans/2026-01-19-configmanager-phases7-9.md diff --git a/NEW/JdeScoping.slnx b/NEW/JdeScoping.slnx index 0be7837..250a777 100644 --- a/NEW/JdeScoping.slnx +++ b/NEW/JdeScoping.slnx @@ -12,6 +12,7 @@ + @@ -26,6 +27,7 @@ + diff --git a/NEW/src/JdeScoping.Api/Controllers/AuthController.cs b/NEW/src/JdeScoping.Api/Controllers/AuthController.cs index 277be27..6ab3a9a 100644 --- a/NEW/src/JdeScoping.Api/Controllers/AuthController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/AuthController.cs @@ -28,6 +28,12 @@ public class AuthController : ApiControllerBase private readonly IRsaKeyService _rsaKeyService; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// + /// The authentication service. + /// The RSA key service for credential encryption. + /// Logger instance. public AuthController( IAuthService authService, IRsaKeyService rsaKeyService, diff --git a/NEW/src/JdeScoping.Api/Controllers/FileIOController.ComponentLots.cs b/NEW/src/JdeScoping.Api/Controllers/FileIOController.ComponentLots.cs index 8b1d8f6..83c6437 100644 --- a/NEW/src/JdeScoping.Api/Controllers/FileIOController.ComponentLots.cs +++ b/NEW/src/JdeScoping.Api/Controllers/FileIOController.ComponentLots.cs @@ -14,6 +14,8 @@ public partial class FileIOController /// /// Uploads an Excel file containing component lot/item pairs and returns the matched lots /// + /// The Excel file containing component lot/item pairs. + /// The cancellation token. [HttpPost("componentlots/upload")] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status400BadRequest)] @@ -63,6 +65,7 @@ public partial class FileIOController /// /// Downloads an Excel template with current component lot data /// + /// The list of lot view models to include in the export. [HttpPost("componentlots/download")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] public IActionResult DownloadComponentLots([FromBody] List? lotNumbers) diff --git a/NEW/src/JdeScoping.Api/Controllers/FileIOController.Items.cs b/NEW/src/JdeScoping.Api/Controllers/FileIOController.Items.cs index a41bf7a..7a63cdc 100644 --- a/NEW/src/JdeScoping.Api/Controllers/FileIOController.Items.cs +++ b/NEW/src/JdeScoping.Api/Controllers/FileIOController.Items.cs @@ -14,6 +14,8 @@ public partial class FileIOController /// /// Uploads an Excel file containing item numbers and returns the matched items /// + /// The uploaded Excel file containing item numbers. + /// The cancellation token. [HttpPost("items/upload")] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status400BadRequest)] @@ -62,6 +64,7 @@ public partial class FileIOController /// /// Downloads an Excel template with current item data /// + /// The list of items to include in the template. [HttpPost("items/download")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] public IActionResult DownloadItems([FromBody] List? items) diff --git a/NEW/src/JdeScoping.Api/Controllers/FileIOController.PartOperations.cs b/NEW/src/JdeScoping.Api/Controllers/FileIOController.PartOperations.cs index 8d2eb27..13f1632 100644 --- a/NEW/src/JdeScoping.Api/Controllers/FileIOController.PartOperations.cs +++ b/NEW/src/JdeScoping.Api/Controllers/FileIOController.PartOperations.cs @@ -14,6 +14,8 @@ public partial class FileIOController /// /// Uploads an Excel file containing part operations and returns the parsed data /// + /// The Excel file to upload. + /// Result containing parsed part operations. [HttpPost("partoperations/upload")] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status400BadRequest)] @@ -54,6 +56,8 @@ public partial class FileIOController /// /// Downloads an Excel template with current part operation data /// + /// Optional list of part operations to include in template. + /// Excel file. [HttpPost("partoperations/download")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] public IActionResult DownloadPartOperations([FromBody] List? partOperations) diff --git a/NEW/src/JdeScoping.Api/Controllers/FileIOController.WorkOrders.cs b/NEW/src/JdeScoping.Api/Controllers/FileIOController.WorkOrders.cs index d3a4803..093fc2b 100644 --- a/NEW/src/JdeScoping.Api/Controllers/FileIOController.WorkOrders.cs +++ b/NEW/src/JdeScoping.Api/Controllers/FileIOController.WorkOrders.cs @@ -14,6 +14,9 @@ public partial class FileIOController /// /// Uploads an Excel file containing work order numbers and returns the matched work orders /// + /// The Excel file to upload. + /// Cancellation token. + /// Result containing parsed work orders. [HttpPost("workorders/upload")] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileUploadResult), StatusCodes.Status400BadRequest)] @@ -63,6 +66,8 @@ public partial class FileIOController /// /// Downloads an Excel template with current work order data /// + /// Optional list of work order numbers to include in template. + /// Excel file. [HttpPost("workorders/download")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] public IActionResult DownloadWorkOrders([FromBody] List? workOrders) diff --git a/NEW/src/JdeScoping.Api/Controllers/FileIOController.cs b/NEW/src/JdeScoping.Api/Controllers/FileIOController.cs index 3fd3cbf..fab54ba 100644 --- a/NEW/src/JdeScoping.Api/Controllers/FileIOController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/FileIOController.cs @@ -21,6 +21,13 @@ public partial class FileIOController : ApiControllerBase private readonly IExcelTemplateService _templateService; private readonly ILogger _logger; + /// + /// Initializes a new instance of the FileIOController class. + /// + /// Repository for accessing lot finder data. + /// Service for parsing Excel files. + /// Service for generating Excel templates. + /// Logger for controller operations. public FileIOController( ILotFinderRepository repository, IExcelParserService parserService, diff --git a/NEW/src/JdeScoping.Api/Controllers/LookupController.cs b/NEW/src/JdeScoping.Api/Controllers/LookupController.cs index e33a9f3..152607f 100644 --- a/NEW/src/JdeScoping.Api/Controllers/LookupController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/LookupController.cs @@ -15,6 +15,10 @@ public class LookupController : ApiControllerBase { private readonly ILotFinderRepository _repository; + /// + /// Initializes a new instance of the class. + /// + /// The LotFinder repository for data access. public LookupController(ILotFinderRepository repository) { _repository = repository; diff --git a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs b/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs index 84953da..45a0986 100644 --- a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs @@ -20,6 +20,12 @@ public class PipelineController : ApiControllerBase private readonly IDataUpdateRepository _dataUpdateRepository; private readonly IPipelineMapper _mapper; + /// + /// Initializes a new instance of the class. + /// + /// The ETL pipeline factory. + /// The data update repository. + /// The pipeline mapper. public PipelineController( IEtlPipelineFactory pipelineFactory, IDataUpdateRepository dataUpdateRepository, @@ -45,6 +51,7 @@ public class PipelineController : ApiControllerBase /// /// Gets configuration for a specific pipeline. /// + /// The pipeline name. [HttpGet(ApiRoutes.Pipelines.ByName)] public ActionResult GetPipeline(string name) { @@ -60,6 +67,8 @@ public class PipelineController : ApiControllerBase /// /// Gets schedule status for a pipeline. /// + /// The pipeline name. + /// The cancellation token. [HttpGet(ApiRoutes.Pipelines.Status)] public async Task> GetStatus( string name, @@ -104,6 +113,9 @@ public class PipelineController : ApiControllerBase /// /// Gets recent execution history for a pipeline. /// + /// The pipeline name. + /// The maximum number of recent executions to retrieve. + /// The cancellation token. [HttpGet(ApiRoutes.Pipelines.Executions)] public async Task> GetExecutions( string name, diff --git a/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs b/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs index 58b8fbf..ec6a476 100644 --- a/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/RefreshStatusController.cs @@ -17,6 +17,10 @@ public class RefreshStatusController : ApiControllerBase { private readonly ILotFinderRepository _repository; + /// + /// Initializes a new instance of the class. + /// + /// The repository for accessing data update records. public RefreshStatusController(ILotFinderRepository repository) { _repository = repository; diff --git a/NEW/src/JdeScoping.Api/Controllers/SearchController.cs b/NEW/src/JdeScoping.Api/Controllers/SearchController.cs index b24fc21..063b5f6 100644 --- a/NEW/src/JdeScoping.Api/Controllers/SearchController.cs +++ b/NEW/src/JdeScoping.Api/Controllers/SearchController.cs @@ -26,6 +26,13 @@ public class SearchController : ApiControllerBase private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + /// + /// Initializes a new instance of the SearchController. + /// + /// The lot finder repository for data access. + /// The SignalR hub context for sending search updates. + /// The logger for recording search operations. + /// The time provider for generating timestamps. public SearchController( ILotFinderRepository repository, IHubContext hubContext, @@ -41,6 +48,7 @@ public class SearchController : ApiControllerBase /// /// Gets all searches for the current user /// + /// The cancellation token. [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -59,6 +67,7 @@ public class SearchController : ApiControllerBase /// /// Gets all queued searches /// + /// The cancellation token. [HttpGet("queue")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetQueuedSearches(CancellationToken ct) @@ -71,6 +80,8 @@ public class SearchController : ApiControllerBase /// /// Gets a single search by ID /// + /// The search identifier. + /// The cancellation token. [HttpGet("{id:int}")] [ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -87,6 +98,8 @@ public class SearchController : ApiControllerBase /// /// Copies an existing search for the current user (returns copy without persisting) /// + /// The search identifier to copy. + /// The cancellation token. [HttpGet("{id:int}/copy")] [ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -121,6 +134,8 @@ public class SearchController : ApiControllerBase /// /// Creates a new search /// + /// The search view model containing criteria and name. + /// The cancellation token. [HttpPost] [ProducesResponseType(typeof(int), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -160,6 +175,8 @@ public class SearchController : ApiControllerBase /// /// Downloads search results as an Excel file /// + /// The search identifier. + /// The cancellation token. [HttpGet("{id:int}/results")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/NEW/src/JdeScoping.Api/Hubs/StatusHub.cs b/NEW/src/JdeScoping.Api/Hubs/StatusHub.cs index e04dfd6..85190c6 100644 --- a/NEW/src/JdeScoping.Api/Hubs/StatusHub.cs +++ b/NEW/src/JdeScoping.Api/Hubs/StatusHub.cs @@ -16,6 +16,12 @@ public class StatusHub : Hub private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + /// + /// Initializes a new instance of the class. + /// + /// The memory cache for caching status updates. + /// The logger instance. + /// The time provider for getting current UTC time. public StatusHub(IMemoryCache cache, ILogger logger, TimeProvider timeProvider) { _cache = cache; diff --git a/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs b/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs index f3ac304..202e017 100644 --- a/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs +++ b/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs @@ -12,15 +12,23 @@ public interface IPipelineMapper /// /// Maps a pipeline configuration to its DTO representation. /// + /// The pipeline name. + /// The pipeline configuration. + /// The default schedule settings. PipelineConfigDto MapToDto(string name, PipelineConfig config, ScheduleDefaults defaults); /// /// Gets the effective interval for a schedule, applying defaults if not specified. /// + /// The schedule configuration, or null to use defaults. + /// The default schedule settings. + /// The type of update to get the interval for. int GetEffectiveInterval(ScheduleConfig? config, ScheduleDefaults defaults, UpdateTypes updateType); /// /// Gets the schedule configuration for a specific update type. /// + /// The pipeline configuration. + /// The type of update to get the configuration for. ScheduleConfig? GetScheduleConfig(PipelineConfig config, UpdateTypes updateType); } diff --git a/NEW/src/JdeScoping.Client/Auth/AuthStateProvider.cs b/NEW/src/JdeScoping.Client/Auth/AuthStateProvider.cs index d6049d3..7b12864 100644 --- a/NEW/src/JdeScoping.Client/Auth/AuthStateProvider.cs +++ b/NEW/src/JdeScoping.Client/Auth/AuthStateProvider.cs @@ -18,6 +18,12 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider private readonly ILogger? _logger; private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity()); + /// + /// Initializes a new instance of the AuthStateProvider class. + /// + /// Service for storing user authentication data. + /// The HTTP client for making API requests. + /// Optional logger for debugging authentication issues. public AuthStateProvider( IUserStorageService userStorage, HttpClient httpClient, @@ -28,6 +34,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider _logger = logger; } + /// public override async Task GetAuthenticationStateAsync() { // First check cached user info @@ -93,6 +100,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider /// /// Called after successful login to update auth state. /// + /// The authenticated user information. public async Task MarkUserAsAuthenticated(UserInfoDto user) { await _userStorage.SetUserAsync(user); diff --git a/NEW/src/JdeScoping.Client/Auth/IUserStorageService.cs b/NEW/src/JdeScoping.Client/Auth/IUserStorageService.cs index 2eaa4e0..3d071d9 100644 --- a/NEW/src/JdeScoping.Client/Auth/IUserStorageService.cs +++ b/NEW/src/JdeScoping.Client/Auth/IUserStorageService.cs @@ -17,6 +17,7 @@ public interface IUserStorageService /// /// Stores the user info. /// + /// The user information to store. Task SetUserAsync(UserInfoDto user); /// diff --git a/NEW/src/JdeScoping.Client/Auth/UserStorageService.cs b/NEW/src/JdeScoping.Client/Auth/UserStorageService.cs index e365ba3..c57f652 100644 --- a/NEW/src/JdeScoping.Client/Auth/UserStorageService.cs +++ b/NEW/src/JdeScoping.Client/Auth/UserStorageService.cs @@ -14,11 +14,19 @@ public class UserStorageService : IUserStorageService private const string UserKey = "jdescoping_user"; private readonly IJSRuntime _jsRuntime; + /// + /// Initializes a new instance of the class. + /// + /// The JS interop runtime for accessing browser storage. public UserStorageService(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } + /// + /// Retrieves the stored user information from browser session storage. + /// + /// A task that completes with the user information, or null if not found. public async Task GetUserAsync() { try @@ -40,12 +48,21 @@ public class UserStorageService : IUserStorageService } } + /// + /// Stores user information in browser session storage. + /// + /// The user information to store. + /// A task that completes when the user information is stored. public async Task SetUserAsync(UserInfoDto user) { var json = JsonSerializer.Serialize(user); await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.setSessionStorage", UserKey, json); } + /// + /// Removes the stored user information from browser session storage. + /// + /// A task that completes when the user information is removed. public async Task RemoveUserAsync() { await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.removeSessionStorage", UserKey); diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs b/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs index 41ff237..6c8ed01 100644 --- a/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs +++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/AutocompleteFilterPanelBase.cs @@ -9,6 +9,9 @@ namespace JdeScoping.Client.Components.FilterPanels; /// The type of items displayed in the grid. public abstract class AutocompleteFilterPanelBase : ComponentBase where TItem : class { + /// + /// Gets or sets the Radzen dialog service for confirming user actions. + /// [Inject] protected DialogService DialogService { get; set; } = default!; @@ -73,21 +76,29 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T /// /// Performs the search API call and returns matching items. /// + /// The search filter text. + /// A task that returns the list of matching items. protected abstract Task> SearchApiAsync(string filter); /// /// Gets the unique key value for an item. /// + /// The item to get the key for. + /// The unique key value. protected abstract object GetItemKey(TItem item); /// /// Gets the display text value for an item (used for matching in autocomplete). /// + /// The item to get the display text for. + /// The display text value. protected abstract string GetDisplayText(TItem item); /// /// Handles the autocomplete search. /// + /// The load data arguments containing the search filter. + /// A task representing the asynchronous search operation. protected async Task OnSearchAsync(LoadDataArgs args) { if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 3) @@ -103,6 +114,7 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T /// /// Handles selection from the autocomplete. /// + /// The selected value from the autocomplete control. protected void OnItemSelected(object value) { if (value is string text && !string.IsNullOrEmpty(text)) @@ -138,6 +150,8 @@ public abstract class AutocompleteFilterPanelBase : ComponentBase where T /// /// Removes an item from the list. /// + /// The item to remove. + /// A task representing the asynchronous operation. protected async Task DeleteItem(TItem item) { Items.Remove(item); diff --git a/NEW/src/JdeScoping.Client/Components/FilterPanels/FileUploadFilterPanelBase.cs b/NEW/src/JdeScoping.Client/Components/FilterPanels/FileUploadFilterPanelBase.cs index 735d7d8..6611895 100644 --- a/NEW/src/JdeScoping.Client/Components/FilterPanels/FileUploadFilterPanelBase.cs +++ b/NEW/src/JdeScoping.Client/Components/FilterPanels/FileUploadFilterPanelBase.cs @@ -11,12 +11,15 @@ namespace JdeScoping.Client.Components.FilterPanels; /// The type of items displayed in the grid. public abstract class FileUploadFilterPanelBase : ComponentBase where TItem : class { + /// Service for managing dialogs. [Inject] protected DialogService DialogService { get; set; } = default!; + /// Service for displaying notifications. [Inject] protected NotificationService NotificationService { get; set; } = default!; + /// JavaScript interop runtime. [Inject] protected IJSRuntime JSRuntime { get; set; } = default!; @@ -81,6 +84,8 @@ public abstract class FileUploadFilterPanelBase : ComponentBase where TIt /// /// Uploads the file and returns the parsed items. /// + /// The file stream to upload. + /// The name of the file being uploaded. protected abstract Task?> UploadFileApiAsync(Stream stream, string filename); /// @@ -106,6 +111,7 @@ public abstract class FileUploadFilterPanelBase : ComponentBase where TIt /// /// Handles file selection and upload. /// + /// The file change event arguments. protected async Task OnFileSelected(InputFileChangeEventArgs e) { if (e.File == null) return; diff --git a/NEW/src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs b/NEW/src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs index 07fcb99..cece621 100644 --- a/NEW/src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs +++ b/NEW/src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs @@ -22,6 +22,7 @@ public static class ViewModelMappingExtensions /// /// Maps Core SearchViewModel to Client SearchViewModel. /// + /// The Core SearchViewModel to map. public static SearchViewModel ToClient(this CoreSearch vm) => new() { Id = vm.Id, @@ -37,6 +38,7 @@ public static class ViewModelMappingExtensions /// /// Maps Client SearchViewModel to Core SearchViewModel. /// + /// The Client SearchViewModel to map. public static CoreSearch ToCore(this SearchViewModel vm) { ArgumentNullException.ThrowIfNull(vm); @@ -58,6 +60,7 @@ public static class ViewModelMappingExtensions /// Maps Core SearchCriteria to Client SearchCriteriaViewModel. /// Core uses primitive lists; Client uses full view model objects. /// + /// The Core SearchCriteria to map. public static SearchCriteriaViewModel ToClientCriteria(this SearchCriteria criteria) { var client = new SearchCriteriaViewModel @@ -107,6 +110,7 @@ public static class ViewModelMappingExtensions /// Maps Client SearchCriteriaViewModel to Core SearchCriteria. /// Client uses full view model objects; Core uses primitive lists. /// + /// The Client SearchCriteriaViewModel to map. public static SearchCriteria ToCoreCriteria(this SearchCriteriaViewModel criteria) { ArgumentNullException.ThrowIfNull(criteria); @@ -131,6 +135,7 @@ public static class ViewModelMappingExtensions /// /// Maps Core JdeUserViewModel to Client OperatorViewModel. /// + /// The Core JdeUserViewModel to map. public static OperatorViewModel ToClientOperator(this CoreJdeUser vm) => new() { AddressNumber = vm.AddressNumber, @@ -141,6 +146,7 @@ public static class ViewModelMappingExtensions /// /// Maps a collection of Core SearchViewModels to Client SearchViewModels. /// + /// The collection of Core SearchViewModels to map. public static List ToClientList(this IEnumerable list) { ArgumentNullException.ThrowIfNull(list); @@ -150,6 +156,7 @@ public static class ViewModelMappingExtensions /// /// Maps a collection of Core JdeUserViewModels to Client OperatorViewModels. /// + /// The collection of Core JdeUserViewModels to map. public static List ToClientOperatorList(this IEnumerable list) { ArgumentNullException.ThrowIfNull(list); diff --git a/NEW/src/JdeScoping.Client/Http/AuthRedirectHandler.cs b/NEW/src/JdeScoping.Client/Http/AuthRedirectHandler.cs index 2731dad..c63b59f 100644 --- a/NEW/src/JdeScoping.Client/Http/AuthRedirectHandler.cs +++ b/NEW/src/JdeScoping.Client/Http/AuthRedirectHandler.cs @@ -11,11 +11,16 @@ public class AuthRedirectHandler : DelegatingHandler { private readonly NavigationManager _navigationManager; + /// + /// Initializes a new instance of the AuthRedirectHandler class. + /// + /// The navigation manager for redirecting to login. public AuthRedirectHandler(NavigationManager navigationManager) { _navigationManager = navigationManager; } + /// protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/NEW/src/JdeScoping.Client/Models/SearchCriteriaViewModel.cs b/NEW/src/JdeScoping.Client/Models/SearchCriteriaViewModel.cs index 2e926f8..49b586a 100644 --- a/NEW/src/JdeScoping.Client/Models/SearchCriteriaViewModel.cs +++ b/NEW/src/JdeScoping.Client/Models/SearchCriteriaViewModel.cs @@ -7,16 +7,53 @@ namespace JdeScoping.Client.Models; /// public class SearchCriteriaViewModel { + /// + /// Gets or sets the minimum date for search filtering. + /// public DateTime? MinimumDt { get; set; } + + /// + /// Gets or sets the maximum date for search filtering. + /// public DateTime? MaximumDt { get; set; } + /// + /// Gets or sets the list of work orders to include in the search. + /// public List WorkOrders { get; set; } = []; + + /// + /// Gets or sets the list of items to include in the search. + /// public List Items { get; set; } = []; + + /// + /// Gets or sets the list of profit centers to include in the search. + /// public List ProfitCenters { get; set; } = []; + + /// + /// Gets or sets the list of work centers to include in the search. + /// public List WorkCenters { get; set; } = []; + + /// + /// Gets or sets the list of component lots to include in the search. + /// public List ComponentLots { get; set; } = []; + + /// + /// Gets or sets the list of operators to include in the search. + /// public List Operators { get; set; } = []; + + /// + /// Gets or sets the list of part operations to include in the search. + /// public List PartOperations { get; set; } = []; + /// + /// Gets or sets a value indicating whether to extract MIS data in the search. + /// public bool ExtractMisData { get; set; } } diff --git a/NEW/src/JdeScoping.Client/Models/SearchViewModel.cs b/NEW/src/JdeScoping.Client/Models/SearchViewModel.cs index 1375469..eaddd62 100644 --- a/NEW/src/JdeScoping.Client/Models/SearchViewModel.cs +++ b/NEW/src/JdeScoping.Client/Models/SearchViewModel.cs @@ -7,15 +7,45 @@ namespace JdeScoping.Client.Models; /// public class SearchViewModel { + /// + /// Gets or sets the unique identifier for the search. + /// public int Id { get; set; } + /// + /// Gets or sets the name of the search. + /// [Required(ErrorMessage = "Name is required.")] public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the username of the user who created the search. + /// public string UserName { get; set; } = string.Empty; + + /// + /// Gets or sets the current status of the search. + /// public string Status { get; set; } = string.Empty; + + /// + /// Gets or sets the date and time when the search was submitted. + /// public DateTime? SubmitDt { get; set; } + + /// + /// Gets or sets the date and time when the search execution started. + /// public DateTime? StartDt { get; set; } + + /// + /// Gets or sets the date and time when the search execution ended. + /// public DateTime? EndDt { get; set; } + + /// + /// Gets or sets the search criteria for filtering. + /// public SearchCriteriaViewModel Criteria { get; set; } = new(); /// diff --git a/NEW/src/JdeScoping.Client/Models/ValidCombination.cs b/NEW/src/JdeScoping.Client/Models/ValidCombination.cs index b5bb4b9..1891fb9 100644 --- a/NEW/src/JdeScoping.Client/Models/ValidCombination.cs +++ b/NEW/src/JdeScoping.Client/Models/ValidCombination.cs @@ -6,21 +6,74 @@ namespace JdeScoping.Client.Models; /// public class ValidCombination { + /// + /// The unique identifier for this combination. + /// public int Id { get; private init; } + + /// + /// The display name for this combination. + /// public string Name { get; private init; } = string.Empty; + + /// + /// Whether the timespan filter is included in this combination. + /// public bool Timespan { get; private init; } + + /// + /// Whether the work order filter is included in this combination. + /// public bool WorkOrder { get; private init; } + + /// + /// Whether the item number filter is included in this combination. + /// public bool ItemNumber { get; private init; } + + /// + /// Whether the profit center filter is included in this combination. + /// public bool ProfitCenter { get; private init; } + + /// + /// Whether the work center filter is included in this combination. + /// public bool WorkCenter { get; private init; } + + /// + /// Whether the component lot filter is included in this combination. + /// public bool ComponentLot { get; private init; } + + /// + /// Whether the operator filter is included in this combination. + /// public bool Operator { get; private init; } + + /// + /// Whether the item operation MIS filter is included in this combination. + /// public bool ItemOperationMis { get; private init; } + + /// + /// Whether the extract MIS filter is included in this combination. + /// public bool ExtractMis { get; private init; } /// /// Checks if the given filter flags match this combination. /// + /// Whether timespan filter is enabled. + /// Whether work order filter is enabled. + /// Whether item number filter is enabled. + /// Whether profit center filter is enabled. + /// Whether work center filter is enabled. + /// Whether component lot filter is enabled. + /// Whether operator filter is enabled. + /// Whether item operation MIS filter is enabled. + /// Whether extract MIS filter is enabled. + /// True if all flags match this combination. public bool Matches( bool timespan, bool workOrder, diff --git a/NEW/src/JdeScoping.Client/Services/ApiClientBase.cs b/NEW/src/JdeScoping.Client/Services/ApiClientBase.cs index ca1eba7..5df550b 100644 --- a/NEW/src/JdeScoping.Client/Services/ApiClientBase.cs +++ b/NEW/src/JdeScoping.Client/Services/ApiClientBase.cs @@ -17,26 +17,55 @@ public abstract class ApiClientBase PropertyNameCaseInsensitive = true }; + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client to use for requests. protected ApiClientBase(HttpClient httpClient) { HttpClient = httpClient; } + /// + /// Executes a GET request and deserializes the response to the specified type. + /// + /// The response type. + /// The API route. + /// Cancellation token. protected async Task> GetAsync(string route, CancellationToken ct = default) { return await ExecuteAsync(() => HttpClient.GetAsync(route, ct)); } + /// + /// Executes a POST request with a JSON body. + /// + /// The response type. + /// The request body type. + /// The API route. + /// The request body. + /// Cancellation token. protected async Task> PostAsync(string route, TBody body, CancellationToken ct = default) { return await ExecuteAsync(() => HttpClient.PostAsJsonAsync(route, body, ct)); } + /// + /// Executes a POST request without a body. + /// + /// The response type. + /// The API route. + /// Cancellation token. protected async Task> PostAsync(string route, CancellationToken ct = default) { return await ExecuteAsync(() => HttpClient.PostAsync(route, null, ct)); } + /// + /// Executes a GET request and returns the response as raw bytes. + /// + /// The API route. + /// Cancellation token. protected async Task> GetBytesAsync(string route, CancellationToken ct = default) { try @@ -50,6 +79,13 @@ public abstract class ApiClientBase } } + /// + /// Executes a POST request with a JSON body and returns the response as raw bytes. + /// + /// The request body type. + /// The API route. + /// The request body. + /// Cancellation token. protected async Task> PostForBytesAsync(string route, TBody body, CancellationToken ct = default) { try @@ -63,6 +99,14 @@ public abstract class ApiClientBase } } + /// + /// Executes a multipart POST request with a file stream. + /// + /// The response type. + /// The API route. + /// The file stream to upload. + /// The file name for the multipart form. + /// Cancellation token. protected async Task> PostMultipartAsync( string route, Stream fileStream, @@ -161,6 +205,9 @@ public abstract class ApiClientBase /// private sealed class ValidationProblemDetails { + /// + /// Gets or sets the validation errors by field name. + /// public Dictionary? Errors { get; set; } } } diff --git a/NEW/src/JdeScoping.Client/Services/AuthApiClient.cs b/NEW/src/JdeScoping.Client/Services/AuthApiClient.cs index 63dc5cf..8f113fb 100644 --- a/NEW/src/JdeScoping.Client/Services/AuthApiClient.cs +++ b/NEW/src/JdeScoping.Client/Services/AuthApiClient.cs @@ -9,17 +9,42 @@ namespace JdeScoping.Client.Services; /// public class AuthApiClient : ApiClientBase, IAuthApiClient { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client for API requests. public AuthApiClient(HttpClient httpClient) : base(httpClient) { } + /// + /// Retrieves the server's RSA public key for credential encryption. + /// + /// Cancellation token. + /// The public key response. public Task> GetPublicKeyAsync(CancellationToken ct = default) => GetAsync(ApiRoutes.Auth.PublicKey, ct); + /// + /// Authenticates a user with encrypted credentials. + /// + /// The encrypted login request. + /// Cancellation token. + /// The login result with user info on success. public Task> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default) => PostAsync(ApiRoutes.Auth.Login, request, ct); + /// + /// Logs out the current user. + /// + /// Cancellation token. + /// Result of logout operation. public Task> LogoutAsync(CancellationToken ct = default) => PostAsync(ApiRoutes.Auth.Logout, ct); + /// + /// Retrieves information about the current authenticated user. + /// + /// Cancellation token. + /// The current user's information. public Task> GetCurrentUserAsync(CancellationToken ct = default) => GetAsync(ApiRoutes.Auth.Me, ct); } diff --git a/NEW/src/JdeScoping.Client/Services/AuthService.cs b/NEW/src/JdeScoping.Client/Services/AuthService.cs index 26fbc63..00c838a 100644 --- a/NEW/src/JdeScoping.Client/Services/AuthService.cs +++ b/NEW/src/JdeScoping.Client/Services/AuthService.cs @@ -13,6 +13,12 @@ public class AuthService : IAuthService private readonly ICryptoService _cryptoService; private readonly IAuthStateProvider _authStateProvider; + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client for API communication. + /// The cryptography service. + /// The authentication state provider. public AuthService( HttpClient httpClient, ICryptoService cryptoService, @@ -23,6 +29,11 @@ public class AuthService : IAuthService _authStateProvider = authStateProvider; } + /// + /// Authenticates a user with encrypted credentials. + /// + /// The login credentials. + /// The login result with user information if successful. public async Task LoginAsync(LoginModel model) { try @@ -54,6 +65,9 @@ public class AuthService : IAuthService } } + /// + /// Logs out the current user and clears authentication state. + /// public async Task LogoutAsync() { try diff --git a/NEW/src/JdeScoping.Client/Services/CryptoService.cs b/NEW/src/JdeScoping.Client/Services/CryptoService.cs index 3b6bd90..f73f151 100644 --- a/NEW/src/JdeScoping.Client/Services/CryptoService.cs +++ b/NEW/src/JdeScoping.Client/Services/CryptoService.cs @@ -17,6 +17,11 @@ public class CryptoService : ICryptoService, IAsyncDisposable private readonly SemaphoreSlim _keyLock = new(1, 1); private bool _disposed; + /// + /// Initializes a new instance of the CryptoService. + /// + /// The HTTP client for fetching the public key from the server. + /// The JavaScript runtime for performing RSA encryption. public CryptoService(HttpClient httpClient, IJSRuntime jsRuntime) { _httpClient = httpClient; diff --git a/NEW/src/JdeScoping.Client/Services/FileApiClient.cs b/NEW/src/JdeScoping.Client/Services/FileApiClient.cs index 58c371e..17b0ff2 100644 --- a/NEW/src/JdeScoping.Client/Services/FileApiClient.cs +++ b/NEW/src/JdeScoping.Client/Services/FileApiClient.cs @@ -9,33 +9,89 @@ namespace JdeScoping.Client.Services; /// public class FileApiClient : ApiClientBase, IFileApiClient { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client for API requests. public FileApiClient(HttpClient httpClient) : base(httpClient) { } // Downloads + /// + /// Downloads an Excel template with optional existing work order data. + /// + /// Optional list of existing work orders to include. + /// Cancellation token. + /// The Excel file bytes. public Task> DownloadWorkOrdersTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadWorkOrders, existingData, ct); + /// + /// Downloads an Excel template with optional existing item data. + /// + /// Optional list of existing items to include. + /// Cancellation token. + /// The Excel file bytes. public Task> DownloadItemsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadItems, existingData, ct); + /// + /// Downloads an Excel template with optional existing component lot data. + /// + /// Optional list of existing lots to include. + /// Cancellation token. + /// The Excel file bytes. public Task> DownloadComponentLotsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadComponentLots, existingData, ct); + /// + /// Downloads an Excel template with optional existing part operation data. + /// + /// Optional list of existing part operations to include. + /// Cancellation token. + /// The Excel file bytes. public Task> DownloadPartOperationsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadPartOperations, existingData, ct); // Uploads + /// + /// Uploads and parses an Excel file containing work orders. + /// + /// The file stream to upload. + /// The original file name. + /// Cancellation token. + /// List of parsed work order view models. public Task>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadWorkOrders, fileStream, fileName, ct); + /// + /// Uploads and parses an Excel file containing items. + /// + /// The file stream to upload. + /// The original file name. + /// Cancellation token. + /// List of parsed item view models. public Task>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadItems, fileStream, fileName, ct); + /// + /// Uploads and parses an Excel file containing component lots. + /// + /// The file stream to upload. + /// The original file name. + /// Cancellation token. + /// List of parsed lot view models. public Task>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadComponentLots, fileStream, fileName, ct); + /// + /// Uploads and parses an Excel file containing part operations. + /// + /// The file stream to upload. + /// The original file name. + /// Cancellation token. + /// List of parsed part operation view models. public Task>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadPartOperations, fileStream, fileName, ct); } diff --git a/NEW/src/JdeScoping.Client/Services/HubConnectionService.cs b/NEW/src/JdeScoping.Client/Services/HubConnectionService.cs index e96e2e6..1ce954d 100644 --- a/NEW/src/JdeScoping.Client/Services/HubConnectionService.cs +++ b/NEW/src/JdeScoping.Client/Services/HubConnectionService.cs @@ -13,16 +13,33 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable private readonly NavigationManager _navigationManager; private HubConnection? _hubConnection; + /// + /// Raised when a search is updated. + /// public event Action? OnSearchUpdate; + + /// + /// Raised when a status update is received. + /// public event Action? OnStatusUpdate; + /// + /// Gets a value indicating whether the SignalR connection is active. + /// public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected; + /// + /// Initializes a new instance of the class. + /// + /// The navigation manager for resolving hub URL. public HubConnectionService(NavigationManager navigationManager) { _navigationManager = navigationManager; } + /// + /// Establishes the SignalR connection and registers message handlers. + /// public async Task StartAsync() { if (_hubConnection != null) @@ -82,6 +99,9 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable } } + /// + /// Stops the SignalR connection. + /// public async Task StopAsync() { if (_hubConnection != null) @@ -92,6 +112,10 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable } } + /// + /// Retrieves the most recent status update from the server cache. + /// + /// The cached status update, or null if not available. public async Task GetCachedStatusAsync() { if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected) @@ -110,6 +134,9 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable } } + /// + /// Releases the resources used by the service. + /// public async ValueTask DisposeAsync() { await StopAsync(); diff --git a/NEW/src/JdeScoping.Client/Services/IAuthService.cs b/NEW/src/JdeScoping.Client/Services/IAuthService.cs index 3cfe66a..e9fa70f 100644 --- a/NEW/src/JdeScoping.Client/Services/IAuthService.cs +++ b/NEW/src/JdeScoping.Client/Services/IAuthService.cs @@ -10,6 +10,8 @@ public interface IAuthService /// /// Attempts to log in with the provided credentials (encrypted). /// + /// The login credentials. + /// The login result containing status and user information. Task LoginAsync(LoginModel model); /// diff --git a/NEW/src/JdeScoping.Client/Services/IRefreshStatusService.cs b/NEW/src/JdeScoping.Client/Services/IRefreshStatusService.cs index ae9e5b6..1129672 100644 --- a/NEW/src/JdeScoping.Client/Services/IRefreshStatusService.cs +++ b/NEW/src/JdeScoping.Client/Services/IRefreshStatusService.cs @@ -10,5 +10,8 @@ public interface IRefreshStatusService /// /// Gets refresh status records within the specified date range. /// + /// The minimum date/time (inclusive). + /// The maximum date/time (inclusive). + /// A list of data update status records. Task> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt); } diff --git a/NEW/src/JdeScoping.Client/Services/ISearchSubmissionService.cs b/NEW/src/JdeScoping.Client/Services/ISearchSubmissionService.cs index bbf7d76..62dcf91 100644 --- a/NEW/src/JdeScoping.Client/Services/ISearchSubmissionService.cs +++ b/NEW/src/JdeScoping.Client/Services/ISearchSubmissionService.cs @@ -38,10 +38,14 @@ public class SearchSubmissionResult /// /// Creates a successful result. /// + /// The ID of the successfully submitted search. + /// A successful search submission result. public static SearchSubmissionResult Success(int searchId) => new() { SearchId = searchId }; /// /// Creates a failure result. /// + /// The error message describing the submission failure. + /// A failed search submission result. public static SearchSubmissionResult Failure(string error) => new() { ErrorMessage = error }; } diff --git a/NEW/src/JdeScoping.Client/Services/LookupApiClient.cs b/NEW/src/JdeScoping.Client/Services/LookupApiClient.cs index ec50a54..ff5306f 100644 --- a/NEW/src/JdeScoping.Client/Services/LookupApiClient.cs +++ b/NEW/src/JdeScoping.Client/Services/LookupApiClient.cs @@ -9,17 +9,45 @@ namespace JdeScoping.Client.Services; /// public class LookupApiClient : ApiClientBase, ILookupApiClient { + /// + /// Initializes a new instance of the LookupApiClient class. + /// + /// The HTTP client for making API requests. public LookupApiClient(HttpClient httpClient) : base(httpClient) { } + /// + /// Finds items matching the search query. + /// + /// The search query to match against item data. + /// Cancellation token. + /// An API result containing matching items. public Task>> FindItemsAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindItems(query), ct); + /// + /// Finds profit centers matching the search query. + /// + /// The search query to match against profit center data. + /// Cancellation token. + /// An API result containing matching profit centers. public Task>> FindProfitCentersAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindProfitCenters(query), ct); + /// + /// Finds work centers matching the search query. + /// + /// The search query to match against work center data. + /// Cancellation token. + /// An API result containing matching work centers. public Task>> FindWorkCentersAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindWorkCenters(query), ct); + /// + /// Finds operators (JDE users) matching the search query. + /// + /// The search query to match against operator data. + /// Cancellation token. + /// An API result containing matching operators. public Task>> FindOperatorsAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindOperators(query), ct); } diff --git a/NEW/src/JdeScoping.Client/Services/PipelineApiClient.cs b/NEW/src/JdeScoping.Client/Services/PipelineApiClient.cs index 4dccd85..0522333 100644 --- a/NEW/src/JdeScoping.Client/Services/PipelineApiClient.cs +++ b/NEW/src/JdeScoping.Client/Services/PipelineApiClient.cs @@ -9,17 +9,45 @@ namespace JdeScoping.Client.Services; /// public class PipelineApiClient : ApiClientBase, IPipelineApiClient { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client for API communication. public PipelineApiClient(HttpClient httpClient) : base(httpClient) { } + /// + /// Gets the list of available pipeline names from the API. + /// + /// Cancellation token for the request. + /// The API result containing the pipeline list. public Task> GetPipelineNamesAsync(CancellationToken ct = default) => GetAsync(ApiRoutes.Pipelines.Base, ct); + /// + /// Gets the configuration for a specific pipeline by name. + /// + /// The pipeline name. + /// Cancellation token for the request. + /// The API result containing the pipeline configuration. public Task> GetPipelineAsync(string name, CancellationToken ct = default) => GetAsync(ApiRoutes.Pipelines.GetByName(name), ct); + /// + /// Gets the current execution status of a pipeline. + /// + /// The pipeline name. + /// Cancellation token for the request. + /// The API result containing the pipeline status. public Task> GetStatusAsync(string name, CancellationToken ct = default) => GetAsync(ApiRoutes.Pipelines.GetStatus(name), ct); + /// + /// Gets the recent execution history for a pipeline. + /// + /// The pipeline name. + /// The maximum number of execution records to retrieve. + /// Cancellation token for the request. + /// The API result containing the execution history. public Task> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default) => GetAsync(ApiRoutes.Pipelines.GetExecutions(name, count), ct); } diff --git a/NEW/src/JdeScoping.Client/Services/RefreshStatusService.cs b/NEW/src/JdeScoping.Client/Services/RefreshStatusService.cs index 61cf94e..5c4b5e8 100644 --- a/NEW/src/JdeScoping.Client/Services/RefreshStatusService.cs +++ b/NEW/src/JdeScoping.Client/Services/RefreshStatusService.cs @@ -11,11 +11,21 @@ public class RefreshStatusService : IRefreshStatusService { private readonly HttpClient _httpClient; + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client for API communication. public RefreshStatusService(HttpClient httpClient) { _httpClient = httpClient; } + /// + /// Gets the data refresh status for a date range. + /// + /// The minimum date for the status query. + /// The maximum date for the status query. + /// A list of data update information for the specified date range. public async Task> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt) { try diff --git a/NEW/src/JdeScoping.Client/Services/SearchApiClient.cs b/NEW/src/JdeScoping.Client/Services/SearchApiClient.cs index 258d589..5c54998 100644 --- a/NEW/src/JdeScoping.Client/Services/SearchApiClient.cs +++ b/NEW/src/JdeScoping.Client/Services/SearchApiClient.cs @@ -9,23 +9,61 @@ namespace JdeScoping.Client.Services; /// public class SearchApiClient : ApiClientBase, ISearchApiClient { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client for API requests. public SearchApiClient(HttpClient httpClient) : base(httpClient) { } + /// + /// Retrieves the current user's searches. + /// + /// Cancellation token. + /// List of user searches. public Task>> GetUserSearchesAsync(CancellationToken ct = default) => GetAsync>(ApiRoutes.Search.Base, ct); + /// + /// Retrieves searches queued for processing. + /// + /// Cancellation token. + /// List of queued searches. public Task>> GetQueuedSearchesAsync(CancellationToken ct = default) => GetAsync>(ApiRoutes.Search.Queue, ct); + /// + /// Retrieves a specific search by ID. + /// + /// The search ID. + /// Cancellation token. + /// The search view model. public Task> GetSearchAsync(int id, CancellationToken ct = default) => GetAsync(ApiRoutes.Search.GetById(id), ct); + /// + /// Creates a copy of an existing search. + /// + /// The ID of the search to copy. + /// Cancellation token. + /// The new copied search view model. public Task> CopySearchAsync(int id, CancellationToken ct = default) => GetAsync(ApiRoutes.Search.GetCopy(id), ct); + /// + /// Creates a new search. + /// + /// The search view model to create. + /// Cancellation token. + /// The ID of the newly created search. public Task> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default) => PostAsync(ApiRoutes.Search.Base, search, ct); + /// + /// Retrieves the results of a completed search. + /// + /// The search ID. + /// Cancellation token. + /// The Excel file bytes containing results. public Task> GetResultsAsync(int id, CancellationToken ct = default) => GetBytesAsync(ApiRoutes.Search.GetResults(id), ct); } diff --git a/NEW/src/JdeScoping.Client/Services/SearchSubmissionService.cs b/NEW/src/JdeScoping.Client/Services/SearchSubmissionService.cs index 34f769d..10d898b 100644 --- a/NEW/src/JdeScoping.Client/Services/SearchSubmissionService.cs +++ b/NEW/src/JdeScoping.Client/Services/SearchSubmissionService.cs @@ -11,6 +11,10 @@ public class SearchSubmissionService : ISearchSubmissionService { private readonly ISearchApiClient _searchApi; + /// + /// Initializes a new instance of the class. + /// + /// The search API client. public SearchSubmissionService(ISearchApiClient searchApi) { _searchApi = searchApi; diff --git a/NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs b/NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs index dd94a54..6ed114c 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs @@ -26,12 +26,18 @@ public static class ApiRoutes public const string Results = "{id:int}/results"; /// Builds the route to get a specific search. + /// The search ID. + /// The formatted route. public static string GetById(int id) => $"api/search/{id}"; /// Builds the route to copy a search. + /// The search ID to copy. + /// The formatted route. public static string GetCopy(int id) => $"api/search/{id}/copy"; /// Builds the route to get search results. + /// The search ID. + /// The formatted route. public static string GetResults(int id) => $"api/search/{id}/results"; } @@ -56,15 +62,23 @@ public static class ApiRoutes public const string Operators = "api/lookup/operators"; /// Builds the route to find items with URL-encoded query. + /// The search query to URL-encode. + /// The formatted route. public static string FindItems(string query) => $"{Items}?q={Uri.EscapeDataString(query)}"; /// Builds the route to find profit centers with URL-encoded query. + /// The search query to URL-encode. + /// The formatted route. public static string FindProfitCenters(string query) => $"{ProfitCenters}?q={Uri.EscapeDataString(query)}"; /// Builds the route to find work centers with URL-encoded query. + /// The search query to URL-encode. + /// The formatted route. public static string FindWorkCenters(string query) => $"{WorkCenters}?q={Uri.EscapeDataString(query)}"; /// Builds the route to find operators with URL-encoded query. + /// The search query to URL-encode. + /// The formatted route. public static string FindOperators(string query) => $"{Operators}?q={Uri.EscapeDataString(query)}"; } @@ -131,6 +145,9 @@ public static class ApiRoutes public const string Base = "api/refresh-status"; /// Builds the route to get refresh status with date range. + /// The minimum date (inclusive). + /// The maximum date (inclusive). + /// The formatted route. public static string Get(DateTime minDt, DateTime maxDt) => $"api/refresh-status?minDT={minDt:yyyy-MM-dd}&maxDT={maxDt:yyyy-MM-dd}"; } @@ -153,12 +170,19 @@ public static class ApiRoutes public const string Executions = "{name}/executions"; /// Builds the route to get a specific pipeline config. + /// The pipeline name to URL-encode. + /// The formatted route. public static string GetByName(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}"; /// Builds the route to get pipeline status. + /// The pipeline name to URL-encode. + /// The formatted route. public static string GetStatus(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}/status"; /// Builds the route to get pipeline executions. + /// The pipeline name to URL-encode. + /// The number of recent executions to retrieve. + /// The formatted route. public static string GetExecutions(string name, int count = 10) => $"api/pipelines/{Uri.EscapeDataString(name)}/executions?count={count}"; } diff --git a/NEW/src/JdeScoping.Core/ApiContracts/IAuthApiClient.cs b/NEW/src/JdeScoping.Core/ApiContracts/IAuthApiClient.cs index 82e3df1..7f2b6c2 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/IAuthApiClient.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/IAuthApiClient.cs @@ -9,14 +9,19 @@ namespace JdeScoping.Core.ApiContracts; public interface IAuthApiClient { /// Gets the server's RSA public key for encrypting login credentials. + /// The cancellation token. Task> GetPublicKeyAsync(CancellationToken ct = default); /// Authenticates with encrypted credentials. + /// The encrypted login request containing credentials. + /// The cancellation token. Task> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default); /// Logs out the current user. + /// The cancellation token. Task> LogoutAsync(CancellationToken ct = default); /// Gets the current authenticated user's information. + /// The cancellation token. Task> GetCurrentUserAsync(CancellationToken ct = default); } diff --git a/NEW/src/JdeScoping.Core/ApiContracts/IFileApiClient.cs b/NEW/src/JdeScoping.Core/ApiContracts/IFileApiClient.cs index e81b18d..6852258 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/IFileApiClient.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/IFileApiClient.cs @@ -12,28 +12,48 @@ public interface IFileApiClient // Downloads (POST with existing data, returns Excel bytes) /// Downloads work orders template, optionally pre-filled with existing data. + /// Optional existing data to pre-fill the template. + /// Cancellation token. Task> DownloadWorkOrdersTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); /// Downloads items template, optionally pre-filled with existing data. + /// Optional existing data to pre-fill the template. + /// Cancellation token. Task> DownloadItemsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); /// Downloads component lots template, optionally pre-filled with existing data. + /// Optional existing data to pre-fill the template. + /// Cancellation token. Task> DownloadComponentLotsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); /// Downloads part operations template, optionally pre-filled with existing data. + /// Optional existing data to pre-fill the template. + /// Cancellation token. Task> DownloadPartOperationsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); // Uploads (multipart form, returns parsed data) /// Uploads work orders Excel file and returns parsed data. + /// The file stream to upload. + /// The name of the file being uploaded. + /// Cancellation token. Task>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default); /// Uploads items Excel file and returns parsed data. + /// The file stream to upload. + /// The name of the file being uploaded. + /// Cancellation token. Task>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default); /// Uploads component lots Excel file and returns parsed data. + /// The file stream to upload. + /// The name of the file being uploaded. + /// Cancellation token. Task>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default); /// Uploads part operations Excel file and returns parsed data. + /// The file stream to upload. + /// The name of the file being uploaded. + /// Cancellation token. Task>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default); } diff --git a/NEW/src/JdeScoping.Core/ApiContracts/ILookupApiClient.cs b/NEW/src/JdeScoping.Core/ApiContracts/ILookupApiClient.cs index 6abd55e..9492454 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/ILookupApiClient.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/ILookupApiClient.cs @@ -9,14 +9,22 @@ namespace JdeScoping.Core.ApiContracts; public interface ILookupApiClient { /// Finds items matching the search query. + /// The search query to match against item data. + /// Cancellation token. Task>> FindItemsAsync(string query, CancellationToken ct = default); /// Finds profit centers matching the search query. + /// The search query to match against profit center data. + /// Cancellation token. Task>> FindProfitCentersAsync(string query, CancellationToken ct = default); /// Finds work centers matching the search query. + /// The search query to match against work center data. + /// Cancellation token. Task>> FindWorkCentersAsync(string query, CancellationToken ct = default); /// Finds operators (JDE users) matching the search query. + /// The search query to match against operator data. + /// Cancellation token. Task>> FindOperatorsAsync(string query, CancellationToken ct = default); } diff --git a/NEW/src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs b/NEW/src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs index 7671f76..48fd2c8 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs @@ -9,14 +9,22 @@ namespace JdeScoping.Core.ApiContracts; public interface IPipelineApiClient { /// Gets list of all available pipeline names. + /// Cancellation token. Task> GetPipelineNamesAsync(CancellationToken ct = default); /// Gets configuration for a specific pipeline. + /// The pipeline name. + /// Cancellation token. Task> GetPipelineAsync(string name, CancellationToken ct = default); /// Gets schedule status for a pipeline. + /// The pipeline name. + /// Cancellation token. Task> GetStatusAsync(string name, CancellationToken ct = default); /// Gets recent execution history for a pipeline. + /// The pipeline name. + /// The maximum number of executions to return. + /// Cancellation token. Task> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default); } diff --git a/NEW/src/JdeScoping.Core/ApiContracts/ISearchApiClient.cs b/NEW/src/JdeScoping.Core/ApiContracts/ISearchApiClient.cs index 2631fce..39df23a 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/ISearchApiClient.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/ISearchApiClient.cs @@ -9,20 +9,30 @@ namespace JdeScoping.Core.ApiContracts; public interface ISearchApiClient { /// Gets all searches for the current user. + /// Cancellation token. Task>> GetUserSearchesAsync(CancellationToken ct = default); /// Gets all queued searches. + /// Cancellation token. Task>> GetQueuedSearchesAsync(CancellationToken ct = default); /// Gets a specific search by ID. + /// The search identifier. + /// Cancellation token. Task> GetSearchAsync(int id, CancellationToken ct = default); /// Copies an existing search to create a new one (returns copy without persisting). + /// The search identifier to copy. + /// Cancellation token. Task> CopySearchAsync(int id, CancellationToken ct = default); /// Creates and submits a new search. + /// The search to create. + /// Cancellation token. Task> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default); /// Downloads the results for a completed search as Excel bytes. + /// The search identifier. + /// Cancellation token. Task> GetResultsAsync(int id, CancellationToken ct = default); } diff --git a/NEW/src/JdeScoping.Core/ApiContracts/Results/ValidationError.cs b/NEW/src/JdeScoping.Core/ApiContracts/Results/ValidationError.cs index ddbe43a..e622af6 100644 --- a/NEW/src/JdeScoping.Core/ApiContracts/Results/ValidationError.cs +++ b/NEW/src/JdeScoping.Core/ApiContracts/Results/ValidationError.cs @@ -10,6 +10,8 @@ public readonly record struct ValidationError(IReadOnlyDictionary /// Creates a ValidationError from a dictionary of field errors. /// + /// Dictionary mapping field names to error message arrays. + /// A ValidationError containing the field errors. public static ValidationError FromDictionary(Dictionary errors) => new(errors); } diff --git a/NEW/src/JdeScoping.Core/Models/Auth/LoginModel.cs b/NEW/src/JdeScoping.Core/Models/Auth/LoginModel.cs index 172e342..54971d4 100644 --- a/NEW/src/JdeScoping.Core/Models/Auth/LoginModel.cs +++ b/NEW/src/JdeScoping.Core/Models/Auth/LoginModel.cs @@ -7,9 +7,15 @@ namespace JdeScoping.Core.Models.Auth; /// public class LoginModel { + /// + /// Gets or sets the user's login username. + /// [Required(ErrorMessage = "Username is required")] public string Username { get; set; } = string.Empty; + /// + /// Gets or sets the user's login password. + /// [Required(ErrorMessage = "Password is required")] public string Password { get; set; } = string.Empty; } diff --git a/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs b/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs index ce07eb5..5bb6d2e 100644 --- a/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs +++ b/NEW/src/JdeScoping.Core/Models/Infrastructure/DataUpdateDto.cs @@ -6,20 +6,34 @@ namespace JdeScoping.Core.Models.Infrastructure; /// public class DataUpdateDto { + /// The start time of the data update. public DateTime StartDt { get; set; } + /// The end time of the data update. public DateTime? EndDt { get; set; } + /// The number of branch records updated. public int BranchRecords { get; set; } + /// The number of profit center records updated. public int ProfitCenterRecords { get; set; } + /// The number of work center records updated. public int WorkCenterRecords { get; set; } + /// The number of organizational hierarchy records updated. public int OrgHierarchyRecords { get; set; } + /// The number of status code records updated. public int StatusCodeRecords { get; set; } + /// The number of user records updated. public int UserRecords { get; set; } + /// The number of item records updated. public int ItemRecords { get; set; } + /// The number of lot records updated. public int LotRecords { get; set; } + /// The number of work order records updated. public int WorkOrderRecords { get; set; } + /// The number of work order step records updated. public int WorkOrderStepRecords { get; set; } + /// The number of work order component records updated. public int WorkOrderComponentRecords { get; set; } + /// Whether the data update was successful. public bool WasSuccessful { get; set; } } diff --git a/NEW/src/JdeScoping.Core/Models/SearchResults/MisNonMatchSearchResult.cs b/NEW/src/JdeScoping.Core/Models/SearchResults/MisNonMatchSearchResult.cs index 4c98756..53d0723 100644 --- a/NEW/src/JdeScoping.Core/Models/SearchResults/MisNonMatchSearchResult.cs +++ b/NEW/src/JdeScoping.Core/Models/SearchResults/MisNonMatchSearchResult.cs @@ -5,16 +5,28 @@ namespace JdeScoping.Core.Models.SearchResults; /// public sealed class MisNonMatchSearchResult { + /// The work center code. public string WorkCenterCode { get; init; } = string.Empty; + /// The work order number. public long WorkOrderNumber { get; init; } + /// The work order start date. public DateTime WorkOrderStartDate { get; init; } + /// The job step number. public decimal JobStepNumber { get; init; } + /// The job step description. public string JobStepDescription { get; init; } = string.Empty; + /// The job step end date. public DateTime? JobStepEndDate { get; init; } + /// The function code. public string FunctionCode { get; init; } = string.Empty; + /// Indicates whether the job step was added. public bool WasJobStepAdded { get; init; } + /// The matched job step number, if any. public decimal? MatchedJobStepNumber { get; init; } + /// The item number. public string ItemNumber { get; init; } = string.Empty; + /// The item description. public string ItemDescription { get; init; } = string.Empty; + /// The routing type. public string RoutingType { get; init; } = string.Empty; } diff --git a/NEW/src/JdeScoping.Core/Models/SearchResults/MisSearchResult.cs b/NEW/src/JdeScoping.Core/Models/SearchResults/MisSearchResult.cs index 37f1194..9d1469d 100644 --- a/NEW/src/JdeScoping.Core/Models/SearchResults/MisSearchResult.cs +++ b/NEW/src/JdeScoping.Core/Models/SearchResults/MisSearchResult.cs @@ -5,23 +5,42 @@ namespace JdeScoping.Core.Models.SearchResults; /// public sealed class MisSearchResult { + /// The item number. public string ItemNumber { get; init; } = string.Empty; + /// The item description. public string ItemDescription { get; init; } = string.Empty; + /// The sequence number. public string SequenceNumber { get; init; } = string.Empty; + /// The MIS (Manufacturing Instruction Sheet) number. public string MisNumber { get; init; } = string.Empty; + /// The revision ID. public string RevId { get; init; } = string.Empty; + /// The status. public string Status { get; init; } = string.Empty; + /// The release date. public DateTime? ReleaseDate { get; init; } + /// The branch code. public string BranchCode { get; init; } = string.Empty; + /// The job step sequence number. public decimal JobStepSequenceNumber { get; init; } + /// The matched sequence number. public decimal? MatchedSequenceNumber { get; init; } + /// Whether the routing matches. public bool RoutingMatch { get; init; } + /// Whether the master matches. public bool MasterMatch { get; init; } + /// The function operation description. public string FunctionOperationDescription { get; init; } = string.Empty; + /// The character number. public string CharNumber { get; init; } = string.Empty; + /// The test description. public string TestDescription { get; init; } = string.Empty; + /// The sampling type. public string SamplingType { get; init; } = string.Empty; + /// The sampling value. public string SamplingValue { get; init; } = string.Empty; + /// The tools and gauges required. public string ToolsGauges { get; init; } = string.Empty; + /// The work instructions. public string WorkInstructions { get; init; } = string.Empty; } diff --git a/NEW/src/JdeScoping.Core/Models/SearchResults/SearchModel.cs b/NEW/src/JdeScoping.Core/Models/SearchResults/SearchModel.cs index 35e5897..351eba9 100644 --- a/NEW/src/JdeScoping.Core/Models/SearchResults/SearchModel.cs +++ b/NEW/src/JdeScoping.Core/Models/SearchResults/SearchModel.cs @@ -5,15 +5,25 @@ namespace JdeScoping.Core.Models.SearchResults; /// public class SearchModel { + /// The unique identifier for the search. public int Id { get; set; } + /// The username of the user who submitted the search. public string UserName { get; set; } = string.Empty; + /// The name of the search. public string Name { get; set; } = string.Empty; + /// The date and time the search was submitted. public DateTime? SubmitDt { get; set; } + /// The date and time when search execution started. public DateTime? StartDt { get; set; } + /// The date and time when search execution ended. public DateTime? EndDt { get; set; } + /// Indicates whether MIS data extraction is enabled for this search. public bool ExtractMisData { get; set; } + /// The main search results. public List Results { get; set; } = []; + /// The MIS-specific search results. public List MisResults { get; set; } = []; + /// The MIS non-match investigation results. public List MisNonMatchResults { get; set; } = []; } diff --git a/NEW/src/JdeScoping.Core/Models/SearchResults/SearchResult.cs b/NEW/src/JdeScoping.Core/Models/SearchResults/SearchResult.cs index 49754aa..50008d1 100644 --- a/NEW/src/JdeScoping.Core/Models/SearchResults/SearchResult.cs +++ b/NEW/src/JdeScoping.Core/Models/SearchResults/SearchResult.cs @@ -5,30 +5,119 @@ namespace JdeScoping.Core.Models.SearchResults; /// public sealed class SearchResult { + /// + /// Work order number. + /// public long WorkOrderNumber { get; init; } + + /// + /// Branch code for the work order. + /// public string WorkOrderBranchCode { get; init; } = string.Empty; + + /// + /// Lot number. + /// public string LotNumber { get; init; } = string.Empty; + + /// + /// Item number. + /// public string ItemNumber { get; init; } = string.Empty; + + /// + /// Planning family. + /// public string PlanningFamily { get; init; } = string.Empty; + + /// + /// Stocking type. + /// public string StockingType { get; init; } = string.Empty; + + /// + /// Order quantity. + /// public decimal OrderQuantity { get; init; } + + /// + /// Held quantity. + /// public decimal HeldQuantity { get; init; } + + /// + /// Scrapped quantity. + /// public decimal ScrappedQuantity { get; init; } + + /// + /// Shipped quantity. + /// public decimal ShippedQuantity { get; init; } + + /// + /// Branch code for the step. + /// public string StepBranchCode { get; init; } = string.Empty; + + /// + /// Step number. + /// public decimal StepNumber { get; init; } + + /// + /// Description of the step. + /// public string StepDescription { get; init; } = string.Empty; + + /// + /// Function operation description. + /// public string FunctionOperationDescription { get; init; } = string.Empty; + + /// + /// Step update date and time. + /// public DateTime StepUpdateDt { get; init; } + + /// + /// Status code. + /// public string StatusCode { get; init; } = string.Empty; + + /// + /// Status description. + /// public string StatusDescription { get; init; } = string.Empty; + + /// + /// Status update date and time. + /// public DateTime? StatusUpdateDt { get; init; } - // Inclusion flags + /// + /// Indicates if manually specified in search criteria. + /// public bool ManuallySpecified { get; init; } + + /// + /// Indicates if part of a split order. + /// public bool SplitOrder { get; init; } + + /// + /// Indicates if flagged for cardex inclusion. + /// public bool Cardex { get; init; } + + /// + /// Indicates if included via parts list. + /// public bool PartsList { get; init; } + + /// + /// Indicates if flagged for inclusion. + /// public bool Flagged { get; init; } /// diff --git a/NEW/src/JdeScoping.Core/ViewModels/ComponentLotViewModel.cs b/NEW/src/JdeScoping.Core/ViewModels/ComponentLotViewModel.cs index bb9d002..7f92f1c 100644 --- a/NEW/src/JdeScoping.Core/ViewModels/ComponentLotViewModel.cs +++ b/NEW/src/JdeScoping.Core/ViewModels/ComponentLotViewModel.cs @@ -8,9 +8,17 @@ namespace JdeScoping.Core.ViewModels; /// public class ComponentLotViewModel { + /// + /// Gets or sets the lot number. + /// public string LotNumber { get; set; } = string.Empty; + + /// + /// Gets or sets the item number. + /// public string ItemNumber { get; set; } = string.Empty; + /// public override bool Equals(object? obj) { if (obj is ComponentLotViewModel other) @@ -20,5 +28,6 @@ public class ComponentLotViewModel return false; } + /// public override int GetHashCode() => HashCode.Combine(LotNumber, ItemNumber); } diff --git a/NEW/src/JdeScoping.Core/ViewModels/OperatorViewModel.cs b/NEW/src/JdeScoping.Core/ViewModels/OperatorViewModel.cs index c949344..6f13cd0 100644 --- a/NEW/src/JdeScoping.Core/ViewModels/OperatorViewModel.cs +++ b/NEW/src/JdeScoping.Core/ViewModels/OperatorViewModel.cs @@ -5,10 +5,22 @@ namespace JdeScoping.Core.ViewModels; /// public class OperatorViewModel { + /// + /// Gets or sets the operator's address number. + /// public long AddressNumber { get; set; } + + /// + /// Gets or sets the operator's user ID. + /// public string UserId { get; set; } = string.Empty; + + /// + /// Gets or sets the operator's full name. + /// public string FullName { get; set; } = string.Empty; + /// public override bool Equals(object? obj) { if (obj is OperatorViewModel other) @@ -18,5 +30,6 @@ public class OperatorViewModel return false; } + /// public override int GetHashCode() => UserId.GetHashCode(); } diff --git a/NEW/src/JdeScoping.Core/ViewModels/ViewModelExtensions.cs b/NEW/src/JdeScoping.Core/ViewModels/ViewModelExtensions.cs index d258a1c..57f970c 100644 --- a/NEW/src/JdeScoping.Core/ViewModels/ViewModelExtensions.cs +++ b/NEW/src/JdeScoping.Core/ViewModels/ViewModelExtensions.cs @@ -13,6 +13,8 @@ public static class ViewModelExtensions /// /// Converts an Item to ItemViewModel /// + /// The item to convert. + /// An ItemViewModel containing the item data. public static ItemViewModel ToViewModel(this Item item) { return new ItemViewModel @@ -25,6 +27,8 @@ public static class ViewModelExtensions /// /// Converts a ProfitCenter to ProfitCenterViewModel /// + /// The profit center to convert. + /// A ProfitCenterViewModel containing the profit center data. public static ProfitCenterViewModel ToViewModel(this ProfitCenter profitCenter) { return new ProfitCenterViewModel @@ -37,6 +41,8 @@ public static class ViewModelExtensions /// /// Converts a WorkCenter to WorkCenterViewModel /// + /// The work center to convert. + /// A WorkCenterViewModel containing the work center data. public static WorkCenterViewModel ToViewModel(this WorkCenter workCenter) { return new WorkCenterViewModel @@ -49,6 +55,8 @@ public static class ViewModelExtensions /// /// Converts a JdeUser to JdeUserViewModel /// + /// The JDE user to convert. + /// A JdeUserViewModel containing the user data. public static JdeUserViewModel ToViewModel(this JdeUser user) { return new JdeUserViewModel @@ -62,6 +70,8 @@ public static class ViewModelExtensions /// /// Converts a WorkOrder to WorkOrderViewModel /// + /// The work order to convert. + /// A WorkOrderViewModel containing the work order data. public static WorkOrderViewModel ToViewModel(this WorkOrder workOrder) { return new WorkOrderViewModel @@ -74,6 +84,8 @@ public static class ViewModelExtensions /// /// Converts a Lot to LotViewModel /// + /// The lot to convert. + /// A LotViewModel containing the lot data. public static LotViewModel ToViewModel(this Lot lot) { return new LotViewModel diff --git a/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.cs b/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.cs index 03104b8..ff4e53e 100644 --- a/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.cs +++ b/NEW/src/JdeScoping.DataAccess/Repositories/LotFinderRepository.cs @@ -21,6 +21,9 @@ public partial class LotFinderRepository : ILotFinderRepository /// /// Initializes a new instance of the class. /// + /// The database connection factory. + /// The logger instance. + /// The data access options. public LotFinderRepository( IDbConnectionFactory connectionFactory, ILogger logger, diff --git a/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs b/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs index fc13639..9eee446 100644 --- a/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs +++ b/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs @@ -27,6 +27,12 @@ public sealed class SearchProcessor : ISearchProcessor /// /// Initializes a new instance of SearchProcessor. /// + /// The database connection factory. + /// The search query builder. + /// The work order traversal service. + /// The MIS query builder. + /// The search processing configuration options. + /// The logger instance. public SearchProcessor( IDbConnectionFactory connectionFactory, ISearchQueryBuilder queryBuilder, diff --git a/NEW/src/JdeScoping.DataSync.Dev/Configuration/DevPipelinesRoot.cs b/NEW/src/JdeScoping.DataSync.Dev/Configuration/DevPipelinesRoot.cs index aa7e8b0..2ce2d4b 100644 --- a/NEW/src/JdeScoping.DataSync.Dev/Configuration/DevPipelinesRoot.cs +++ b/NEW/src/JdeScoping.DataSync.Dev/Configuration/DevPipelinesRoot.cs @@ -7,6 +7,9 @@ public record DevPipelinesRoot( DevPipelineSettings? Settings, Dictionary Pipelines) { + /// + /// Gets the effective settings, using defaults if not specified. + /// public DevPipelineSettings EffectiveSettings => Settings ?? new DevPipelineSettings(); } diff --git a/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs b/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs index 0b34dde..e9cb41f 100644 --- a/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs +++ b/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs @@ -15,6 +15,12 @@ public class DevEtlRegistry private readonly string _cacheDirectory; private readonly ILogger? _logger; + /// + /// Initializes a new instance of the DevEtlRegistry. + /// + /// The factory for creating ETL pipelines. + /// The directory containing cached protobuf files. + /// Optional logger for recording ETL operations. public DevEtlRegistry( IDevEtlPipelineFactory pipelineFactory, string cacheDirectory, @@ -32,8 +38,16 @@ public class DevEtlRegistry _logger = logger; } + /// + /// Gets the list of available tables that can be loaded. + /// public IEnumerable GetAvailableTables() => _pipelineFactory.GetAvailableTables(); + /// + /// Runs the ETL pipeline for a single table. + /// + /// The name of the table to load. + /// The cancellation token. public async Task RunAsync(string tableName, CancellationToken cancellationToken = default) { _logger?.LogInformation("Running dev ETL for {TableName}", tableName); @@ -51,6 +65,10 @@ public class DevEtlRegistry return result; } + /// + /// Runs the ETL pipeline for all available tables sequentially. + /// + /// The cancellation token. public async Task> RunAllAsync(CancellationToken cancellationToken = default) { var results = new List(); diff --git a/NEW/src/JdeScoping.DataSync.Dev/Services/DevEtlPipelineFactory.cs b/NEW/src/JdeScoping.DataSync.Dev/Services/DevEtlPipelineFactory.cs index 7dcb357..dd69011 100644 --- a/NEW/src/JdeScoping.DataSync.Dev/Services/DevEtlPipelineFactory.cs +++ b/NEW/src/JdeScoping.DataSync.Dev/Services/DevEtlPipelineFactory.cs @@ -31,6 +31,9 @@ public class DevEtlPipelineFactory : IDevEtlPipelineFactory /// /// Creates a new development pipeline factory. /// + /// Factory for creating database connections. + /// Configuration options for the development pipeline. + /// Logger for pipeline operations. public DevEtlPipelineFactory( IDbConnectionFactory connectionFactory, IOptions options, @@ -49,6 +52,9 @@ public class DevEtlPipelineFactory : IDevEtlPipelineFactory /// /// Creates a new development pipeline factory with pre-loaded config (for testing). /// + /// Factory for creating database connections. + /// Pre-loaded pipeline configuration. + /// Logger for pipeline operations. internal DevEtlPipelineFactory( IDbConnectionFactory connectionFactory, DevPipelinesRoot config, diff --git a/NEW/src/JdeScoping.DataSync.Dev/Sources/ProtobufZstdFileSource.cs b/NEW/src/JdeScoping.DataSync.Dev/Sources/ProtobufZstdFileSource.cs index ff4fe3d..764c78a 100644 --- a/NEW/src/JdeScoping.DataSync.Dev/Sources/ProtobufZstdFileSource.cs +++ b/NEW/src/JdeScoping.DataSync.Dev/Sources/ProtobufZstdFileSource.cs @@ -26,8 +26,17 @@ public sealed class ProtobufZstdFileSource : IImportSource private BufferedStream? _bufferedStream; private IDataReader? _reader; + /// + /// The name of this source, including the file name. + /// public string SourceName => $"Protobuf:{Path.GetFileName(_filePath)}"; + /// + /// Initializes a new instance of the class. + /// + /// The path to the zstd-compressed protobuf file. + /// Thrown when filePath is null or empty. + /// Thrown when the file does not exist. public ProtobufZstdFileSource(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) @@ -39,6 +48,12 @@ public sealed class ProtobufZstdFileSource : IImportSource _filePath = filePath; } + /// + /// Reads and decompresses the protobuf file, returning an IDataReader for deserialization. + /// + /// The cancellation token. + /// An IDataReader for reading the decompressed protobuf data. + /// Thrown if ReadDataAsync has already been called. public async Task ReadDataAsync(CancellationToken cancellationToken = default) { if (_fileStream != null) @@ -85,6 +100,9 @@ public sealed class ProtobufZstdFileSource : IImportSource _fileStream = null; } + /// + /// Disposes of all unmanaged resources associated with this source. + /// public async ValueTask DisposeAsync() { if (_reader != null) diff --git a/NEW/src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs b/NEW/src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs index b783b82..9ed6270 100644 --- a/NEW/src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs +++ b/NEW/src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs @@ -5,7 +5,9 @@ public record PipelinesRoot( ScheduleDefaults? ScheduleDefaults, // Optional - defaults applied if missing Dictionary Pipelines) { + /// Gets the effective pipeline settings, using defaults if not specified. public PipelineSettings EffectiveSettings => Settings ?? new PipelineSettings(); + /// Gets the effective schedule defaults, using defaults if not specified. public ScheduleDefaults EffectiveScheduleDefaults => ScheduleDefaults ?? new ScheduleDefaults(); } diff --git a/NEW/src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs b/NEW/src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs index b78a121..4b4db65 100644 --- a/NEW/src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs +++ b/NEW/src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs @@ -33,6 +33,7 @@ public record ScheduleConfig /// /// Merges this config with defaults. Non-null/non-default values in this config override defaults. /// + /// The default configuration to merge with. public ScheduleConfig MergeWith(ScheduleConfig defaults) { return new ScheduleConfig @@ -92,7 +93,18 @@ public record ScheduleDefaults /// public record PipelineSchedules { + /// + /// Gets or initializes the Mass schedule configuration override. + /// public ScheduleConfig? Mass { get; init; } + + /// + /// Gets or initializes the Daily schedule configuration override. + /// public ScheduleConfig? Daily { get; init; } + + /// + /// Gets or initializes the Hourly schedule configuration override. + /// public ScheduleConfig? Hourly { get; init; } } diff --git a/NEW/src/JdeScoping.DataSync/Etl/EtlServiceCollectionExtensions.cs b/NEW/src/JdeScoping.DataSync/Etl/EtlServiceCollectionExtensions.cs index 5c6bbb0..4f196ca 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/EtlServiceCollectionExtensions.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/EtlServiceCollectionExtensions.cs @@ -11,6 +11,8 @@ public static class EtlServiceCollectionExtensions /// /// Adds ETL pipeline services to the service collection. /// + /// The service collection to add ETL services to. + /// The service collection for method chaining. public static IServiceCollection AddEtlPipeline(this IServiceCollection services) { // Register the builder as transient so each request gets a fresh builder diff --git a/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipeline.cs b/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipeline.cs index f9edfee..d78d6a1 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipeline.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipeline.cs @@ -15,8 +15,21 @@ public class EtlPipeline private readonly IReadOnlyList _postScripts; private readonly ILogger _logger; + /// + /// Gets the name of the pipeline. + /// public string PipelineName { get; } + /// + /// Initializes a new instance of the EtlPipeline. + /// + /// The name of the pipeline. + /// The data source for the pipeline. + /// The data transformers to apply. + /// The destination for the transformed data. + /// Scripts to run before the pipeline executes. + /// Scripts to run after the pipeline executes. + /// The logger for recording pipeline execution. internal EtlPipeline( string name, IImportSource source, @@ -35,6 +48,10 @@ public class EtlPipeline _logger = logger; } + /// + /// Executes the ETL pipeline, running all steps in sequence. + /// + /// The cancellation token. public async Task ExecuteAsync(CancellationToken cancellationToken = default) { var steps = new List(); diff --git a/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipelineBuilder.cs b/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipelineBuilder.cs index 0a17c9b..bfe7464 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipelineBuilder.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Pipeline/EtlPipelineBuilder.cs @@ -15,18 +15,33 @@ public class EtlPipelineBuilder private ILogger? _logger; private int _defaultCommandTimeoutSeconds = 600; + /// + /// Sets the pipeline name. + /// + /// The pipeline name. + /// The builder for method chaining. public EtlPipelineBuilder WithName(string name) { _name = name ?? throw new ArgumentNullException(nameof(name)); return this; } + /// + /// Sets the data source for the pipeline. + /// + /// The import source. + /// The builder for method chaining. public EtlPipelineBuilder WithSource(IImportSource source) { _source = source ?? throw new ArgumentNullException(nameof(source)); return this; } + /// + /// Adds a data transformer to the pipeline. + /// + /// The data transformer. + /// The builder for method chaining. public EtlPipelineBuilder WithTransformer(IDataTransformer transformer) { ArgumentNullException.ThrowIfNull(transformer); @@ -34,12 +49,22 @@ public class EtlPipelineBuilder return this; } + /// + /// Sets the destination for the transformed data. + /// + /// The import destination. + /// The builder for method chaining. public EtlPipelineBuilder WithDestination(IImportDestination destination) { _destination = destination ?? throw new ArgumentNullException(nameof(destination)); return this; } + /// + /// Adds a pre-execution script to run before the pipeline starts. + /// + /// The script runner. + /// The builder for method chaining. public EtlPipelineBuilder WithPreScript(IScriptRunner script) { ArgumentNullException.ThrowIfNull(script); @@ -47,6 +72,11 @@ public class EtlPipelineBuilder return this; } + /// + /// Adds a post-execution script to run after the pipeline completes. + /// + /// The script runner. + /// The builder for method chaining. public EtlPipelineBuilder WithPostScript(IScriptRunner script) { ArgumentNullException.ThrowIfNull(script); @@ -54,6 +84,11 @@ public class EtlPipelineBuilder return this; } + /// + /// Sets the logger for the pipeline. + /// + /// The logger instance. + /// The builder for method chaining. public EtlPipelineBuilder WithLogger(ILogger logger) { _logger = logger; @@ -63,6 +98,11 @@ public class EtlPipelineBuilder // TODO: Currently this timeout value is stored but not passed to destinations. // In the future, the pipeline should pass this timeout to destinations that support it. // For now, destinations use their own default timeout (600 seconds). + /// + /// Sets the command timeout for database operations. + /// + /// The timeout duration (must be between 0 and 24 hours). + /// The builder for method chaining. public EtlPipelineBuilder WithCommandTimeout(TimeSpan timeout) { if (timeout < TimeSpan.Zero || timeout > TimeSpan.FromHours(24)) @@ -72,6 +112,11 @@ public class EtlPipelineBuilder return this; } + /// + /// Builds and returns the configured ETL pipeline. + /// + /// The constructed ETL pipeline. + /// Thrown when source or destination is not configured. public EtlPipeline Build() { if (_source == null) diff --git a/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs b/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs index baade44..e2c5a75 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs @@ -9,6 +9,7 @@ public static class CommonScripts /// Parses a table name, extracting schema if present. /// Supports: "Table", "dbo.Table", "[dbo].[Table]" /// + /// The table name to parse. public static (string Schema, string Table) ParseTableName(string tableName) { var cleaned = tableName.Replace("[", "").Replace("]", ""); @@ -21,12 +22,19 @@ public static class CommonScripts /// /// Formats a table name as a properly quoted [schema].[table] identifier. /// + /// The table name to format. public static string FormatQualifiedTableName(string tableName) { var (schema, table) = ParseTableName(tableName); return $"[{schema}].[{table}]"; } + /// + /// Creates a script runner that disables all indexes on a table. + /// + /// The database connection factory. + /// The table name to disable indexes for. + /// The execution timeout in seconds (default 300). public static IScriptRunner DisableIndexes( IDbConnectionFactory factory, string tableName, @@ -54,6 +62,12 @@ IF LEN(@sql) > 0 EXEC sp_executesql @sql;"; timeoutSeconds: timeoutSeconds); } + /// + /// Creates a script runner that rebuilds all indexes on a table. + /// + /// The database connection factory. + /// The table name to rebuild indexes for. + /// The execution timeout in seconds (default 3600). public static IScriptRunner RebuildIndexes( IDbConnectionFactory factory, string tableName, @@ -70,6 +84,12 @@ EXEC sp_executesql @sql;"; timeoutSeconds: timeoutSeconds); } + /// + /// Creates a script runner that updates statistics on a table. + /// + /// The database connection factory. + /// The table name to update statistics for. + /// The execution timeout in seconds (default 600). public static IScriptRunner UpdateStatistics( IDbConnectionFactory factory, string tableName, @@ -86,6 +106,14 @@ EXEC sp_executesql @sql;"; timeoutSeconds: timeoutSeconds); } + /// + /// Creates a script runner for executing custom SQL. + /// + /// The database connection factory. + /// The SQL statement to execute. + /// The name/description of the script. + /// Optional parameters for the SQL statement. + /// The execution timeout in seconds (default 30). public static IScriptRunner CustomSql( IDbConnectionFactory factory, string sql, diff --git a/NEW/src/JdeScoping.DataSync/Etl/Scripts/SqlScriptRunner.cs b/NEW/src/JdeScoping.DataSync/Etl/Scripts/SqlScriptRunner.cs index 8f033b2..0949974 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Scripts/SqlScriptRunner.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Scripts/SqlScriptRunner.cs @@ -4,6 +4,9 @@ using JdeScoping.DataSync.Etl.Contracts; namespace JdeScoping.DataSync.Etl.Scripts; +/// +/// SQL script runner that executes SQL commands against the database. +/// public class SqlScriptRunner : IScriptRunner { private readonly IDbConnectionFactory _connectionFactory; @@ -11,8 +14,19 @@ public class SqlScriptRunner : IScriptRunner private readonly object? _parameters; private readonly int _timeoutSeconds; + /// + /// The name of this script. + /// public string ScriptName { get; } + /// + /// Initializes a new instance of the class. + /// + /// The database connection factory. + /// The SQL command to execute. + /// The optional name of the script. + /// The optional parameters for the SQL command. + /// The command timeout in seconds. public SqlScriptRunner( IDbConnectionFactory connectionFactory, string sql, @@ -30,6 +44,10 @@ public class SqlScriptRunner : IScriptRunner ScriptName = name ?? "SqlScript"; } + /// + /// Executes the SQL script asynchronously. + /// + /// The cancellation token. public async Task ExecuteAsync(CancellationToken cancellationToken = default) { await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken); diff --git a/NEW/src/JdeScoping.DataSync/Etl/Transformers/DataTransformerBase.cs b/NEW/src/JdeScoping.DataSync/Etl/Transformers/DataTransformerBase.cs index a105846..32ebb6f 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Transformers/DataTransformerBase.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Transformers/DataTransformerBase.cs @@ -31,36 +31,53 @@ public abstract class DataTransformerBase : IDataTransformer /// Gets the field count from the source reader. /// Override to add or remove fields. /// + /// The source data reader. + /// The field count. public virtual int GetFieldCount(IDataReader source) => source.FieldCount; /// /// Gets the name of a field at the specified ordinal. /// Override to rename fields. /// + /// The field ordinal. + /// The source data reader. + /// The field name. public virtual string GetName(int ordinal, IDataReader source) => source.GetName(ordinal); /// /// Gets the type of a field at the specified ordinal. /// Override to change field types. /// + /// The field ordinal. + /// The source data reader. + /// The field type. public virtual Type GetFieldType(int ordinal, IDataReader source) => source.GetFieldType(ordinal); /// /// Gets the value of a field at the specified ordinal. /// Override to transform values. /// + /// The field ordinal. + /// The source data reader. + /// The field value. public virtual object GetValue(int ordinal, IDataReader source) => source.GetValue(ordinal); /// /// Gets the ordinal of a field by name. /// Override to support renamed fields. /// + /// The field name. + /// The source data reader. + /// The field ordinal. public virtual int GetOrdinal(string name, IDataReader source) => source.GetOrdinal(name); /// /// Checks if a field value is DBNull. /// Override to handle null transformations. /// + /// The field ordinal. + /// The source data reader. + /// True if the field value is DBNull; otherwise, false. public virtual bool IsDBNull(int ordinal, IDataReader source) => source.IsDBNull(ordinal); /// @@ -76,6 +93,13 @@ public abstract class DataTransformerBase : IDataTransformer /// Gets bytes from a field at the specified ordinal. /// Throws NotSupportedException for computed columns (where MapOrdinal returns -1). /// + /// The field ordinal. + /// The offset in the field data. + /// The buffer to copy bytes into. + /// The offset in the buffer where copying begins. + /// The maximum number of bytes to copy. + /// The source data reader. + /// The number of bytes actually copied. public virtual long GetBytes(int ordinal, long fieldOffset, byte[]? buffer, int bufferOffset, int length, IDataReader source) { @@ -90,6 +114,13 @@ public abstract class DataTransformerBase : IDataTransformer /// Gets characters from a field at the specified ordinal. /// Throws NotSupportedException for computed columns (where MapOrdinal returns -1). /// + /// The field ordinal. + /// The offset in the field data. + /// The buffer to copy characters into. + /// The offset in the buffer where copying begins. + /// The maximum number of characters to copy. + /// The source data reader. + /// The number of characters actually copied. public virtual long GetChars(int ordinal, long fieldOffset, char[]? buffer, int bufferOffset, int length, IDataReader source) { @@ -104,6 +135,9 @@ public abstract class DataTransformerBase : IDataTransformer /// Gets nested data reader for a field at the specified ordinal. /// Throws NotSupportedException for computed columns (where MapOrdinal returns -1). /// + /// The field ordinal. + /// The source data reader. + /// A nested data reader for the field. public virtual IDataReader GetData(int ordinal, IDataReader source) { var sourceOrdinal = MapOrdinal(ordinal, source); @@ -117,6 +151,9 @@ public abstract class DataTransformerBase : IDataTransformer /// Gets the data type name for a field at the specified ordinal. /// Throws NotSupportedException for computed columns (where MapOrdinal returns -1). /// + /// The field ordinal. + /// The source data reader. + /// The data type name. public virtual string GetDataTypeName(int ordinal, IDataReader source) { var sourceOrdinal = MapOrdinal(ordinal, source); diff --git a/NEW/src/JdeScoping.DataSync/Etl/Transformers/TransformingDataReader.cs b/NEW/src/JdeScoping.DataSync/Etl/Transformers/TransformingDataReader.cs index c400bc9..2ca55dc 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Transformers/TransformingDataReader.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Transformers/TransformingDataReader.cs @@ -11,6 +11,11 @@ internal sealed class TransformingDataReader : IDataReader private readonly IDataReader _source; private readonly DataTransformerBase _transformer; + /// + /// Initializes a new instance of the class. + /// + /// The underlying data reader to wrap. + /// The transformer to apply to field values. public TransformingDataReader(IDataReader source, DataTransformerBase transformer) { _source = source ?? throw new ArgumentNullException(nameof(source)); @@ -18,41 +23,196 @@ internal sealed class TransformingDataReader : IDataReader } // Properties and methods delegated to transformer + /// + /// Gets the number of columns in the current row. + /// public int FieldCount => _transformer.GetFieldCount(_source); + + /// + /// Gets the name of the specified column. + /// + /// The zero-based column ordinal. + /// The name of the specified column. public string GetName(int i) => _transformer.GetName(i, _source); + + /// + /// Gets the data type of the specified column. + /// + /// The zero-based column ordinal. + /// The data type of the specified column. public Type GetFieldType(int i) => _transformer.GetFieldType(i, _source); + + /// + /// Gets the column ordinal given the name of the column. + /// + /// The name of the column. + /// The zero-based column ordinal. public int GetOrdinal(string name) => _transformer.GetOrdinal(name, _source); + + /// + /// Gets the value of the specified column as an object, applying transformations. + /// + /// The zero-based column ordinal. + /// The transformed value of the specified column. public object GetValue(int i) => _transformer.GetValue(i, _source); + + /// + /// Gets whether the value of the specified column is null. + /// + /// The zero-based column ordinal. + /// True if the value is null; otherwise, false. public bool IsDBNull(int i) => _transformer.IsDBNull(i, _source); + + /// + /// Gets the value of the column at the specified ordinal, applying transformations. + /// + /// The zero-based column ordinal. + /// The transformed value of the column. public object this[int i] => GetValue(i); + + /// + /// Gets the value of the column with the specified name, applying transformations. + /// + /// The name of the column. + /// The transformed value of the column. public object this[string name] => GetValue(GetOrdinal(name)); // Row navigation - delegated directly to source + /// + /// Advances the reader to the next record. + /// + /// True if there are more rows; otherwise, false. public bool Read() => _source.Read(); + + /// + /// Advances the reader to the next result set. + /// + /// True if there are more result sets; otherwise, false. public bool NextResult() => _source.NextResult(); + + /// + /// Gets the nesting depth of the current row. + /// public int Depth => _source.Depth; + + /// + /// Gets whether the reader is closed. + /// public bool IsClosed => _source.IsClosed; + + /// + /// Gets the number of rows affected by the last operation. + /// public int RecordsAffected => _source.RecordsAffected; + + /// + /// Closes the reader. + /// public void Close() => _source.Close(); + + /// + /// Releases all resources used by the reader. + /// public void Dispose() => _source.Dispose(); // Typed accessors - use GetValue for transformation support + /// + /// Gets the value of the specified column as a boolean. + /// + /// The zero-based column ordinal. + /// The boolean value of the column. public bool GetBoolean(int i) => (bool)GetValue(i); + + /// + /// Gets the value of the specified column as a byte. + /// + /// The zero-based column ordinal. + /// The byte value of the column. public byte GetByte(int i) => (byte)GetValue(i); + + /// + /// Gets the value of the specified column as a character. + /// + /// The zero-based column ordinal. + /// The character value of the column. public char GetChar(int i) => (char)GetValue(i); + + /// + /// Gets the value of the specified column as a DateTime. + /// + /// The zero-based column ordinal. + /// The DateTime value of the column. public DateTime GetDateTime(int i) => (DateTime)GetValue(i); + + /// + /// Gets the value of the specified column as a decimal. + /// + /// The zero-based column ordinal. + /// The decimal value of the column. public decimal GetDecimal(int i) => (decimal)GetValue(i); + + /// + /// Gets the value of the specified column as a double. + /// + /// The zero-based column ordinal. + /// The double value of the column. public double GetDouble(int i) => (double)GetValue(i); + + /// + /// Gets the value of the specified column as a float. + /// + /// The zero-based column ordinal. + /// The float value of the column. public float GetFloat(int i) => (float)GetValue(i); + + /// + /// Gets the value of the specified column as a GUID. + /// + /// The zero-based column ordinal. + /// The GUID value of the column. public Guid GetGuid(int i) => (Guid)GetValue(i); + + /// + /// Gets the value of the specified column as a 16-bit signed integer. + /// + /// The zero-based column ordinal. + /// The 16-bit signed integer value of the column. public short GetInt16(int i) => (short)GetValue(i); + + /// + /// Gets the value of the specified column as a 32-bit signed integer. + /// + /// The zero-based column ordinal. + /// The 32-bit signed integer value of the column. public int GetInt32(int i) => (int)GetValue(i); + + /// + /// Gets the value of the specified column as a 64-bit signed integer. + /// + /// The zero-based column ordinal. + /// The 64-bit signed integer value of the column. public long GetInt64(int i) => (long)GetValue(i); + + /// + /// Gets the value of the specified column as a string. + /// + /// The zero-based column ordinal. + /// The string value of the column. public string GetString(int i) => (string)GetValue(i); // Schema and bulk data access - delegated to transformer + /// + /// Gets the name of the type for the specified column. + /// + /// The zero-based column ordinal. + /// The name of the data type of the specified column. public string GetDataTypeName(int i) => _transformer.GetDataTypeName(i, _source); + /// + /// Populates an object array with the values of the current row. + /// + /// An array of objects to populate with column values. + /// The number of values populated. public int GetValues(object[] values) { var count = Math.Min(values.Length, FieldCount); @@ -61,13 +221,40 @@ internal sealed class TransformingDataReader : IDataReader return count; } + /// + /// Gets the byte values of the specified column starting at the given offset. + /// + /// The zero-based column ordinal. + /// The offset within the field. + /// The buffer to read bytes into. + /// The offset in the buffer to start writing. + /// The maximum number of bytes to read. + /// The number of bytes read. public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => _transformer.GetBytes(i, fieldOffset, buffer, bufferoffset, length, _source); + /// + /// Gets the character values of the specified column starting at the given offset. + /// + /// The zero-based column ordinal. + /// The offset within the field. + /// The buffer to read characters into. + /// The offset in the buffer to start writing. + /// The maximum number of characters to read. + /// The number of characters read. public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => _transformer.GetChars(i, fieldoffset, buffer, bufferoffset, length, _source); + /// + /// Gets a nested data reader for the specified column. + /// + /// The zero-based column ordinal. + /// A nested data reader for the specified column. public IDataReader GetData(int i) => _transformer.GetData(i, _source); + /// + /// Gets a DataTable that describes the schema of the result set. + /// + /// A DataTable describing the result set schema, or null if no schema is available. public DataTable? GetSchemaTable() => _source.GetSchemaTable(); } diff --git a/NEW/src/JdeScoping.DataSync/HealthChecks/DataSyncHealthCheck.cs b/NEW/src/JdeScoping.DataSync/HealthChecks/DataSyncHealthCheck.cs index 81179a5..02f132b 100644 --- a/NEW/src/JdeScoping.DataSync/HealthChecks/DataSyncHealthCheck.cs +++ b/NEW/src/JdeScoping.DataSync/HealthChecks/DataSyncHealthCheck.cs @@ -13,6 +13,7 @@ public class DataSyncHealthCheck : IHealthCheck /// /// Initializes a new instance of the class. /// + /// The data update repository. public DataSyncHealthCheck(IDataUpdateRepository repository) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); diff --git a/NEW/src/JdeScoping.DataSync/Options/PipelineOptions.cs b/NEW/src/JdeScoping.DataSync/Options/PipelineOptions.cs index 7f7f2ee..572f3e9 100644 --- a/NEW/src/JdeScoping.DataSync/Options/PipelineOptions.cs +++ b/NEW/src/JdeScoping.DataSync/Options/PipelineOptions.cs @@ -3,5 +3,9 @@ namespace JdeScoping.DataSync.Options; public class PipelineOptions { public const string SectionName = "Pipelines"; + + /// + /// Gets or sets the path to the pipeline configuration file. + /// public string ConfigPath { get; set; } = "Pipelines/pipelines.json"; } diff --git a/NEW/src/JdeScoping.DataSync/Services/DataUpdateRepository.cs b/NEW/src/JdeScoping.DataSync/Services/DataUpdateRepository.cs index f0aa012..0dea1bc 100644 --- a/NEW/src/JdeScoping.DataSync/Services/DataUpdateRepository.cs +++ b/NEW/src/JdeScoping.DataSync/Services/DataUpdateRepository.cs @@ -22,6 +22,8 @@ public class DataUpdateRepository : IDataUpdateRepository /// /// Initializes a new instance of the class. /// + /// The database connection factory. + /// The logger instance. public DataUpdateRepository( IDbConnectionFactory connectionFactory, ILogger logger) diff --git a/NEW/src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs b/NEW/src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs index 5efa4db..a59a0f0 100644 --- a/NEW/src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs +++ b/NEW/src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs @@ -53,6 +53,9 @@ public class EtlPipelineFactory : IEtlPipelineFactory /// /// Creates a new pipeline factory with a pre-loaded configuration (for testing). /// + /// Factory for creating database connections. + /// Pre-loaded pipeline configuration. + /// Logger for pipeline execution. internal EtlPipelineFactory( IDbConnectionFactory connectionFactory, PipelinesRoot config, @@ -167,6 +170,15 @@ public class EtlPipelineFactory : IEtlPipelineFactory private UpdateTypes _updateType = UpdateTypes.Hourly; private DateTime? _minDtOverride; + /// + /// Initializes a new instance of the PipelineBuilder class. + /// + /// Factory for creating database connections. + /// The name of the table for this pipeline. + /// The pipeline configuration. + /// Global pipeline settings. + /// Default schedule configuration. + /// Logger for pipeline execution. public PipelineBuilder( IDbConnectionFactory connectionFactory, string tableName, @@ -183,18 +195,32 @@ public class EtlPipelineFactory : IEtlPipelineFactory _logger = logger; } + /// + /// Specifies the update type for this pipeline. + /// + /// The type of update (Mass, Daily, or Hourly). + /// The builder for fluent configuration. public IEtlPipelineBuilder WithUpdateType(UpdateTypes updateType) { _updateType = updateType; return this; } + /// + /// Specifies the minimum date for incremental data extraction. + /// + /// The minimum date, or null for no filter. + /// The builder for fluent configuration. public IEtlPipelineBuilder WithMinimumDate(DateTime? minDt) { _minDtOverride = minDt; return this; } + /// + /// Builds and returns the configured ETL pipeline. + /// + /// A configured ETL pipeline ready for execution. public EtlPipeline Build() { return BuildWithSchedules(); diff --git a/NEW/src/JdeScoping.DataSync/Services/ScheduleChecker.cs b/NEW/src/JdeScoping.DataSync/Services/ScheduleChecker.cs index e590a5b..04d80e5 100644 --- a/NEW/src/JdeScoping.DataSync/Services/ScheduleChecker.cs +++ b/NEW/src/JdeScoping.DataSync/Services/ScheduleChecker.cs @@ -21,6 +21,9 @@ public class ScheduleChecker : IScheduleChecker /// /// Initializes a new instance of the class. /// + /// Repository for data update records. + /// Data sync configuration options. + /// Logger instance. public ScheduleChecker( IDataUpdateRepository repository, IOptions options, diff --git a/NEW/src/JdeScoping.DataSync/Services/SearchExecutionService.cs b/NEW/src/JdeScoping.DataSync/Services/SearchExecutionService.cs index 146bca2..15fb809 100644 --- a/NEW/src/JdeScoping.DataSync/Services/SearchExecutionService.cs +++ b/NEW/src/JdeScoping.DataSync/Services/SearchExecutionService.cs @@ -21,6 +21,15 @@ public class SearchExecutionService : ISearchExecutionService private readonly WorkProcessorOptions _options; private readonly ILogger _logger; + /// + /// Initializes a new instance of the SearchExecutionService. + /// + /// The repository for search data access. + /// The processor for executing search queries. + /// The service for generating Excel export files. + /// The service for sending search status notifications. + /// The work processor configuration options. + /// The logger for recording search execution events. public SearchExecutionService( ISearchRepository searchRepository, ISearchProcessor searchProcessor, diff --git a/NEW/src/JdeScoping.DataSync/Services/SyncOrchestrator.cs b/NEW/src/JdeScoping.DataSync/Services/SyncOrchestrator.cs index 3ead9ee..8793aab 100644 --- a/NEW/src/JdeScoping.DataSync/Services/SyncOrchestrator.cs +++ b/NEW/src/JdeScoping.DataSync/Services/SyncOrchestrator.cs @@ -21,6 +21,11 @@ public class SyncOrchestrator : ISyncOrchestrator /// /// Initializes a new instance of the class. /// + /// Factory for creating service scopes. + /// Checker for determining pending sync tasks. + /// Data sync configuration options. + /// Logger instance. + /// Metrics collector for sync operations. public SyncOrchestrator( IServiceScopeFactory scopeFactory, IScheduleChecker scheduleChecker, diff --git a/NEW/src/JdeScoping.DataSync/Services/TableSyncOperation.cs b/NEW/src/JdeScoping.DataSync/Services/TableSyncOperation.cs index ec566b1..3b16576 100644 --- a/NEW/src/JdeScoping.DataSync/Services/TableSyncOperation.cs +++ b/NEW/src/JdeScoping.DataSync/Services/TableSyncOperation.cs @@ -24,6 +24,11 @@ public class TableSyncOperation : ITableSyncOperation /// /// Initializes a new instance of the class. /// + /// Factory for creating ETL pipelines. + /// Repository for managing data update records. + /// Data sync configuration options. + /// Logger for operation events. + /// Metrics collector for operation tracking. public TableSyncOperation( IEtlPipelineFactory pipelineFactory, IDataUpdateRepository updateRepository, diff --git a/NEW/src/JdeScoping.DataSync/Telemetry/DataSyncMetrics.cs b/NEW/src/JdeScoping.DataSync/Telemetry/DataSyncMetrics.cs index f13d2ca..1ac7961 100644 --- a/NEW/src/JdeScoping.DataSync/Telemetry/DataSyncMetrics.cs +++ b/NEW/src/JdeScoping.DataSync/Telemetry/DataSyncMetrics.cs @@ -18,6 +18,7 @@ public class DataSyncMetrics /// /// Initializes a new instance of the class. /// + /// The meter factory for creating metrics. public DataSyncMetrics(IMeterFactory meterFactory) { var meter = meterFactory.Create("JdeScoping.DataSync"); @@ -61,6 +62,8 @@ public class DataSyncMetrics /// /// Records that a sync operation has started. /// + /// The name of the table being synced. + /// The type of update (e.g., Mass, Daily, Hourly). public void RecordOperationStarted(string tableName, string updateType) { _operationsStarted.Add(1, @@ -71,6 +74,10 @@ public class DataSyncMetrics /// /// Records that a sync operation completed successfully. /// + /// The name of the table being synced. + /// The type of update (e.g., Mass, Daily, Hourly). + /// The number of records processed. + /// The operation duration in seconds. public void RecordOperationCompleted(string tableName, string updateType, long recordCount, double durationSeconds) { var tags = new KeyValuePair[] @@ -87,6 +94,8 @@ public class DataSyncMetrics /// /// Records that a sync operation failed. /// + /// The name of the table being synced. + /// The type of update (e.g., Mass, Daily, Hourly). public void RecordOperationFailed(string tableName, string updateType) { _operationsFailed.Add(1, @@ -105,6 +114,9 @@ public class DataSyncMetrics /// /// Records completion of a sync cycle. /// + /// Number of successful operations in the cycle. + /// Number of failed operations in the cycle. + /// The cycle duration in seconds. public void RecordCycleCompleted(int successCount, int failedCount, double durationSeconds) { _cyclesCompleted.Add(1, diff --git a/NEW/src/JdeScoping.DataSync/WorkProcessor.cs b/NEW/src/JdeScoping.DataSync/WorkProcessor.cs index 1ec43d8..fa06e0f 100644 --- a/NEW/src/JdeScoping.DataSync/WorkProcessor.cs +++ b/NEW/src/JdeScoping.DataSync/WorkProcessor.cs @@ -26,6 +26,10 @@ public class WorkProcessor : BackgroundService /// /// Initializes a new instance of the class. /// + /// Factory for creating service scopes. + /// Configuration options for work processing. + /// Logger for recording work processor events. + /// Metrics collector for tracking work cycles. public WorkProcessor( IServiceScopeFactory scopeFactory, IOptions options, diff --git a/NEW/src/JdeScoping.ExcelIO/Generators/FluentTableWriter.cs b/NEW/src/JdeScoping.ExcelIO/Generators/FluentTableWriter.cs index e1a7c42..1ef8c3c 100644 --- a/NEW/src/JdeScoping.ExcelIO/Generators/FluentTableWriter.cs +++ b/NEW/src/JdeScoping.ExcelIO/Generators/FluentTableWriter.cs @@ -12,6 +12,10 @@ public sealed class FluentTableWriter { private readonly ExcelMapRegistry _registry; + /// + /// Initializes a new instance of the class. + /// + /// The registry containing Excel column maps for types. public FluentTableWriter(ExcelMapRegistry registry) { _registry = registry; @@ -20,6 +24,15 @@ public sealed class FluentTableWriter /// /// Writes a table to the worksheet using the registered map for type T. /// + /// The type of objects to write to the table. + /// The Excel worksheet to write to. + /// The starting row number (1-based). + /// The starting column number (1-based). + /// The data to write to the table. + /// Optional override for the table name; uses map name if null. + /// Whether to show a merged header above the table. + /// Optional text to display in the merged header. + /// The created Excel table, or null if no columns are configured. public IXLTable? WriteTable( IXLWorksheet worksheet, int startRow, diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/ColumnBuilder.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/ColumnBuilder.cs index d82fded..1c29d55 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/ColumnBuilder.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/ColumnBuilder.cs @@ -9,12 +9,17 @@ public sealed class ColumnBuilder { private readonly ColumnDefinition _definition; + /// + /// Initializes a new instance of the class. + /// + /// The column definition to configure. internal ColumnBuilder(ColumnDefinition definition) { _definition = definition; } /// Sets the column display order. + /// The display order position. public ColumnBuilder Order(int order) { _definition.Order = order; @@ -22,6 +27,7 @@ public sealed class ColumnBuilder } /// Sets the column header text. + /// The header text to display. public ColumnBuilder Header(string text) { _definition.HeaderText = text; @@ -29,6 +35,7 @@ public sealed class ColumnBuilder } /// Sets the Excel number format. + /// The Excel number format string. public ColumnBuilder Format(string format) { _definition.Format = format; @@ -36,6 +43,7 @@ public sealed class ColumnBuilder } /// Sets a fixed column width (disables auto-width). + /// The fixed column width. public ColumnBuilder Width(double width) { _definition.AutoWidth = false; @@ -44,6 +52,7 @@ public sealed class ColumnBuilder } /// Enables text wrapping for the column. + /// Whether to enable text wrapping. public ColumnBuilder WrapText(bool wrap = true) { _definition.WrapText = wrap; diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelClassMap.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelClassMap.cs index 7f607ef..0ddc508 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelClassMap.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelClassMap.cs @@ -44,6 +44,8 @@ public abstract class ExcelClassMap : IExcelClassMap /// /// Configures the table and tab names for this model. /// + /// The Excel table name (for named ranges). + /// The worksheet tab name. protected void Table(string tableName, string tabName) { TableName = tableName; @@ -53,6 +55,9 @@ public abstract class ExcelClassMap : IExcelClassMap /// /// Maps a property to an Excel column. /// + /// The type of the property being mapped. + /// Expression specifying the property to map. + /// A column builder for further configuration of the mapped property. protected ColumnBuilder Map(Expression> property) { var memberExpr = property.Body as MemberExpression diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelMapRegistry.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelMapRegistry.cs index c5e96b8..c204ea0 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelMapRegistry.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/ExcelMapRegistry.cs @@ -10,6 +10,8 @@ public sealed class ExcelMapRegistry /// /// Registers a map for a type. /// + /// The type to register a map for. + /// The Excel class map. public void Register(ExcelClassMap map) { _maps[typeof(T)] = map; @@ -18,11 +20,15 @@ public sealed class ExcelMapRegistry /// /// Gets the map for a type. /// + /// The type to get the map for. + /// The registered Excel class map. public IExcelClassMap GetMap() => GetMap(typeof(T)); /// /// Gets the map for a type. /// + /// The type to get the map for. + /// The registered Excel class map. public IExcelClassMap GetMap(Type type) { if (_maps.TryGetValue(type, out var map)) @@ -36,10 +42,14 @@ public sealed class ExcelMapRegistry /// /// Checks if a map exists for a type. /// + /// The type to check for. + /// True if a map exists for the type; otherwise, false. public bool HasMap() => HasMap(typeof(T)); /// /// Checks if a map exists for a type. /// + /// The type to check for. + /// True if a map exists for the type; otherwise, false. public bool HasMap(Type type) => _maps.ContainsKey(type); } diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/FilterEntryMaps.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/FilterEntryMaps.cs index cc5db1a..10d1804 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/FilterEntryMaps.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/FilterEntryMaps.cs @@ -8,6 +8,9 @@ namespace JdeScoping.ExcelIO.Mapping.Maps; /// public sealed class TimespanFilterMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public TimespanFilterMap() { Table("Timespan_Filter", "Timespan Filter"); @@ -22,6 +25,9 @@ public sealed class TimespanFilterMap : ExcelClassMap /// public sealed class WorkOrderFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public WorkOrderFilterEntryMap() { Table("Work_Order_Filter", "Work Order Filter"); @@ -36,6 +42,9 @@ public sealed class WorkOrderFilterEntryMap : ExcelClassMap public sealed class ItemNumberFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public ItemNumberFilterEntryMap() { Table("Item_Number_Filter", "Item Number Filter"); @@ -50,6 +59,9 @@ public sealed class ItemNumberFilterEntryMap : ExcelClassMap public sealed class ProfitCenterFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public ProfitCenterFilterEntryMap() { Table("Profit_Center_Filter", "Profit Center Filter"); @@ -64,6 +76,9 @@ public sealed class ProfitCenterFilterEntryMap : ExcelClassMap public sealed class WorkCenterFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public WorkCenterFilterEntryMap() { Table("Work_Center_Filter", "Work Center Filter"); @@ -78,6 +93,9 @@ public sealed class WorkCenterFilterEntryMap : ExcelClassMap public sealed class OperatorFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public OperatorFilterEntryMap() { Table("Operator_Filter", "Operator Filter"); @@ -93,6 +111,9 @@ public sealed class OperatorFilterEntryMap : ExcelClassMap /// public sealed class ComponentLotFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public ComponentLotFilterEntryMap() { Table("Component_Lot_Filter", "Component Lot Filter"); @@ -107,6 +128,9 @@ public sealed class ComponentLotFilterEntryMap : ExcelClassMap public sealed class ItemOperationMisFilterEntryMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public ItemOperationMisFilterEntryMap() { Table("Item_Operation_MIS_Filter", "Item/Operation/MIS Filter"); diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisNonMatchSearchResultMap.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisNonMatchSearchResultMap.cs index 66ef306..6da5361 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisNonMatchSearchResultMap.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisNonMatchSearchResultMap.cs @@ -8,6 +8,10 @@ namespace JdeScoping.ExcelIO.Mapping.Maps; /// public sealed class MisNonMatchSearchResultMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// Defines the column mappings and headers for exporting MisNonMatchSearchResult to Excel. + /// public MisNonMatchSearchResultMap() { Table("Investigation", "Investigation"); diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisSearchResultMap.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisSearchResultMap.cs index 857d7fe..48b6b40 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisSearchResultMap.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/MisSearchResultMap.cs @@ -8,6 +8,9 @@ namespace JdeScoping.ExcelIO.Mapping.Maps; /// public sealed class MisSearchResultMap : ExcelClassMap { + /// + /// Initializes a new instance of the class. + /// public MisSearchResultMap() { Table("MIS_Info", "MIS Info"); diff --git a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/SearchResultMap.cs b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/SearchResultMap.cs index 12ba329..20e637d 100644 --- a/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/SearchResultMap.cs +++ b/NEW/src/JdeScoping.ExcelIO/Mapping/Maps/SearchResultMap.cs @@ -8,6 +8,9 @@ namespace JdeScoping.ExcelIO.Mapping.Maps; /// public sealed class SearchResultMap : ExcelClassMap { + /// + /// Initializes a new instance of the SearchResultMap class. + /// public SearchResultMap() { Table("Search_Results", "Search Results"); diff --git a/NEW/src/JdeScoping.ExcelIO/Parsing/ExcelParserService.cs b/NEW/src/JdeScoping.ExcelIO/Parsing/ExcelParserService.cs index 1ea29d1..d2e4525 100644 --- a/NEW/src/JdeScoping.ExcelIO/Parsing/ExcelParserService.cs +++ b/NEW/src/JdeScoping.ExcelIO/Parsing/ExcelParserService.cs @@ -12,6 +12,10 @@ public class ExcelParserService : IExcelParserService { private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. public ExcelParserService(ILogger logger) { _logger = logger; diff --git a/NEW/src/JdeScoping.Infrastructure/Auth/LdapAuthService.cs b/NEW/src/JdeScoping.Infrastructure/Auth/LdapAuthService.cs index b4b32f8..057e073 100644 --- a/NEW/src/JdeScoping.Infrastructure/Auth/LdapAuthService.cs +++ b/NEW/src/JdeScoping.Infrastructure/Auth/LdapAuthService.cs @@ -38,6 +38,11 @@ public sealed class LdapAuthService : IAuthenticationService private readonly LdapOptions _options; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// + /// The LDAP configuration options. + /// Logger for recording LDAP authentication events. public LdapAuthService( IOptions options, ILogger logger) diff --git a/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs b/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs index f68283b..8acd9dd 100644 --- a/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs +++ b/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs @@ -56,6 +56,9 @@ public class RsaKeyService : IRsaKeyService, IDisposable public byte[] Decrypt(byte[] ciphertext) => _rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256); + /// + /// Releases the RSA key resources. + /// public void Dispose() { _rsa.Dispose(); diff --git a/NEW/src/JdeScoping.Infrastructure/Security/SecretsMigrator.cs b/NEW/src/JdeScoping.Infrastructure/Security/SecretsMigrator.cs index 5c801aa..5807241 100644 --- a/NEW/src/JdeScoping.Infrastructure/Security/SecretsMigrator.cs +++ b/NEW/src/JdeScoping.Infrastructure/Security/SecretsMigrator.cs @@ -22,6 +22,13 @@ public class SecretsMigrator public const string ExcelCriteriaPasswordKey = "ExcelExport:CriteriaSheetPassword"; public const string ExcelDataPasswordKey = "ExcelExport:DataSheetPassword"; + /// + /// Initializes a new instance of the class. + /// + /// Service for secure secret storage. + /// Application configuration containing existing secrets. + /// Options controlling secret migration behavior. + /// Logger for recording migration operations. public SecretsMigrator( ISecureStoreService secureStore, IConfiguration configuration, diff --git a/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreRsaKeyService.cs b/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreRsaKeyService.cs index f7de386..c80e195 100644 --- a/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreRsaKeyService.cs +++ b/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreRsaKeyService.cs @@ -22,6 +22,8 @@ public class SecureStoreRsaKeyService : IRsaKeyService, IDisposable /// /// Creates a new SecureStore-backed RSA key service. /// + /// Service for storing keys securely. + /// Logger for key service operations. public SecureStoreRsaKeyService( ISecureStoreService secureStore, ILogger logger) @@ -61,6 +63,9 @@ public class SecureStoreRsaKeyService : IRsaKeyService, IDisposable return _rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256); } + /// + /// Releases resources used by the RSA key service. + /// public void Dispose() { if (_disposed) return; diff --git a/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs b/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs index bb37c83..595cde1 100644 --- a/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs +++ b/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs @@ -22,6 +22,8 @@ public class SecureStoreService : ISecureStoreService /// /// Creates a new SecureStoreService instance. /// + /// The SecureStore configuration options. + /// The logger instance. public SecureStoreService(IOptions options, ILogger logger) { _logger = logger; @@ -160,6 +162,9 @@ public class SecureStoreService : ISecureStoreService /// public IEnumerable Keys => _keys.ToList().AsReadOnly(); + /// + /// Disposes the secure store and saves any pending changes. + /// public void Dispose() { if (_disposed) return; diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs index 7a19289..076458e 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs @@ -7,69 +7,226 @@ namespace JdeScoping.ConfigManager.Models; /// public class ConfigModel { + /// + /// Gets or sets the data synchronization configuration. + /// public DataSyncSection DataSync { get; set; } = new(); + + /// + /// Gets or sets the data access configuration. + /// public DataAccessSection DataAccess { get; set; } = new(); + + /// + /// Gets or sets the authentication configuration. + /// public AuthSection Auth { get; set; } = new(); + + /// + /// Gets or sets the LDAP directory configuration. + /// public LdapSection Ldap { get; set; } = new(); + + /// + /// Gets or sets the search processing configuration. + /// public SearchSection Search { get; set; } = new(); + + /// + /// Gets or sets the Excel export configuration. + /// public ExcelExportSection ExcelExport { get; set; } = new(); + + /// + /// Gets or sets the connection strings for external data sources. + /// public Dictionary ConnectionStrings { get; set; } = new(); } public class DataSyncSection { + /// + /// Gets or sets the interval between successive data sync checks. + /// public TimeSpan CheckInterval { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the maximum degree of parallelism for sync operations. + /// public int MaxDegreeOfParallelism { get; set; } = 4; + + /// + /// Gets or sets the batch size for data sync operations. + /// public int BatchSize { get; set; } = 50000; + + /// + /// Gets or sets the batch size for bulk copy operations. + /// public int BulkCopyBatchSize { get; set; } = 5000; + + /// + /// Gets or sets the lookback multiplier for data sync delta calculations. + /// public double LookbackMultiplier { get; set; } = 1.5; + + /// + /// Gets or sets the number of days to retain synced data before purging. + /// public int PurgeRetentionDays { get; set; } = 90; + + /// + /// Gets or sets the timeout in seconds for sync operations. + /// public int SyncTimeoutSeconds { get; set; } = 3600; + + /// + /// Gets or sets a value indicating whether data synchronization is enabled. + /// public bool Enabled { get; set; } = true; } public class DataAccessSection { + /// + /// Gets or sets the default timeout in seconds for database queries. + /// public int DefaultTimeoutSeconds { get; set; } = 30; + + /// + /// Gets or sets the timeout in seconds for lot usage queries. + /// public int LotUsageTimeoutSeconds { get; set; } = 120; + + /// + /// Gets or sets the timeout in seconds for MIS data queries. + /// public int MisDataTimeoutSeconds { get; set; } = 300; + + /// + /// Gets or sets the schema name for production data. + /// public string ProductionSchema { get; set; } = "prod"; + + /// + /// Gets or sets the schema name for archive data. + /// public string ArchiveSchema { get; set; } = "archive"; + + /// + /// Gets or sets the schema name for staging data. + /// public string StageSchema { get; set; } = "stage"; + + /// + /// Gets or sets a value indicating whether detailed query logging is enabled. + /// public bool EnableDetailedLogging { get; set; } = false; } public class AuthSection { + /// + /// Gets or sets the name of the authentication cookie. + /// public string CookieName { get; set; } = ".JdeScoping.Auth"; + + /// + /// Gets or sets the cookie expiration time in minutes. + /// public int CookieExpirationMinutes { get; set; } = 480; } public class LdapSection { + /// + /// Gets or sets the LDAP server URLs to connect to. + /// public string[] ServerUrls { get; set; } = []; + + /// + /// Gets or sets the distinguished name of the LDAP group for authorization. + /// public string GroupDn { get; set; } = string.Empty; + + /// + /// Gets or sets the base distinguished name for LDAP searches. + /// public string SearchBase { get; set; } = string.Empty; + + /// + /// Gets or sets the connection timeout in seconds for LDAP operations. + /// public int ConnectionTimeoutSeconds { get; set; } = 30; + + /// + /// Gets or sets a value indicating whether to use fake authentication instead of LDAP. + /// public bool UseFakeAuth { get; set; } = false; + + /// + /// Gets or sets an array of user names that bypass group membership validation. + /// public string[] AdminBypassUsers { get; set; } = []; } public class SearchSection { + /// + /// Gets or sets the maximum number of result rows returned by a search. + /// public int MaxResultRows { get; set; } = 100000; + + /// + /// Gets or sets the timeout in seconds for search operations. + /// public int TimeoutSeconds { get; set; } = 300; + + /// + /// Gets or sets the maximum number of concurrent search operations allowed. + /// public int MaxConcurrentSearches { get; set; } = 5; } public class ExcelExportSection { + /// + /// Gets or sets the password for protecting the criteria worksheet. + /// public string CriteriaSheetPassword { get; set; } = string.Empty; + + /// + /// Gets or sets the password for protecting the data worksheet. + /// public string DataSheetPassword { get; set; } = string.Empty; + + /// + /// Gets or sets the maximum number of rows per Excel worksheet. + /// public int MaxRowsPerSheet { get; set; } = 1000000; + + /// + /// Gets or sets the default date format for Excel exports. + /// public string DefaultDateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss"; + + /// + /// Gets or sets a value indicating whether to write debug output to files. + /// public bool DebugWriteToFile { get; set; } = false; + + /// + /// Gets or sets the directory path for debug output files. + /// public string DebugOutputDirectory { get; set; } = string.Empty; + + /// + /// Gets or sets the time zone identifier for date/time conversions. + /// public string TimezoneId { get; set; } = "America/Chicago"; + + /// + /// Gets or sets the time zone abbreviation for display purposes. + /// public string TimezoneAbbreviation { get; set; } = "CT"; } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs index 43a5401..325f6e5 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs @@ -5,56 +5,144 @@ namespace JdeScoping.ConfigManager.Models; /// public class PipelinesConfigModel { + /// + /// Gets or sets the pipeline settings. + /// public PipelineSettings Settings { get; set; } = new(); + + /// + /// Gets or sets the default schedules for all pipelines. + /// public ScheduleDefaults ScheduleDefaults { get; set; } = new(); + + /// + /// Gets or sets the collection of named pipelines. + /// public Dictionary Pipelines { get; set; } = new(); } public class PipelineSettings { + /// + /// Gets or sets the timezone for scheduling operations. + /// public string Timezone { get; set; } = "UTC"; } public class ScheduleDefaults { + /// + /// Gets or sets the default mass data refresh schedule. + /// public ScheduleModel Mass { get; set; } = new() { Enabled = true, IntervalMinutes = 10080, PrePurge = true, ReIndex = true }; + + /// + /// Gets or sets the default daily data refresh schedule. + /// public ScheduleModel Daily { get; set; } = new() { Enabled = true, IntervalMinutes = 1440 }; + + /// + /// Gets or sets the default hourly data refresh schedule. + /// public ScheduleModel Hourly { get; set; } = new() { Enabled = true, IntervalMinutes = 60 }; } public class PipelineModel { + /// + /// Gets or sets the source configuration for data extraction. + /// public PipelineSource Source { get; set; } = new(); + + /// + /// Gets or sets the schedule configurations for this pipeline. + /// public PipelineSchedules Schedules { get; set; } = new(); + + /// + /// Gets or sets the destination configuration for data loading. + /// public PipelineDestination Destination { get; set; } = new(); + + /// + /// Gets or sets optional scripts to execute after pipeline completion. + /// public string[]? PostScripts { get; set; } } public class PipelineSource { + /// + /// Gets or sets the source database connection name. + /// public string Connection { get; set; } = string.Empty; + + /// + /// Gets or sets the query to extract data from the source. + /// public string Query { get; set; } = string.Empty; + + /// + /// Gets or sets the optional mass query for full data extraction. + /// public string? MassQuery { get; set; } + + /// + /// Gets or sets the query parameters and their definitions. + /// public Dictionary Parameters { get; set; } = new(); } public class ParameterDefinition { + /// + /// Gets or sets the parameter name. + /// public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the optional parameter format string. + /// public string? Format { get; set; } + + /// + /// Gets or sets the optional parameter source or derivation logic. + /// public string? Source { get; set; } } public class PipelineSchedules { + /// + /// Gets or sets the mass refresh schedule for this pipeline. + /// public ScheduleModel? Mass { get; set; } + + /// + /// Gets or sets the daily refresh schedule for this pipeline. + /// public ScheduleModel? Daily { get; set; } + + /// + /// Gets or sets the hourly refresh schedule for this pipeline. + /// public ScheduleModel? Hourly { get; set; } } public class PipelineDestination { + /// + /// Gets or sets the destination table name. + /// public string Table { get; set; } = string.Empty; + + /// + /// Gets or sets the columns used to match existing records for updates. + /// public string[] MatchColumns { get; set; } = []; + + /// + /// Gets or sets the columns to exclude from update operations. + /// public string[] ExcludeFromUpdate { get; set; } = []; } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ScheduleModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ScheduleModel.cs index f36413d..bf6f845 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ScheduleModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ScheduleModel.cs @@ -5,8 +5,23 @@ namespace JdeScoping.ConfigManager.Models; /// public class ScheduleModel { + /// + /// Gets or sets a value indicating whether the scheduled task is enabled. + /// public bool Enabled { get; set; } = true; + + /// + /// Gets or sets the interval in minutes between scheduled task executions. + /// public int IntervalMinutes { get; set; } = 60; + + /// + /// Gets or sets a value indicating whether to purge data before task execution. + /// public bool PrePurge { get; set; } = false; + + /// + /// Gets or sets a value indicating whether to reindex after task execution. + /// public bool ReIndex { get; set; } = false; } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Program.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Program.cs index da77ac8..73b8a23 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Program.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Program.cs @@ -4,10 +4,18 @@ namespace JdeScoping.ConfigManager; class Program { + /// + /// The entry point of the application. + /// + /// Command-line arguments. [STAThread] public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); + /// + /// Builds the Avalonia application builder with platform and font configuration. + /// + /// A configured AppBuilder ready for application startup. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/AutoDiscoveryService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/AutoDiscoveryService.cs index 5e2f0c8..a95520d 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/AutoDiscoveryService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/AutoDiscoveryService.cs @@ -17,12 +17,22 @@ public class AutoDiscoveryService : IAutoDiscoveryService private const string EnvVarName = "JDESCOPING_CONFIG_PATH"; private const string AppSettingsFileName = "appsettings.json"; + /// + /// Initializes a new instance of the class. + /// + /// The file system abstraction to use for directory and file checks. + /// Optional logger for recording discovery process information. public AutoDiscoveryService(IFileSystem fileSystem, ILogger? logger = null) { _fileSystem = fileSystem; _logger = logger; } + /// + /// Finds the configuration folder using a prioritized search strategy. + /// + /// Cancellation token for the async operation. + /// The path to a valid configuration folder, or null if none is found. public Task FindConfigFolderAsync(CancellationToken ct = default) { // 1. Check environment variable diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/BackupService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/BackupService.cs index 264431e..5bec3ff 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/BackupService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/BackupService.cs @@ -12,12 +12,24 @@ public class BackupService : IBackupService private readonly ILogger? _logger; private const string TimestampFormat = "yyyy-MM-dd_HHmmss"; + /// + /// Initializes a new instance of the class. + /// + /// The file system abstraction for file operations. + /// Optional logger for diagnostic messages. public BackupService(IFileSystem fileSystem, ILogger? logger = null) { _fileSystem = fileSystem; _logger = logger; } + /// + /// Creates a backup copy of the specified file with a timestamp suffix. + /// + /// The path of the file to backup. + /// Cancellation token for the operation. + /// The path of the created backup file. + /// Thrown when the source file does not exist. public async Task CreateBackupAsync(string filePath, CancellationToken ct = default) { if (!_fileSystem.FileExists(filePath)) @@ -34,6 +46,12 @@ public class BackupService : IBackupService return backupPath; } + /// + /// Retrieves a list of existing backups for the specified file, sorted by timestamp descending. + /// + /// The original file path to find backups for. + /// Cancellation token for the operation. + /// A read-only list of backup information sorted by timestamp. public async Task> GetBackupsAsync(string filePath, CancellationToken ct = default) { var directory = _fileSystem.GetDirectoryName(filePath); @@ -59,12 +77,24 @@ public class BackupService : IBackupService return backups.OrderByDescending(b => b.Timestamp).ToList(); } + /// + /// Restores a backup file by copying it to the target location. + /// + /// The path of the backup file to restore from. + /// The target path where the backup should be restored. + /// Cancellation token for the operation. public async Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default) { _logger?.LogInformation("Restoring backup from {BackupPath} to {TargetPath}", backupPath, targetPath); await _fileSystem.CopyFileAsync(backupPath, targetPath, ct); } + /// + /// Removes old backup files, keeping only the most recent backups. + /// + /// The original file path to find backups for. + /// The number of most recent backups to retain (default: 10). + /// Cancellation token for the operation. public async Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default) { var backups = await GetBackupsAsync(filePath, ct); diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs index a11c952..020d1cf 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs @@ -21,12 +21,24 @@ public class ConfigFileService : IConfigFileService PropertyNameCaseInsensitive = true }; + /// + /// Initializes a new instance of the class. + /// + /// The file system abstraction for file operations. + /// Optional logger for diagnostic messages. public ConfigFileService(IFileSystem fileSystem, ILogger? logger = null) { _fileSystem = fileSystem; _logger = logger; } + /// + /// Loads the application settings configuration from the specified file path. + /// + /// The file path to load appsettings from. + /// Cancellation token for the operation. + /// The loaded configuration model or a new empty model if deserialization fails. + /// Thrown when the JSON cannot be parsed. public async Task LoadAppSettingsAsync(string path, CancellationToken ct = default) { _logger?.LogInformation("Loading appsettings from {Path}", path); @@ -43,6 +55,13 @@ public class ConfigFileService : IConfigFileService } } + /// + /// Loads the pipelines configuration from the specified file path. + /// + /// The file path to load pipelines from. + /// Cancellation token for the operation. + /// The loaded pipelines configuration model or a new empty model if deserialization fails. + /// Thrown when the JSON cannot be parsed. public async Task LoadPipelinesAsync(string path, CancellationToken ct = default) { _logger?.LogInformation("Loading pipelines from {Path}", path); @@ -59,6 +78,12 @@ public class ConfigFileService : IConfigFileService } } + /// + /// Saves the application settings configuration to the specified file path. + /// + /// The file path to save appsettings to. + /// The configuration model to save. + /// Cancellation token for the operation. public async Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default) { _logger?.LogInformation("Saving appsettings to {Path}", path); @@ -66,6 +91,12 @@ public class ConfigFileService : IConfigFileService await _fileSystem.WriteAllTextAsync(path, json, ct); } + /// + /// Saves the pipelines configuration to the specified file path. + /// + /// The file path to save pipelines to. + /// The pipelines configuration model to save. + /// Cancellation token for the operation. public async Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default) { _logger?.LogInformation("Saving pipelines to {Path}", path); diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs index 12a78de..c0d6f8b 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs @@ -5,8 +5,17 @@ namespace JdeScoping.ConfigManager.Services; /// public class ConfigLoadException : Exception { + /// + /// Gets the path to the configuration file that failed to load. + /// public string FilePath { get; } + /// + /// Initializes a new instance of the ConfigLoadException class. + /// + /// The path to the configuration file that failed to load. + /// The error message describing the failure. + /// The inner exception that caused this exception, if any. public ConfigLoadException(string filePath, string message, Exception? inner = null) : base(message, inner) { diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/DiffService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/DiffService.cs index b5974a5..7235c49 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/DiffService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/DiffService.cs @@ -9,6 +9,12 @@ namespace JdeScoping.ConfigManager.Services; /// public class DiffService : IDiffService { + /// + /// Generates a diff between original and modified text content. + /// + /// The original text content. + /// The modified text content. + /// A diff result containing added, removed, and unchanged lines with counts. public DiffResult GenerateDiff(string original, string modified) { var diffBuilder = new InlineDiffBuilder(new Differ()); diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/FileSystem.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/FileSystem.cs index 00f5de1..dcc5c18 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/FileSystem.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/FileSystem.cs @@ -5,36 +5,96 @@ namespace JdeScoping.ConfigManager.Services; /// public class FileSystem : IFileSystem { + /// + /// Determines whether the specified file exists. + /// + /// The file path to check. + /// True if the file exists; otherwise, false. public bool FileExists(string path) => File.Exists(path); + /// + /// Determines whether the specified directory exists. + /// + /// The directory path to check. + /// True if the directory exists; otherwise, false. public bool DirectoryExists(string path) => Directory.Exists(path); + /// + /// Reads all text from the specified file asynchronously. + /// + /// The file path to read from. + /// Cancellation token for the operation. + /// The file contents as a string. public async Task ReadAllTextAsync(string path, CancellationToken ct = default) => await File.ReadAllTextAsync(path, ct); + /// + /// Writes all text to the specified file asynchronously, overwriting if it exists. + /// + /// The file path to write to. + /// The text content to write. + /// Cancellation token for the operation. public async Task WriteAllTextAsync(string path, string content, CancellationToken ct = default) => await File.WriteAllTextAsync(path, content, ct); + /// + /// Gets an array of file paths matching the pattern in the specified directory. + /// + /// The directory path to search in. + /// The search pattern to match files. + /// Cancellation token for the operation. + /// An array of file paths matching the pattern. public Task GetFilesAsync(string directory, string pattern, CancellationToken ct = default) => Task.FromResult(Directory.GetFiles(directory, pattern)); + /// + /// Copies a file from source to destination asynchronously. + /// + /// The source file path. + /// The destination file path. + /// Cancellation token for the operation. public async Task CopyFileAsync(string source, string destination, CancellationToken ct = default) { var content = await File.ReadAllBytesAsync(source, ct); await File.WriteAllBytesAsync(destination, content, ct); } + /// + /// Deletes the specified file asynchronously. + /// + /// The file path to delete. + /// Cancellation token for the operation. public Task DeleteFileAsync(string path, CancellationToken ct = default) { File.Delete(path); return Task.CompletedTask; } + /// + /// Returns the directory name for the specified path. + /// + /// The file or directory path. + /// The directory name, or an empty string if not available. public string GetDirectoryName(string path) => Path.GetDirectoryName(path) ?? string.Empty; + /// + /// Returns the file name and extension for the specified path. + /// + /// The file path. + /// The file name with extension. public string GetFileName(string path) => Path.GetFileName(path); + /// + /// Returns the file name without the extension for the specified path. + /// + /// The file path. + /// The file name without extension. public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path); + /// + /// Combines multiple path components into a single path. + /// + /// The path components to combine. + /// The combined path. public string Combine(params string[] paths) => Path.Combine(paths); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IBackupService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IBackupService.cs index f93cb82..fcfc101 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IBackupService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IBackupService.cs @@ -5,8 +5,19 @@ namespace JdeScoping.ConfigManager.Services; /// public class BackupInfo { + /// + /// Gets the full path to the backup file. + /// public required string Path { get; init; } + + /// + /// Gets the timestamp when the backup was created. + /// public required DateTime Timestamp { get; init; } + + /// + /// Gets the file size in bytes. + /// public required long Size { get; init; } } @@ -15,8 +26,35 @@ public class BackupInfo /// public interface IBackupService { + /// + /// Creates a backup copy of the specified file with a timestamp suffix. + /// + /// The path of the file to backup. + /// Cancellation token for the operation. + /// The path of the created backup file. Task CreateBackupAsync(string filePath, CancellationToken ct = default); + + /// + /// Retrieves a list of existing backups for the specified file, sorted by timestamp descending. + /// + /// The original file path to find backups for. + /// Cancellation token for the operation. + /// A read-only list of backup information sorted by timestamp. Task> GetBackupsAsync(string filePath, CancellationToken ct = default); + + /// + /// Restores a backup file by copying it to the target location. + /// + /// The path of the backup file to restore from. + /// The target path where the backup should be restored. + /// Cancellation token for the operation. Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default); + + /// + /// Removes old backup files, keeping only the most recent backups. + /// + /// The original file path to find backups for. + /// The number of most recent backups to retain (default: 10). + /// Cancellation token for the operation. Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs index c532253..797fa11 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs @@ -7,8 +7,35 @@ namespace JdeScoping.ConfigManager.Services; /// public interface IConfigFileService { + /// + /// Loads the application settings configuration from the specified file path. + /// + /// The file path to load appsettings from. + /// Cancellation token for the operation. + /// The loaded configuration model or a new empty model if deserialization fails. Task LoadAppSettingsAsync(string path, CancellationToken ct = default); + + /// + /// Loads the pipelines configuration from the specified file path. + /// + /// The file path to load pipelines from. + /// Cancellation token for the operation. + /// The loaded pipelines configuration model or a new empty model if deserialization fails. Task LoadPipelinesAsync(string path, CancellationToken ct = default); + + /// + /// Saves the application settings configuration to the specified file path. + /// + /// The file path to save appsettings to. + /// The configuration model to save. + /// Cancellation token for the operation. Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default); + + /// + /// Saves the pipelines configuration to the specified file path. + /// + /// The file path to save pipelines to. + /// The pipelines configuration model to save. + /// Cancellation token for the operation. Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IDiffService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IDiffService.cs index 502edaa..8d0ef61 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IDiffService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IDiffService.cs @@ -5,9 +5,24 @@ namespace JdeScoping.ConfigManager.Services; /// public class DiffLine { + /// + /// Gets the line number in the original text, or null for added lines. + /// public required int? OldLineNumber { get; init; } + + /// + /// Gets the line number in the modified text, or null for removed lines. + /// public required int? NewLineNumber { get; init; } + + /// + /// Gets the text content of the line. + /// public required string Text { get; init; } + + /// + /// Gets the type of change for this line (added, removed, or unchanged). + /// public required DiffLineType Type { get; init; } } @@ -23,9 +38,24 @@ public enum DiffLineType /// public class DiffResult { + /// + /// Gets a value indicating whether the diff contains any changes. + /// public bool HasChanges { get; init; } + + /// + /// Gets the list of lines representing the diff result. + /// public List Lines { get; init; } = []; + + /// + /// Gets the number of lines added in the modification. + /// public int Insertions { get; init; } + + /// + /// Gets the number of lines removed in the modification. + /// public int Deletions { get; init; } } @@ -34,5 +64,11 @@ public class DiffResult /// public interface IDiffService { + /// + /// Generates a diff between original and modified text content. + /// + /// The original text content. + /// The modified text content. + /// A diff result containing added, removed, and unchanged lines with counts. DiffResult GenerateDiff(string original, string modified); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IFileSystem.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IFileSystem.cs index b5d265f..cbee86e 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IFileSystem.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IFileSystem.cs @@ -5,15 +5,85 @@ namespace JdeScoping.ConfigManager.Services; /// public interface IFileSystem { + /// + /// Determines whether the specified file exists. + /// + /// The file path to check. + /// True if the file exists; otherwise, false. bool FileExists(string path); + + /// + /// Determines whether the specified directory exists. + /// + /// The directory path to check. + /// True if the directory exists; otherwise, false. bool DirectoryExists(string path); + + /// + /// Reads all text from the specified file asynchronously. + /// + /// The file path to read from. + /// Cancellation token for the operation. + /// The file contents as a string. Task ReadAllTextAsync(string path, CancellationToken ct = default); + + /// + /// Writes all text to the specified file asynchronously, overwriting if it exists. + /// + /// The file path to write to. + /// The text content to write. + /// Cancellation token for the operation. Task WriteAllTextAsync(string path, string content, CancellationToken ct = default); + + /// + /// Gets an array of file paths matching the pattern in the specified directory. + /// + /// The directory path to search in. + /// The search pattern to match files. + /// Cancellation token for the operation. + /// An array of file paths matching the pattern. Task GetFilesAsync(string directory, string pattern, CancellationToken ct = default); + + /// + /// Copies a file from source to destination asynchronously. + /// + /// The source file path. + /// The destination file path. + /// Cancellation token for the operation. Task CopyFileAsync(string source, string destination, CancellationToken ct = default); + + /// + /// Deletes the specified file asynchronously. + /// + /// The file path to delete. + /// Cancellation token for the operation. Task DeleteFileAsync(string path, CancellationToken ct = default); + + /// + /// Returns the directory name for the specified path. + /// + /// The file or directory path. + /// The directory name, or an empty string if not available. string GetDirectoryName(string path); + + /// + /// Returns the file name and extension for the specified path. + /// + /// The file path. + /// The file name with extension. string GetFileName(string path); + + /// + /// Returns the file name without the extension for the specified path. + /// + /// The file path. + /// The file name without extension. string GetFileNameWithoutExtension(string path); + + /// + /// Combines multiple path components into a single path. + /// + /// The path components to combine. + /// The combined path. string Combine(params string[] paths); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs index 1cb4499..44337fd 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs @@ -7,11 +7,31 @@ namespace JdeScoping.ConfigManager.Services; /// public class ValidationResult { + /// + /// Gets a value indicating whether the validation succeeded (no errors). + /// public bool IsValid => Errors.Count == 0; + + /// + /// Gets the list of validation errors encountered. + /// public List Errors { get; } = []; + + /// + /// Gets the list of validation warnings encountered. + /// public List Warnings { get; } = []; + /// + /// Adds an error message to the validation result. + /// + /// The error message to add. public void AddError(string message) => Errors.Add(message); + + /// + /// Adds a warning message to the validation result. + /// + /// The warning message to add. public void AddWarning(string message) => Warnings.Add(message); } @@ -20,6 +40,17 @@ public class ValidationResult /// public interface IValidationService { + /// + /// Validates the application settings configuration. + /// + /// The configuration model to validate. + /// A validation result containing any errors or warnings found. ValidationResult ValidateAppSettings(ConfigModel config); + + /// + /// Validates the pipelines configuration. + /// + /// The pipelines configuration model to validate. + /// A validation result containing any errors or warnings found. ValidationResult ValidatePipelines(PipelinesConfigModel config); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs index 753c6e9..1c1211c 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs @@ -9,6 +9,11 @@ public class ValidationService : IValidationService { private static readonly string[] ValidConnections = ["jde", "cms", "giw", "lotfinderdb"]; + /// + /// Validates the application settings configuration. + /// + /// The configuration model to validate. + /// A validation result containing any errors or warnings found. public ValidationResult ValidateAppSettings(ConfigModel config) { var result = new ValidationResult(); @@ -50,6 +55,11 @@ public class ValidationService : IValidationService return result; } + /// + /// Validates the pipelines configuration. + /// + /// The pipelines configuration model to validate. + /// A validation result containing any errors or warnings found. public ValidationResult ValidatePipelines(PipelinesConfigModel config) { var result = new ValidationResult(); diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/AsyncRelayCommand.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/AsyncRelayCommand.cs index 33b0592..d8bbb25 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/AsyncRelayCommand.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/AsyncRelayCommand.cs @@ -12,20 +12,38 @@ public class AsyncRelayCommand : ICommand private bool _isExecuting; private EventHandler? _canExecuteChanged; + /// + /// Occurs when the result of has changed. + /// public event EventHandler? CanExecuteChanged { add => _canExecuteChanged += value; remove => _canExecuteChanged -= value; } + /// + /// Initializes a new instance of the class. + /// + /// The async action to execute when the command is invoked. + /// An optional predicate to determine if the command can execute. + /// Thrown when is null. public AsyncRelayCommand(Func execute, Func? canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } + /// + /// Determines whether the command can execute in its current state. + /// + /// Unused parameter required by interface. + /// False if the command is currently executing; otherwise returns the result of the canExecute predicate. public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true); + /// + /// Executes the async command, preventing concurrent execution. + /// + /// Unused parameter required by interface. public async void Execute(object? parameter) { if (!CanExecute(parameter)) return; @@ -44,5 +62,8 @@ public class AsyncRelayCommand : ICommand } } + /// + /// Raises the event to notify command bindings of state changes. + /// public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/RelayCommand.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/RelayCommand.cs index 076536e..2b1ef7f 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/RelayCommand.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/RelayCommand.cs @@ -11,26 +11,52 @@ public class RelayCommand : ICommand private readonly Predicate? _canExecute; private EventHandler? _canExecuteChanged; + /// + /// Occurs when the result of has changed. + /// public event EventHandler? CanExecuteChanged { add => _canExecuteChanged += value; remove => _canExecuteChanged -= value; } + /// + /// Initializes a new instance of the class with a parameterized action. + /// + /// The action to execute when the command is invoked. + /// An optional predicate to determine if the command can execute. + /// Thrown when is null. public RelayCommand(Action execute, Predicate? canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } + /// + /// Initializes a new instance of the class with a parameterless action. + /// + /// The parameterless action to execute when the command is invoked. + /// An optional predicate to determine if the command can execute. public RelayCommand(Action execute, Func? canExecute = null) : this(_ => execute(), canExecute != null ? _ => canExecute() : null) { } + /// + /// Determines whether the command can execute in its current state. + /// + /// The parameter to pass to the canExecute predicate, or null. + /// True if the command can execute; otherwise, false. public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true; + /// + /// Executes the command with the specified parameter. + /// + /// The parameter to pass to the execute action. public void Execute(object? parameter) => _execute(parameter); + /// + /// Raises the event to notify command bindings of state changes. + /// public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs index f8db87b..87998e1 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs @@ -27,30 +27,61 @@ public class TreeNodeViewModel : ViewModelBase private bool _isSelected; private ValidationState _validationState = ValidationState.Unknown; + /// + /// Gets the name of the tree node. + /// public string Name { get; } + + /// + /// Gets the icon identifier for the tree node. + /// public string Icon { get; } + + /// + /// Gets the type of tree node (folder, settings section, or pipeline). + /// public TreeNodeType NodeType { get; } + + /// + /// Gets or sets the configuration section key associated with this node. + /// public string? SectionKey { get; init; } + + /// + /// Gets the collection of child nodes. + /// public ObservableCollection Children { get; } = []; + /// + /// Gets or sets a value indicating whether this node has been modified. + /// public bool IsModified { get => _isModified; set => SetProperty(ref _isModified, value); } + /// + /// Gets or sets a value indicating whether this node is expanded in the tree view. + /// public bool IsExpanded { get => _isExpanded; set => SetProperty(ref _isExpanded, value); } + /// + /// Gets or sets a value indicating whether this node is selected in the tree view. + /// public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected, value); } + /// + /// Gets or sets the validation state of this node. + /// public ValidationState ValidationState { get => _validationState; @@ -61,6 +92,9 @@ public class TreeNodeViewModel : ViewModelBase } } + /// + /// Gets the icon character representing the validation state. + /// public string StatusIcon => ValidationState switch { ValidationState.Valid => "✓", @@ -69,6 +103,12 @@ public class TreeNodeViewModel : ViewModelBase _ => "" }; + /// + /// Initializes a new instance of the class. + /// + /// The name of the node. + /// The icon identifier for the node. + /// The type of the node. public TreeNodeViewModel(string name, string icon, TreeNodeType nodeType) { Name = name; diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ViewModelBase.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ViewModelBase.cs index 752d7bf..965975a 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ViewModelBase.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ViewModelBase.cs @@ -8,13 +8,28 @@ namespace JdeScoping.ConfigManager.ViewModels; /// public abstract class ViewModelBase : INotifyPropertyChanged { + /// + /// Occurs when a property value changes. + /// public event PropertyChangedEventHandler? PropertyChanged; + /// + /// Raises the PropertyChanged event with the specified property name. + /// + /// The name of the property that changed. Automatically captured from the calling member. protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + /// + /// Updates a field value and raises PropertyChanged if the value actually changed. + /// + /// The type of the property value. + /// The backing field to update by reference. + /// The new value to assign. + /// The name of the property being changed. Automatically captured from the calling member. + /// True if the value was changed; otherwise, false. protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml.cs index 8b1ac54..2ad8e13 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml.cs @@ -4,6 +4,9 @@ namespace JdeScoping.ConfigManager.Views; public partial class MainWindow : Window { + /// + /// Initializes a new instance of the MainWindow. + /// public MainWindow() { InitializeComponent(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/App.axaml.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/App.axaml.cs index 226b3fa..047bf36 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/App.axaml.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/App.axaml.cs @@ -13,13 +13,18 @@ namespace JdeScoping.SecureStoreManager; public partial class App : Avalonia.Application { + /// + /// Gets the service provider instance for dependency injection. + /// public static IServiceProvider Services { get; private set; } = null!; + /// public override void Initialize() { AvaloniaXamlLoader.Load(this); } + /// public override void OnFrameworkInitializationCompleted() { var services = new ServiceCollection(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/SecretUseCases.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/SecretUseCases.cs index ce2a5d9..9316a73 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/SecretUseCases.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/SecretUseCases.cs @@ -11,6 +11,11 @@ public class SecretUseCases private readonly ISecureStoreManager _storeManager; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// + /// The secure store manager. + /// The logger instance. public SecretUseCases(ISecureStoreManager storeManager, ILogger logger) { _storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager)); @@ -20,6 +25,8 @@ public class SecretUseCases /// /// Sets a secret with the given key and value. /// + /// The secret key. + /// The secret value. public void SetSecret(string key, string value) { _logger.LogInformation("Setting secret {Key}", key); @@ -29,6 +36,7 @@ public class SecretUseCases /// /// Removes a secret by key. /// + /// The secret key to remove. public void RemoveSecret(string key) { _logger.LogInformation("Removing secret {Key}", key); @@ -46,6 +54,8 @@ public class SecretUseCases /// /// Gets the value of a secret by key. /// + /// The secret key. + /// The secret value. public string GetSecret(string key) { return _storeManager.GetSecret(key); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/StoreUseCases.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/StoreUseCases.cs index 95be69a..14d2d49 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/StoreUseCases.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Application/StoreUseCases.cs @@ -11,6 +11,11 @@ public class StoreUseCases private readonly ISecureStoreManager _storeManager; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// + /// The secure store manager instance. + /// The logger instance. public StoreUseCases(ISecureStoreManager storeManager, ILogger logger) { _storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager)); @@ -20,6 +25,9 @@ public class StoreUseCases /// /// Creates a new store with either key file or password authentication. /// + /// The path where the store will be created. + /// The path to the key file, or null for password-based authentication. + /// The password for authentication, or null for key file-based authentication. public void CreateStore(string storePath, string? keyFilePath, string? password) { _logger.LogInformation("Creating store at {StorePath}", storePath); @@ -43,6 +51,9 @@ public class StoreUseCases /// /// Opens an existing store with either key file or password authentication. /// + /// The path to the existing store. + /// The path to the key file, or null for password-based authentication. + /// The password for authentication, or null for key file-based authentication. public void OpenStore(string storePath, string? keyFilePath, string? password) { _logger.LogInformation("Opening store at {StorePath}", storePath); @@ -84,6 +95,7 @@ public class StoreUseCases /// /// Generates a new key file at the specified path. /// + /// The path where the key file will be generated. public void GenerateKeyFile(string path) { _logger.LogInformation("Generating key file at {Path}", path); @@ -94,6 +106,7 @@ public class StoreUseCases /// /// Exports the current store's key to a file. /// + /// The path where the key will be exported. public void ExportKey(string path) { _logger.LogInformation("Exporting key to {Path}", path); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Converters/BooleanConverters.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Converters/BooleanConverters.cs index 057a108..d1221c7 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Converters/BooleanConverters.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Converters/BooleanConverters.cs @@ -8,6 +8,14 @@ namespace JdeScoping.SecureStoreManager.Converters; /// public class InverseBooleanConverter : IValueConverter { + /// + /// Converts a boolean value to its inverted counterpart. + /// + /// The boolean value to invert. + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// The inverted boolean value, or false if the input is not a boolean. public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is bool boolValue) @@ -17,6 +25,14 @@ public class InverseBooleanConverter : IValueConverter return false; } + /// + /// Converts a value back to a boolean by inverting it. + /// + /// The value to invert. + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// The inverted boolean value, or false if the input is not a boolean. public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is bool boolValue) @@ -32,6 +48,14 @@ public class InverseBooleanConverter : IValueConverter /// public class BooleanToVisibilityIconConverter : IValueConverter { + /// + /// Converts a boolean to a visibility icon string. + /// + /// The boolean value indicating visibility. + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// "Hide" if true, "Show" if false or input is not boolean. public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is bool isVisible) @@ -42,6 +66,15 @@ public class BooleanToVisibilityIconConverter : IValueConverter return "Show"; } + /// + /// Converts a value back (not implemented for visibility icons). + /// + /// The value to convert back (ignored). + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// Not implemented. + /// Always thrown. public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); @@ -53,11 +86,28 @@ public class BooleanToVisibilityIconConverter : IValueConverter /// public class NullToBoolConverter : IValueConverter { + /// + /// Converts a value to a boolean based on null status. + /// + /// The value to check for null. + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// True if value is not null, false otherwise. public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { return value != null; } + /// + /// Converts a value back (not implemented for null checks). + /// + /// The value to convert back (ignored). + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// Not implemented. + /// Always thrown. public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); @@ -69,6 +119,14 @@ public class NullToBoolConverter : IValueConverter /// public class StringToBoolConverter : IValueConverter { + /// + /// Converts a string to a boolean based on whether it's empty or null. + /// + /// The string value to check. + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// True if string is not null or whitespace, false otherwise. public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is string str) @@ -78,6 +136,15 @@ public class StringToBoolConverter : IValueConverter return false; } + /// + /// Converts a value back (not implemented for string checks). + /// + /// The value to convert back (ignored). + /// The target type (ignored). + /// An optional parameter (ignored). + /// The culture information (ignored). + /// Not implemented. + /// Always thrown. public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Program.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Program.cs index 0192a46..b37ef8e 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Program.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Program.cs @@ -4,13 +4,17 @@ namespace JdeScoping.SecureStoreManager; internal class Program { - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called. + /// + /// The main entry point for the application. + /// + /// Command-line arguments. [STAThread] public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); - // Avalonia configuration, don't remove; also used by visual designer. + /// + /// Builds the Avalonia application configuration. + /// public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaClipboardService.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaClipboardService.cs index 339646a..a0cb4a5 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaClipboardService.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaClipboardService.cs @@ -18,6 +18,10 @@ public class AvaloniaClipboardService : IClipboardService _getClipboard = getClipboard ?? throw new ArgumentNullException(nameof(getClipboard)); } + /// + /// Sets the clipboard text asynchronously. + /// + /// The text to set on the clipboard. public async Task SetTextAsync(string text) { var clipboard = _getClipboard(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaDialogService.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaDialogService.cs index 59c7f46..9461b22 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaDialogService.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/AvaloniaDialogService.cs @@ -22,6 +22,12 @@ public class AvaloniaDialogService : IDialogService _getOwnerWindow = getOwnerWindow ?? throw new ArgumentNullException(nameof(getOwnerWindow)); } + /// + /// Displays an error dialog with the specified message and title. + /// + /// The error message to display. + /// The title of the error dialog. + /// A task that completes when the dialog is dismissed. public async Task ShowErrorAsync(string message, string title) { var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Error); @@ -36,6 +42,12 @@ public class AvaloniaDialogService : IDialogService } } + /// + /// Displays an informational dialog with the specified message and title. + /// + /// The information message to display. + /// The title of the information dialog. + /// A task that completes when the dialog is dismissed. public async Task ShowInfoAsync(string message, string title) { var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Info); @@ -50,6 +62,12 @@ public class AvaloniaDialogService : IDialogService } } + /// + /// Displays a confirmation dialog with Yes/No buttons. + /// + /// The confirmation message to display. + /// The title of the confirmation dialog. + /// A task that completes with true if Yes was clicked; otherwise, false. public async Task ShowConfirmationAsync(string message, string title) { var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.YesNo, Icon.Warning); @@ -66,6 +84,10 @@ public class AvaloniaDialogService : IDialogService return result == ButtonResult.Yes; } + /// + /// Displays a prompt asking the user whether to save unsaved changes. + /// + /// A task that completes with the user's choice: Save, DontSave, or Cancel. public async Task ShowUnsavedChangesPromptAsync() { var box = MessageBoxManager.GetMessageBoxStandard( @@ -93,6 +115,14 @@ public class AvaloniaDialogService : IDialogService }; } + /// + /// Displays a save file dialog allowing the user to select a file path. + /// + /// The title of the save file dialog. + /// The friendly name of the file type (e.g., "Excel Files"). + /// The file extension pattern (e.g., "*.xlsx"). + /// The default file extension (e.g., ".xlsx"). + /// A task that completes with the selected file path, or null if canceled. public async Task ShowSaveFileDialogAsync(string title, string fileTypeName, string pattern, string defaultExtension) { var window = _getOwnerWindow(); @@ -113,6 +143,13 @@ public class AvaloniaDialogService : IDialogService return file?.Path.LocalPath; } + /// + /// Displays an open file dialog allowing the user to select a file to open. + /// + /// The title of the open file dialog. + /// The friendly name of the file type (e.g., "Excel Files"). + /// The file extension pattern (e.g., "*.xlsx"). + /// A task that completes with the selected file path, or null if canceled. public async Task ShowOpenFileDialogAsync(string title, string fileTypeName, string pattern) { var window = _getOwnerWindow(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/IDialogService.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/IDialogService.cs index bd2d856..ef7e384 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/IDialogService.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/IDialogService.cs @@ -19,16 +19,22 @@ public interface IDialogService /// /// Shows an error message dialog. /// + /// The error message to display. + /// The dialog title. Task ShowErrorAsync(string message, string title); /// /// Shows an informational message dialog. /// + /// The informational message to display. + /// The dialog title. Task ShowInfoAsync(string message, string title); /// /// Shows a confirmation dialog with Yes/No options. /// + /// The confirmation message to display. + /// The dialog title. /// True if user clicked Yes, false otherwise. Task ShowConfirmationAsync(string message, string title); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/SecureStoreManager.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/SecureStoreManager.cs index 5366a3a..df26b26 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/SecureStoreManager.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Services/SecureStoreManager.cs @@ -35,6 +35,7 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable /// /// Creates a new SecureStoreManager with the specified logger. /// + /// Logger instance for diagnostic output. public SecureStoreManager(ILogger logger) { _logger = logger ?? NullLogger.Instance; @@ -320,6 +321,9 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); } + /// + /// Releases the resources used by the . + /// public void Dispose() { if (_disposed) return; diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/AsyncRelayCommand.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/AsyncRelayCommand.cs index 6d88813..b148468 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/AsyncRelayCommand.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/AsyncRelayCommand.cs @@ -12,6 +12,9 @@ public class AsyncRelayCommand : ICommand private bool _isExecuting; private EventHandler? _canExecuteChanged; + /// + /// Raised when the result of CanExecute() may have changed. + /// public event EventHandler? CanExecuteChanged { add => _canExecuteChanged += value; @@ -38,11 +41,19 @@ public class AsyncRelayCommand : ICommand _canExecute = canExecute; } + /// + /// Determines whether the command can be executed in its current state. + /// + /// The command parameter (unused). public bool CanExecute(object? parameter) { return !_isExecuting && (_canExecute?.Invoke() ?? true); } + /// + /// Executes the async command if it can execute. + /// + /// The command parameter (unused). public async void Execute(object? parameter) { if (!CanExecute(parameter)) diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/DialogViewModels.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/DialogViewModels.cs index e27dcca..7b8695a 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/DialogViewModels.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/DialogViewModels.cs @@ -15,12 +15,18 @@ public class NewStoreDialogViewModel : ViewModelBase private bool _useKeyFile = true; private bool _usePassword; + /// + /// Initializes a new instance of the class. + /// public NewStoreDialogViewModel() { BrowseStorePathCommand = new RelayCommand(BrowseStorePath); BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath); } + /// + /// Gets or sets the path to the store file to create. + /// public string StorePath { get => _storePath; @@ -31,6 +37,9 @@ public class NewStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets the path to the key file for encryption. + /// public string KeyFilePath { get => _keyFilePath; @@ -41,6 +50,9 @@ public class NewStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets the password for store encryption. + /// public string Password { get => _password; @@ -51,6 +63,9 @@ public class NewStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets the password confirmation value. + /// public string ConfirmPassword { get => _confirmPassword; @@ -61,6 +76,9 @@ public class NewStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets whether to use key file for store encryption. + /// public bool UseKeyFile { get => _useKeyFile; @@ -74,6 +92,9 @@ public class NewStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets whether to use password for store encryption. + /// public bool UsePassword { get => _usePassword; @@ -93,9 +114,19 @@ public class NewStoreDialogViewModel : ViewModelBase OnPropertyChanged(nameof(ValidationError)); } + /// + /// Gets the command to browse for store path location. + /// public ICommand BrowseStorePathCommand { get; } + + /// + /// Gets the command to browse for key file path location. + /// public ICommand BrowseKeyFilePathCommand { get; } + /// + /// Gets a value indicating whether the dialog input is valid. + /// public bool IsValid { get @@ -113,6 +144,9 @@ public class NewStoreDialogViewModel : ViewModelBase } } + /// + /// Gets the validation error message, or null if valid. + /// public string? ValidationError { get @@ -187,12 +221,18 @@ public class OpenStoreDialogViewModel : ViewModelBase private bool _useKeyFile = true; private bool _usePassword; + /// + /// Initializes a new instance of the class. + /// public OpenStoreDialogViewModel() { BrowseStorePathCommand = new RelayCommand(BrowseStorePath); BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath); } + /// + /// Gets or sets the path to the store file to open. + /// public string StorePath { get => _storePath; @@ -203,6 +243,9 @@ public class OpenStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets the path to the key file for decryption. + /// public string KeyFilePath { get => _keyFilePath; @@ -213,6 +256,9 @@ public class OpenStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets the password for store decryption. + /// public string Password { get => _password; @@ -223,6 +269,9 @@ public class OpenStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets whether to use key file for store decryption. + /// public bool UseKeyFile { get => _useKeyFile; @@ -236,6 +285,9 @@ public class OpenStoreDialogViewModel : ViewModelBase } } + /// + /// Gets or sets whether to use password for store decryption. + /// public bool UsePassword { get => _usePassword; @@ -255,9 +307,19 @@ public class OpenStoreDialogViewModel : ViewModelBase OnPropertyChanged(nameof(ValidationError)); } + /// + /// Gets the command to browse for store path location. + /// public ICommand BrowseStorePathCommand { get; } + + /// + /// Gets the command to browse for key file path location. + /// public ICommand BrowseKeyFilePathCommand { get; } + /// + /// Gets a value indicating whether the dialog input is valid. + /// public bool IsValid { get @@ -275,6 +337,9 @@ public class OpenStoreDialogViewModel : ViewModelBase } } + /// + /// Gets the validation error message, or null if valid. + /// public string? ValidationError { get @@ -348,10 +413,18 @@ public class SecretEditDialogViewModel : ViewModelBase private string _value = string.Empty; private bool _isNewSecret = true; + /// + /// Initializes a new instance of the class. + /// public SecretEditDialogViewModel() { } + /// + /// Initializes a new instance of the class with a key and value for editing. + /// + /// The secret key. + /// The secret value. public SecretEditDialogViewModel(string key, string value) { _key = key; @@ -359,6 +432,9 @@ public class SecretEditDialogViewModel : ViewModelBase _isNewSecret = false; } + /// + /// Gets or sets the secret key. + /// public string Key { get => _key; @@ -369,24 +445,42 @@ public class SecretEditDialogViewModel : ViewModelBase } } + /// + /// Gets or sets the secret value. + /// public string Value { get => _value; set => SetProperty(ref _value, value); } + /// + /// Gets or sets a value indicating whether this is a new secret being added. + /// public bool IsNewSecret { get => _isNewSecret; set => SetProperty(ref _isNewSecret, value); } + /// + /// Gets a value indicating whether the key field is editable. + /// public bool IsKeyEditable => _isNewSecret; + /// + /// Gets the dialog title based on whether this is a new secret or edit. + /// public string DialogTitle => _isNewSecret ? "Add Secret" : "Edit Secret"; + /// + /// Gets a value indicating whether the dialog input is valid. + /// public bool IsValid => !string.IsNullOrWhiteSpace(Key); + /// + /// Gets the validation error message, or null if valid. + /// public string? ValidationError { get diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/MainWindowViewModel.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/MainWindowViewModel.cs index 2aa94c1..2d841fc 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/MainWindowViewModel.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/MainWindowViewModel.cs @@ -16,6 +16,12 @@ public class MainWindowViewModel : ViewModelBase private SecretItemViewModel? _selectedSecret; private string _statusMessage = "Ready"; + /// + /// Initializes a new instance of the MainWindowViewModel. + /// + /// The secure store manager service. + /// The dialog service for user interactions. + /// The clipboard service for copying secrets. public MainWindowViewModel( ISecureStoreManager storeManager, IDialogService dialogService, @@ -102,24 +108,64 @@ public class MainWindowViewModel : ViewModelBase public bool HasUnsavedChanges => _storeManager.HasUnsavedChanges; // File Commands + /// + /// Gets the command to create a new store. + /// public ICommand NewStoreCommand { get; } + + /// + /// Gets the command to open an existing store. + /// public ICommand OpenStoreCommand { get; } + + /// + /// Gets the command to save the current store. + /// public ICommand SaveCommand { get; } + + /// + /// Gets the command to close the current store. + /// public ICommand CloseStoreCommand { get; } + + /// + /// Gets the command to exit the application. + /// public ICommand ExitCommand { get; } // Secret Commands + /// + /// Gets the command to add a new secret. + /// public ICommand AddSecretCommand { get; } + + /// + /// Gets the command to edit the selected secret. + /// public ICommand EditSecretCommand { get; } + + /// + /// Gets the command to delete the selected secret. + /// public ICommand DeleteSecretCommand { get; } // Tools Commands + /// + /// Gets the command to generate a new key file. + /// public ICommand GenerateKeyFileCommand { get; } + + /// + /// Gets the command to export the store's key. + /// public ICommand ExportKeyCommand { get; } /// /// Creates a new store. Called by the dialog. /// + /// The path where the store file will be created. + /// Optional path to a key file for encryption. + /// Optional password for encryption. public async Task CreateNewStoreAsync(string storePath, string? keyFilePath, string? password) { try @@ -154,6 +200,9 @@ public class MainWindowViewModel : ViewModelBase /// /// Opens an existing store. Called by the dialog. /// + /// The path to the store file to open. + /// Optional path to a key file for decryption. + /// Optional password for decryption. public async Task OpenExistingStoreAsync(string storePath, string? keyFilePath, string? password) { try @@ -188,6 +237,9 @@ public class MainWindowViewModel : ViewModelBase /// /// Adds or updates a secret. Called by the dialog. /// + /// The secret key identifier. + /// The secret value to store. + /// True if this is a new secret, false if updating an existing one. public async Task SaveSecretAsync(string key, string value, bool isNew) { try @@ -415,9 +467,28 @@ public class MainWindowViewModel : ViewModelBase } // Events for view to show dialogs (these require view-specific DataContext setup) + /// + /// Raised when a new store creation dialog should be shown. + /// public event Action? OnRequestNewStoreDialog; + + /// + /// Raised when an open store dialog should be shown. + /// public event Action? OnRequestOpenStoreDialog; + + /// + /// Raised when a new secret dialog should be shown. + /// public event Action? OnRequestAddSecretDialog; + + /// + /// Raised when an edit secret dialog should be shown with the specified key and value. + /// public event Action? OnRequestEditSecretDialog; + + /// + /// Raised when the application should close. + /// public event Action? OnRequestClose; } diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/RelayCommand.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/RelayCommand.cs index 4acde6c..6ba9e0a 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/RelayCommand.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/RelayCommand.cs @@ -11,6 +11,9 @@ public class RelayCommand : ICommand private readonly Predicate? _canExecute; private EventHandler? _canExecuteChanged; + /// + /// Raised when the command's ability to execute may have changed. + /// public event EventHandler? CanExecuteChanged { add => _canExecuteChanged += value; @@ -56,11 +59,20 @@ public class RelayCommand : ICommand { } + /// + /// Determines whether the command can execute. + /// + /// An optional command parameter. + /// True if the command can execute, false otherwise. public bool CanExecute(object? parameter) { return _canExecute == null || _canExecute(parameter); } + /// + /// Executes the command with the specified parameter. + /// + /// An optional command parameter. public void Execute(object? parameter) { _execute(parameter); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/SecretItemViewModel.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/SecretItemViewModel.cs index 91b1fa6..b99c074 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/SecretItemViewModel.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/SecretItemViewModel.cs @@ -13,6 +13,12 @@ public class SecretItemViewModel : ViewModelBase private bool _isValueVisible; private const string MaskedValue = "********"; + /// + /// Initializes a new instance of the class. + /// + /// The secret key name. + /// The secret value. + /// The clipboard service for copy operations. public SecretItemViewModel(string key, string value, IClipboardService clipboardService) { Key = key; diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/ViewModelBase.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/ViewModelBase.cs index 8fcb132..2e7fea0 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/ViewModelBase.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/ViewModels/ViewModelBase.cs @@ -8,6 +8,7 @@ namespace JdeScoping.SecureStoreManager.ViewModels; /// public abstract class ViewModelBase : INotifyPropertyChanged { + /// Occurs when a property value changes. public event PropertyChangedEventHandler? PropertyChanged; /// diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/MainWindow.axaml.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/MainWindow.axaml.cs index 8379a24..a7a1e78 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/MainWindow.axaml.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/MainWindow.axaml.cs @@ -9,6 +9,9 @@ public partial class MainWindow : Window { private MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel; + /// + /// Initializes a new instance of the class. + /// public MainWindow() { InitializeComponent(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/NewStoreDialog.axaml.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/NewStoreDialog.axaml.cs index c891df9..f55da26 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/NewStoreDialog.axaml.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/NewStoreDialog.axaml.cs @@ -10,8 +10,14 @@ namespace JdeScoping.SecureStoreManager.Views; public partial class NewStoreDialog : Window { + /// + /// Gets the view model for this dialog. + /// public NewStoreDialogViewModel ViewModel => (NewStoreDialogViewModel)DataContext!; + /// + /// Initializes a new instance of the NewStoreDialog. + /// public NewStoreDialog() { InitializeComponent(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/OpenStoreDialog.axaml.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/OpenStoreDialog.axaml.cs index 479913f..d114a29 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/OpenStoreDialog.axaml.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/OpenStoreDialog.axaml.cs @@ -10,8 +10,14 @@ namespace JdeScoping.SecureStoreManager.Views; public partial class OpenStoreDialog : Window { + /// + /// Gets the view model for this dialog. + /// public OpenStoreDialogViewModel ViewModel => (OpenStoreDialogViewModel)DataContext!; + /// + /// Initializes a new instance of the class. + /// public OpenStoreDialog() { InitializeComponent(); diff --git a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/SecretEditDialog.axaml.cs b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/SecretEditDialog.axaml.cs index 02c654d..7cac77c 100644 --- a/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/SecretEditDialog.axaml.cs +++ b/NEW/src/Utils/JdeScoping.SecureStoreManager/Views/SecretEditDialog.axaml.cs @@ -8,14 +8,25 @@ namespace JdeScoping.SecureStoreManager.Views; public partial class SecretEditDialog : Window { + /// + /// Gets the view model for this dialog. + /// public SecretEditDialogViewModel ViewModel => (SecretEditDialogViewModel)DataContext!; + /// + /// Initializes a new instance of the class for creating a new secret. + /// public SecretEditDialog() { InitializeComponent(); DataContext = new SecretEditDialogViewModel(); } + /// + /// Initializes a new instance of the class for editing an existing secret. + /// + /// The secret key. + /// The secret value. public SecretEditDialog(string key, string value) { InitializeComponent(); diff --git a/docs/designs/configmanager-ui-design.md b/docs/designs/configmanager-ui-design.md new file mode 100644 index 0000000..21c0293 --- /dev/null +++ b/docs/designs/configmanager-ui-design.md @@ -0,0 +1,1013 @@ +# ConfigManager UI Design Specification + +## Design Philosophy + +**Aesthetic Direction:** Industrial Precision + +A professional configuration tool demands clarity, efficiency, and trust. This design embraces an **industrial utilitarian** aesthetic with refined execution: + +- **Dark theme** reduces eye strain during extended editing sessions +- **Monospace typography** for data values ensures alignment and scanability +- **Geometric precision** conveys reliability and control +- **Purposeful color** limited to validation states and actionable elements +- **Generous spacing** prevents cognitive overload when managing complex configs + +## Color System + +### Core Palette + +``` +/* Background Layers (darkest to lightest) */ +--bg-base: #0D0F12 /* App background */ +--bg-surface: #151920 /* Panels, cards */ +--bg-elevated: #1C2128 /* Hover states, popups */ +--bg-input: #232A35 /* Form inputs */ + +/* Border & Dividers */ +--border-subtle: #2D3540 /* Panel dividers */ +--border-default: #3D4550 /* Input borders */ +--border-focus: #5C9AFF /* Focus rings */ + +/* Text Hierarchy */ +--text-primary: #E6EDF5 /* Headings, labels */ +--text-secondary: #9BA8B8 /* Descriptions, hints */ +--text-muted: #5C6A7A /* Disabled, placeholders */ +--text-inverse: #0D0F12 /* Text on light backgrounds */ + +/* Accent Colors */ +--accent-primary: #5C9AFF /* Primary actions, links */ +--accent-hover: #7DB0FF /* Hover state */ + +/* Status Colors */ +--status-success: #3DD68C /* Valid, connected */ +--status-warning: #FFB84D /* Warnings */ +--status-error: #FF6B6B /* Errors, invalid */ +--status-info: #5C9AFF /* Info, neutral */ + +/* Status Backgrounds (10% opacity variants) */ +--status-success-bg: rgba(61, 214, 140, 0.1) +--status-warning-bg: rgba(255, 184, 77, 0.1) +--status-error-bg: rgba(255, 107, 107, 0.1) +``` + +### Semantic Usage + +| Element | Color Token | Usage | +|---------|-------------|-------| +| Tree node - valid | `--status-success` | Checkmark icon | +| Tree node - warning | `--status-warning` | Warning icon | +| Tree node - error | `--status-error` | X icon | +| Tree node - modified | `--accent-primary` | Asterisk indicator | +| Input validation error | `--status-error` | Border + message | +| Save button (enabled) | `--accent-primary` | Background | +| Save button (disabled) | `--text-muted` | Background | + +## Typography + +### Font Stack + +``` +/* UI Chrome - Clean, professional */ +--font-ui: "IBM Plex Sans", -apple-system, sans-serif; + +/* Data Values - Monospace for alignment */ +--font-mono: "JetBrains Mono", "Fira Code", monospace; + +/* Section Headers - Slightly condensed */ +--font-heading: "IBM Plex Sans Condensed", sans-serif; +``` + +### Type Scale + +``` +/* Headings */ +--text-xl: 18px / 1.3 /* Panel titles */ +--text-lg: 15px / 1.4 /* Section headers */ +--text-base: 13px / 1.5 /* Body text, labels */ +--text-sm: 12px / 1.4 /* Hints, captions */ +--text-xs: 11px / 1.3 /* Status bar, badges */ + +/* Weights */ +--weight-normal: 400 +--weight-medium: 500 +--weight-semibold: 600 +``` + +### Typography Rules + +1. **Labels** use `--font-ui` at `--text-base`, `--weight-medium` +2. **Input values** use `--font-mono` at `--text-base` +3. **Tree node names** use `--font-ui` at `--text-base` +4. **Tooltips** use `--font-ui` at `--text-sm` +5. **Status bar** uses `--font-mono` at `--text-xs` + +## Spacing System + +``` +--space-xs: 4px +--space-sm: 8px +--space-md: 12px +--space-lg: 16px +--space-xl: 24px +--space-2xl: 32px +``` + +### Application + +| Element | Spacing | +|---------|---------| +| Panel padding | `--space-lg` | +| Form field gap | `--space-md` | +| Tree item indent | `--space-lg` | +| Button padding | `--space-sm` horizontal, `--space-xs` vertical | +| Section divider margin | `--space-xl` | + +## Component Specifications + +### 1. Menu Bar + +``` +Height: 28px +Background: --bg-surface +Border-bottom: 1px solid --border-subtle + +Menu Items: + Padding: 0 12px + Font: --font-ui, --text-sm, --weight-medium + Color: --text-secondary + Hover: --text-primary, background --bg-elevated + +Keyboard shortcut hints: + Color: --text-muted + Font: --font-mono, --text-xs +``` + +**Menu Structure:** +``` +File + ├─ Open Folder... Ctrl+O + ├─ Save Ctrl+S + ├─ Save As... Ctrl+Shift+S + ├─ ───────────────────── + ├─ Recent Folders → + └─ Exit Alt+F4 + +Edit + ├─ Undo Ctrl+Z + ├─ Redo Ctrl+Y + ├─ ───────────────────── + ├─ Cut Ctrl+X + ├─ Copy Ctrl+C + └─ Paste Ctrl+V + +Tools + ├─ Validate All F5 + ├─ Test Connection F6 + ├─ ───────────────────── + ├─ View Backups... + └─ Options... + +Help + ├─ Documentation F1 + ├─ Keyboard Shortcuts + ├─ ───────────────────── + └─ About ConfigManager +``` + +### 2. Toolbar + +``` +Height: 40px +Background: --bg-surface +Border-bottom: 1px solid --border-subtle +Padding: 0 --space-md + +Button Style: + Height: 28px + Padding: 0 --space-md + Border-radius: 4px + Gap between icon and text: --space-sm + + Default: + Background: transparent + Color: --text-secondary + + Hover: + Background: --bg-elevated + Color: --text-primary + + Active/Pressed: + Background: --bg-input + + Disabled: + Color: --text-muted + Opacity: 0.6 + +Separator: + Width: 1px + Height: 20px + Background: --border-subtle + Margin: 0 --space-sm +``` + +**Toolbar Layout:** +``` +[📁 Open] [💾 Save] | [↩ Undo] [↪ Redo] | [🔌 Test Connection] [✓ Validate] + [stretch] +``` + +### 3. Tree View Panel + +``` +Width: 280px (resizable, min 200px, max 400px) +Background: --bg-base +Border-right: 1px solid --border-subtle + +Header: + Height: 36px + Padding: 0 --space-lg + Background: --bg-surface + Font: --font-heading, --text-base, --weight-semibold + Text: "CONFIGURATION" + Letter-spacing: 0.5px + Color: --text-muted +``` + +**Tree Node Design:** + +``` +Standard Node: + Height: 32px + Padding-left: (depth * 16px) + 8px + + Layout: [Expand] [Icon] [Name] [Status] [Modified] + + Expand Arrow: + Size: 16x16 + Color: --text-muted + Rotation: 0° collapsed, 90° expanded + Transition: transform 150ms ease + + Icon: + Size: 16x16 + Margin-right: --space-sm + + Name: + Font: --font-ui, --text-base + Color: --text-primary + Flex: 1 + + Status Icon: + Size: 14x14 + Margin-left: --space-sm + ✓ = --status-success + ⚠ = --status-warning + ✗ = --status-error + + Modified Indicator: + Character: "*" + Color: --accent-primary + Font: --font-ui, --weight-semibold + Margin-left: --space-xs + +Hover State: + Background: --bg-elevated + +Selected State: + Background: rgba(92, 154, 255, 0.15) + Border-left: 2px solid --accent-primary + +Folder Node (Settings, Pipelines): + Font-weight: --weight-semibold + Icon: 📁 (closed) / 📂 (open) +``` + +**Tree Icons:** +``` +Settings Section Icons: + DataSync → ⟳ (sync arrows) + DataAccess → ⛁ (database) + Auth → 🔐 (lock) + Ldap → 👥 (users) + Connections → 🔌 (plug) + +Pipeline Icons: + Default → ⚡ (lightning bolt) + With errors → ⚡ with red dot overlay +``` + +### 4. Form Panel + +``` +Background: --bg-surface +Padding: --space-xl + +Header: + Font: --font-heading, --text-xl, --weight-semibold + Color: --text-primary + Margin-bottom: --space-lg + Padding-bottom: --space-md + Border-bottom: 1px solid --border-subtle +``` + +**Form Field Components:** + +``` +Text Input: + Height: 36px + Background: --bg-input + Border: 1px solid --border-default + Border-radius: 4px + Padding: 0 --space-md + Font: --font-mono, --text-base + Color: --text-primary + + Placeholder: + Color: --text-muted + + Focus: + Border-color: --border-focus + Box-shadow: 0 0 0 2px rgba(92, 154, 255, 0.2) + + Error: + Border-color: --status-error + Box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2) + + Disabled/ReadOnly: + Background: --bg-elevated + Color: --text-muted + Cursor: not-allowed + +Numeric Input: + Same as text input + Text-align: right + + Spin Buttons: + Width: 24px + Background: --bg-elevated + Border-left: 1px solid --border-default + +Checkbox: + Size: 18x18 + Border: 2px solid --border-default + Border-radius: 3px + + Checked: + Background: --accent-primary + Border-color: --accent-primary + Checkmark: --text-inverse + +Toggle Switch: + Width: 40px + Height: 22px + Background: --bg-input + Border-radius: 11px + + Thumb: + Size: 18x18 + Background: --text-secondary + + On State: + Background: --accent-primary + Thumb: --bg-base + +Dropdown/Select: + Same as text input + Padding-right: 36px (for arrow) + + Arrow: + Color: --text-muted + + Dropdown Menu: + Background: --bg-elevated + Border: 1px solid --border-default + Border-radius: 4px + Box-shadow: 0 4px 12px rgba(0,0,0,0.3) + + Menu Item: + Height: 32px + Padding: 0 --space-md + Hover: --bg-input + +TextArea (Query Editor): + Min-height: 120px + Font: --font-mono, --text-base + Line-height: 1.6 + Padding: --space-md + Resize: vertical + +Field Label: + Font: --font-ui, --text-sm, --weight-medium + Color: --text-secondary + Margin-bottom: --space-xs + + Required Indicator: + Character: "*" + Color: --status-error + Margin-left: 2px + +Field Hint: + Font: --font-ui, --text-xs + Color: --text-muted + Margin-top: --space-xs + +Validation Message: + Font: --font-ui, --text-xs + Color: --status-error + Margin-top: --space-xs + Icon: ⚠ inline before text +``` + +**Form Layouts:** + +``` +Single Column (default): + Max-width: 600px + Field margin-bottom: --space-lg + +Two Column: + Gap: --space-xl + Used for related pairs (e.g., Min/Max values) + +Section Grouping: + Background: --bg-base + Border: 1px solid --border-subtle + Border-radius: 6px + Padding: --space-lg + Margin-bottom: --space-xl + + Section Title: + Font: --font-ui, --text-base, --weight-semibold + Color: --text-primary + Margin-bottom: --space-md +``` + +### 5. Collapsible Sections (Pipeline Forms) + +``` +Container: + Background: --bg-base + Border: 1px solid --border-subtle + Border-radius: 6px + Margin-bottom: --space-md + +Header (clickable): + Height: 44px + Padding: 0 --space-lg + Cursor: pointer + Display: flex + Align-items: center + + Chevron: + Size: 16x16 + Color: --text-muted + Margin-right: --space-sm + Rotation: 0° collapsed, 90° expanded + Transition: transform 200ms ease + + Title: + Font: --font-ui, --text-base, --weight-semibold + Color: --text-primary + Flex: 1 + + Badge (item count): + Background: --bg-elevated + Color: --text-secondary + Font: --font-mono, --text-xs + Padding: 2px 8px + Border-radius: 10px + + Status: + Same as tree node status icons + +Content: + Padding: --space-lg + Border-top: 1px solid --border-subtle + + Animation: + Height transition: 200ms ease + Opacity: 0 → 1 over 150ms +``` + +### 6. Data Grid (Parameters, Columns Lists) + +``` +Container: + Border: 1px solid --border-subtle + Border-radius: 4px + Overflow: hidden + +Header Row: + Height: 32px + Background: --bg-elevated + Border-bottom: 1px solid --border-subtle + + Cell: + Padding: 0 --space-md + Font: --font-ui, --text-xs, --weight-semibold + Color: --text-muted + Text-transform: uppercase + Letter-spacing: 0.5px + +Data Row: + Height: 36px + Border-bottom: 1px solid --border-subtle + + Cell: + Padding: 0 --space-md + Font: --font-mono, --text-sm + Color: --text-primary + + Hover: + Background: --bg-elevated + + Selected: + Background: rgba(92, 154, 255, 0.1) + +Inline Edit: + Cell becomes editable input on double-click + Same styling as text input + +Add Row Button: + Height: 32px + Background: transparent + Border: 1px dashed --border-default + Color: --text-muted + Font: --font-ui, --text-sm + + Hover: + Border-color: --accent-primary + Color: --accent-primary + +Delete Button (per row): + Size: 24x24 + Color: --text-muted + Opacity on row hover only + + Hover: + Color: --status-error +``` + +### 7. Status Bar + +``` +Height: 24px +Background: --bg-surface +Border-top: 1px solid --border-subtle +Padding: 0 --space-md +Font: --font-mono, --text-xs + +Layout: [File Path] | [Modified] | [Validation] ───────── [Connection Status] + +File Path: + Color: --text-muted + Max-width: 400px + Overflow: ellipsis from start + +Modified Indicator: + Color: --accent-primary + Text: "Modified" (or hidden if clean) + +Validation Summary: + Clickable (opens validation panel) + + No issues: + Color: --status-success + Text: "✓ Valid" + + Warnings only: + Color: --status-warning + Text: "⚠ 3 warnings" + + Errors: + Color: --status-error + Text: "✗ 2 errors, 1 warning" + +Connection Status: + Color: --text-muted + Shows last test result: + "● Connected" (green dot) + "○ Not tested" (gray dot) + "● Failed" (red dot) +``` + +### 8. Dialogs + +``` +Overlay: + Background: rgba(0, 0, 0, 0.6) + Backdrop-filter: blur(2px) + +Dialog Container: + Background: --bg-surface + Border: 1px solid --border-subtle + Border-radius: 8px + Box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) + Min-width: 400px + Max-width: 600px + +Header: + Padding: --space-lg + Border-bottom: 1px solid --border-subtle + Font: --font-heading, --text-lg, --weight-semibold + +Content: + Padding: --space-xl + +Footer: + Padding: --space-lg + Border-top: 1px solid --border-subtle + Display: flex + Justify-content: flex-end + Gap: --space-sm +``` + +**Dialog Types:** + +``` +Diff Preview Dialog: + Width: 800px + + Diff View: + Font: --font-mono, --text-sm + Line-height: 1.5 + + Added line: + Background: rgba(61, 214, 140, 0.15) + Border-left: 3px solid --status-success + + Removed line: + Background: rgba(255, 107, 107, 0.15) + Border-left: 3px solid --status-error + + Context line: + Color: --text-muted + + Line numbers: + Color: --text-muted + Width: 50px + Text-align: right + Padding-right: --space-md + Border-right: 1px solid --border-subtle + +Validation Results Dialog: + Width: 600px + + Issue List: + Error item: + Border-left: 3px solid --status-error + Background: --status-error-bg + + Warning item: + Border-left: 3px solid --status-warning + Background: --status-warning-bg + + Item content: + Padding: --space-md + + Location link: + Color: --accent-primary + Cursor: pointer + Underline on hover +``` + +### 9. Buttons + +``` +Primary Button: + Height: 36px + Padding: 0 --space-lg + Background: --accent-primary + Color: --text-inverse + Border: none + Border-radius: 4px + Font: --font-ui, --text-base, --weight-medium + + Hover: + Background: --accent-hover + + Active: + Transform: scale(0.98) + + Disabled: + Background: --text-muted + Cursor: not-allowed + +Secondary Button: + Same dimensions + Background: transparent + Border: 1px solid --border-default + Color: --text-secondary + + Hover: + Background: --bg-elevated + Border-color: --text-muted + Color: --text-primary + +Danger Button: + Same as Primary + Background: --status-error + + Hover: + Background: #ff8585 + +Icon Button: + Size: 32x32 + Background: transparent + Border-radius: 4px + + Hover: + Background: --bg-elevated +``` + +### 10. Tooltips + +``` +Container: + Background: --bg-elevated + Border: 1px solid --border-default + Border-radius: 4px + Padding: --space-sm --space-md + Box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) + Max-width: 300px + +Text: + Font: --font-ui, --text-sm + Color: --text-primary + +Keyboard Shortcut: + Font: --font-mono, --text-xs + Color: --text-muted + Background: --bg-input + Padding: 2px 6px + Border-radius: 3px + Margin-left: --space-sm + +Delay: 500ms +Animation: Fade in 150ms +Position: Below element with 8px gap (flip if near edge) +``` + +## Screen Layouts + +### Main Window + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ File Edit Tools Help [—][□][×] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ [📁 Open] [💾 Save] │ [↩] [↪] │ [🔌 Test] [✓ Validate] │ +├────────────────────┬────────────────────────────────────────────────────┤ +│ CONFIGURATION │ │ +│ ────────────────── │ DataSync Options │ +│ │ ═══════════════════════════════════════════ │ +│ ▼ 📂 Settings │ │ +│ ├─ ⟳ DataSync ✓ │ ┌─ Sync Intervals ─────────────────────────────┐ │ +│ ├─ ⛁ DataAccess │ │ │ │ +│ ├─ 🔐 Auth │ │ Mass Sync Interval (minutes) │ │ +│ ├─ 👥 Ldap │ │ ┌──────────────────────────────────────┐ │ │ +│ └─ 🔌 Connections│ │ │ 1440 │ │ │ +│ │ │ └──────────────────────────────────────┘ │ │ +│ ▼ 📂 Pipelines │ │ ℹ Default: 1440 (24 hours) │ │ +│ ├─ ⚡ WorkOrder ✓│ │ │ │ +│ ├─ ⚡ Lot ✓ │ │ Daily Sync Interval (minutes) │ │ +│ ├─ ⚡ Item ⚠ │ │ ┌──────────────────────────────────────┐ │ │ +│ └─ ⚡ Operator * │ │ │ 60 │ │ │ +│ │ │ └──────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ │ Hourly Sync Interval (minutes) │ │ +│ │ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ │ 15 │ │ │ +│ │ │ └──────────────────────────────────────┘ │ │ +│ │ │ ⚠ Minimum: 15 minutes │ │ +│ │ └───────────────────────────────────────────────┘ │ +│ │ │ +│ │ ┌─ Data Retention ──────────────────────────────┐ │ +│ │ │ │ │ +│ │ │ Lookback Multiplier Retention Days │ │ +│ │ │ ┌─────────────────┐ ┌─────────────────┐│ │ +│ │ │ │ 1.5 │ │ 90 ││ │ +│ │ │ └─────────────────┘ └─────────────────┘│ │ +│ │ │ │ │ +│ │ └───────────────────────────────────────────────┘ │ +│ │ │ +│ │ [Reset] [Apply Changes] │ +├────────────────────┴────────────────────────────────────────────────────┤ +│ ~/projects/jdescoping/appsettings.json │ Modified │ ✓ Valid ● Connected │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Pipeline Form + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ WorkOrder Pipeline │ +│ ═══════════════════════════════════════════════════════════════════ │ +│ │ +│ ▼ Source ✓ Valid │ +│ ├──────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ Connection * │ │ +│ │ ┌────────────────────────────────────────────────────────┐ │ │ +│ │ │ JdeOracle ▼ │ │ │ +│ │ └────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Query │ │ +│ │ ┌────────────────────────────────────────────────────────┐ │ │ +│ │ │ SELECT │ │ │ +│ │ │ WLDOCO as OrderNumber, │ │ │ +│ │ │ WLDCTO as OrderType, │ │ │ +│ │ │ ... │ │ │ +│ │ └────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Parameters [+ Add] │ │ +│ │ ┌──────────────┬───────────────────────────────┬──────────┐ │ │ +│ │ │ NAME │ VALUE │ │ │ │ +│ │ ├──────────────┼───────────────────────────────┼──────────┤ │ │ +│ │ │ @StartDate │ {LastSync} │ 🗑 │ │ │ +│ │ │ @Company │ 00200 │ 🗑 │ │ │ +│ │ └──────────────┴───────────────────────────────┴──────────┘ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ▶ Schedules ⚠ 1 warning │ +│ ├──────────────────────────────────────────────────────────────────┤ │ +│ │ (collapsed) │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ▶ Destination ✓ Valid │ +│ ├──────────────────────────────────────────────────────────────────┤ │ +│ │ (collapsed) │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +### Diff Preview Dialog + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Preview Changes [×] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ appsettings.json │ +│ ─────────────────────────────────────────────────────────────────── │ +│ │ +│ 15 │ "MassSyncIntervalMinutes": 1440, │ +│ 16 │ "DailySyncIntervalMinutes": 60, │ +│ ▬ 17 │ - "HourlySyncIntervalMinutes": 30, │ +│ ▬ 17 │ + "HourlySyncIntervalMinutes": 15, │ +│ 18 │ "LookbackMultiplier": 1.5, │ +│ 19 │ "RetentionDays": 90 │ +│ │ +│ ─────────────────────────────────────────────────────────────────── │ +│ 1 file changed, 1 insertion, 1 deletion │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ [Cancel] [💾 Save Changes] │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Interaction Patterns + +### Keyboard Navigation + +| Shortcut | Action | +|----------|--------| +| `Ctrl+O` | Open folder | +| `Ctrl+S` | Save | +| `Ctrl+Z` | Undo | +| `Ctrl+Y` | Redo | +| `F5` | Validate all | +| `F6` | Test connection | +| `Tab` | Next field | +| `Shift+Tab` | Previous field | +| `↑/↓` | Navigate tree | +| `←/→` | Collapse/expand tree node | +| `Enter` | Select tree node / submit form | +| `Esc` | Cancel dialog / clear focus | + +### Drag & Drop + +- Tree nodes (Pipelines only) can be reordered via drag +- Visual indicator: Blue line showing drop position +- Constraint: Cannot drag into Settings node + +### Context Menus + +**Pipeline Node Context Menu:** +``` +┌─────────────────────┐ +│ ⚡ New Pipeline │ +│ 📋 Duplicate │ +│ ────────────────── │ +│ 🗑 Delete │ +└─────────────────────┘ +``` + +**Text Input Context Menu:** +``` +┌─────────────────────┐ +│ ✂ Cut Ctrl+X│ +│ 📋 Copy Ctrl+C│ +│ 📋 Paste Ctrl+V│ +│ ────────────────── │ +│ ⟲ Reset to default │ +└─────────────────────┘ +``` + +## Animation Specifications + +```css +/* Standard easing */ +--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1); +--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1); + +/* Durations */ +--duration-fast: 150ms; +--duration-normal: 200ms; +--duration-slow: 300ms; + +/* Specific animations */ +Tree expand/collapse: height --duration-normal --ease-out +Section expand/collapse: height --duration-normal --ease-out +Button hover: background --duration-fast --ease-out +Focus ring: box-shadow --duration-fast --ease-out +Dialog appear: opacity + scale(0.95→1) --duration-normal --ease-out +Tooltip appear: opacity --duration-fast --ease-out +Status icon change: color --duration-fast --ease-out +``` + +## Responsive Behavior + +**Minimum Window Size:** 900×600px + +**Panel Resizing:** +- Tree panel: min 200px, max 400px, default 280px +- Form panel: fills remaining space +- Resize handle: 4px wide, cursor: col-resize + +**Small Window Adaptations (< 1100px):** +- Toolbar labels hidden, icons only +- Form sections stack vertically +- Two-column form fields become single column + +## Accessibility + +### Focus Indicators +- All interactive elements have visible focus ring +- Focus ring: `0 0 0 2px var(--border-focus)` +- Tab order follows logical reading order + +### Screen Reader Support +- Tree nodes announce: name, type, validation state, expanded/collapsed +- Form fields have associated labels via `aria-labelledby` +- Validation errors announced via `aria-live="polite"` +- Status bar updates announced via `aria-live="polite"` + +### Color Contrast +- All text meets WCAG AA (4.5:1 for normal text) +- Status colors have accompanying icons, not color alone +- Focus indicators visible against all backgrounds + +## Implementation Notes + +### Avalonia-Specific Guidance + +**Theme Resources (App.axaml):** +```xml + + + + #0D0F12 + #151920 + + + + + + + +``` + +**Custom Control Templates:** +- Override default Fluent theme styles for matching dark theme +- Create custom `TreeViewItem` template for validation icons +- Create `CollapsibleSection` user control for pipeline forms + +**Font Loading:** +```xml + + avares://JdeScoping.ConfigManager/Assets/Fonts#IBM Plex Sans + avares://JdeScoping.ConfigManager/Assets/Fonts#JetBrains Mono + +``` + +### File References + +This design specification is referenced by: +- `docs/plans/2026-01-19-config-manager-design.md` - Implementation plan + +Related files: +- `NEW/src/Utils/JdeScoping.ConfigManager/` - Implementation location +- `NEW/src/JdeScoping.Core/Options/` - Options classes for form models diff --git a/docs/plans/2026-01-19-config-manager-design.md b/docs/plans/2026-01-19-config-manager-design.md new file mode 100644 index 0000000..5d949f2 --- /dev/null +++ b/docs/plans/2026-01-19-config-manager-design.md @@ -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 _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. diff --git a/docs/plans/2026-01-19-configmanager-implementation.md b/docs/plans/2026-01-19-configmanager-implementation.md new file mode 100644 index 0000000..29a6737 --- /dev/null +++ b/docs/plans/2026-01-19-configmanager-implementation.md @@ -0,0 +1,2484 @@ +# ConfigManager Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build an Avalonia desktop application that provides a GUI for editing `appsettings.json` and `pipelines.json` configuration files. + +**Architecture:** MVVM pattern with services layer. Tree view for navigation, dynamic forms for editing. File operations through abstracted `IFileSystem` for testability. Validation at schema and business-rule levels. + +**Tech Stack:** Avalonia 11.2, .NET 10, Microsoft.Extensions.DependencyInjection, DiffPlex, Serilog + +--- + +## Phase 1: Project Setup & Infrastructure + +### Task 1: Create Project Structure + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Program.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/App.axaml` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs` + +**Step 1: Create project file** + +```xml + + + WinExe + net10.0 + enable + enable + true + app.manifest + true + + + + + + + + + + + + + + + + + + + + + +``` + +**Step 2: Create Program.cs** + +```csharp +using Avalonia; + +namespace JdeScoping.ConfigManager; + +class Program +{ + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} +``` + +**Step 3: Create App.axaml** + +```xml + + + + + +``` + +**Step 4: Create App.axaml.cs** + +```csharp +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace JdeScoping.ConfigManager; + +public partial class App : Application +{ + public static IServiceProvider Services { get; private set; } = null!; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + var services = new ServiceCollection(); + ConfigureServices(services); + Services = services.BuildServiceProvider(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new Views.MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + + private void ConfigureServices(IServiceCollection services) + { + services.AddLogging(builder => builder + .AddConsole() + .SetMinimumLevel(LogLevel.Debug)); + } +} +``` + +**Step 5: Create app.manifest** + +```xml + + + + + + + + + + + +``` + +**Step 6: Verify project builds** + +Run: `dotnet build NEW/src/Utils/JdeScoping.ConfigManager/` +Expected: Build succeeded + +**Step 7: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/ +git commit -m "feat(configmanager): create initial project structure" +``` + +--- + +### Task 2: Create Test Project + +**Files:** +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/JdeScoping.ConfigManager.Tests.csproj` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/GlobalUsings.cs` + +**Step 1: Create test project file** + +```xml + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + +``` + +**Step 2: Create GlobalUsings.cs** + +```csharp +global using Xunit; +global using Shouldly; +global using NSubstitute; +``` + +**Step 3: Verify test project builds** + +Run: `dotnet build NEW/tests/JdeScoping.ConfigManager.Tests/` +Expected: Build succeeded + +**Step 4: Commit** + +```bash +git add NEW/tests/JdeScoping.ConfigManager.Tests/ +git commit -m "feat(configmanager): add test project" +``` + +--- + +### Task 3: Create IFileSystem Abstraction + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IFileSystem.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/FileSystem.cs` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/FileSystemTests.cs` + +**Step 1: Write the failing test** + +```csharp +namespace JdeScoping.ConfigManager.Tests.Services; + +public class FileSystemTests +{ + [Fact] + public void FileExists_WithExistingFile_ReturnsTrue() + { + // Arrange + var sut = new FileSystem(); + var tempFile = Path.GetTempFileName(); + + try + { + // Act + var result = sut.FileExists(tempFile); + + // Assert + result.ShouldBeTrue(); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public void FileExists_WithNonExistingFile_ReturnsFalse() + { + // Arrange + var sut = new FileSystem(); + + // Act + var result = sut.FileExists("/nonexistent/path/file.txt"); + + // Assert + result.ShouldBeFalse(); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "FileSystemTests"` +Expected: FAIL with "FileSystem not found" + +**Step 3: Create IFileSystem interface** + +```csharp +namespace JdeScoping.ConfigManager.Services; + +/// +/// Abstraction for file system operations to enable testing. +/// +public interface IFileSystem +{ + bool FileExists(string path); + bool DirectoryExists(string path); + Task ReadAllTextAsync(string path, CancellationToken ct = default); + Task WriteAllTextAsync(string path, string content, CancellationToken ct = default); + Task GetFilesAsync(string directory, string pattern, CancellationToken ct = default); + Task CopyFileAsync(string source, string destination, CancellationToken ct = default); + Task DeleteFileAsync(string path, CancellationToken ct = default); + string GetDirectoryName(string path); + string GetFileName(string path); + string GetFileNameWithoutExtension(string path); + string Combine(params string[] paths); +} +``` + +**Step 4: Create FileSystem implementation** + +```csharp +namespace JdeScoping.ConfigManager.Services; + +/// +/// Real file system implementation. +/// +public class FileSystem : IFileSystem +{ + public bool FileExists(string path) => File.Exists(path); + + public bool DirectoryExists(string path) => Directory.Exists(path); + + public async Task ReadAllTextAsync(string path, CancellationToken ct = default) + => await File.ReadAllTextAsync(path, ct); + + public async Task WriteAllTextAsync(string path, string content, CancellationToken ct = default) + => await File.WriteAllTextAsync(path, content, ct); + + public Task GetFilesAsync(string directory, string pattern, CancellationToken ct = default) + => Task.FromResult(Directory.GetFiles(directory, pattern)); + + public async Task CopyFileAsync(string source, string destination, CancellationToken ct = default) + { + var content = await File.ReadAllBytesAsync(source, ct); + await File.WriteAllBytesAsync(destination, content, ct); + } + + public Task DeleteFileAsync(string path, CancellationToken ct = default) + { + File.Delete(path); + return Task.CompletedTask; + } + + public string GetDirectoryName(string path) => Path.GetDirectoryName(path) ?? string.Empty; + + public string GetFileName(string path) => Path.GetFileName(path); + + public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path); + + public string Combine(params string[] paths) => Path.Combine(paths); +} +``` + +**Step 5: Run test to verify it passes** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "FileSystemTests"` +Expected: PASS + +**Step 6: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Services/ +git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/ +git commit -m "feat(configmanager): add IFileSystem abstraction" +``` + +--- + +### Task 4: Create ViewModelBase and Command Classes + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ViewModelBase.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/RelayCommand.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/AsyncRelayCommand.cs` + +**Step 1: Create ViewModelBase** + +```csharp +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace JdeScoping.ConfigManager.ViewModels; + +/// +/// Base class for all view models providing INotifyPropertyChanged implementation. +/// +public abstract class ViewModelBase : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return false; + + field = value; + OnPropertyChanged(propertyName); + return true; + } +} +``` + +**Step 2: Create RelayCommand** + +```csharp +using System.Windows.Input; + +namespace JdeScoping.ConfigManager.ViewModels; + +/// +/// A command implementation that delegates to action methods. +/// +public class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Predicate? _canExecute; + private EventHandler? _canExecuteChanged; + + public event EventHandler? CanExecuteChanged + { + add => _canExecuteChanged += value; + remove => _canExecuteChanged -= value; + } + + public RelayCommand(Action execute, Predicate? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public RelayCommand(Action execute, Func? canExecute = null) + : this(_ => execute(), canExecute != null ? _ => canExecute() : null) + { + } + + public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true; + + public void Execute(object? parameter) => _execute(parameter); + + public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); +} +``` + +**Step 3: Create AsyncRelayCommand** + +```csharp +using System.Windows.Input; + +namespace JdeScoping.ConfigManager.ViewModels; + +/// +/// An async command implementation that properly handles async operations. +/// +public class AsyncRelayCommand : ICommand +{ + private readonly Func _execute; + private readonly Func? _canExecute; + private bool _isExecuting; + private EventHandler? _canExecuteChanged; + + public event EventHandler? CanExecuteChanged + { + add => _canExecuteChanged += value; + remove => _canExecuteChanged -= value; + } + + public AsyncRelayCommand(Func execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true); + + public async void Execute(object? parameter) + { + if (!CanExecute(parameter)) return; + + _isExecuting = true; + RaiseCanExecuteChanged(); + + try + { + await _execute(); + } + finally + { + _isExecuting = false; + RaiseCanExecuteChanged(); + } + } + + public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); +} +``` + +**Step 4: Verify build** + +Run: `dotnet build NEW/src/Utils/JdeScoping.ConfigManager/` +Expected: Build succeeded + +**Step 5: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ +git commit -m "feat(configmanager): add MVVM base classes" +``` + +--- + +## Phase 2: Configuration Models + +### Task 5: Create Configuration Models + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Models/ScheduleModel.cs` + +**Step 1: Create ConfigModel** + +```csharp +using System.Text.Json.Serialization; + +namespace JdeScoping.ConfigManager.Models; + +/// +/// Root model for appsettings.json configuration. +/// +public class ConfigModel +{ + public DataSyncSection DataSync { get; set; } = new(); + public DataAccessSection DataAccess { get; set; } = new(); + public AuthSection Auth { get; set; } = new(); + public LdapSection Ldap { get; set; } = new(); + public SearchSection Search { get; set; } = new(); + public ExcelExportSection ExcelExport { get; set; } = new(); + public Dictionary ConnectionStrings { get; set; } = new(); +} + +public class DataSyncSection +{ + public TimeSpan CheckInterval { get; set; } = TimeSpan.FromMinutes(1); + public int MaxDegreeOfParallelism { get; set; } = 4; + public int BatchSize { get; set; } = 50000; + public int BulkCopyBatchSize { get; set; } = 5000; + public double LookbackMultiplier { get; set; } = 1.5; + public int PurgeRetentionDays { get; set; } = 90; + public int SyncTimeoutSeconds { get; set; } = 3600; + public bool Enabled { get; set; } = true; +} + +public class DataAccessSection +{ + public int DefaultTimeoutSeconds { get; set; } = 30; + public int LotUsageTimeoutSeconds { get; set; } = 120; + public int MisDataTimeoutSeconds { get; set; } = 300; + public string ProductionSchema { get; set; } = "prod"; + public string ArchiveSchema { get; set; } = "archive"; + public string StageSchema { get; set; } = "stage"; + public bool EnableDetailedLogging { get; set; } = false; +} + +public class AuthSection +{ + public string CookieName { get; set; } = ".JdeScoping.Auth"; + public int CookieExpirationMinutes { get; set; } = 480; +} + +public class LdapSection +{ + public string[] ServerUrls { get; set; } = []; + public string GroupDn { get; set; } = string.Empty; + public string SearchBase { get; set; } = string.Empty; + public int ConnectionTimeoutSeconds { get; set; } = 30; + public bool UseFakeAuth { get; set; } = false; + public string[] AdminBypassUsers { get; set; } = []; +} + +public class SearchSection +{ + public int MaxResultRows { get; set; } = 100000; + public int TimeoutSeconds { get; set; } = 300; + public int MaxConcurrentSearches { get; set; } = 5; +} + +public class ExcelExportSection +{ + public string CriteriaSheetPassword { get; set; } = string.Empty; + public string DataSheetPassword { get; set; } = string.Empty; + public int MaxRowsPerSheet { get; set; } = 1000000; + public string DefaultDateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss"; + public bool DebugWriteToFile { get; set; } = false; + public string DebugOutputDirectory { get; set; } = string.Empty; + public string TimezoneId { get; set; } = "America/Chicago"; + public string TimezoneAbbreviation { get; set; } = "CT"; +} +``` + +**Step 2: Create PipelineModel** + +```csharp +namespace JdeScoping.ConfigManager.Models; + +/// +/// Root model for pipelines.json configuration. +/// +public class PipelinesConfigModel +{ + public PipelineSettings Settings { get; set; } = new(); + public ScheduleDefaults ScheduleDefaults { get; set; } = new(); + public Dictionary Pipelines { get; set; } = new(); +} + +public class PipelineSettings +{ + public string Timezone { get; set; } = "UTC"; +} + +public class ScheduleDefaults +{ + public ScheduleModel Mass { get; set; } = new() { Enabled = true, IntervalMinutes = 10080, PrePurge = true, ReIndex = true }; + public ScheduleModel Daily { get; set; } = new() { Enabled = true, IntervalMinutes = 1440 }; + public ScheduleModel Hourly { get; set; } = new() { Enabled = true, IntervalMinutes = 60 }; +} + +public class PipelineModel +{ + public PipelineSource Source { get; set; } = new(); + public PipelineSchedules Schedules { get; set; } = new(); + public PipelineDestination Destination { get; set; } = new(); + public string[]? PostScripts { get; set; } +} + +public class PipelineSource +{ + public string Connection { get; set; } = string.Empty; + public string Query { get; set; } = string.Empty; + public string? MassQuery { get; set; } + public Dictionary Parameters { get; set; } = new(); +} + +public class ParameterDefinition +{ + public string Name { get; set; } = string.Empty; + public string? Format { get; set; } + public string? Source { get; set; } +} + +public class PipelineSchedules +{ + public ScheduleModel? Mass { get; set; } + public ScheduleModel? Daily { get; set; } + public ScheduleModel? Hourly { get; set; } +} + +public class PipelineDestination +{ + public string Table { get; set; } = string.Empty; + public string[] MatchColumns { get; set; } = []; + public string[] ExcludeFromUpdate { get; set; } = []; +} +``` + +**Step 3: Create ScheduleModel** + +```csharp +namespace JdeScoping.ConfigManager.Models; + +/// +/// Model for schedule configuration. +/// +public class ScheduleModel +{ + public bool Enabled { get; set; } = true; + public int IntervalMinutes { get; set; } = 60; + public bool PrePurge { get; set; } = false; + public bool ReIndex { get; set; } = false; +} +``` + +**Step 4: Verify build** + +Run: `dotnet build NEW/src/Utils/JdeScoping.ConfigManager/` +Expected: Build succeeded + +**Step 5: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Models/ +git commit -m "feat(configmanager): add configuration models" +``` + +--- + +### Task 6: Create ConfigFileService with Tests + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs` + +**Step 1: Write the failing test** + +```csharp +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.Tests.Services; + +public class ConfigFileServiceTests +{ + private readonly IFileSystem _fileSystem; + private readonly ConfigFileService _sut; + + public ConfigFileServiceTests() + { + _fileSystem = Substitute.For(); + _sut = new ConfigFileService(_fileSystem); + } + + [Fact] + public async Task LoadAppSettingsAsync_WithValidJson_ReturnsConfigModel() + { + // Arrange + var json = """ + { + "DataSync": { + "Enabled": true, + "MaxDegreeOfParallelism": 8 + } + } + """; + _fileSystem.ReadAllTextAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(json)); + + // Act + var result = await _sut.LoadAppSettingsAsync("/config/appsettings.json"); + + // Assert + result.ShouldNotBeNull(); + result.DataSync.Enabled.ShouldBeTrue(); + result.DataSync.MaxDegreeOfParallelism.ShouldBe(8); + } + + [Fact] + public async Task LoadAppSettingsAsync_WithInvalidJson_ThrowsWithHelpfulMessage() + { + // Arrange + var json = "{ invalid json }"; + _fileSystem.ReadAllTextAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(json)); + + // Act & Assert + var ex = await Should.ThrowAsync( + () => _sut.LoadAppSettingsAsync("/config/appsettings.json")); + ex.Message.ShouldContain("parse"); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "ConfigFileServiceTests"` +Expected: FAIL with "ConfigFileService not found" + +**Step 3: Create IConfigFileService interface** + +```csharp +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for loading and saving configuration files. +/// +public interface IConfigFileService +{ + Task LoadAppSettingsAsync(string path, CancellationToken ct = default); + Task LoadPipelinesAsync(string path, CancellationToken ct = default); + Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default); + Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default); +} +``` + +**Step 4: Create ConfigLoadException** + +```csharp +namespace JdeScoping.ConfigManager.Services; + +/// +/// Exception thrown when configuration file loading fails. +/// +public class ConfigLoadException : Exception +{ + public string FilePath { get; } + + public ConfigLoadException(string filePath, string message, Exception? inner = null) + : base(message, inner) + { + FilePath = filePath; + } +} +``` + +**Step 5: Create ConfigFileService** + +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; +using JdeScoping.ConfigManager.Models; +using Microsoft.Extensions.Logging; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for loading and saving configuration files. +/// +public class ConfigFileService : IConfigFileService +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger? _logger; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true + }; + + public ConfigFileService(IFileSystem fileSystem, ILogger? logger = null) + { + _fileSystem = fileSystem; + _logger = logger; + } + + public async Task LoadAppSettingsAsync(string path, CancellationToken ct = default) + { + _logger?.LogInformation("Loading appsettings from {Path}", path); + + try + { + var json = await _fileSystem.ReadAllTextAsync(path, ct); + var config = JsonSerializer.Deserialize(json, JsonOptions); + return config ?? new ConfigModel(); + } + catch (JsonException ex) + { + throw new ConfigLoadException(path, $"Failed to parse appsettings.json: {ex.Message}", ex); + } + } + + public async Task LoadPipelinesAsync(string path, CancellationToken ct = default) + { + _logger?.LogInformation("Loading pipelines from {Path}", path); + + try + { + var json = await _fileSystem.ReadAllTextAsync(path, ct); + var config = JsonSerializer.Deserialize(json, JsonOptions); + return config ?? new PipelinesConfigModel(); + } + catch (JsonException ex) + { + throw new ConfigLoadException(path, $"Failed to parse pipelines.json: {ex.Message}", ex); + } + } + + public async Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default) + { + _logger?.LogInformation("Saving appsettings to {Path}", path); + var json = JsonSerializer.Serialize(config, JsonOptions); + await _fileSystem.WriteAllTextAsync(path, json, ct); + } + + public async Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default) + { + _logger?.LogInformation("Saving pipelines to {Path}", path); + var json = JsonSerializer.Serialize(config, JsonOptions); + await _fileSystem.WriteAllTextAsync(path, json, ct); + } +} +``` + +**Step 6: Run test to verify it passes** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "ConfigFileServiceTests"` +Expected: PASS + +**Step 7: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Services/ +git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/ +git commit -m "feat(configmanager): add ConfigFileService with tests" +``` + +--- + +### Task 7: Create BackupService with Tests + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IBackupService.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/BackupService.cs` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/BackupServiceTests.cs` + +**Step 1: Write the failing test** + +```csharp +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.Tests.Services; + +public class BackupServiceTests +{ + private readonly IFileSystem _fileSystem; + private readonly BackupService _sut; + + public BackupServiceTests() + { + _fileSystem = Substitute.For(); + _sut = new BackupService(_fileSystem); + } + + [Fact] + public async Task CreateBackupAsync_CreatesTimestampedBackup() + { + // Arrange + var sourcePath = "/config/appsettings.json"; + _fileSystem.FileExists(sourcePath).Returns(true); + _fileSystem.GetDirectoryName(sourcePath).Returns("/config"); + _fileSystem.GetFileNameWithoutExtension(sourcePath).Returns("appsettings"); + + // Act + var backupPath = await _sut.CreateBackupAsync(sourcePath); + + // Assert + backupPath.ShouldStartWith("/config/appsettings."); + backupPath.ShouldEndWith(".bak"); + await _fileSystem.Received(1).CopyFileAsync(sourcePath, backupPath, Arg.Any()); + } + + [Fact] + public async Task CleanupOldBackupsAsync_KeepsOnlySpecifiedCount() + { + // Arrange + var filePath = "/config/appsettings.json"; + _fileSystem.GetDirectoryName(filePath).Returns("/config"); + _fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings"); + + var backups = Enumerable.Range(1, 15) + .Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak") + .ToArray(); + _fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any()) + .Returns(Task.FromResult(backups)); + + // Act + await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10); + + // Assert - should delete 5 oldest backups + await _fileSystem.Received(5).DeleteFileAsync(Arg.Any(), Arg.Any()); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "BackupServiceTests"` +Expected: FAIL with "BackupService not found" + +**Step 3: Create IBackupService interface** + +```csharp +namespace JdeScoping.ConfigManager.Services; + +/// +/// Represents backup file information. +/// +public class BackupInfo +{ + public required string Path { get; init; } + public required DateTime Timestamp { get; init; } + public required long Size { get; init; } +} + +/// +/// Service for managing configuration file backups. +/// +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); +} +``` + +**Step 4: Create BackupService** + +```csharp +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for managing configuration file backups. +/// +public class BackupService : IBackupService +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger? _logger; + private const string TimestampFormat = "yyyy-MM-dd_HHmmss"; + + public BackupService(IFileSystem fileSystem, ILogger? logger = null) + { + _fileSystem = fileSystem; + _logger = logger; + } + + public async Task CreateBackupAsync(string filePath, CancellationToken ct = default) + { + if (!_fileSystem.FileExists(filePath)) + throw new FileNotFoundException("Source file not found", filePath); + + var directory = _fileSystem.GetDirectoryName(filePath); + var baseName = _fileSystem.GetFileNameWithoutExtension(filePath); + var timestamp = DateTime.Now.ToString(TimestampFormat); + var backupPath = _fileSystem.Combine(directory, $"{baseName}.{timestamp}.bak"); + + await _fileSystem.CopyFileAsync(filePath, backupPath, ct); + _logger?.LogInformation("Created backup at {BackupPath}", backupPath); + + return backupPath; + } + + public async Task> GetBackupsAsync(string filePath, CancellationToken ct = default) + { + var directory = _fileSystem.GetDirectoryName(filePath); + var baseName = _fileSystem.GetFileNameWithoutExtension(filePath); + var pattern = $"{baseName}.*.bak"; + + var files = await _fileSystem.GetFilesAsync(directory, pattern, ct); + var backups = new List(); + + foreach (var file in files) + { + if (TryParseTimestamp(file, baseName, out var timestamp)) + { + backups.Add(new BackupInfo + { + Path = file, + Timestamp = timestamp, + Size = 0 // Would need file info for actual size + }); + } + } + + return backups.OrderByDescending(b => b.Timestamp).ToList(); + } + + public async Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default) + { + _logger?.LogInformation("Restoring backup from {BackupPath} to {TargetPath}", backupPath, targetPath); + await _fileSystem.CopyFileAsync(backupPath, targetPath, ct); + } + + public async Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default) + { + var backups = await GetBackupsAsync(filePath, ct); + var toDelete = backups.Skip(keepCount).ToList(); + + foreach (var backup in toDelete) + { + await _fileSystem.DeleteFileAsync(backup.Path, ct); + _logger?.LogInformation("Deleted old backup {BackupPath}", backup.Path); + } + } + + private bool TryParseTimestamp(string filePath, string baseName, out DateTime timestamp) + { + timestamp = default; + var fileName = _fileSystem.GetFileNameWithoutExtension(filePath); + + // Expected format: baseName.yyyy-MM-dd_HHmmss + var prefix = $"{baseName}."; + if (!fileName.StartsWith(prefix)) + return false; + + var timestampPart = fileName[prefix.Length..]; + return DateTime.TryParseExact(timestampPart, TimestampFormat, + CultureInfo.InvariantCulture, DateTimeStyles.None, out timestamp); + } +} +``` + +**Step 5: Run test to verify it passes** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "BackupServiceTests"` +Expected: PASS + +**Step 6: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Services/ +git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/ +git commit -m "feat(configmanager): add BackupService with tests" +``` + +--- + +## Phase 3: Validation Services + +### Task 8: Create ValidationService with Tests + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs` + +**Step 1: Write the failing test** + +```csharp +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.Tests.Services; + +public class ValidationServiceTests +{ + private readonly ValidationService _sut; + + public ValidationServiceTests() + { + _sut = new ValidationService(); + } + + [Fact] + public void ValidateAppSettings_WithValidConfig_ReturnsNoErrors() + { + // Arrange + var config = new ConfigModel + { + DataSync = new DataSyncSection { MaxDegreeOfParallelism = 4 } + }; + + // Act + var result = _sut.ValidateAppSettings(config); + + // Assert + result.IsValid.ShouldBeTrue(); + result.Errors.ShouldBeEmpty(); + } + + [Fact] + public void ValidateAppSettings_WithInvalidParallelism_ReturnsError() + { + // Arrange + var config = new ConfigModel + { + DataSync = new DataSyncSection { MaxDegreeOfParallelism = 0 } + }; + + // Act + var result = _sut.ValidateAppSettings(config); + + // Assert + result.IsValid.ShouldBeFalse(); + result.Errors.ShouldContain(e => e.Contains("MaxDegreeOfParallelism")); + } + + [Fact] + public void ValidatePipelines_WithDuplicateNames_ReturnsError() + { + // Arrange - duplicate keys not possible in dictionary, but empty names are invalid + var config = new PipelinesConfigModel + { + Pipelines = new Dictionary + { + [""] = new PipelineModel() + } + }; + + // Act + var result = _sut.ValidatePipelines(config); + + // Assert + result.IsValid.ShouldBeFalse(); + } + + [Fact] + public void ValidatePipelines_WithInvalidConnection_ReturnsError() + { + // Arrange + var config = new PipelinesConfigModel + { + Pipelines = new Dictionary + { + ["Test"] = new PipelineModel + { + Source = new PipelineSource { Connection = "invalid" } + } + } + }; + + // Act + var result = _sut.ValidatePipelines(config); + + // Assert + result.IsValid.ShouldBeFalse(); + result.Errors.ShouldContain(e => e.Contains("Connection")); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "ValidationServiceTests"` +Expected: FAIL with "ValidationService not found" + +**Step 3: Create IValidationService interface** + +```csharp +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Result of a validation operation. +/// +public class ValidationResult +{ + public bool IsValid => Errors.Count == 0; + public List Errors { get; } = []; + public List Warnings { get; } = []; + + public void AddError(string message) => Errors.Add(message); + public void AddWarning(string message) => Warnings.Add(message); +} + +/// +/// Service for validating configuration files. +/// +public interface IValidationService +{ + ValidationResult ValidateAppSettings(ConfigModel config); + ValidationResult ValidatePipelines(PipelinesConfigModel config); +} +``` + +**Step 4: Create ValidationService** + +```csharp +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for validating configuration files. +/// +public class ValidationService : IValidationService +{ + private static readonly string[] ValidConnections = ["jde", "cms", "giw", "lotfinderdb"]; + + public ValidationResult ValidateAppSettings(ConfigModel config) + { + var result = new ValidationResult(); + + // DataSync validation + if (config.DataSync.MaxDegreeOfParallelism < 1 || config.DataSync.MaxDegreeOfParallelism > 32) + result.AddError("DataSync.MaxDegreeOfParallelism must be between 1 and 32"); + + if (config.DataSync.BatchSize < 1000 || config.DataSync.BatchSize > 10_000_000) + result.AddError("DataSync.BatchSize must be between 1,000 and 10,000,000"); + + if (config.DataSync.BulkCopyBatchSize < 100 || config.DataSync.BulkCopyBatchSize > 100_000) + result.AddError("DataSync.BulkCopyBatchSize must be between 100 and 100,000"); + + if (config.DataSync.LookbackMultiplier < 1 || config.DataSync.LookbackMultiplier > 10) + result.AddError("DataSync.LookbackMultiplier must be between 1 and 10"); + + if (config.DataSync.PurgeRetentionDays < 1 || config.DataSync.PurgeRetentionDays > 365) + result.AddError("DataSync.PurgeRetentionDays must be between 1 and 365"); + + if (config.DataSync.SyncTimeoutSeconds < 60 || config.DataSync.SyncTimeoutSeconds > 86400) + result.AddError("DataSync.SyncTimeoutSeconds must be between 60 and 86,400"); + + // DataAccess validation + if (config.DataAccess.DefaultTimeoutSeconds < 1) + result.AddError("DataAccess.DefaultTimeoutSeconds must be at least 1"); + + // Ldap validation + if (config.Ldap.ConnectionTimeoutSeconds < 1 || config.Ldap.ConnectionTimeoutSeconds > 300) + result.AddError("Ldap.ConnectionTimeoutSeconds must be between 1 and 300"); + + // Search validation + if (config.Search.MaxResultRows < 1) + result.AddError("Search.MaxResultRows must be at least 1"); + + if (config.Search.MaxConcurrentSearches < 1) + result.AddError("Search.MaxConcurrentSearches must be at least 1"); + + return result; + } + + public ValidationResult ValidatePipelines(PipelinesConfigModel config) + { + var result = new ValidationResult(); + + foreach (var (name, pipeline) in config.Pipelines) + { + if (string.IsNullOrWhiteSpace(name)) + { + result.AddError("Pipeline name cannot be empty"); + continue; + } + + // Source validation + if (string.IsNullOrWhiteSpace(pipeline.Source.Connection)) + { + result.AddError($"Pipeline '{name}': Source.Connection is required"); + } + else if (!ValidConnections.Contains(pipeline.Source.Connection.ToLowerInvariant())) + { + result.AddError($"Pipeline '{name}': Source.Connection '{pipeline.Source.Connection}' is not valid. Must be one of: {string.Join(", ", ValidConnections)}"); + } + + if (string.IsNullOrWhiteSpace(pipeline.Source.Query)) + { + result.AddError($"Pipeline '{name}': Source.Query is required"); + } + + // Destination validation + if (string.IsNullOrWhiteSpace(pipeline.Destination.Table)) + { + result.AddError($"Pipeline '{name}': Destination.Table is required"); + } + + if (pipeline.Destination.MatchColumns.Length == 0) + { + result.AddWarning($"Pipeline '{name}': No MatchColumns specified - all rows will be inserted"); + } + + // Schedule validation + ValidateSchedule(result, name, "Mass", pipeline.Schedules.Mass, 60); + ValidateSchedule(result, name, "Daily", pipeline.Schedules.Daily, 60); + ValidateSchedule(result, name, "Hourly", pipeline.Schedules.Hourly, 15); + } + + return result; + } + + private void ValidateSchedule(ValidationResult result, string pipelineName, string scheduleName, ScheduleModel? schedule, int minInterval) + { + if (schedule == null) return; + + if (schedule.Enabled && schedule.IntervalMinutes < minInterval) + { + result.AddError($"Pipeline '{pipelineName}': {scheduleName} schedule interval must be at least {minInterval} minutes"); + } + } +} +``` + +**Step 5: Run test to verify it passes** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "ValidationServiceTests"` +Expected: PASS + +**Step 6: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Services/ +git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/ +git commit -m "feat(configmanager): add ValidationService with tests" +``` + +--- + +## Phase 4: Diff Service + +### Task 9: Create DiffService with Tests + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IDiffService.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/DiffService.cs` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/DiffServiceTests.cs` + +**Step 1: Write the failing test** + +```csharp +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.Tests.Services; + +public class DiffServiceTests +{ + private readonly DiffService _sut; + + public DiffServiceTests() + { + _sut = new DiffService(); + } + + [Fact] + public void GenerateDiff_WithNoChanges_ReturnsEmptyDiff() + { + // Arrange + var original = "line1\nline2\nline3"; + var modified = "line1\nline2\nline3"; + + // Act + var result = _sut.GenerateDiff(original, modified); + + // Assert + result.HasChanges.ShouldBeFalse(); + } + + [Fact] + public void GenerateDiff_WithChanges_ReturnsDiffLines() + { + // Arrange + var original = "line1\nline2\nline3"; + var modified = "line1\nmodified\nline3"; + + // Act + var result = _sut.GenerateDiff(original, modified); + + // Assert + result.HasChanges.ShouldBeTrue(); + result.Lines.ShouldNotBeEmpty(); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "DiffServiceTests"` +Expected: FAIL with "DiffService not found" + +**Step 3: Create IDiffService interface** + +```csharp +namespace JdeScoping.ConfigManager.Services; + +/// +/// Represents a line in a diff output. +/// +public class DiffLine +{ + public required int? OldLineNumber { get; init; } + public required int? NewLineNumber { get; init; } + public required string Text { get; init; } + public required DiffLineType Type { get; init; } +} + +public enum DiffLineType +{ + Unchanged, + Added, + Removed +} + +/// +/// Result of a diff operation. +/// +public class DiffResult +{ + public bool HasChanges { get; init; } + public List Lines { get; init; } = []; + public int Insertions { get; init; } + public int Deletions { get; init; } +} + +/// +/// Service for generating diffs between text content. +/// +public interface IDiffService +{ + DiffResult GenerateDiff(string original, string modified); +} +``` + +**Step 4: Create DiffService** + +```csharp +using DiffPlex; +using DiffPlex.DiffBuilder; +using DiffPlex.DiffBuilder.Model; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for generating diffs between text content. +/// +public class DiffService : IDiffService +{ + public DiffResult GenerateDiff(string original, string modified) + { + var diffBuilder = new InlineDiffBuilder(new Differ()); + var diff = diffBuilder.BuildDiffModel(original, modified); + + var lines = new List(); + int oldLineNum = 1; + int newLineNum = 1; + int insertions = 0; + int deletions = 0; + + foreach (var line in diff.Lines) + { + var diffLine = new DiffLine + { + Text = line.Text, + Type = line.Type switch + { + ChangeType.Inserted => DiffLineType.Added, + ChangeType.Deleted => DiffLineType.Removed, + _ => DiffLineType.Unchanged + }, + OldLineNumber = line.Type == ChangeType.Inserted ? null : oldLineNum, + NewLineNumber = line.Type == ChangeType.Deleted ? null : newLineNum + }; + + lines.Add(diffLine); + + switch (line.Type) + { + case ChangeType.Inserted: + newLineNum++; + insertions++; + break; + case ChangeType.Deleted: + oldLineNum++; + deletions++; + break; + default: + oldLineNum++; + newLineNum++; + break; + } + } + + return new DiffResult + { + HasChanges = insertions > 0 || deletions > 0, + Lines = lines, + Insertions = insertions, + Deletions = deletions + }; + } +} +``` + +**Step 5: Run test to verify it passes** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "DiffServiceTests"` +Expected: PASS + +**Step 6: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Services/ +git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/ +git commit -m "feat(configmanager): add DiffService with tests" +``` + +--- + +## Phase 5: Auto-Discovery Service + +### Task 10: Create AutoDiscoveryService with Tests + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IAutoDiscoveryService.cs` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/AutoDiscoveryService.cs` +- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/AutoDiscoveryServiceTests.cs` + +**Step 1: Write the failing test** + +```csharp +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.Tests.Services; + +public class AutoDiscoveryServiceTests +{ + private readonly IFileSystem _fileSystem; + private readonly AutoDiscoveryService _sut; + + public AutoDiscoveryServiceTests() + { + _fileSystem = Substitute.For(); + _sut = new AutoDiscoveryService(_fileSystem); + } + + [Fact] + public async Task FindConfigFolderAsync_WhenEnvVarSet_ReturnsEnvPath() + { + // Arrange + Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", "/custom/config"); + _fileSystem.DirectoryExists("/custom/config").Returns(true); + _fileSystem.FileExists("/custom/config/appsettings.json").Returns(true); + + try + { + // Act + var result = await _sut.FindConfigFolderAsync(); + + // Assert + result.ShouldBe("/custom/config"); + } + finally + { + Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", null); + } + } + + [Fact] + public async Task FindConfigFolderAsync_WhenNotFound_ReturnsNull() + { + // Arrange + Environment.SetEnvironmentVariable("JDESCOPING_CONFIG_PATH", null); + _fileSystem.DirectoryExists(Arg.Any()).Returns(false); + _fileSystem.FileExists(Arg.Any()).Returns(false); + + // Act + var result = await _sut.FindConfigFolderAsync(); + + // Assert + result.ShouldBeNull(); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "AutoDiscoveryServiceTests"` +Expected: FAIL with "AutoDiscoveryService not found" + +**Step 3: Create IAutoDiscoveryService interface** + +```csharp +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for auto-discovering configuration file locations. +/// +public interface IAutoDiscoveryService +{ + Task FindConfigFolderAsync(CancellationToken ct = default); +} +``` + +**Step 4: Create AutoDiscoveryService** + +```csharp +using Microsoft.Extensions.Logging; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for auto-discovering configuration file locations. +/// +public class AutoDiscoveryService : IAutoDiscoveryService +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger? _logger; + private const string EnvVarName = "JDESCOPING_CONFIG_PATH"; + private const string AppSettingsFileName = "appsettings.json"; + + public AutoDiscoveryService(IFileSystem fileSystem, ILogger? logger = null) + { + _fileSystem = fileSystem; + _logger = logger; + } + + public Task FindConfigFolderAsync(CancellationToken ct = default) + { + // 1. Check environment variable + var envPath = Environment.GetEnvironmentVariable(EnvVarName); + if (!string.IsNullOrEmpty(envPath) && IsValidConfigFolder(envPath)) + { + _logger?.LogInformation("Found config folder from environment variable: {Path}", envPath); + return Task.FromResult(envPath); + } + + // 2. Check same directory as executable + var exeDir = AppContext.BaseDirectory; + if (IsValidConfigFolder(exeDir)) + { + _logger?.LogInformation("Found config folder in executable directory: {Path}", exeDir); + return Task.FromResult(exeDir); + } + + // 3. Check ../JdeScoping.Host/ relative to executable + var hostDir = _fileSystem.Combine(exeDir, "..", "JdeScoping.Host"); + if (IsValidConfigFolder(hostDir)) + { + _logger?.LogInformation("Found config folder in host directory: {Path}", hostDir); + return Task.FromResult(hostDir); + } + + // 4. Check user config directory + var userConfigDir = GetUserConfigDirectory(); + if (userConfigDir != null && IsValidConfigFolder(userConfigDir)) + { + _logger?.LogInformation("Found config folder in user directory: {Path}", userConfigDir); + return Task.FromResult(userConfigDir); + } + + _logger?.LogWarning("Could not find config folder in any standard location"); + return Task.FromResult(null); + } + + private bool IsValidConfigFolder(string path) + { + if (!_fileSystem.DirectoryExists(path)) + return false; + + var appSettingsPath = _fileSystem.Combine(path, AppSettingsFileName); + return _fileSystem.FileExists(appSettingsPath); + } + + private string? GetUserConfigDirectory() + { + if (OperatingSystem.IsWindows()) + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + return _fileSystem.Combine(localAppData, "JdeScoping"); + } + else + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return _fileSystem.Combine(home, ".jdescoping"); + } + } +} +``` + +**Step 5: Run test to verify it passes** + +Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "AutoDiscoveryServiceTests"` +Expected: PASS + +**Step 6: Commit** + +```bash +git add NEW/src/Utils/JdeScoping.ConfigManager/Services/ +git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/ +git commit -m "feat(configmanager): add AutoDiscoveryService with tests" +``` + +--- + +## Phase 6: Basic UI Shell + +### Task 11: Create MainWindow View + +**Files:** +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml` +- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml.cs` + +**Step 1: Create MainWindow.axaml** + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +