feature: Ward Gateway Service (Local-First Clinical Path)

This commit is contained in:
voltsrage
2026-06-23 16:45:38 +08:00
parent d8e142fffe
commit 1bf8359097
100 changed files with 5474 additions and 4 deletions
@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
public sealed class EncounterReplicaReadyCheck : IHealthCheck
{
private readonly GatewayDbContext _db;
public EncounterReplicaReadyCheck(GatewayDbContext db) => _db = db;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
var encounterCount = await _db.Encounters.CountAsync(ct);
if (encounterCount > 0)
return HealthCheckResult.Healthy("Replica data available.");
var state = await _db.SyncState.FirstOrDefaultAsync(ct);
if (state?.InitialSyncCompleted == true)
return HealthCheckResult.Healthy("Initial sync completed.");
return HealthCheckResult.Unhealthy(
"No encounter replica data — first boot requires central sync.");
}
}
@@ -0,0 +1,33 @@
using System.Text.Json;
using Microsoft.Extensions.Diagnostics.HealthChecks;
public static class HealthCheckResponseWriter
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
public static async Task WriteAsync(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var response = new
{
status = report.Status.ToString(),
totalDurationMs = report.TotalDuration.TotalMilliseconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
durationMs = e.Value.Duration.TotalMilliseconds,
description = e.Value.Description,
data = e.Value.Data.Count > 0 ? e.Value.Data : null,
exception = e.Value.Exception?.Message
})
};
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
}
}
@@ -0,0 +1,26 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
public sealed class RabbitMqHealthCheck : IHealthCheck
{
private readonly RabbitMqOptions _options;
public RabbitMqHealthCheck(IOptions<RabbitMqOptions> options) => _options = options.Value;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
var factory = new ConnectionFactory
{
HostName = _options.Host,
Port = _options.Port,
UserName = _options.Username,
Password = _options.Password
};
using var connection = await Task.Run(() => factory.CreateConnection(), cancellationToken);
var data = new Dictionary<string, object> { ["endpoint"] = connection.Endpoint.ToString() };
return HealthCheckResult.Healthy(data: data);
}
}
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using StackExchange.Redis;
public sealed class RedisHealthCheck : IHealthCheck
{
private readonly IConnectionMultiplexer _redis;
public RedisHealthCheck(IConnectionMultiplexer redis) => _redis = redis;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
var db = _redis.GetDatabase();
var latency = await db.PingAsync();
var data = new Dictionary<string, object> { ["ping_ms"] = latency.TotalMilliseconds };
return HealthCheckResult.Healthy(data: data);
}
}