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>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.14" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
||||||
</ItemGroup>
|
</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 @@
|
|||||||
|
// 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);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// --- 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();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
|
||||||
builder.Services.AddOpenApi();
|
// 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();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// --- Middleware Pipeline ---
|
||||||
if (app.Environment.IsDevelopment())
|
// Middleware order matters in ASP.NET Core -- each middleware runs in the order
|
||||||
{
|
// it is registered. CORS must be applied before routing and authorization
|
||||||
app.MapOpenApi();
|
// so that preflight (OPTIONS) requests are handled correctly.
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
// 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();
|
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.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.14" />
|
<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.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.14" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
@inherits LayoutComponentBase
|
@* MainLayout.razor -- The root layout component for the application.
|
||||||
<div class="page">
|
|
||||||
<div class="sidebar">
|
In Blazor, layout components wrap page content. Every routed page (@page)
|
||||||
<NavMenu />
|
is rendered inside the layout's @Body placeholder. This is similar to
|
||||||
</div>
|
_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>
|
<main>
|
||||||
<div class="top-row px-4">
|
@* @Body is where the routed page content renders.
|
||||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
When the user navigates to "/", the Home.razor component's markup
|
||||||
</div>
|
appears here. When they navigate to another @page, that component
|
||||||
|
renders here instead. The layout stays the same -- only @Body changes. *@
|
||||||
<article class="content px-4">
|
|
||||||
@Body
|
@Body
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,7 +1,86 @@
|
|||||||
@page "/"
|
@* Home.razor -- The landing page for ChatAgent.
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
@page "/" maps this component to the root URL. When a user navigates to "/",
|
||||||
|
the Blazor router renders this component inside MainLayout's @Body placeholder.
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
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)
|
||||||
|
*@
|
||||||
|
|
||||||
Welcome to your new app.
|
@* @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 @@
|
|||||||
|
// 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.Web;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using ChatAgent.Client;
|
using ChatAgent.Client;
|
||||||
|
using ChatAgent.Client.Services;
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
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");
|
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");
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
// 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();
|
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,4 +1,11 @@
|
|||||||
@using System.Net.Http
|
@* _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 System.Net.Http.Json
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@@ -8,3 +15,5 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using ChatAgent.Client
|
@using ChatAgent.Client
|
||||||
@using ChatAgent.Client.Layout
|
@using ChatAgent.Client.Layout
|
||||||
|
@using ChatAgent.Client.Services
|
||||||
|
@using ChatAgent.Shared.Models
|
||||||
|
|||||||
@@ -1,41 +1,55 @@
|
|||||||
|
/* 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 {
|
html, body {
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1:focus {
|
main {
|
||||||
outline: none;
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
a, .btn-link {
|
h1 {
|
||||||
color: #0071c1;
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.health-status {
|
||||||
color: #fff;
|
padding: 1rem;
|
||||||
background-color: #1b6ec2;
|
border: 1px solid #e0e0e0;
|
||||||
border-color: #1861ac;
|
border-radius: 8px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
.health-status p {
|
||||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.error-message {
|
||||||
padding-top: 1.1rem;
|
color: #d32f2f;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #d32f2f;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fce4ec;
|
||||||
}
|
}
|
||||||
|
|
||||||
.valid.modified:not([type=checkbox]) {
|
.loading {
|
||||||
outline: 1px solid #26b050;
|
color: #666666;
|
||||||
}
|
font-style: italic;
|
||||||
|
|
||||||
.invalid {
|
|
||||||
outline: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-message {
|
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
#blazor-error-ui {
|
||||||
color-scheme: light only;
|
color-scheme: light only;
|
||||||
background: lightyellow;
|
background: lightyellow;
|
||||||
@@ -67,6 +81,8 @@ a, .btn-link {
|
|||||||
content: "An error has occurred."
|
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 {
|
.loading-progress {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -99,16 +115,3 @@ a, .btn-link {
|
|||||||
.loading-progress-text:after {
|
.loading-progress-text:after {
|
||||||
content: var(--blazor-load-percentage-text, "Loading");
|
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;
|
|
||||||
}
|
|
||||||
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