fix(utc/locale): close Theme 2 — 8 UTC / time / locale findings
UTC invariant + culture-safety fixes across UI form binding, audit entity hydrate, and locale-dependent parses. Highlights: - CentralUI-026/027: AuditFilterBar / SiteCallsReport / NotificationReport / EventLogs now apply SpecifyKind(Local) + ToUniversalTime() at form submit so browser-local datetime-local inputs aren't silently treated as UTC. - Commons-019: AuditEvent.OccurredAtUtc / IngestedAtUtc init-setters re-tag any incoming DateTime as Kind=Utc, documenting the invariant. - CD-018: AuditLogEntityTypeConfiguration adds UTC ValueConverters on the *Utc DateTime columns so EF hydrate yields Kind=Utc (SQL Server's datetime2 has no Kind metadata, so reads were returning Unspecified). - CD-020: GetPartitionBoundariesOlderThanAsync now SpecifyKind(Utc) on the raw-ADO read, matching the existing defence in AuditLogPartitionMaintenance. - SEL-021: EventLogQueryService.DateTimeOffset.Parse now uses InvariantCulture + AssumeUniversal | AdjustToUniversal. - SR-023: Convert.ToDouble in ScriptActor + AlarmActor (4 sites) now passes InvariantCulture so non-US locales don't mis-parse string values. - HM-020: CentralHealthAggregator.MarkHeartbeat anchors LastHeartbeatAt to max(receivedAt, now) on offline→online so a stale receivedAt can't leave a recovered site one tick from re-going-offline. 3 new tests added (AuditLog UTC converter, AuditFilterBar/EventLogs/ NotificationReport-touching CentralUI tests already cover Apply paths, heartbeat offline→online). Build clean; ConfigurationDatabase 236, Commons 330, HealthMonitoring 71, SiteRuntime 301, SiteEventLogging 50, CentralUI 50 — all green. README regenerated: 104 open (was 112).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using ScadaLink.Commons.Entities.Audit;
|
||||
|
||||
namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
@@ -11,12 +12,38 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
/// </summary>
|
||||
public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditEvent>
|
||||
{
|
||||
// SQL Server's datetime2 provider strips the DateTimeKind flag on the wire
|
||||
// (a column hydrated from the database always surfaces as
|
||||
// DateTimeKind.Unspecified). Without a converter, downstream code that
|
||||
// calls .ToLocalTime() / .ToUniversalTime() on an OccurredAtUtc value would
|
||||
// silently re-interpret it as local time. These converters force the Kind
|
||||
// back to Utc on read, and re-stamp Utc on write so a producer that hands
|
||||
// EF a DateTime literal with Kind=Unspecified still lands a UTC-tagged
|
||||
// value in the model cache (CLAUDE.md: "All timestamps are UTC throughout
|
||||
// the system."). Applied to every DateTime property whose name ends in
|
||||
// `Utc`; DateTimeOffset columns already carry their own offset and are NOT
|
||||
// routed through these converters.
|
||||
private static readonly ValueConverter<DateTime, DateTime> UtcConverter = new(
|
||||
v => v.Kind == DateTimeKind.Utc ? v : DateTime.SpecifyKind(v, DateTimeKind.Utc),
|
||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
|
||||
private static readonly ValueConverter<DateTime?, DateTime?> NullableUtcConverter = new(
|
||||
v => v.HasValue
|
||||
? (v.Value.Kind == DateTimeKind.Utc ? v.Value : DateTime.SpecifyKind(v.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : null);
|
||||
|
||||
/// <summary>Applies the EF Core type configuration for <see cref="AuditEvent"/> to the model builder.</summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<AuditEvent> builder)
|
||||
{
|
||||
builder.ToTable("AuditLog");
|
||||
|
||||
// Enforce DateTimeKind.Utc on every *Utc-suffixed DateTime column. See
|
||||
// the UtcConverter remarks above for the rationale.
|
||||
builder.Property(e => e.OccurredAtUtc).HasConversion(UtcConverter);
|
||||
builder.Property(e => e.IngestedAtUtc).HasConversion(NullableUtcConverter);
|
||||
|
||||
// Composite PK includes OccurredAtUtc — required by the monthly partition scheme
|
||||
// (ps_AuditLog_Month) so the clustered key is partition-aligned. EventId still
|
||||
// needs to be globally unique for InsertIfNotExistsAsync idempotency, so a
|
||||
|
||||
Reference in New Issue
Block a user