Notify.Send mints NotificationId as Guid "N" (no dashes) and the outbox parses it back into
the audit row's CorrelationId, so the Flow Debugger's Notify node linked and relayed a "D"
string the Notifications table never matches (found live: the report opened empty). The
link and RetryNotificationRequest now use "N", and the Notification report normalises any
Guid-shaped ?correlationId= to "N" so Audit Log and Flow Debugger callers both land.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
FlowDebugService.PredictMethodAsync resolved route targets with a sync-over-async
GetAwaiter().GetResult() inside the predictor's synchronous resolver — on the circuit's
single-threaded renderer sync context that blocked the dispatcher forever the moment a
method whose script routes to a site script was chosen (every later click and JS-interop
reply queued behind it; the page looked inert with a healthy websocket and no error).
Misdiagnosed on 2026-09-06 as a login-disabled localStorage stall. Now a resolve-and-rewalk
loop: each pass records the targets it could not answer, they are awaited between passes,
and the walk converges once a pass asks for nothing new (bounded by MaxPredictionPasses).
Regression test drives the service under a single-thread pump sync context (fails in 10 s
on the old code, passes now) plus a nested Scripts.Call resolve-across-passes test.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
IFlowDebugService/FlowDebugService (scoped, registered beside IDataConnectionMoveService).
Run/replay go to the per-node ManagementActor through ManagementActorHolder with the Blazor
principal projected to AuthenticatedUser, exactly as DataConnectionMoveService does; the
FlowDebugRunResult payload is deserialised (camelCase) and fed to FlowGraphBuilder, while
ManagementError/Unauthorized/holder-null/transport faults collapse into FlowDebugOutcome
with the server's error code verbatim (STANDBY_NODE reaches the page untouched). Prediction
is DB-only: PredictMethodAsync hands FlowShapePredictor a memoised resolver that maps a
route target → Instance.UniqueName → template parent chain (nearest definition wins, cycle
guarded) → TemplateScript.Code; PredictValueChangeAsync roots at the write and appends each
script whose value-change trigger names the attribute (same attributeName/attribute keys
the site's TriggerRouting parser reads).
Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
P6-7 (CWE-1236): AuditEndpoints CSV export quoted cells on comma/quote/CR/LF
but never neutralized a leading =, +, -, @, TAB, or CR — inbound-API-caller
text landing in Target/ErrorMessage/RequestSummary/ResponseSummary/Extra
could execute as a formula/DDE trigger when opened in Excel. AppendField now
prefixes a literal single quote on free-text columns only (never the
GUID/enum/numeric/date/bool columns the exporter formats itself), applied
before RFC 4180 quoting. The CSV was also missing ExecutionId/
ParentExecutionId/SourceNode that the JSONL export already carried; both are
now 24 columns in the same AuditExportRow order. A second, independently
maintained CSV writer (CentralUI's AuditLogExportService, feeding the Audit
Log page's bulk-export button) had the identical gap and got the identical
fix + column alignment.
P6-8: `audit export` was the one CLI HTTP path with no deadline at all — a
residual gap from the ManagementHttpClient timeout-honesty fix, which made
HttpClient.Timeout infinite and the per-call token the single deadline
everywhere except this streamed GET. RunExportAsync now takes a headers-
received deadline (CancelAfter, reusing ManagementHttpClient.
DefaultConnectTimeout) around the initial GetAsync, an inactivity deadline
around the streaming copy (rearmed on every successful read, so large exports
are bounded by silence, not total size), and an external CancellationToken
wired to Console.CancelKeyPress in AuditCommands so Ctrl-C cancels promptly.
A stalled/faulted stream now produces a friendly error noting the partial
output file instead of hanging or dumping a stack trace.
Wire the operator Alarm Summary page to the transient per-site live alarm
cache (ISiteAlarmLiveCache, T4). Live-cache-first: on site select the page
subscribes and rebuilds rows/rollup from near-real-time onChanged deltas; the
15s poll is kept untouched as the authority for NotReporting and as the
safety net whenever the cache is not live (pre-seed or degraded stream). Both
paths mutate shared state only via the Blazor dispatcher, so they never race,
and each rebuild is an idempotent snapshot.
- IAlarmSummaryService.BuildFromLiveAlarms: flattens a live AlarmStateChanged
snapshot to AlarmSummaryRows with the same deterministic instance-then-name
sort as GetSiteAlarmsAsync; NotReporting always empty on the live path.
- AlarmSummary.razor: inject ISiteAlarmLiveCache; subscribe on select,
re-subscribe on site change, unsubscribe on leave + Dispose.
- Tests: service BuildFromLiveAlarms flatten/sort/empty; page subscribe,
poll-fallback render, onChanged rebuild, unsubscribe on leave/change/dispose.
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
GetSeriesAsync now branches on window width: windows wider than
RollupThresholdHours (default 168h/7d) read pre-aggregated hourly rollups
via GetHourlySeriesAsync; windows at or below the threshold read raw
samples via GetRawSeriesAsync (preserving intra-minute detail on 24h/7d).
The boundary is strict greater-than, so exactly 168h stays on the raw
path. KpiSeriesBucketer.Bucket runs unchanged on whichever series (a 90d
rollup can still exceed the point ceiling). Both ctors route identically
via a shared FetchSeriesAsync helper reading _options.RollupThresholdHours,
the same options instance that already supplies DefaultMaxSeriesPoints.
GetSeriesAsync public signature unchanged.
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj