Skip to content

Darling viewer wave-3: write-back surfaces (finding mute, mute-rule mgmt, alert history) (#1262)#1341

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/1262-darling-viewer-writeback
Jul 3, 2026
Merged

Darling viewer wave-3: write-back surfaces (finding mute, mute-rule mgmt, alert history) (#1262)#1341
erikdarlingdata merged 2 commits into
devfrom
feature/1262-darling-viewer-writeback

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

What

Turns the read-only Darling viewer (Darling/PerformanceMonitor.Darling.Viewer) into one that performs the plan's three write-back surfaces. Per the architecture plan's "Hard Problem 3" resolution, all user-initiated writes go straight to Postgres — the store is the coordination point, there is no viewer-to-service channel, and the running service honors the writes on its next read. The viewer writes only these coordination tables (analysis_muted, config_mute_rules), never collector data.

Surfaces added

1. Finding mute/unmute — Recommendations tab. Right-click a finding for a context menu:

  • Mute finding / Unmute finding, wired through the shared PgFindingStore.MuteStoryAsync/UnmuteStoryAsync (the exact statements the mute_analysis_finding MCP tool writes — reused, not duplicated).
  • A new Muted column + dimmed/italic row flag the state, driven by a new PgFindingStore.GetMutedStoriesAsync read that carries the mute_id the existing GetMutedHashesSql omits.
  • Muting doesn't retract the already-persisted latest batch — the engine drops the pattern on its next analysis run — so a just-muted finding stays visible, flagged. This matches Lite/Dashboard mute semantics.

2. Mute-rule management. A Manage Mute Rules window (Add / Edit / Toggle / Delete / Purge-Expired) reachable from the new Alerts tab, plus a Create mute rule from this alert context action. INSERT/UPDATE/DELETE against config_mute_rules with the same columns/semantics the service's PgMuteRuleStore reads.

3. Alert history. A new Alerts tab: a read over config_alert_log for the selected server (newest first, dismissed = FALSE, selectable time range) with a detail pane surfacing the stored detail text and the pretty-printed context_json dedup fingerprint. Read-only — no dismiss.

Lite behaviors mirrored (citations)

  • Finding mute (no Lite UI equivalent — Lite only mutes via MCP Lite/Mcp/McpAnalysisTools.cs:581): the interaction shape mirrors the Dashboard Recommendations mute (Dashboard/Controls/RecommendationsContent.xaml.cs:402-446, keyed on (serverId, story_path_hash), reason "Muted from Recommendations"), which is the only in-app finding-mute UI. Store semantics match Lite/Analysis/FindingStore.cs:203 and the store's twins.
  • Mute-rule editor: Lite/Windows/MuteRuleDialog.xaml(.cs) field-for-field (reason, expiration, server, metric, the metric-driven pattern-field show/hide, the "no filters = mute all" confirmation).
  • Mute-rule manager: Lite/Windows/ManageMuteRulesWindow.xaml(.cs) (Add/Edit/Toggle/Delete/Purge-Expired, double-click to edit).
  • Alert history: presentation from Lite/Controls/AlertsHistoryTab.xaml + Lite/Services/LocalDataService.AlertHistory.cs — the AlertHistoryRow metric-keyed value/threshold formatting (Lite Alert History: Value column shows raw float instead of a formatted value #1134), email-vs-tray StatusDisplay, the shared AlertMetricClassifier critical/warning/resolved row tints + muted dimming, dismissed = FALSE filter, newest-first. "Create mute rule from this alert" mirrors AlertsHistoryTab.MuteThisAlert_Click.
  • Store SQL parity: mute-rule CRUD mirrors Darling/PerformanceMonitor.Darling.Service/PgMuteRuleStore.cs statement-shape; alert read adapts LocalDataService.GetAlertHistoryAsync to Darling's config_alert_log (no source/v_config_alert_log — Darling has no archive tier).

The server_id=0 quirk

The brief flagged a "cross-app server_id=0 quirk in mute rules." On inspection, config_mute_rules has no server_id column — a rule scopes by server_name (nullable = all), exactly as Lite's DuckDbMuteRuleStore/MuteRule.Matches do — so the quirk does not live there. The real server_id = 0 quirk is in the analysis-finding mute path: the MCP "mute across all servers" writes server_id = 0 while the read filter is server_id = $1 OR server_id IS NULL, so a 0-scoped mute only matches server 0. I mirrored, did not fix it, and documented it where it actually lives (PgFindingStore.GetMutedStoriesAsync) plus a clarifying note in ViewerDataService.MuteRules.cs. The viewer always mutes per-selected-server (a real non-zero id), so it never takes the 0 path.

Tests

Darling/Darling.Tests/ViewerWave3Tests.cs:

  • Ungated: SQL/dialect/schema-parity pins (alert-history read, mute-rule CRUD columns vs the V3 schema, the muted-stories read) and pure-display pins (metric-keyed value formatting, status mapping, classification, ComposeAlertDetailText fingerprint disclosure, muted label).
  • Gated (DARLING_TEST_PG), following the existing [Collection("live-postgres")] pattern: finding mute -> fresh viewer read shows muted with its mute_id -> unmute; mute-rule insert/update/toggle/delete + purge-expired; alert-history read excluding dismissed rows.

Both existing suites stay green. Ran the full Darling.Tests suite fully gated (211 passed, 2 skipped on gates needing a live SQL Server / the managed-PG runtime) against the portable dev PG 17.10. A code-reviewer pass found 2 mediums (global-mute unmute safety, non-deterministic hash resolution) + a few lows — all fixed in this branch and re-verified.

UI structure (for driving via UIA)

  • Main window MainWindow now has five tabs (was four): Overview, Queries, Blocking, Recommendations, Alerts (all in the MainTabs TabControl, reached by header text).
  • Recommendations tab: FindingsGrid gains a right-click ContextMenu with MuteFindingMenuItem ("Mute finding") and UnmuteFindingMenuItem ("Unmute finding"), and a new "Muted" column.
  • Alerts tab: AlertsTimeRangeCombo (time range), Refresh + Manage Mute Rules… buttons, AlertsGrid (with a "Create mute rule from this alert…" ContextMenu), AlertDetailText detail pane.
  • MuteRulesWindow (title "Manage Mute Rules"): RulesGrid + Add Rule.../Edit/Toggle/Delete/Purge Expired/Close buttons.
  • MuteRuleEditDialog (title "Create Mute Rule" / "Edit Mute Rule"): ReasonBox, ExpirationCombo, ServerNameBox, MetricCombo, pattern boxes, Save/Cancel.

Deliberately NOT done

  • No alert dismiss in the viewer. Lite's alert-history dismiss writes a durable dismissed = TRUE hide flag (not cosmetic), but surface 3 is specified as a read surface and the brief says not to invent durable dismiss — so the read simply filters dismissed = FALSE like Lite and offers no dismiss action.
  • No fix to the analysis-finding server_id = 0 quirk (mirrored + documented, pending a cross-app decision).
  • Did not restructure the four existing tabs or touch collector-data paths.

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 2, 2026 19:09
The Darling viewer was read-only. This adds the plan's three write-back
surfaces, all writing straight to Postgres (the store is the coordination
point; the service honors them on its next read):

1. Finding mute/unmute in the Recommendations tab — a right-click context
   menu mutes/unmutes the selected finding's story pattern through the shared
   PgFindingStore.MuteStoryAsync/UnmuteStoryAsync (the statements the
   mute_analysis_finding MCP tool writes). A new "Muted" column + dimmed row
   flag the state via a new PgFindingStore.GetMutedStoriesAsync read (carrying
   the mute_id GetMutedHashesSql omits). Per-server mutes are unmutable here;
   a global (server_id IS NULL) mute shows muted but is not unmutable, so the
   viewer never silently changes an all-servers scope.

2. Mute-rule management — a Manage Mute Rules window (Add/Edit/Toggle/Delete/
   Purge-Expired, a dark port of Lite's ManageMuteRulesWindow + MuteRuleDialog)
   plus a "Create mute rule from this alert" action, doing CRUD against
   config_mute_rules with the same columns/semantics PgMuteRuleStore reads.

3. Alert history — a new Alerts tab reading config_alert_log for the selected
   server (newest first, dismissed = FALSE, selectable range), mirroring Lite's
   AlertHistoryRow presentation with a detail pane surfacing the stored detail
   and the pretty-printed context_json dedup fingerprint. Read-only (no dismiss,
   matching the wave brief).

Additive: the four existing tabs are untouched; the new dark windows share one
ViewerDarkTheme dictionary; all writes are parameterized naive-UTC. The viewer
writes ONLY these coordination tables, never collector data.

Tests: ViewerWave3Tests pins the SQL/dialect/schema-parity + pure display
logic (ungated) and adds gated (DARLING_TEST_PG) live round-trips for every
write path — mute -> fresh read shows muted -> unmute; mute-rule CRUD +
purge-expired; alert-history read excluding dismissed rows. Full Darling.Tests
suite green (211 passed) fully gated against a portable PG 17.10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The explanatory text was mirrored from Lite's window, but Darling is a
headless service — there is no tray. Muted alerts are suppressed from
alert delivery (email and webhooks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit ac1b7b6 into dev Jul 3, 2026
2 checks passed
@erikdarlingdata erikdarlingdata deleted the feature/1262-darling-viewer-writeback branch July 3, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant