diff --git a/src/ChatAgent.Api/ChatAgent.Api.csproj b/src/ChatAgent.Api/ChatAgent.Api.csproj
index 86a2035..b7f0468 100644
--- a/src/ChatAgent.Api/ChatAgent.Api.csproj
+++ b/src/ChatAgent.Api/ChatAgent.Api.csproj
@@ -6,10 +6,6 @@
enable
-
-
-
-
diff --git a/src/ChatAgent.Api/Controllers/HealthController.cs b/src/ChatAgent.Api/Controllers/HealthController.cs
new file mode 100644
index 0000000..2321339
--- /dev/null
+++ b/src/ChatAgent.Api/Controllers/HealthController.cs
@@ -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
+ });
+ }
+ }
+}
diff --git a/src/ChatAgent.Api/Program.cs b/src/ChatAgent.Api/Program.cs
index 6189b36..6a4e491 100644
--- a/src/ChatAgent.Api/Program.cs
+++ b/src/ChatAgent.Api/Program.cs
@@ -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();
diff --git a/src/ChatAgent.Client/ChatAgent.Client.csproj b/src/ChatAgent.Client/ChatAgent.Client.csproj
index 6e3a9b4..5ba2fba 100644
--- a/src/ChatAgent.Client/ChatAgent.Client.csproj
+++ b/src/ChatAgent.Client/ChatAgent.Client.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/ChatAgent.Client/Layout/MainLayout.razor b/src/ChatAgent.Client/Layout/MainLayout.razor
index e465845..f946585 100644
--- a/src/ChatAgent.Client/Layout/MainLayout.razor
+++ b/src/ChatAgent.Client/Layout/MainLayout.razor
@@ -1,16 +1,23 @@
-@inherits LayoutComponentBase
-
+@* 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
+
+
+ @* @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
+
diff --git a/src/ChatAgent.Client/Pages/Home.razor b/src/ChatAgent.Client/Pages/Home.razor
index df05023..f3d3630 100644
--- a/src/ChatAgent.Client/Pages/Home.razor
+++ b/src/ChatAgent.Client/Pages/Home.razor
@@ -1,7 +1,86 @@
-@page "/"
-
-Home
-
-
Hello, world!
-
-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.
+ Blazor creates one instance per component and injects it here. *@
+@inject ChatApiClient ApiClient
+
+Chat Agent
+
+
Chat Agent
+
+@* 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 *@
+
Checking API connection...
+}
+else if (_healthResponse is not null)
+{
+ @* Success state: API responded with a HealthResponse *@
+
+
API Status: @_healthResponse.Status
+
Server Time: @_healthResponse.Timestamp.ToString("yyyy-MM-dd HH:mm:ss UTC")
+
+}
+else if (_error is not null)
+{
+ @* Error state: the API call failed (network error, CORS blocked, server down, etc.) *@
+
+
Connection Error: @_error
+
+}
+
+@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;
+
+ ///
+ /// 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).
+ ///
+ 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}";
+ }
+ }
+}
diff --git a/src/ChatAgent.Client/Program.cs b/src/ChatAgent.Client/Program.cs
index d93447e..2d60c4e 100644
--- a/src/ChatAgent.Client/Program.cs
+++ b/src/ChatAgent.Client/Program.cs
@@ -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");
-builder.RootComponents.Add("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") tells Blazor to render the App component inside the
+//
element in wwwroot/index.html. This is the "mount point"
+// for the entire Blazor component tree.
+builder.RootComponents.Add("#app");
+
+// HeadOutlet allows Razor components to modify elements (e.g., )
+// using the and components. "head::after" means
+// content is appended after existing head elements.
+builder.RootComponents.Add("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 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(client =>
+{
+ client.BaseAddress = new Uri(apiBaseUrl);
+});
+
+await builder.Build().RunAsync();
diff --git a/src/ChatAgent.Client/Services/ChatApiClient.cs b/src/ChatAgent.Client/Services/ChatApiClient.cs
new file mode 100644
index 0000000..b7e177e
--- /dev/null
+++ b/src/ChatAgent.Client/Services/ChatApiClient.cs
@@ -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.
+
+using System.Net.Http.Json;
+using ChatAgent.Shared.Models;
+
+namespace ChatAgent.Client.Services
+{
+ ///
+ /// 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.
+ ///
+ 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;
+
+ ///
+ /// Constructor receives an HttpClient from IHttpClientFactory DI registration.
+ /// The client is already configured with the API base URL.
+ ///
+ public ChatApiClient(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ ///
+ /// 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).
+ ///
+ public async Task 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("api/health");
+ }
+ }
+}
diff --git a/src/ChatAgent.Client/_Imports.razor b/src/ChatAgent.Client/_Imports.razor
index f256289..28adc3a 100644
--- a/src/ChatAgent.Client/_Imports.razor
+++ b/src/ChatAgent.Client/_Imports.razor
@@ -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
diff --git a/src/ChatAgent.Client/wwwroot/css/app.css b/src/ChatAgent.Client/wwwroot/css/app.css
index d919e04..60635aa 100644
--- a/src/ChatAgent.Client/wwwroot/css/app.css
+++ b/src/ChatAgent.Client/wwwroot/css/app.css
@@ -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;
-}
\ No newline at end of file
+/* 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");
+}
diff --git a/src/ChatAgent.Shared/Models/HealthResponse.cs b/src/ChatAgent.Shared/Models/HealthResponse.cs
new file mode 100644
index 0000000..42d9312
--- /dev/null
+++ b/src/ChatAgent.Shared/Models/HealthResponse.cs
@@ -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
+{
+ ///
+ /// 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).
+ ///
+ public class HealthResponse
+ {
+ ///
+ /// Server health status string (e.g., "healthy").
+ /// Initialized to empty string to avoid null warnings with nullable enabled.
+ ///
+ public string Status { get; set; } = string.Empty;
+
+ ///
+ /// Server-side UTC timestamp at the moment the health check ran.
+ /// The client displays this to confirm the response is fresh.
+ ///
+ public DateTime Timestamp { get; set; }
+ }
+}