feat(01-02): implement health check round-trip with CORS and tutorial comments
- Add shared HealthResponse DTO in ChatAgent.Shared - Add HealthController API endpoint with CORS policy for localhost:5200 - Add ChatApiClient typed HttpClient wrapper in WASM client - Update Home.razor to display health check result on load - Simplify MainLayout to minimal centered layout - Add global imports for Services and Shared.Models - Replace app.css with clean Phase 1 light theme styles - Remove unused OpenAPI package from API project - All files include tutorial-style inline comments (CODE-01)
This commit is contained in:
@@ -6,10 +6,6 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
47
src/ChatAgent.Api/Controllers/HealthController.cs
Normal file
47
src/ChatAgent.Api/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// HealthController.cs -- API endpoint that proves the WASM-to-API connection works.
|
||||
//
|
||||
// This is the first controller in the project. It exists to verify that:
|
||||
// 1. The API server starts and responds to HTTP requests
|
||||
// 2. CORS allows the Blazor WASM client to call across origins
|
||||
// 3. The shared HealthResponse DTO serializes/deserializes correctly
|
||||
//
|
||||
// In ASP.NET Core, a "controller" is a class that handles HTTP requests.
|
||||
// The framework routes requests to controller methods based on attributes.
|
||||
|
||||
using ChatAgent.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ChatAgent.Api.Controllers
|
||||
{
|
||||
// [ApiController] enables several conveniences for API development:
|
||||
// - Automatic HTTP 400 responses for invalid model state
|
||||
// - Automatic inference of [FromBody] for complex types
|
||||
// - Problem details responses for error status codes
|
||||
// It signals "this class handles API requests, not HTML pages."
|
||||
[ApiController]
|
||||
|
||||
// [Route("api/[controller]")] maps this class to the URL path /api/health.
|
||||
// The [controller] token is replaced with the class name minus "Controller",
|
||||
// so HealthController becomes "health" -> /api/health.
|
||||
[Route("api/[controller]")]
|
||||
public class HealthController : ControllerBase
|
||||
{
|
||||
// [HttpGet] maps this method to HTTP GET requests at the controller's route.
|
||||
// When a client sends GET /api/health, ASP.NET Core calls this method.
|
||||
// It returns an IActionResult, which gives us control over the HTTP status code.
|
||||
[HttpGet]
|
||||
public IActionResult Get()
|
||||
{
|
||||
// Return HTTP 200 OK with a HealthResponse body.
|
||||
// The framework serializes the object to JSON automatically
|
||||
// because this is an [ApiController].
|
||||
// DateTime.UtcNow provides a live timestamp so the client can verify
|
||||
// the response is fresh (not cached).
|
||||
return Ok(new HealthResponse
|
||||
{
|
||||
Status = "healthy",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,59 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
// Program.cs -- ASP.NET Core Web API entry point for ChatAgent.
|
||||
//
|
||||
// This is the backend server. In Phase 1, it only serves a health check endpoint.
|
||||
// In later phases, it will proxy OpenAI API calls (keeping the API key server-side)
|
||||
// and manage JSON file storage for conversation persistence.
|
||||
//
|
||||
// ASP.NET Core uses a "builder pattern": first configure services (DI container),
|
||||
// then build the app, configure middleware pipeline, and run.
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// --- Service Registration (Dependency Injection container) ---
|
||||
|
||||
// AddControllers() registers MVC controller services so ASP.NET Core discovers
|
||||
// classes decorated with [ApiController]. We use Controllers (not Minimal API)
|
||||
// for explicit structure -- each controller is a separate file with clear routing (D-05).
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// AddCors() registers Cross-Origin Resource Sharing services.
|
||||
// CORS is REQUIRED because the Blazor WASM client runs on a different origin
|
||||
// (https://localhost:5200) than this API (https://localhost:7100).
|
||||
// Browsers block cross-origin HTTP requests by default as a security measure.
|
||||
// Without this policy, the client's fetch() calls to the API would be rejected.
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowBlazorClient", policy =>
|
||||
{
|
||||
policy
|
||||
// Only allow requests from the Blazor WASM client origin
|
||||
.WithOrigins("https://localhost:5200")
|
||||
// Allow any HTTP header (Content-Type, Accept, etc.)
|
||||
.AllowAnyHeader()
|
||||
// Allow any HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// --- Middleware Pipeline ---
|
||||
// Middleware order matters in ASP.NET Core -- each middleware runs in the order
|
||||
// it is registered. CORS must be applied before routing and authorization
|
||||
// so that preflight (OPTIONS) requests are handled correctly.
|
||||
|
||||
// Apply the CORS policy globally -- every response includes the correct
|
||||
// Access-Control-Allow-Origin header for the Blazor client's origin.
|
||||
app.UseCors("AllowBlazorClient");
|
||||
|
||||
// UseAuthorization() enables the authorization middleware. Even though we have
|
||||
// no auth in Phase 1, it is included because ASP.NET Core expects it in the
|
||||
// pipeline when controllers are used. It is a no-op without [Authorize] attributes.
|
||||
app.UseAuthorization();
|
||||
|
||||
// MapControllers() scans the assembly for all classes with [ApiController]
|
||||
// and maps their routes. This is what connects HealthController's
|
||||
// [Route("api/[controller]")] to the URL path /api/health.
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.14" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.14" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@* MainLayout.razor -- The root layout component for the application.
|
||||
|
||||
In Blazor, layout components wrap page content. Every routed page (@page)
|
||||
is rendered inside the layout's @Body placeholder. This is similar to
|
||||
_Layout.cshtml in MVC or master pages in Web Forms.
|
||||
|
||||
Phase 1 uses a minimal layout -- just centered content with padding.
|
||||
Later phases will add a sidebar for conversation management.
|
||||
*@
|
||||
|
||||
@* @inherits LayoutComponentBase makes this a layout component.
|
||||
LayoutComponentBase provides the Body property, which is a RenderFragment
|
||||
containing the routed page's content. Without this base class, @Body
|
||||
would not be available. *@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main>
|
||||
@* @Body is where the routed page content renders.
|
||||
When the user navigates to "/", the Home.razor component's markup
|
||||
appears here. When they navigate to another @page, that component
|
||||
renders here instead. The layout stays the same -- only @Body changes. *@
|
||||
@Body
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,86 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
@* Home.razor -- The landing page for ChatAgent.
|
||||
|
||||
@page "/" maps this component to the root URL. When a user navigates to "/",
|
||||
the Blazor router renders this component inside MainLayout's @Body placeholder.
|
||||
|
||||
This page demonstrates the health check round-trip:
|
||||
1. On load, it calls the API's /api/health endpoint via ChatApiClient
|
||||
2. Displays the server's health status and timestamp
|
||||
3. Proves CORS is working (WASM on :5200 calling API on :7100)
|
||||
*@
|
||||
|
||||
@* @page directive maps this component to a URL route.
|
||||
"/" means this is the default/home page. *@
|
||||
@page "/"
|
||||
|
||||
@* Import the service and model namespaces for this component.
|
||||
These could also be in _Imports.razor for global access. *@
|
||||
@using ChatAgent.Client.Services
|
||||
@using ChatAgent.Shared.Models
|
||||
|
||||
@* @inject requests a service from the Dependency Injection (DI) container.
|
||||
ChatApiClient was registered in Program.cs via AddHttpClient<ChatApiClient>.
|
||||
Blazor creates one instance per component and injects it here. *@
|
||||
@inject ChatApiClient ApiClient
|
||||
|
||||
<PageTitle>Chat Agent</PageTitle>
|
||||
|
||||
<h1>Chat Agent</h1>
|
||||
|
||||
@* Conditional rendering: Blazor re-renders the component after OnInitializedAsync completes.
|
||||
We show different content based on the state of our data fields. *@
|
||||
|
||||
@if (_healthResponse is null && _error is null)
|
||||
{
|
||||
@* Loading state: OnInitializedAsync has not completed yet *@
|
||||
<p class="loading">Checking API connection...</p>
|
||||
}
|
||||
else if (_healthResponse is not null)
|
||||
{
|
||||
@* Success state: API responded with a HealthResponse *@
|
||||
<div class="health-status">
|
||||
<p><strong>API Status:</strong> @_healthResponse.Status</p>
|
||||
<p><strong>Server Time:</strong> @_healthResponse.Timestamp.ToString("yyyy-MM-dd HH:mm:ss UTC")</p>
|
||||
</div>
|
||||
}
|
||||
else if (_error is not null)
|
||||
{
|
||||
@* Error state: the API call failed (network error, CORS blocked, server down, etc.) *@
|
||||
<div class="error-message">
|
||||
<p><strong>Connection Error:</strong> @_error</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
// Private fields to hold the health check result or error message.
|
||||
// Blazor components use private fields for state that drives the UI.
|
||||
private HealthResponse? _healthResponse;
|
||||
private string? _error;
|
||||
|
||||
/// <summary>
|
||||
/// OnInitializedAsync is a Blazor lifecycle method called once when the component
|
||||
/// is first rendered. It runs after the component receives its initial parameters.
|
||||
///
|
||||
/// Because it returns Task, Blazor awaits it and automatically calls
|
||||
/// StateHasChanged() when it completes, triggering a re-render.
|
||||
/// This means we do NOT need to call StateHasChanged() manually here.
|
||||
///
|
||||
/// The component renders twice: once immediately (showing "Checking..."),
|
||||
/// and again after this method completes (showing the result or error).
|
||||
/// </summary>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Call the API health endpoint via our typed HttpClient wrapper.
|
||||
// This makes an HTTP GET to https://localhost:7100/api/health.
|
||||
_healthResponse = await ApiClient.GetHealthAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Catch any exception (network error, CORS block, timeout, etc.)
|
||||
// and store the message for display in the error UI.
|
||||
_error = $"Failed to reach API: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,47 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using ChatAgent.Client;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
// Program.cs -- Blazor WebAssembly application entry point for ChatAgent.
|
||||
//
|
||||
// This is where the WASM client is configured and launched. Unlike a server-side
|
||||
// ASP.NET Core app, a Blazor WASM app runs entirely in the browser. The
|
||||
// WebAssemblyHostBuilder configures:
|
||||
// - Root components (what gets rendered into the HTML page)
|
||||
// - Services (dependency injection container, similar to server-side DI)
|
||||
// - Configuration (reads from wwwroot/appsettings.json)
|
||||
//
|
||||
// This file will grow as we add more services in later phases (e.g., chat state management).
|
||||
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using ChatAgent.Client;
|
||||
using ChatAgent.Client.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// Add<App>("#app") tells Blazor to render the App component inside the
|
||||
// <div id="app"> element in wwwroot/index.html. This is the "mount point"
|
||||
// for the entire Blazor component tree.
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
|
||||
// HeadOutlet allows Razor components to modify <head> elements (e.g., <title>)
|
||||
// using the <PageTitle> and <HeadContent> components. "head::after" means
|
||||
// content is appended after existing head elements.
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
// Read the API base URL from configuration.
|
||||
// In Blazor WASM, configuration comes from wwwroot/appsettings.json.
|
||||
// IMPORTANT: wwwroot/ files are PUBLIC -- they are downloaded to the browser.
|
||||
// Never put secrets (API keys, passwords) in appsettings.json for a WASM app.
|
||||
// The API key lives server-side in the ChatAgent.Api project.
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? "https://localhost:7100";
|
||||
|
||||
// AddHttpClient<ChatApiClient> registers a typed HttpClient using IHttpClientFactory.
|
||||
// IHttpClientFactory manages the underlying HttpMessageHandler lifetime to prevent
|
||||
// socket exhaustion (a common problem with raw HttpClient in long-running apps).
|
||||
// The lambda configures the client with the API base URL so ChatApiClient
|
||||
// does not need to know the URL -- it is injected with a pre-configured HttpClient.
|
||||
// In Blazor WASM, HttpClient uses the browser's Fetch API under the hood.
|
||||
builder.Services.AddHttpClient<ChatApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
53
src/ChatAgent.Client/Services/ChatApiClient.cs
Normal file
53
src/ChatAgent.Client/Services/ChatApiClient.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// ChatApiClient.cs -- Typed HttpClient wrapper for communicating with the ChatAgent API.
|
||||
//
|
||||
// WHY A TYPED CLIENT? Instead of injecting HttpClient directly into components,
|
||||
// we wrap it in a dedicated service class. This follows the "typed client" pattern (D-04):
|
||||
// - Components depend on ChatApiClient, not HttpClient (loose coupling)
|
||||
// - API URL paths are centralized here, not scattered across components
|
||||
// - Easy to mock for testing -- swap ChatApiClient, not HttpClient
|
||||
// - IHttpClientFactory manages the underlying HttpClient lifetime
|
||||
//
|
||||
// In Blazor WASM, HttpClient is backed by the browser's Fetch API.
|
||||
// The base URL is configured in Program.cs via AddHttpClient<ChatApiClient>.
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using ChatAgent.Shared.Models;
|
||||
|
||||
namespace ChatAgent.Client.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Typed HttpClient for the ChatAgent API. Each public method maps to one API endpoint.
|
||||
/// Constructor injection of HttpClient is provided by IHttpClientFactory,
|
||||
/// which was configured in Program.cs with the API base URL.
|
||||
/// </summary>
|
||||
public class ChatApiClient
|
||||
{
|
||||
// The HttpClient instance is injected by the DI container (IHttpClientFactory).
|
||||
// It is pre-configured with BaseAddress pointing to the API server.
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor receives an HttpClient from IHttpClientFactory DI registration.
|
||||
/// The client is already configured with the API base URL.
|
||||
/// </summary>
|
||||
public ChatApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls GET /api/health on the API server and deserializes the JSON response
|
||||
/// into a HealthResponse object. Returns null if deserialization fails.
|
||||
///
|
||||
/// GetFromJsonAsync is an extension method from System.Net.Http.Json that
|
||||
/// combines the HTTP GET call and JSON deserialization into a single step.
|
||||
/// It uses System.Text.Json internally (the built-in .NET JSON serializer).
|
||||
/// </summary>
|
||||
public async Task<HealthResponse?> GetHealthAsync()
|
||||
{
|
||||
// "api/health" is a relative URL -- it is appended to the BaseAddress
|
||||
// configured in Program.cs (e.g., https://localhost:7100/api/health).
|
||||
return await _httpClient.GetFromJsonAsync<HealthResponse>("api/health");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using ChatAgent.Client
|
||||
@using ChatAgent.Client.Layout
|
||||
@* _Imports.razor -- Global using directives for all .razor files in this project.
|
||||
|
||||
Any @using directive placed here is automatically available in every .razor file
|
||||
in the ChatAgent.Client project. This avoids repeating common imports in each component.
|
||||
It works like a "global usings" file but specifically for Razor components.
|
||||
*@
|
||||
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using ChatAgent.Client
|
||||
@using ChatAgent.Client.Layout
|
||||
@using ChatAgent.Client.Services
|
||||
@using ChatAgent.Shared.Models
|
||||
|
||||
@@ -1,114 +1,117 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
/* app.css -- Application styles for ChatAgent (Phase 1).
|
||||
*
|
||||
* Phase 1 uses plain HTML/CSS (D-10) with a light theme (D-11).
|
||||
* MudBlazor will be introduced in Phase 5 for UI polish.
|
||||
* These styles provide a clean, minimal appearance for the health check page.
|
||||
*/
|
||||
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
padding: 1rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.health-status p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #d32f2f;
|
||||
padding: 1rem;
|
||||
border: 1px solid #d32f2f;
|
||||
border-radius: 8px;
|
||||
background-color: #fce4ec;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #666666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Blazor error UI -- shown when an unhandled exception occurs.
|
||||
* This is built into the Blazor template's index.html and should be kept. */
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
/* Loading progress indicator -- shown while the WASM runtime downloads.
|
||||
* This SVG-based progress circle is defined in index.html. */
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
32
src/ChatAgent.Shared/Models/HealthResponse.cs
Normal file
32
src/ChatAgent.Shared/Models/HealthResponse.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// HealthResponse.cs -- Shared Data Transfer Object (DTO) for the health check endpoint.
|
||||
//
|
||||
// WHY SHARED? This class lives in the ChatAgent.Shared project, which is referenced by
|
||||
// both the API (ChatAgent.Api) and the client (ChatAgent.Client). By sharing the type:
|
||||
// - The API serializes a HealthResponse to JSON when responding to GET /api/health
|
||||
// - The Client deserializes that same JSON back into a HealthResponse object
|
||||
// - Both sides agree on the shape of the data -- no mismatched property names or types
|
||||
//
|
||||
// This is the "shared contract" pattern: one type definition, two consumers.
|
||||
|
||||
namespace ChatAgent.Shared.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Data Transfer Object (DTO) returned by the API health check endpoint.
|
||||
/// Contains the server's health status and current timestamp to prove
|
||||
/// the response is live (not cached or stale).
|
||||
/// </summary>
|
||||
public class HealthResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Server health status string (e.g., "healthy").
|
||||
/// Initialized to empty string to avoid null warnings with nullable enabled.
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Server-side UTC timestamp at the moment the health check ran.
|
||||
/// The client displays this to confirm the response is fresh.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user