Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Implemented so far:
- Phase 12: automated test expansion for invalid and edge-case coverage
- Phase 13: publish and deployment validation
- Phase 14: GitHub Actions CI/CD and Dependabot automation
- Post-v1: endpoint import flow with live probe, YAML preview, and diff comparison

Not implemented yet:
- Backlog items tracked for post-v1 work
Expand Down Expand Up @@ -70,6 +71,7 @@ The app uses locally bundled AdminLTE assets under [`src/ApiHealthDashboard/wwwr
Current UI pages:
- dashboard summary page: [`src/ApiHealthDashboard/Pages/Index.cshtml`](src/ApiHealthDashboard/Pages/Index.cshtml)
- endpoint details page: [`src/ApiHealthDashboard/Pages/Endpoints/Details.cshtml`](src/ApiHealthDashboard/Pages/Endpoints/Details.cshtml)
- endpoint import preview page: [`src/ApiHealthDashboard/Pages/Import.cshtml`](src/ApiHealthDashboard/Pages/Import.cshtml)

### YAML Configuration

Expand Down Expand Up @@ -183,10 +185,24 @@ Current dashboard behavior:
- shows configured, enabled, disabled, and actively polling endpoint counts
- highlights healthy, degraded, unhealthy, and unknown totals in summary cards
- includes a client-side search field for filtering endpoint rows by name, id, status, or error text
- refreshes the live dashboard section with same-origin timed GET requests instead of reloading the whole page
- renders a live endpoint table with last check, duration, error summary, and manual refresh actions
- surfaces degraded and unhealthy endpoints in an active issues panel for faster triage
- shows a clearer empty state when no endpoints are configured

### Endpoint Import Flow

The app now includes an endpoint import preview flow for deriving YAML from a live API probe without writing files automatically.

Current import behavior:
- sends a live request using the entered URL, headers, timeout, and enabled/frequency settings
- auto-suggests endpoint id and name when those fields are left blank
- shows a soft warning when the chosen poll frequency is below the configured appsettings recommendation
- parses discovered checks from the response and can optionally populate `includeChecks`
- generates a normalized YAML snippet for manual copy into `dashboard.yaml` or a separate endpoint file
- compares the generated YAML against the currently loaded config when an existing endpoint matches by id or URL
- shows a diff preview plus a raw response preview for review before any manual save

### Endpoint Details

The endpoint details page now acts as a diagnostic view for a single configured endpoint.
Expand Down Expand Up @@ -401,7 +417,6 @@ Test file:
## Future Plans

These are planned enhancements after the current v1 path:
- add an import flow that can derive YAML endpoint config from API request and response inspection, with preview and diff comparison before any manual save
- add CLI execution with machine-readable output for automation and scripting scenarios
- allow per-endpoint priority so important endpoints can be surfaced and scheduled differently
- optionally allow email sending, either through direct SMTP configuration or by calling an external API
Expand Down
1 change: 0 additions & 1 deletion api-health-dashboard-build-checklist-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,6 @@ These should not block v1.
- [ ] health status mini charts
- [ ] SBOM generation
- [ ] artifact signing
- [ ] import YAML endpoint definitions from API endpoint request/response inspection with preview and diff comparison before manual save
- [ ] CLI execution mode with machine-readable output
- [ ] per-endpoint priority support
- [ ] optional email sending via SMTP or an external API
Expand Down
1 change: 0 additions & 1 deletion api-health-dashboard-codex-requirements-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,6 @@ These are optional later improvements and should not block v1:
- tag-based filtering
- retry policy with backoff
- readonly config viewer page
- import YAML endpoint definitions from API endpoint request/response inspection with preview and diff comparison before manual file save
- CLI execution mode with machine-readable output for automation workflows
- per-endpoint priority for display and future scheduling behavior
- optional email sending via directly configured SMTP or an external API integration
Expand Down
13 changes: 13 additions & 0 deletions src/ApiHealthDashboard/Configuration/ConfigurationWarningState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace ApiHealthDashboard.Configuration;

public sealed class ConfigurationWarningState
{
public ConfigurationWarningState(IReadOnlyList<string> warnings)
{
Warnings = warnings;
}

public IReadOnlyList<string> Warnings { get; }

public bool HasWarnings => Warnings.Count > 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ApiHealthDashboard.Configuration;

public sealed class DashboardConfigLoadResult
{
public required DashboardConfig Config { get; init; }

public IReadOnlyList<string> Warnings { get; init; } = [];
}
2 changes: 1 addition & 1 deletion src/ApiHealthDashboard/Configuration/IYamlConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace ApiHealthDashboard.Configuration;

public interface IYamlConfigLoader
{
DashboardConfig Load(string path);
DashboardConfigLoadResult Load(string path);
}
8 changes: 8 additions & 0 deletions src/ApiHealthDashboard/Configuration/ImportUiOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ApiHealthDashboard.Configuration;

public sealed class ImportUiOptions
{
public const string SectionName = "Import";

public int MinimumRecommendedPollFrequencySeconds { get; set; } = 180;
}
27 changes: 25 additions & 2 deletions src/ApiHealthDashboard/Configuration/YamlConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,25 @@ public YamlConfigLoader(DashboardConfigValidator validator)
.Build();
}

public DashboardConfig Load(string path)
public DashboardConfigLoadResult Load(string path)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);

if (!File.Exists(path))
{
return new DashboardConfigLoadResult
{
Config = new DashboardConfig(),
Warnings =
[
$"Dashboard configuration file '{path}' was not found. The dashboard started with no configured endpoints."
]
};
}

var dashboardConfig = DeserializeDashboardConfig(path);
Normalize(dashboardConfig);
var warnings = new List<string>();

var mergedConfig = new DashboardConfig
{
Expand All @@ -39,6 +52,12 @@ public DashboardConfig Load(string path)
foreach (var endpointFilePath in dashboardConfig.EndpointFiles)
{
var resolvedEndpointFilePath = ResolveConfigPath(endpointFilePath, dashboardDirectory);
if (!File.Exists(resolvedEndpointFilePath))
{
warnings.Add($"Endpoint configuration file '{resolvedEndpointFilePath}' was not found. It was skipped.");
continue;
}

var fileEndpoints = LoadEndpointsFromFile(resolvedEndpointFilePath);
mergedConfig.Endpoints.AddRange(fileEndpoints);
}
Expand All @@ -51,7 +70,11 @@ public DashboardConfig Load(string path)
throw new DashboardConfigurationException(path, errors);
}

return mergedConfig;
return new DashboardConfigLoadResult
{
Config = mergedConfig,
Warnings = warnings
};
}

private DashboardConfig DeserializeDashboardConfig(string path)
Expand Down
27 changes: 27 additions & 0 deletions src/ApiHealthDashboard/Formatting/DisplayValueFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections;
using System.Text.Json;

namespace ApiHealthDashboard.Formatting;

public static class DisplayValueFormatter
{
public static string Format(object? value)
{
return value switch
{
null => "(null)",
string text when string.IsNullOrWhiteSpace(text) => "(empty)",
string text => text,
IEnumerable values => FormatEnumerable(values),
_ => JsonSerializer.Serialize(value)
};
}

private static string FormatEnumerable(IEnumerable values)
{
var items = values.Cast<object?>().ToList();
return items.Count == 0
? "(empty)"
: JsonSerializer.Serialize(items);
}
}
32 changes: 25 additions & 7 deletions src/ApiHealthDashboard/Pages/Endpoints/Details.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ApiHealthDashboard.Configuration;
using ApiHealthDashboard.Domain;
using ApiHealthDashboard.Formatting;
using ApiHealthDashboard.Scheduling;
using ApiHealthDashboard.State;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -234,7 +235,7 @@ public static EndpointDetailsViewModel From(
DegradedCheckCount = flattenedNodes.Count(static node => node.Status == "Degraded"),
UnhealthyCheckCount = flattenedNodes.Count(static node => node.Status == "Unhealthy"),
UnknownCheckCount = flattenedNodes.Count(static node => node.Status is not ("Healthy" or "Degraded" or "Unhealthy")),
RawPayload = showRawPayload ? state?.Snapshot?.RawPayload : null,
RawPayload = showRawPayload ? FormatPayloadPreview(state?.Snapshot?.RawPayload) : null,
ShowRawPayload = showRawPayload
};
}
Expand Down Expand Up @@ -285,13 +286,30 @@ private static string BuildStatusSummary(bool enabled, bool isPolling, string st

private static string FormatMetadataValue(object? value)
{
return value switch
return DisplayValueFormatter.Format(value);
}

private static string? FormatPayloadPreview(string? rawPayload)
{
if (string.IsNullOrWhiteSpace(rawPayload))
{
null => "(null)",
string text when string.IsNullOrWhiteSpace(text) => "(empty)",
string text => text,
_ => JsonSerializer.Serialize(value)
};
return rawPayload;
}

try
{
using var document = JsonDocument.Parse(rawPayload);
return JsonSerializer.Serialize(
document.RootElement,
new JsonSerializerOptions
{
WriteIndented = true
});
}
catch (JsonException)
{
return rawPayload;
}
}

private static string ToBadgeClass(string status)
Expand Down
Loading