` with monospace font
-- Copy to clipboard button
-- Basic SQL formatting (line breaks at clauses)
-
-### PipelineScheduleSection.razor
-
-- Props: ScheduleType (UpdateTypes), Config (PipelineScheduleDto), Query, OnViewQuery callback
-- Shows enabled badge
-- Settings table with Default/Override indicators
-- Query preview with "View Full" button
-
----
-
-## Implementation Notes
-
-### Override Detection (in API)
-
-```csharp
-bool IsOverride(T? pipelineValue, T defaultValue) where T : struct =>
- pipelineValue.HasValue && !pipelineValue.Value.Equals(defaultValue);
-```
-
-### Overdue Calculation (reuse existing)
-
-```csharp
-// Use DataUpdateRepository.IsOverdue() which includes 50% grace period
-var isOverdue = DataUpdateRepository.IsOverdue(
- lastSuccessfulRun, tableName, updateType, customIntervals);
-```
-
-### Connection Type Badge Colors
-
-| Connection | Badge Style |
-|------------|-------------|
-| jde | `BadgeStyle.Info` (blue) |
-| cms | `BadgeStyle.Success` (green) |
-| giw | `BadgeStyle.Warning` (orange) |
-
-### SQL Query Truncation
-
-```csharp
-static string Truncate(string? sql, int maxLength = 100) =>
- sql is null ? "" :
- sql.Length <= maxLength ? sql : sql[..maxLength] + "...";
-```
-
----
-
-## Security Considerations
-
-1. **Authorization**: Page and API require `[Authorize]`
-2. **SQL Visibility**: Full SQL/scripts only exposed if user has appropriate permissions (consider admin-only)
-3. **No write operations**: This is read-only, no mutations exposed
-
----
-
-## Dependencies
-
-- Radzen.Blazor (existing)
-- Existing `IEtlPipelineFactory` and `IDataUpdateRepository`
-- Existing `pipelines.json` configuration
-- Existing `UpdateTypes` enum
-- Existing `ApiClientBase` pattern
diff --git a/NEW/docs/plans/2026-01-07-pipeline-viewer-implementation.md b/NEW/docs/plans/2026-01-07-pipeline-viewer-implementation.md
deleted file mode 100644
index dec6b51..0000000
--- a/NEW/docs/plans/2026-01-07-pipeline-viewer-implementation.md
+++ /dev/null
@@ -1,1327 +0,0 @@
-# ETL Pipeline Viewer Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Implement the ETL Pipeline Viewer Blazor component with API backend.
-
-**Architecture:** API-first approach - build DTOs and API endpoints first, then client components.
-
-**Tech Stack:** .NET 10, ASP.NET Core API, Blazor WebAssembly, Radzen, Dapper
-
----
-
-## Task 1: Create Pipeline DTOs in Core
-
-**Files:**
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineListResponse.cs`
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineConfigDto.cs`
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineStatusDto.cs`
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineExecutionDto.cs`
-
-**Step 1: Create the Pipelines directory**
-
-```bash
-mkdir -p src/JdeScoping.Core/ApiContracts/Pipelines
-```
-
-**Step 2: Create PipelineListResponse.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-///
-/// Response containing list of available pipeline names.
-///
-public record PipelineListResponse(List PipelineNames);
-```
-
-**Step 3: Create PipelineConfigDto.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-///
-/// Pipeline configuration DTO for display.
-///
-public record PipelineConfigDto(
- string Name,
- PipelineSourceDto Source,
- PipelineDestinationDto Destination,
- PipelineSchedulesDto Schedules,
- int PreScriptCount,
- int PostScriptCount,
- List? PreScripts,
- List? PostScripts);
-
-public record PipelineSourceDto(
- string Connection,
- string? QueryPreview,
- string? MassQueryPreview,
- string? Query,
- string? MassQuery,
- List Parameters);
-
-public record PipelineParameterDto(
- string Name,
- string? Format,
- string Source);
-
-public record PipelineDestinationDto(
- string Table,
- string OperationType,
- List? MatchColumns,
- List? ExcludeFromUpdate);
-
-public record PipelineSchedulesDto(
- PipelineScheduleDto Mass,
- PipelineScheduleDto Daily,
- PipelineScheduleDto Hourly);
-
-public record PipelineScheduleDto(
- bool Enabled,
- int IntervalMinutes,
- bool PrePurge,
- bool ReIndex,
- bool IntervalIsOverride,
- bool PrePurgeIsOverride,
- bool ReIndexIsOverride);
-```
-
-**Step 4: Create PipelineStatusDto.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-using JdeScoping.Core.Models.Enums;
-
-///
-/// Pipeline schedule status for each update type.
-///
-public record PipelineStatusResponse(List Statuses);
-
-public record PipelineScheduleStatusDto(
- UpdateTypes ScheduleType,
- DateTime? LastRun,
- bool LastRunWasSuccessful,
- DateTime? LastSuccessfulRun,
- DateTime? NextRequiredRun,
- bool IsOverdue,
- int IntervalMinutes);
-```
-
-**Step 5: Create PipelineExecutionDto.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-using JdeScoping.Core.Models.Enums;
-
-///
-/// Pipeline execution history.
-///
-public record PipelineExecutionsResponse(List Executions);
-
-public record PipelineExecutionDto(
- UpdateTypes ScheduleType,
- DateTime StartTime,
- DateTime? EndTime,
- TimeSpan? Duration,
- long RecordCount,
- bool WasSuccessful);
-```
-
-**Step 6: Verify build**
-
-```bash
-dotnet build src/JdeScoping.Core
-```
-
-**Step 7: Commit**
-
-```bash
-git add src/JdeScoping.Core/ApiContracts/Pipelines/
-git commit -m "feat(core): add pipeline viewer DTOs"
-```
-
----
-
-## Task 2: Add Pipeline Routes to ApiRoutes
-
-**Files:**
-- Modify: `src/JdeScoping.Core/ApiContracts/ApiRoutes.cs`
-
-**Step 1: Add Pipelines routes class after FileIO class**
-
-Add at the end of `ApiRoutes.cs`, before the closing brace:
-
-```csharp
- ///
- /// Routes for pipeline configuration API endpoints.
- ///
- public static class Pipelines
- {
- /// Base route for pipeline endpoints.
- public const string Base = "api/pipelines";
-
- /// Route template for getting a pipeline by name.
- public const string ByName = "{name}";
-
- /// Route template for getting pipeline status.
- public const string Status = "{name}/status";
-
- /// Route template for getting pipeline executions.
- public const string Executions = "{name}/executions";
-
- /// Builds the route to get a specific pipeline config.
- public static string GetByName(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}";
-
- /// Builds the route to get pipeline status.
- public static string GetStatus(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}/status";
-
- /// Builds the route to get pipeline executions.
- public static string GetExecutions(string name, int count = 10) =>
- $"api/pipelines/{Uri.EscapeDataString(name)}/executions?count={count}";
- }
-```
-
-**Step 2: Verify build**
-
-```bash
-dotnet build src/JdeScoping.Core
-```
-
-**Step 3: Commit**
-
-```bash
-git add src/JdeScoping.Core/ApiContracts/ApiRoutes.cs
-git commit -m "feat(core): add pipeline API routes"
-```
-
----
-
-## Task 3: Add Repository Methods to IDataUpdateRepository
-
-**Files:**
-- Modify: `src/JdeScoping.DataSync/Contracts/IDataUpdateRepository.cs`
-
-**Step 1: Add new method signatures**
-
-Add these methods to the interface:
-
-```csharp
- ///
- /// Gets the last N execution records for a specific table.
- ///
- /// The table name to filter by.
- /// Optional update type filter. If null, returns all types.
- /// Maximum records to return (total, not per type).
- /// Cancellation token.
- /// List of DataUpdate records ordered by StartDt descending.
- Task> GetRecentUpdatesAsync(
- string tableName,
- UpdateTypes? updateType = null,
- int count = 30,
- CancellationToken cancellationToken = default);
-
- ///
- /// Gets the last run (successful or not) for each update type for a table.
- ///
- /// The table name.
- /// Cancellation token.
- /// Dictionary keyed by UpdateType.
- Task> GetLastRunsAsync(
- string tableName,
- CancellationToken cancellationToken = default);
-```
-
-**Step 2: Verify build**
-
-```bash
-dotnet build src/JdeScoping.DataSync
-```
-
-Expected: Build fails (not implemented yet)
-
-**Step 3: Commit interface changes**
-
-```bash
-git add src/JdeScoping.DataSync/Contracts/IDataUpdateRepository.cs
-git commit -m "feat(datasync): add repository methods for pipeline viewer"
-```
-
----
-
-## Task 4: Implement Repository Methods
-
-**Files:**
-- Modify: `src/JdeScoping.DataSync/Services/DataUpdateRepository.cs`
-
-**Step 1: Implement GetRecentUpdatesAsync**
-
-Add this method to `DataUpdateRepository`:
-
-```csharp
- ///
- public async Task> GetRecentUpdatesAsync(
- string tableName,
- UpdateTypes? updateType = null,
- int count = 30,
- CancellationToken cancellationToken = default)
- {
- var sql = updateType.HasValue
- ? @"SELECT TOP (@count) du.Id, du.SourceSystem, du.SourceData, du.TableName,
- du.StartDt, du.EndDt, du.UpdateType, du.WasSuccessful, du.NumberRecords
- FROM dbo.DataUpdate du
- WHERE du.TableName = @tableName AND du.UpdateType = @updateType
- ORDER BY du.StartDt DESC"
- : @"SELECT TOP (@count) du.Id, du.SourceSystem, du.SourceData, du.TableName,
- du.StartDt, du.EndDt, du.UpdateType, du.WasSuccessful, du.NumberRecords
- FROM dbo.DataUpdate du
- WHERE du.TableName = @tableName
- ORDER BY du.StartDt DESC";
-
- await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken);
- var results = await connection.QueryAsync(
- sql,
- new { tableName, updateType = updateType.HasValue ? (int)updateType.Value : 0, count },
- commandTimeout: 30);
-
- return results.ToList();
- }
-```
-
-**Step 2: Implement GetLastRunsAsync**
-
-Add this method to `DataUpdateRepository`:
-
-```csharp
- ///
- public async Task> GetLastRunsAsync(
- string tableName,
- CancellationToken cancellationToken = default)
- {
- const string sql = @"
-WITH LastRuns AS (
- SELECT du.*,
- ROW_NUMBER() OVER (PARTITION BY du.UpdateType ORDER BY du.StartDt DESC) AS RN
- FROM dbo.DataUpdate du
- WHERE du.TableName = @tableName
-)
-SELECT Id, SourceSystem, SourceData, TableName, StartDt, EndDt, UpdateType, WasSuccessful, NumberRecords
-FROM LastRuns
-WHERE RN = 1";
-
- await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken);
- var results = await connection.QueryAsync(sql, new { tableName }, commandTimeout: 30);
-
- return results.ToDictionary(du => du.UpdateType, du => du);
- }
-```
-
-**Step 3: Verify build**
-
-```bash
-dotnet build src/JdeScoping.DataSync
-```
-
-**Step 4: Run existing tests**
-
-```bash
-dotnet test tests/JdeScoping.DataSync.Tests --filter "DataUpdateRepository" --no-build
-```
-
-**Step 5: Commit**
-
-```bash
-git add src/JdeScoping.DataSync/Services/DataUpdateRepository.cs
-git commit -m "feat(datasync): implement GetRecentUpdatesAsync and GetLastRunsAsync"
-```
-
----
-
-## Task 5: Create PipelineController
-
-**Files:**
-- Create: `src/JdeScoping.Api/Controllers/PipelineController.cs`
-
-**Step 1: Create the controller**
-
-```csharp
-using JdeScoping.Core.ApiContracts;
-using JdeScoping.Core.ApiContracts.Pipelines;
-using JdeScoping.Core.Models.Enums;
-using JdeScoping.DataSync.Contracts;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-
-namespace JdeScoping.Api.Controllers;
-
-///
-/// API endpoints for pipeline configuration and status.
-///
-[ApiController]
-[Route(ApiRoutes.Pipelines.Base)]
-[Authorize]
-public class PipelineController : ControllerBase
-{
- private readonly IEtlPipelineFactory _pipelineFactory;
- private readonly IDataUpdateRepository _dataUpdateRepository;
-
- public PipelineController(
- IEtlPipelineFactory pipelineFactory,
- IDataUpdateRepository dataUpdateRepository)
- {
- _pipelineFactory = pipelineFactory;
- _dataUpdateRepository = dataUpdateRepository;
- }
-
- ///
- /// Gets list of all available pipeline names.
- ///
- [HttpGet]
- public ActionResult GetPipelineNames()
- {
- var names = _pipelineFactory.GetAvailableTables()
- .OrderBy(n => n)
- .ToList();
- return Ok(new PipelineListResponse(names));
- }
-
- ///
- /// Gets configuration for a specific pipeline.
- ///
- [HttpGet(ApiRoutes.Pipelines.ByName)]
- public ActionResult GetPipeline(string name)
- {
- var config = _pipelineFactory.GetPipelineConfig(name);
- if (config is null)
- return NotFound();
-
- var defaults = _pipelineFactory.GetScheduleDefaults();
- var dto = MapToDto(name, config, defaults);
- return Ok(dto);
- }
-
- ///
- /// Gets schedule status for a pipeline.
- ///
- [HttpGet(ApiRoutes.Pipelines.Status)]
- public async Task> GetStatus(
- string name,
- CancellationToken cancellationToken)
- {
- var config = _pipelineFactory.GetPipelineConfig(name);
- if (config is null)
- return NotFound();
-
- var tableName = config.Destination.Table;
- var lastRuns = await _dataUpdateRepository.GetLastRunsAsync(tableName, cancellationToken);
- var lastSuccessful = await _dataUpdateRepository.GetLastDataUpdatesAsync(cancellationToken);
- var defaults = _pipelineFactory.GetScheduleDefaults();
-
- var statuses = new List();
- foreach (var updateType in new[] { UpdateTypes.Mass, UpdateTypes.Daily, UpdateTypes.Hourly })
- {
- var scheduleConfig = GetScheduleConfig(config, updateType);
- var interval = GetEffectiveInterval(scheduleConfig, defaults, updateType);
-
- lastRuns.TryGetValue(updateType, out var lastRun);
- var successKey = $"{tableName}_{(int)updateType}";
- lastSuccessful.TryGetValue(successKey, out var lastSuccess);
-
- var nextRequired = lastSuccess?.EndDt.AddMinutes(interval);
- var isOverdue = DataUpdateRepository.IsOverdue(
- lastSuccess?.EndDt, tableName, updateType, null);
-
- statuses.Add(new PipelineScheduleStatusDto(
- updateType,
- lastRun?.StartDt,
- lastRun?.WasSuccessful ?? false,
- lastSuccess?.EndDt,
- nextRequired,
- isOverdue,
- interval));
- }
-
- return Ok(new PipelineStatusResponse(statuses));
- }
-
- ///
- /// Gets recent execution history for a pipeline.
- ///
- [HttpGet(ApiRoutes.Pipelines.Executions)]
- public async Task> GetExecutions(
- string name,
- [FromQuery] int count = 30,
- CancellationToken cancellationToken = default)
- {
- var config = _pipelineFactory.GetPipelineConfig(name);
- if (config is null)
- return NotFound();
-
- var tableName = config.Destination.Table;
- var updates = await _dataUpdateRepository.GetRecentUpdatesAsync(
- tableName, null, count, cancellationToken);
-
- var executions = updates.Select(u => new PipelineExecutionDto(
- u.UpdateType,
- u.StartDt,
- u.EndDt == default ? null : u.EndDt,
- u.EndDt == default ? null : u.EndDt - u.StartDt,
- u.NumberRecords,
- u.WasSuccessful
- )).ToList();
-
- return Ok(new PipelineExecutionsResponse(executions));
- }
-
- private PipelineConfigDto MapToDto(
- string name,
- DataSync.Configuration.PipelineConfig config,
- DataSync.Configuration.ScheduleDefaults defaults)
- {
- var source = new PipelineSourceDto(
- config.Source.Connection,
- Truncate(config.Source.Query),
- Truncate(config.Source.MassQuery),
- config.Source.Query,
- config.Source.MassQuery,
- config.Source.Parameters?.Select(p => new PipelineParameterDto(
- p.Key, p.Value.Format, p.Value.Source)).ToList() ?? []);
-
- var matchCols = config.Destination.MatchColumns?.ToList();
- var destination = new PipelineDestinationDto(
- config.Destination.Table,
- matchCols?.Count > 0 ? "BulkMerge" : "BulkImport",
- matchCols,
- config.Destination.ExcludeFromUpdate?.ToList());
-
- var schedules = new PipelineSchedulesDto(
- MapSchedule(config.Schedules?.Mass, defaults.Mass),
- MapSchedule(config.Schedules?.Daily, defaults.Daily),
- MapSchedule(config.Schedules?.Hourly, defaults.Hourly));
-
- return new PipelineConfigDto(
- name,
- source,
- destination,
- schedules,
- config.PreScripts?.Count ?? 0,
- config.PostScripts?.Count ?? 0,
- config.PreScripts,
- config.PostScripts);
- }
-
- private static PipelineScheduleDto MapSchedule(
- DataSync.Configuration.ScheduleConfig? config,
- DataSync.Configuration.ScheduleConfig defaults)
- {
- return new PipelineScheduleDto(
- config?.Enabled ?? defaults.Enabled ?? true,
- config?.IntervalMinutes ?? defaults.IntervalMinutes ?? 60,
- config?.PrePurge ?? defaults.PrePurge ?? false,
- config?.ReIndex ?? defaults.ReIndex ?? false,
- config?.IntervalMinutes.HasValue == true && config.IntervalMinutes != defaults.IntervalMinutes,
- config?.PrePurge.HasValue == true && config.PrePurge != defaults.PrePurge,
- config?.ReIndex.HasValue == true && config.ReIndex != defaults.ReIndex);
- }
-
- private static DataSync.Configuration.ScheduleConfig? GetScheduleConfig(
- DataSync.Configuration.PipelineConfig config,
- UpdateTypes updateType) => updateType switch
- {
- UpdateTypes.Mass => config.Schedules?.Mass,
- UpdateTypes.Daily => config.Schedules?.Daily,
- UpdateTypes.Hourly => config.Schedules?.Hourly,
- _ => null
- };
-
- private static int GetEffectiveInterval(
- DataSync.Configuration.ScheduleConfig? config,
- DataSync.Configuration.ScheduleDefaults defaults,
- UpdateTypes updateType)
- {
- if (config?.IntervalMinutes.HasValue == true)
- return config.IntervalMinutes.Value;
-
- return updateType switch
- {
- UpdateTypes.Mass => defaults.Mass?.IntervalMinutes ?? 10080,
- UpdateTypes.Daily => defaults.Daily?.IntervalMinutes ?? 1440,
- UpdateTypes.Hourly => defaults.Hourly?.IntervalMinutes ?? 60,
- _ => 60
- };
- }
-
- private static string? Truncate(string? value, int maxLength = 100) =>
- value is null ? null :
- value.Length <= maxLength ? value : value[..maxLength] + "...";
-}
-```
-
-**Step 2: Verify IEtlPipelineFactory has needed methods**
-
-Check if `GetPipelineConfig` and `GetScheduleDefaults` exist. If not, we need to add them.
-
-**Step 3: Build and fix any issues**
-
-```bash
-dotnet build src/JdeScoping.Api
-```
-
-**Step 4: Commit**
-
-```bash
-git add src/JdeScoping.Api/Controllers/PipelineController.cs
-git commit -m "feat(api): add PipelineController for pipeline viewer"
-```
-
----
-
-## Task 6: Add Missing Factory Methods (if needed)
-
-**Files:**
-- Modify: `src/JdeScoping.DataSync/Contracts/IEtlPipelineFactory.cs`
-- Modify: `src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs`
-
-**Step 1: Check if methods exist**
-
-Read `IEtlPipelineFactory.cs` and check for `GetPipelineConfig` and `GetScheduleDefaults`.
-
-**Step 2: If missing, add to interface**
-
-```csharp
- ///
- /// Gets the configuration for a specific pipeline.
- ///
- PipelineConfig? GetPipelineConfig(string tableName);
-
- ///
- /// Gets the schedule defaults.
- ///
- ScheduleDefaults GetScheduleDefaults();
-```
-
-**Step 3: Implement in EtlPipelineFactory**
-
-```csharp
- public PipelineConfig? GetPipelineConfig(string tableName)
- {
- return _config.Pipelines.TryGetValue(tableName, out var config) ? config : null;
- }
-
- public ScheduleDefaults GetScheduleDefaults()
- {
- return _config.ScheduleDefaults ?? new ScheduleDefaults(
- new ScheduleConfig { IntervalMinutes = 10080, PrePurge = true, ReIndex = true },
- new ScheduleConfig { IntervalMinutes = 1440 },
- new ScheduleConfig { IntervalMinutes = 60 });
- }
-```
-
-**Step 4: Build**
-
-```bash
-dotnet build src/JdeScoping.DataSync && dotnet build src/JdeScoping.Api
-```
-
-**Step 5: Commit**
-
-```bash
-git add src/JdeScoping.DataSync/
-git commit -m "feat(datasync): add GetPipelineConfig and GetScheduleDefaults to factory"
-```
-
----
-
-## Task 7: Create Client API Service
-
-**Files:**
-- Create: `src/JdeScoping.Client/Services/IPipelineApiClient.cs`
-- Create: `src/JdeScoping.Client/Services/PipelineApiClient.cs`
-
-**Step 1: Create IPipelineApiClient.cs**
-
-```csharp
-using JdeScoping.Core.ApiContracts.Pipelines;
-using JdeScoping.Core.ApiContracts.Results;
-
-namespace JdeScoping.Client.Services;
-
-///
-/// Client for pipeline configuration API.
-///
-public interface IPipelineApiClient
-{
- Task> GetPipelineNamesAsync(CancellationToken ct = default);
- Task> GetPipelineAsync(string name, CancellationToken ct = default);
- Task> GetStatusAsync(string name, CancellationToken ct = default);
- Task> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default);
-}
-```
-
-**Step 2: Create PipelineApiClient.cs**
-
-```csharp
-using JdeScoping.Core.ApiContracts;
-using JdeScoping.Core.ApiContracts.Pipelines;
-using JdeScoping.Core.ApiContracts.Results;
-
-namespace JdeScoping.Client.Services;
-
-///
-/// Client implementation for pipeline configuration API.
-///
-public class PipelineApiClient : ApiClientBase, IPipelineApiClient
-{
- public PipelineApiClient(HttpClient httpClient) : base(httpClient) { }
-
- public Task> GetPipelineNamesAsync(CancellationToken ct = default)
- => GetAsync(ApiRoutes.Pipelines.Base, ct);
-
- public Task> GetPipelineAsync(string name, CancellationToken ct = default)
- => GetAsync(ApiRoutes.Pipelines.GetByName(name), ct);
-
- public Task> GetStatusAsync(string name, CancellationToken ct = default)
- => GetAsync(ApiRoutes.Pipelines.GetStatus(name), ct);
-
- public Task> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default)
- => GetAsync(ApiRoutes.Pipelines.GetExecutions(name, count), ct);
-}
-```
-
-**Step 3: Register in DI (Program.cs)**
-
-Add to client DI registration:
-
-```csharp
-builder.Services.AddScoped();
-```
-
-**Step 4: Build**
-
-```bash
-dotnet build src/JdeScoping.Client
-```
-
-**Step 5: Commit**
-
-```bash
-git add src/JdeScoping.Client/Services/IPipelineApiClient.cs src/JdeScoping.Client/Services/PipelineApiClient.cs
-git commit -m "feat(client): add PipelineApiClient"
-```
-
----
-
-## Task 8: Create SqlQueryModal Component
-
-**Files:**
-- Create: `src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor`
-
-**Step 1: Create Admin directory**
-
-```bash
-mkdir -p src/JdeScoping.Client/Components/Admin
-```
-
-**Step 2: Create SqlQueryModal.razor**
-
-```razor
-@namespace JdeScoping.Client.Components.Admin
-
-
-
- @Title
-
-
-
- @FormattedSql
-
-
-
-
-
-
-
-
-@code {
- [Parameter] public bool Visible { get; set; }
- [Parameter] public EventCallback VisibleChanged { get; set; }
- [Parameter] public string? Title { get; set; }
- [Parameter] public string? Sql { get; set; }
-
- [Inject] private IJSRuntime JS { get; set; } = default!;
-
- private string FormattedSql => FormatSql(Sql);
-
- private static string FormatSql(string? sql)
- {
- if (string.IsNullOrWhiteSpace(sql))
- return "";
-
- // Basic SQL formatting - add line breaks before major clauses
- return sql
- .Replace(" SELECT ", "\nSELECT ")
- .Replace(" FROM ", "\nFROM ")
- .Replace(" WHERE ", "\nWHERE ")
- .Replace(" AND ", "\n AND ")
- .Replace(" OR ", "\n OR ")
- .Replace(" LEFT ", "\nLEFT ")
- .Replace(" RIGHT ", "\nRIGHT ")
- .Replace(" INNER ", "\nINNER ")
- .Replace(" OUTER ", "\nOUTER ")
- .Replace(" JOIN ", " JOIN\n ")
- .Replace(" ORDER BY ", "\nORDER BY ")
- .Replace(" GROUP BY ", "\nGROUP BY ")
- .Replace(" HAVING ", "\nHAVING ")
- .Trim();
- }
-
- private async Task CopyToClipboard()
- {
- if (!string.IsNullOrWhiteSpace(Sql))
- {
- await JS.InvokeVoidAsync("navigator.clipboard.writeText", Sql);
- }
- }
-
- private async Task Close()
- {
- await VisibleChanged.InvokeAsync(false);
- }
-}
-```
-
-**Step 3: Build**
-
-```bash
-dotnet build src/JdeScoping.Client
-```
-
-**Step 4: Commit**
-
-```bash
-git add src/JdeScoping.Client/Components/Admin/
-git commit -m "feat(client): add SqlQueryModal component"
-```
-
----
-
-## Task 9: Create PipelineScheduleSection Component
-
-**Files:**
-- Create: `src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor`
-
-**Step 1: Create PipelineScheduleSection.razor**
-
-```razor
-@namespace JdeScoping.Client.Components.Admin
-@using JdeScoping.Core.ApiContracts.Pipelines
-@using JdeScoping.Core.Models.Enums
-
-
-
-
-
- @GetScheduleTypeName(ScheduleType) Refresh
-
-
-
- @if (Config?.Enabled == true)
- {
-
- }
- else
- {
-
- }
-
-
-
- @if (Config is not null)
- {
- Schedule Settings
-
-
-
- Setting
- Value
- Source
-
-
-
-
- Interval
- @FormatInterval(Config.IntervalMinutes)
- @(Config.IntervalIsOverride ? "Override" : "Default")
-
-
- Pre-Purge
- @(Config.PrePurge ? "Yes" : "No")
- @(Config.PrePurgeIsOverride ? "Override" : "Default")
-
-
- Re-Index
- @(Config.ReIndex ? "Yes" : "No")
- @(Config.ReIndexIsOverride ? "Override" : "Default")
-
-
-
-
- @if (!string.IsNullOrWhiteSpace(QueryPreview))
- {
- Query
-
- @QueryPreview
-
- @if (!string.IsNullOrWhiteSpace(FullQuery))
- {
-
- }
- }
- }
-
-
-@code {
- [Parameter] public UpdateTypes ScheduleType { get; set; }
- [Parameter] public PipelineScheduleDto? Config { get; set; }
- [Parameter] public string? QueryPreview { get; set; }
- [Parameter] public string? FullQuery { get; set; }
- [Parameter] public EventCallback OnViewQuery { get; set; }
-
- private static string GetScheduleTypeName(UpdateTypes type) => type switch
- {
- UpdateTypes.Mass => "Mass",
- UpdateTypes.Daily => "Daily",
- UpdateTypes.Hourly => "Hourly",
- _ => type.ToString()
- };
-
- private static string FormatInterval(int minutes)
- {
- if (minutes >= 1440)
- return $"{minutes / 1440} day(s) ({minutes} min)";
- if (minutes >= 60)
- return $"{minutes / 60} hour(s) ({minutes} min)";
- return $"{minutes} minutes";
- }
-}
-```
-
-**Step 2: Build**
-
-```bash
-dotnet build src/JdeScoping.Client
-```
-
-**Step 3: Commit**
-
-```bash
-git add src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor
-git commit -m "feat(client): add PipelineScheduleSection component"
-```
-
----
-
-## Task 10: Create PipelineViewer Page
-
-**Files:**
-- Create: `src/JdeScoping.Client/Pages/Admin/PipelineViewer.razor`
-
-**Step 1: Create Admin directory**
-
-```bash
-mkdir -p src/JdeScoping.Client/Pages/Admin
-```
-
-**Step 2: Create PipelineViewer.razor**
-
-```razor
-@page "/admin/pipeline-viewer"
-@attribute [Authorize]
-@using JdeScoping.Client.Components.Admin
-@using JdeScoping.Client.Components.Shared
-@using JdeScoping.Core.ApiContracts.Pipelines
-@using JdeScoping.Core.Models.Enums
-@inject IPipelineApiClient PipelineApi
-
-Pipeline Configuration Viewer - JDE Scoping Tool
-
-ETL Pipeline Configuration Viewer
-
-
-
-
-
-
-
-
-@if (_isLoading)
-{
-
-}
-else if (_config is not null)
-{
-
-
- Schedule Status Summary
-
-
-
- @item.ScheduleType.ToString()
-
-
- @(item.LastRun?.ToString("MM/dd/yyyy hh:mm tt") ?? "Never")
-
-
-
- @if (item.LastRun.HasValue)
- {
- @if (item.LastRunWasSuccessful)
- {
-
- }
- else
- {
-
- }
- }
-
-
-
- @(item.NextRequiredRun?.ToString("MM/dd/yyyy hh:mm tt") ?? "N/A")
-
-
-
- @if (item.IsOverdue)
- {
-
- }
- else
- {
-
- }
-
-
-
-
-
-
-
-
- Recent Execution History
-
-
-
- @item.ScheduleType.ToString()
-
-
- @item.StartTime.ToString("MM/dd/yyyy hh:mm tt")
-
-
- @(item.EndTime?.ToString("MM/dd/yyyy hh:mm tt") ?? "In Progress")
-
-
- @FormatDuration(item.Duration)
-
-
- @item.RecordCount.ToString("N0")
-
-
-
- @if (item.WasSuccessful)
- {
-
- }
- else
- {
-
- }
-
-
-
-
-
-
-
-
-
-
-
- Source
- Connection:
- @switch (_config.Source.Connection.ToLower())
- {
- case "jde":
-
- break;
- case "cms":
-
- break;
- case "giw":
-
- break;
- default:
-
- break;
- }
-
- @if (_config.Source.Parameters.Count > 0)
- {
- Parameters:
-
- @foreach (var param in _config.Source.Parameters)
- {
- - @param.Name (@(param.Format ?? "default"))
- }
-
- }
-
-
-
-
-
-
- Destination
- Table: @_config.Destination.Table
- Operation:
-
-
- @if (_config.Destination.MatchColumns?.Count > 0)
- {
- Match Columns: @string.Join(", ", _config.Destination.MatchColumns)
- }
- @if (_config.Destination.ExcludeFromUpdate?.Count > 0)
- {
- Exclude: @string.Join(", ", _config.Destination.ExcludeFromUpdate)
- }
-
-
-
-
-
-
- Scripts
- Pre-Scripts: @_config.PreScriptCount
- Post-Scripts: @_config.PostScriptCount
- @if (_config.PreScripts?.Count > 0)
- {
- Pre-Scripts:
- @for (int i = 0; i < _config.PreScripts.Count; i++)
- {
- var script = _config.PreScripts[i];
- var index = i + 1;
-
-
-
- }
- }
- @if (_config.PostScripts?.Count > 0)
- {
- Post-Scripts:
- @for (int i = 0; i < _config.PostScripts.Count; i++)
- {
- var script = _config.PostScripts[i];
- var index = i + 1;
-
-
-
- }
- }
-
-
-
-
-
-
-
-
-
-
-}
-
-
-
-@code {
- private List _pipelineNames = [];
- private string? _selectedPipeline;
- private bool _isLoading;
- private PipelineConfigDto? _config;
- private List _statuses = [];
- private List _executions = [];
-
- private bool _showSqlModal;
- private string? _sqlModalTitle;
- private string? _sqlModalContent;
-
- protected override async Task OnInitializedAsync()
- {
- var result = await PipelineApi.GetPipelineNamesAsync();
- if (result.TryGetValue(out var response))
- {
- _pipelineNames = response.PipelineNames;
- }
- }
-
- private async Task OnPipelineChanged()
- {
- if (string.IsNullOrWhiteSpace(_selectedPipeline))
- {
- _config = null;
- _statuses = [];
- _executions = [];
- return;
- }
-
- _isLoading = true;
- try
- {
- var configTask = PipelineApi.GetPipelineAsync(_selectedPipeline);
- var statusTask = PipelineApi.GetStatusAsync(_selectedPipeline);
- var executionsTask = PipelineApi.GetExecutionsAsync(_selectedPipeline);
-
- await Task.WhenAll(configTask, statusTask, executionsTask);
-
- if (configTask.Result.TryGetValue(out var config))
- _config = config;
-
- if (statusTask.Result.TryGetValue(out var status))
- _statuses = status.Statuses;
-
- if (executionsTask.Result.TryGetValue(out var execs))
- _executions = execs.Executions;
- }
- finally
- {
- _isLoading = false;
- }
- }
-
- private void ShowSqlModal(string title, string sql)
- {
- _sqlModalTitle = $"{title} - {_selectedPipeline}";
- _sqlModalContent = sql;
- _showSqlModal = true;
- }
-
- private static string FormatDuration(TimeSpan? duration)
- {
- if (!duration.HasValue) return "-";
- var d = duration.Value;
- if (d.TotalHours >= 1) return $"{d.Hours}h {d.Minutes}m";
- if (d.TotalMinutes >= 1) return $"{d.Minutes}m {d.Seconds}s";
- return $"{d.Seconds}s";
- }
-}
-```
-
-**Step 3: Build**
-
-```bash
-dotnet build src/JdeScoping.Client
-```
-
-**Step 4: Commit**
-
-```bash
-git add src/JdeScoping.Client/Pages/Admin/
-git commit -m "feat(client): add PipelineViewer page"
-```
-
----
-
-## Task 11: Register Services and Build Full Solution
-
-**Files:**
-- Modify: `src/JdeScoping.Client/Program.cs`
-
-**Step 1: Register PipelineApiClient**
-
-Add to Program.cs DI section:
-
-```csharp
-builder.Services.AddScoped();
-```
-
-**Step 2: Build full solution**
-
-```bash
-dotnet build
-```
-
-**Step 3: Run tests**
-
-```bash
-dotnet test
-```
-
-**Step 4: Commit**
-
-```bash
-git add src/JdeScoping.Client/Program.cs
-git commit -m "feat(client): register PipelineApiClient in DI"
-```
-
----
-
-## Task 12: Add Navigation Link (Optional)
-
-**Files:**
-- Modify: `src/JdeScoping.Client/Layout/MainLayout.razor` (or NavMenu)
-
-**Step 1: Add link to admin section**
-
-Find navigation and add:
-
-```razor
-
-```
-
-**Step 2: Build and test manually**
-
-```bash
-dotnet run --project src/JdeScoping.Host
-```
-
-Navigate to `/admin/pipeline-viewer` in browser.
-
-**Step 3: Commit**
-
-```bash
-git add src/JdeScoping.Client/
-git commit -m "feat(client): add pipeline viewer to navigation"
-```
-
----
-
-## Final Verification
-
-```bash
-# Build
-dotnet build
-
-# Run tests
-dotnet test
-
-# Start application
-dotnet run --project src/JdeScoping.Host
-```
-
-Navigate to `/admin/pipeline-viewer`, select a pipeline, and verify:
-- Pipeline list loads
-- Status summary shows
-- Execution history shows
-- Configuration cards display
-- Schedule sections show with Default/Override indicators
-- SQL modal works with copy button
diff --git a/NEW/docs/plans/2026-01-21-remove-pipeline-viewer.md b/NEW/docs/plans/2026-01-21-remove-pipeline-viewer.md
new file mode 100644
index 0000000..27383e9
--- /dev/null
+++ b/NEW/docs/plans/2026-01-21-remove-pipeline-viewer.md
@@ -0,0 +1,329 @@
+# Remove Pipeline Viewer Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Remove the pipeline viewer feature from the web UI and clean up all supporting code (controllers, DTOs, client services, routes).
+
+**Architecture:** Delete files in dependency order (DTOs/interfaces first would break compilation), so we delete leaf components first (views, client), then API layer, then shared contracts/DTOs. Modify DI registrations and navigation.
+
+**Tech Stack:** Blazor WebAssembly, ASP.NET Core API, C#
+
+---
+
+## Task 1: Remove Client Components (Blazor Pages)
+
+**Files:**
+- Delete: `src/JdeScoping.Client/Pages/Admin/PipelineViewer.razor`
+- Delete: `src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor`
+- Delete: `src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor`
+
+**Step 1: Delete PipelineViewer.razor**
+
+```bash
+rm src/JdeScoping.Client/Pages/Admin/PipelineViewer.razor
+```
+
+**Step 2: Delete PipelineScheduleSection.razor**
+
+```bash
+rm src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor
+```
+
+**Step 3: Delete SqlQueryModal.razor (if exists)**
+
+```bash
+rm -f src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor
+```
+
+**Step 4: Verify files deleted**
+
+```bash
+ls src/JdeScoping.Client/Pages/Admin/ | grep -i pipeline || echo "No pipeline pages remain"
+ls src/JdeScoping.Client/Components/Admin/ | grep -i pipeline || echo "No pipeline components remain"
+```
+
+Expected: No pipeline-related files in Admin folders.
+
+---
+
+## Task 2: Remove Navigation Link
+
+**Files:**
+- Modify: `src/JdeScoping.Client/Layout/MainLayout.razor:16`
+
+**Step 1: Remove the pipeline viewer nav link**
+
+In `MainLayout.razor`, delete this line (around line 16):
+```razor
+Pipeline Viewer
+```
+
+**Step 2: Verify the change**
+
+```bash
+grep -n "pipeline-viewer" src/JdeScoping.Client/Layout/MainLayout.razor || echo "Nav link removed"
+```
+
+Expected: "Nav link removed"
+
+---
+
+## Task 3: Remove Client Service
+
+**Files:**
+- Delete: `src/JdeScoping.Client/Services/PipelineApiClient.cs`
+- Modify: `src/JdeScoping.Client/Program.cs:58`
+
+**Step 1: Delete PipelineApiClient.cs**
+
+```bash
+rm src/JdeScoping.Client/Services/PipelineApiClient.cs
+```
+
+**Step 2: Remove DI registration from Program.cs**
+
+In `src/JdeScoping.Client/Program.cs`, delete this line (around line 58):
+```csharp
+builder.Services.AddScoped();
+```
+
+**Step 3: Verify the changes**
+
+```bash
+grep -n "PipelineApiClient" src/JdeScoping.Client/Program.cs || echo "DI registration removed"
+ls src/JdeScoping.Client/Services/PipelineApiClient.cs 2>/dev/null || echo "File deleted"
+```
+
+Expected: Both checks show removal confirmed.
+
+---
+
+## Task 4: Remove API Controller and Mapper
+
+**Files:**
+- Delete: `src/JdeScoping.Api/Controllers/PipelineController.cs`
+- Delete: `src/JdeScoping.Api/Mapping/PipelineMapper.cs`
+- Delete: `src/JdeScoping.Api/Mapping/IPipelineMapper.cs`
+- Modify: `src/JdeScoping.Api/DependencyInjection.cs:43`
+
+**Step 1: Delete PipelineController.cs**
+
+```bash
+rm src/JdeScoping.Api/Controllers/PipelineController.cs
+```
+
+**Step 2: Delete PipelineMapper.cs**
+
+```bash
+rm src/JdeScoping.Api/Mapping/PipelineMapper.cs
+```
+
+**Step 3: Delete IPipelineMapper.cs**
+
+```bash
+rm src/JdeScoping.Api/Mapping/IPipelineMapper.cs
+```
+
+**Step 4: Remove DI registration from DependencyInjection.cs**
+
+In `src/JdeScoping.Api/DependencyInjection.cs`, delete this line (around line 43):
+```csharp
+services.AddSingleton();
+```
+
+**Step 5: Verify the changes**
+
+```bash
+ls src/JdeScoping.Api/Controllers/PipelineController.cs 2>/dev/null || echo "Controller deleted"
+ls src/JdeScoping.Api/Mapping/*Pipeline* 2>/dev/null || echo "Mapper files deleted"
+grep -n "PipelineMapper" src/JdeScoping.Api/DependencyInjection.cs || echo "DI registration removed"
+```
+
+Expected: All three checks confirm removal.
+
+---
+
+## Task 5: Remove Core DTOs and Interface
+
+**Files:**
+- Delete: `src/JdeScoping.Core/Models/Pipelines/` (entire folder)
+- Delete: `src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs`
+
+**Step 1: Delete the Pipelines DTO folder**
+
+```bash
+rm -rf src/JdeScoping.Core/Models/Pipelines/
+```
+
+**Step 2: Delete IPipelineApiClient.cs**
+
+```bash
+rm src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs
+```
+
+**Step 3: Verify deletions**
+
+```bash
+ls src/JdeScoping.Core/Models/Pipelines/ 2>/dev/null || echo "Pipelines folder deleted"
+ls src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs 2>/dev/null || echo "Interface deleted"
+```
+
+Expected: Both checks confirm deletion.
+
+---
+
+## Task 6: Remove API Routes
+
+**Files:**
+- Modify: `src/JdeScoping.Core/ApiContracts/ApiRoutes.cs:155-188`
+
+**Step 1: Remove the Pipelines class from ApiRoutes.cs**
+
+Delete the entire `Pipelines` static class (lines 155-188):
+```csharp
+ ///
+ /// Routes for pipeline configuration API endpoints.
+ ///
+ public static class Pipelines
+ {
+ /// Base route for pipeline endpoints.
+ public const string Base = "api/pipelines";
+
+ /// Route template for getting a pipeline by name.
+ public const string ByName = "{name}";
+
+ /// Route template for getting pipeline status.
+ public const string Status = "{name}/status";
+
+ /// Route template for getting pipeline executions.
+ 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}";
+ }
+```
+
+**Step 2: Verify the change**
+
+```bash
+grep -n "Pipelines" src/JdeScoping.Core/ApiContracts/ApiRoutes.cs || echo "Pipelines routes removed"
+```
+
+Expected: "Pipelines routes removed"
+
+---
+
+## Task 7: Delete Plan Documents
+
+**Files:**
+- Delete: `docs/plans/2026-01-07-pipeline-viewer-design.md`
+- Delete: `docs/plans/2026-01-07-pipeline-viewer-implementation.md`
+
+**Step 1: Delete design document**
+
+```bash
+rm docs/plans/2026-01-07-pipeline-viewer-design.md
+```
+
+**Step 2: Delete implementation plan**
+
+```bash
+rm docs/plans/2026-01-07-pipeline-viewer-implementation.md
+```
+
+**Step 3: Verify deletions**
+
+```bash
+ls docs/plans/*pipeline-viewer* 2>/dev/null || echo "Plan documents deleted"
+```
+
+Expected: "Plan documents deleted"
+
+---
+
+## Task 8: Build and Verify
+
+**Step 1: Build the solution**
+
+```bash
+cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && dotnet build
+```
+
+Expected: Build succeeds with no errors.
+
+**Step 2: Search for any remaining pipeline viewer references**
+
+```bash
+grep -r "PipelineViewer\|PipelineApiClient\|IPipelineApiClient\|PipelineController\|IPipelineMapper\|PipelineMapper\|pipeline-viewer" src/ --include="*.cs" --include="*.razor" || echo "No references remain"
+```
+
+Expected: "No references remain"
+
+**Step 3: Run tests**
+
+```bash
+dotnet test
+```
+
+Expected: All tests pass.
+
+---
+
+## Task 9: Commit
+
+**Step 1: Stage all changes**
+
+```bash
+git add -A
+```
+
+**Step 2: Commit**
+
+```bash
+git commit -m "$(cat <<'EOF'
+refactor(webui): remove pipeline viewer feature
+
+Remove the read-only pipeline viewer from the web UI:
+- Delete PipelineViewer.razor page and supporting components
+- Delete PipelineController and PipelineMapper from API
+- Delete Pipeline DTOs from Core
+- Delete PipelineApiClient from Client
+- Remove navigation link and DI registrations
+- Delete obsolete plan documents
+
+The ConfigManager utility retains pipeline editing capabilities.
+EOF
+)"
+```
+
+---
+
+## Summary
+
+| Task | Action | Files Affected |
+|------|--------|----------------|
+| 1 | Delete Blazor components | 3 files deleted |
+| 2 | Remove nav link | 1 file modified |
+| 3 | Remove client service | 1 deleted, 1 modified |
+| 4 | Remove API layer | 3 deleted, 1 modified |
+| 5 | Remove Core DTOs | 5 files deleted |
+| 6 | Remove API routes | 1 file modified |
+| 7 | Delete plan docs | 2 files deleted |
+| 8 | Build and verify | - |
+| 9 | Commit | - |
+
+**Total:** 14 files deleted, 4 files modified
diff --git a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs b/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs
deleted file mode 100644
index 45a0986..0000000
--- a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-using JdeScoping.Api.Mapping;
-using JdeScoping.Core.ApiContracts;
-using JdeScoping.Core.Models.Pipelines;
-using JdeScoping.Core.Models.Enums;
-using JdeScoping.DataSync.Contracts;
-using JdeScoping.DataSync.Services;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-
-namespace JdeScoping.Api.Controllers;
-
-///
-/// API endpoints for pipeline configuration and status.
-///
-[Route(ApiRoutes.Pipelines.Base)]
-[Authorize]
-public class PipelineController : ApiControllerBase
-{
- private readonly IEtlPipelineFactory _pipelineFactory;
- 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,
- IPipelineMapper mapper)
- {
- _pipelineFactory = pipelineFactory;
- _dataUpdateRepository = dataUpdateRepository;
- _mapper = mapper;
- }
-
- ///
- /// Gets list of all available pipeline names.
- ///
- [HttpGet]
- public ActionResult GetPipelineNames()
- {
- var names = _pipelineFactory.GetAvailableTables()
- .OrderBy(n => n)
- .ToList();
- return Ok(new PipelineListResponse(names));
- }
-
- ///
- /// Gets configuration for a specific pipeline.
- ///
- /// The pipeline name.
- [HttpGet(ApiRoutes.Pipelines.ByName)]
- public ActionResult GetPipeline(string name)
- {
- var config = _pipelineFactory.GetPipelineConfig(name);
- if (config is null)
- return NotFound();
-
- var defaults = _pipelineFactory.GetScheduleDefaults();
- var dto = _mapper.MapToDto(name, config, defaults);
- return Ok(dto);
- }
-
- ///
- /// Gets schedule status for a pipeline.
- ///
- /// The pipeline name.
- /// The cancellation token.
- [HttpGet(ApiRoutes.Pipelines.Status)]
- public async Task> GetStatus(
- string name,
- CancellationToken cancellationToken)
- {
- var config = _pipelineFactory.GetPipelineConfig(name);
- if (config is null)
- return NotFound();
-
- var tableName = config.Destination.Table;
- var lastRuns = await _dataUpdateRepository.GetLastRunsAsync(tableName, cancellationToken);
- var lastSuccessful = await _dataUpdateRepository.GetLastDataUpdatesAsync(cancellationToken);
- var defaults = _pipelineFactory.GetScheduleDefaults();
-
- var statuses = new List();
- foreach (var updateType in new[] { UpdateTypes.Mass, UpdateTypes.Daily, UpdateTypes.Hourly })
- {
- var scheduleConfig = _mapper.GetScheduleConfig(config, updateType);
- var interval = _mapper.GetEffectiveInterval(scheduleConfig, defaults, updateType);
-
- lastRuns.TryGetValue(updateType, out var lastRun);
- var successKey = $"{tableName}_{(int)updateType}";
- lastSuccessful.TryGetValue(successKey, out var lastSuccess);
-
- var nextRequired = lastSuccess?.EndDt?.AddMinutes(interval);
- var isOverdue = DataUpdateRepository.IsOverdue(
- lastSuccess?.EndDt, tableName, updateType, null);
-
- statuses.Add(new PipelineScheduleStatusDto(
- updateType,
- lastRun?.StartDt,
- lastRun?.WasSuccessful ?? false,
- lastSuccess?.EndDt,
- nextRequired,
- isOverdue,
- interval));
- }
-
- return Ok(new PipelineStatusResponse(statuses));
- }
-
- ///
- /// 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,
- [FromQuery] int count = 30,
- CancellationToken cancellationToken = default)
- {
- var config = _pipelineFactory.GetPipelineConfig(name);
- if (config is null)
- return NotFound();
-
- var tableName = config.Destination.Table;
- var updates = await _dataUpdateRepository.GetRecentUpdatesAsync(
- tableName, null, count, cancellationToken);
-
- var executions = updates.Select(u => new PipelineExecutionDto(
- u.UpdateType,
- u.StartDt,
- u.EndDt == default ? null : u.EndDt,
- u.EndDt == default ? null : u.EndDt - u.StartDt,
- u.NumberRecords,
- u.WasSuccessful
- )).ToList();
-
- return Ok(new PipelineExecutionsResponse(executions));
- }
-}
diff --git a/NEW/src/JdeScoping.Api/DependencyInjection.cs b/NEW/src/JdeScoping.Api/DependencyInjection.cs
index 886b7c1..d7be958 100644
--- a/NEW/src/JdeScoping.Api/DependencyInjection.cs
+++ b/NEW/src/JdeScoping.Api/DependencyInjection.cs
@@ -1,6 +1,5 @@
using System.Text.Json.Serialization;
using JdeScoping.Api.Hubs;
-using JdeScoping.Api.Mapping;
using JdeScoping.Api.Options;
using JdeScoping.Api.Services;
using JdeScoping.Core.Interfaces;
@@ -39,9 +38,6 @@ public static class ApiDependencyInjection
// Register TimeProvider for testability (allows mocking DateTime.UtcNow)
services.AddSingleton(TimeProvider.System);
- // Register mappers
- services.AddSingleton();
-
// Configure SignalR
services.AddSignalR();
diff --git a/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs b/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs
deleted file mode 100644
index 202e017..0000000
--- a/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using JdeScoping.Core.Models.Enums;
-using JdeScoping.Core.Models.Pipelines;
-using JdeScoping.DataSync.Configuration;
-
-namespace JdeScoping.Api.Mapping;
-
-///
-/// Mapper for pipeline configuration to DTOs.
-///
-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.Api/Mapping/PipelineMapper.cs b/NEW/src/JdeScoping.Api/Mapping/PipelineMapper.cs
deleted file mode 100644
index 657e3f0..0000000
--- a/NEW/src/JdeScoping.Api/Mapping/PipelineMapper.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using JdeScoping.Core.Models.Enums;
-using JdeScoping.Core.Models.Pipelines;
-using JdeScoping.DataSync.Configuration;
-
-namespace JdeScoping.Api.Mapping;
-
-///
-/// Maps pipeline configuration to DTOs.
-///
-public class PipelineMapper : IPipelineMapper
-{
- ///
- public PipelineConfigDto MapToDto(
- string name,
- PipelineConfig config,
- ScheduleDefaults defaults)
- {
- var source = new PipelineSourceDto(
- config.Source.Connection,
- Truncate(config.Source.Query),
- Truncate(config.Source.MassQuery),
- config.Source.Query,
- config.Source.MassQuery,
- config.Source.Parameters?.Select(p => new PipelineParameterDto(
- p.Key, p.Value.Format, p.Value.Source)).ToList() ?? []);
-
- var matchCols = config.Destination.MatchColumns?.ToList();
- var destination = new PipelineDestinationDto(
- config.Destination.Table,
- matchCols?.Count > 0 ? "BulkMerge" : "BulkImport",
- matchCols,
- config.Destination.ExcludeFromUpdate?.ToList());
-
- // Mass uses massQuery with no parameters; Daily/Hourly use query with parameters
- var parameters = config.Source.Parameters?.Select(p => new PipelineParameterDto(
- p.Key, p.Value.Format, p.Value.Source)).ToList() ?? [];
-
- var schedules = new PipelineSchedulesDto(
- MapSchedule(config.Schedules?.Mass, defaults.Mass, config.Source.MassQuery, [], config.PreScripts, config.PostScripts),
- MapSchedule(config.Schedules?.Daily, defaults.Daily, config.Source.Query, parameters, config.PreScripts, config.PostScripts),
- MapSchedule(config.Schedules?.Hourly, defaults.Hourly, config.Source.Query, parameters, config.PreScripts, config.PostScripts));
-
- return new PipelineConfigDto(
- name,
- source,
- destination,
- schedules,
- config.PreScripts?.Count ?? 0,
- config.PostScripts?.Count ?? 0,
- config.PreScripts,
- config.PostScripts);
- }
-
- ///
- public ScheduleConfig? GetScheduleConfig(
- PipelineConfig config,
- UpdateTypes updateType) => updateType switch
- {
- UpdateTypes.Mass => config.Schedules?.Mass,
- UpdateTypes.Daily => config.Schedules?.Daily,
- UpdateTypes.Hourly => config.Schedules?.Hourly,
- _ => null
- };
-
- ///
- public int GetEffectiveInterval(
- ScheduleConfig? config,
- ScheduleDefaults defaults,
- UpdateTypes updateType)
- {
- if (config?.IntervalMinutes > 0)
- return config.IntervalMinutes;
-
- return updateType switch
- {
- UpdateTypes.Mass => defaults.Mass.IntervalMinutes,
- UpdateTypes.Daily => defaults.Daily.IntervalMinutes,
- UpdateTypes.Hourly => defaults.Hourly.IntervalMinutes,
- _ => 60
- };
- }
-
- private static PipelineScheduleDto MapSchedule(
- ScheduleConfig? config,
- ScheduleConfig defaults,
- string? query,
- List parameters,
- List? preScripts,
- List? postScripts)
- {
- return new PipelineScheduleDto(
- config?.Enabled ?? defaults.Enabled,
- config?.IntervalMinutes > 0 ? config.IntervalMinutes : defaults.IntervalMinutes,
- config?.PrePurge ?? defaults.PrePurge,
- config?.ReIndex ?? defaults.ReIndex,
- config?.IntervalMinutes > 0 && config.IntervalMinutes != defaults.IntervalMinutes,
- config?.PrePurge != null && config.PrePurge != defaults.PrePurge,
- config?.ReIndex != null && config.ReIndex != defaults.ReIndex,
- query,
- parameters,
- preScripts,
- postScripts);
- }
-
- private static string? Truncate(string? value, int maxLength = 100) =>
- value is null ? null :
- value.Length <= maxLength ? value : value[..maxLength] + "...";
-}
diff --git a/NEW/src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor b/NEW/src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor
deleted file mode 100644
index f50ef28..0000000
--- a/NEW/src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor
+++ /dev/null
@@ -1,116 +0,0 @@
-@namespace JdeScoping.Client.Components.Admin
-@using JdeScoping.Core.Models.Pipelines
-@using JdeScoping.Core.Models.Enums
-@using JdeScoping.Client.Helpers
-
-
-
-
-
- @GetScheduleTypeName(ScheduleType) Refresh
-
-
-
- @if (Config?.Enabled == true)
- {
-
- }
- else
- {
-
- }
-
-
-
- @if (Config is not null)
- {
- Schedule Settings
-
-
-
- Setting
- Value
- Source
-
-
-
-
- Interval
- @FormatInterval(Config.IntervalMinutes)
- @(Config.IntervalIsOverride ? "Override" : "Default")
-
-
- Pre-Purge
- @(Config.PrePurge ? "Yes" : "No")
- @(Config.PrePurgeIsOverride ? "Override" : "Default")
-
-
- Re-Index
- @(Config.ReIndex ? "Yes" : "No")
- @(Config.ReIndexIsOverride ? "Override" : "Default")
-
-
-
-
- @if (Config.Parameters?.Count > 0)
- {
- Parameters
-
- @foreach (var param in Config.Parameters)
- {
- - @param.Name: @(param.Format ?? "default") (source: @param.Source)
- }
-
- }
-
- @if (!string.IsNullOrWhiteSpace(Config.Query))
- {
- Query
- @SqlFormatHelper.FormatSql(Config.Query)
- }
-
- @if (Config.PreScripts?.Count > 0)
- {
- Pre-Scripts (@Config.PreScripts.Count)
- @for (int i = 0; i < Config.PreScripts.Count; i++)
- {
- var script = Config.PreScripts[i];
- Script @(i + 1):
- @SqlFormatHelper.FormatSql(script)
- }
- }
-
- @if (Config.PostScripts?.Count > 0)
- {
- Post-Scripts (@Config.PostScripts.Count)
- @for (int i = 0; i < Config.PostScripts.Count; i++)
- {
- var script = Config.PostScripts[i];
- Script @(i + 1):
- @SqlFormatHelper.FormatSql(script)
- }
- }
- }
-
-
-@code {
- [Parameter] public UpdateTypes ScheduleType { get; set; }
- [Parameter] public PipelineScheduleDto? Config { get; set; }
-
- private static string GetScheduleTypeName(UpdateTypes type) => type switch
- {
- UpdateTypes.Mass => "Mass",
- UpdateTypes.Daily => "Daily",
- UpdateTypes.Hourly => "Hourly",
- _ => type.ToString()
- };
-
- private static string FormatInterval(int minutes)
- {
- if (minutes >= 1440)
- return $"{minutes / 1440} day(s) ({minutes} min)";
- if (minutes >= 60)
- return $"{minutes / 60} hour(s) ({minutes} min)";
- return $"{minutes} minutes";
- }
-}
diff --git a/NEW/src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor b/NEW/src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor
deleted file mode 100644
index 48d87ec..0000000
--- a/NEW/src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor
+++ /dev/null
@@ -1,101 +0,0 @@
-@namespace JdeScoping.Client.Components.Admin
-@using JdeScoping.Client.Helpers
-@inject IJSRuntime JS
-
-@if (Visible)
-{
-
-}
-
-
-
-@code {
- [Parameter] public bool Visible { get; set; }
- [Parameter] public EventCallback VisibleChanged { get; set; }
- [Parameter] public string? Title { get; set; }
- [Parameter] public string? Sql { get; set; }
-
- private string FormattedSql => SqlFormatHelper.FormatSql(Sql);
-
- private async Task CopyToClipboard()
- {
- if (!string.IsNullOrWhiteSpace(Sql))
- {
- await JS.InvokeVoidAsync("navigator.clipboard.writeText", Sql);
- }
- }
-
- private async Task Close()
- {
- await VisibleChanged.InvokeAsync(false);
- }
-}
diff --git a/NEW/src/JdeScoping.Client/Layout/MainLayout.razor b/NEW/src/JdeScoping.Client/Layout/MainLayout.razor
index 5e76e26..c78b451 100644
--- a/NEW/src/JdeScoping.Client/Layout/MainLayout.razor
+++ b/NEW/src/JdeScoping.Client/Layout/MainLayout.razor
@@ -13,7 +13,6 @@
New Search
Search Queue
Refresh Status
- Pipeline Viewer