Darling viewer wave-3: write-back surfaces (finding mute, mute-rule mgmt, alert history) (#1262)#1341
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
PgFindingStore.MuteStoryAsync/UnmuteStoryAsync(the exact statements themute_analysis_findingMCP tool writes — reused, not duplicated).PgFindingStore.GetMutedStoriesAsyncread that carries themute_idthe existingGetMutedHashesSqlomits.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_ruleswith the same columns/semantics the service'sPgMuteRuleStorereads.3. Alert history. A new Alerts tab: a read over
config_alert_logfor the selected server (newest first,dismissed = FALSE, selectable time range) with a detail pane surfacing the stored detail text and the pretty-printedcontext_jsondedup fingerprint. Read-only — no dismiss.Lite behaviors mirrored (citations)
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 matchLite/Analysis/FindingStore.cs:203and the store's twins.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).Lite/Windows/ManageMuteRulesWindow.xaml(.cs)(Add/Edit/Toggle/Delete/Purge-Expired, double-click to edit).Lite/Controls/AlertsHistoryTab.xaml+Lite/Services/LocalDataService.AlertHistory.cs— theAlertHistoryRowmetric-keyed value/threshold formatting (Lite Alert History: Value column shows raw float instead of a formatted value #1134), email-vs-trayStatusDisplay, the sharedAlertMetricClassifiercritical/warning/resolved row tints + muted dimming,dismissed = FALSEfilter, newest-first. "Create mute rule from this alert" mirrorsAlertsHistoryTab.MuteThisAlert_Click.Darling/PerformanceMonitor.Darling.Service/PgMuteRuleStore.csstatement-shape; alert read adaptsLocalDataService.GetAlertHistoryAsyncto Darling'sconfig_alert_log(nosource/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_ruleshas noserver_idcolumn — a rule scopes byserver_name(nullable = all), exactly as Lite'sDuckDbMuteRuleStore/MuteRule.Matchesdo — so the quirk does not live there. The realserver_id = 0quirk is in the analysis-finding mute path: the MCP "mute across all servers" writesserver_id = 0while the read filter isserver_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 inViewerDataService.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:ComposeAlertDetailTextfingerprint disclosure, muted label).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.Testssuite fully gated (211 passed, 2 skipped on gates needing a live SQL Server / the managed-PG runtime) against the portable dev PG 17.10. Acode-reviewerpass 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)
MainWindownow has five tabs (was four): Overview, Queries, Blocking, Recommendations, Alerts (all in theMainTabsTabControl, reached by header text).FindingsGridgains a right-click ContextMenu withMuteFindingMenuItem("Mute finding") andUnmuteFindingMenuItem("Unmute finding"), and a new "Muted" column.AlertsTimeRangeCombo(time range),Refresh+Manage Mute Rules…buttons,AlertsGrid(with a "Create mute rule from this alert…" ContextMenu),AlertDetailTextdetail 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
dismissed = TRUEhide 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 filtersdismissed = FALSElike Lite and offers no dismiss action.server_id = 0quirk (mirrored + documented, pending a cross-app decision).🤖 Generated with Claude Code