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:
Joseph Doherty
2026-05-28 06:36:44 -04:00
parent 487859bff0
commit 344379a40a
20 changed files with 382 additions and 55 deletions
@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-28 |
| Reviewer | claude-agent |
| Commit reviewed | `1eb6e97` |
| Open findings | 8 |
| Open findings | 6 |
## Summary
@@ -1038,9 +1038,11 @@ when the real RowVersion is supplied.
|--|--|
| Severity | Medium |
| Category | Code organization & conventions |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs`, `Configurations/SiteCallEntityTypeConfiguration.cs` (mappings for `OccurredAtUtc`, `IngestedAtUtc`, `CreatedAtUtc`, `UpdatedAtUtc`, `TerminalAtUtc`) |
**Resolution (2026-05-28):** Added two private static `ValueConverter<DateTime, DateTime>` / `ValueConverter<DateTime?, DateTime?>` UTC-enforcing converters to `AuditLogEntityTypeConfiguration` and applied them to `AuditEvent.OccurredAtUtc` and `AuditEvent.IngestedAtUtc` via `HasConversion(...)`. The converter re-tags `DateTimeKind.Utc` on hydrate (where SQL Server's `datetime2` provider strips the Kind flag) and on write (so a producer-supplied `Kind=Unspecified` literal still lands as UTC in the model cache). Coordinates with the sibling `Commons-019` resolution (init-setter on `AuditEvent` re-tags Kind=Utc at construction). Regression test in `tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs::Configure_UtcConverter_HydratesOccurredAtUtcAsKindUtc` inserts an Unspecified-Kind value, re-reads through a cleared change-tracker, and asserts `Kind == Utc` on both columns. The `SiteCall` mapping is out of scope for this close (sibling component task).
**Description**
`AuditEvent.OccurredAtUtc` / `IngestedAtUtc` and `SiteCall.CreatedAtUtc` /
@@ -1136,9 +1138,11 @@ aborts after the first failure with no further SPLITs.
|--|--|
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs:378-387` |
**Resolution (2026-05-28):** Wrapped the `reader.GetDateTime(0)` read with `DateTime.SpecifyKind(..., DateTimeKind.Utc)` so each returned boundary now carries `Kind=Utc`, matching the explicit defensive pattern already in `AuditLogPartitionMaintenance.GetMaxBoundaryAsync`. Added an inline comment explaining the rationale (SQL Server `datetime2` strips Kind through ADO.NET; boundary values are stored UTC). With sibling CD-018 also closed, the EF read path now enforces UTC at the column level — the raw-ADO defence here is belt-and-braces for this method, which bypasses EF entirely.
**Description**
`GetPartitionBoundariesOlderThanAsync` reads `reader.GetDateTime(0)` and adds the