Skip to content

Release v1.3.0#216

Merged
erikdarlingdata merged 49 commits intomainfrom
dev
Feb 20, 2026
Merged

Release v1.3.0#216
erikdarlingdata merged 49 commits intomainfrom
dev

Conversation

@erikdarlingdata
Copy link
Copy Markdown
Owner

Summary

  • Full release of v1.3.0 — see CHANGELOG for complete details

Highlights

  • New: Memory Overview columns (Physical/SQL Server/Target Memory), Current Configuration view, drill-down filters and context menus, sidebar alert badge
  • Schema upgrade: collect.memory_stats gains total_physical_memory_mb and committed_target_memory_mb (automatic via installer, idempotent)
  • Fixed: Deadlock XML processor UTC timezone mismatch, alert badge sync, NOC timestamps, drill-down chart sizing, and 10+ other bug fixes
  • Lite: DuckDB IO fixes, parquet archives, checkpoint optimization, expanded database config collector

Test plan

  • CLI installer tested: fresh install (sql2016), upgrade (sql2017, sql2019), idempotency (sql2022)
  • All 52 install scripts succeeded on all servers
  • Deadlock XML timezone fix verified — 192 stuck events parsed successfully
  • Dashboard launched and validated post-upgrade
  • Lite launched and validated
  • CHANGELOG entries for 1.2.0 and 1.3.0

🤖 Generated with Claude Code

erikdarlingdata and others added 30 commits February 19, 2026 13:51
ReleaseMutex() throws ApplicationException if called by a thread
that doesn't own the mutex. This happens when the app exits after
detecting a second instance, or when shutdown is triggered from a
non-owning thread. Track ownership and only release if we own it.

Fixed in both Dashboard and Lite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add checkpoint_threshold=1GB to connection string to prevent auto-checkpoint stalls
- Add manual CHECKPOINT after each collection cycle during idle time
- Change collector execution from parallel Task.WhenAll to sequential per-server
- Fix using var timing bug across all 16 collector files: change to explicit
  using blocks so appender Dispose (flush + connection close) is captured
  inside the DuckDB stopwatch, giving accurate timing in collection_log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t-optimization

DuckDB checkpoint optimization and timing fix
)

Display queries now read from views that UNION hot DuckDB tables with
archived parquet files, so users can see the full 90-day retention window
instead of only the 7-day hot data window. Views are created at startup
and refreshed after each archive cycle.

Adds daily database compaction to prevent file bloat from DuckDB's
append-only storage. Compaction exports all tables to a fresh database
via ATTACH/CREATE TABLE AS, swaps files, and recreates indexes/views.
Includes a 1GB size watchdog that logs warnings between compaction cycles.

Tested: 24/24 — archive round-trip (export to parquet, delete hot rows,
verify view still returns all rows) and compaction (19MB → 5MB, all 1055
rows preserved across 25 tables).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…queries

Parquet archive visibility and scheduled compaction
…) (#163)

Collector now reports state, collation, RCSI, snapshot isolation, stats settings,
encryption, security, CDC, broker, and three version-gated columns (ADR 2019+,
memory optimized 2019+, optimized locking 2025+). Dynamic SQL builds the SELECT
list based on SQL Server version; Azure SQL DB always gets 2019+ columns.

Schema version bumped to v11 (drop/recreate database_config table).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes for "DuckDBOpen failed: Cannot open file" errors introduced
by PR #159 (checkpoint) and PR #162 (compaction):

1. Timer initialization: DateTime.MinValue → DateTime.UtcNow prevents
   compaction/archival from firing on the very first collection cycle

2. Inline checkpoint: moved CHECKPOINT to end of RunDueCollectorsAsync
   using the existing connection pool instead of opening a separate
   DuckDB instance that conflicts via OS file locks

3. Atomic file swap: replaced two-step File.Move in CompactAsync with
   File.Replace (single OS operation, no window where the database file
   is missing) plus retry logic for locked files and WAL cleanup

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ed (#139) (#172)

SQL view TOP (20) → TOP (50) ensures low-volume poison waits like
THREADPOOL reach the client. UI default selection cap raised from
20 to 30 across both Dashboard and Lite, giving enough room for
poison waits + usual suspects + top 10 without squeezing.

Closes #139

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Moved the Blocking/Deadlock Trends sub-tab before Blocking and
Deadlocks so it's the default view when opening the Locking section.

Closes #171

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
DeadlockSeverity was only checking the perfmon counter delta between
poll cycles, so deadlocks that occurred before Dashboard startup or
between the first two polls showed green. Now also checks
LastDeadlockMinutesAgo from extended events: <=10 min = Critical,
<=60 min = Warning. Added missing OnPropertyChanged notification
so the severity dot updates when the timestamp arrives.

Closes #170

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…) (#175)

Default Trace Events and Trace Analysis tabs belong under Overview, not
Resource Metrics. Extracted into new DefaultTraceContent UserControl
following the same pattern as DailySummaryContent and CriticalIssuesContent.
Removed dead column filter popup infrastructure from ResourceMetricsContent.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…176)

The view report.long_running_query_patterns aggregates ALL time then
takes TOP 50 by avg_duration_ms. When recent patterns are shorter than
old load test patterns, the dashboard's time filter eliminates all 50
rows. Fix by inlining the query with time filter inside the CTE so
aggregation only considers data within the selected time range.

The second part of #168 (Trace Analysis in wrong location) was already
fixed in PR #175.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Tooltips used Euclidean pixel distance requiring the mouse to be within
50px of a data point marker. With sparse time-series data (points every
15 mins), large gaps between markers made tooltips unreliable or absent.

Fix: use X-axis (time) proximity as primary filter (80px horizontal),
Y-axis distance as tiebreaker for multi-series charts. Tooltips now
appear reliably when hovering at any Y position near a data point's
time. Added try-catch for robustness, reduced throttle to 30ms.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Charts in query/procedure drill-down windows were fixed at 250px height,
making them hard to read especially on larger monitors. Changed from
fixed height to proportional layout (2* chart / 3* grid) with MinHeight
250 so charts scale with window size while maintaining a minimum.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All three drill-down windows (Query Store, Query Stats, Procedure Stats)
fetched ALL historical data with no time limit — up to 3776 rows per
query, each carrying ~10KB of plan XML. Added 7-day time filter to
collection_time on all three queries to keep result sets manageable.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
New "Current Configuration" tab in Overview shows the latest state of
server settings, database settings, and trace flags using ROW_NUMBER
windowing over the change history tables. Three sub-tabs with inline
column filters for quick searching.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…iew (#140) (#181)

Adds total_physical_memory_mb and committed_target_memory_mb columns to
collect.memory_stats table, collected from sys.dm_os_sys_memory and
sys.dm_os_sys_info. Memory Overview summary panel now shows absolute GB
values for Physical Memory, SQL Server Memory, Target Memory, Buffer Pool,
and Plan Cache alongside percentages. Includes ALTER TABLE migration for
existing installs.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Red badge on the Alerts sidebar button shows the count of active alerts
from the last 24 hours. Updates on each alert check cycle and on startup.
Shows "99+" when count exceeds 99, collapses when no alerts.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ing, aggregate by collection_time (#189)

- Remove query_plan_text/query_plan from drill-down SELECT queries (was loading 122 MB inline, hanging the app)
- Add on-demand plan fetch methods for all three drill-down types
- Pass parent view's time range (hoursBack/fromDate/toDate) through to drill-down queries
- Aggregate query_stats drill-down by collection_time with MAX for cumulative counters (matches report.query_stats_summary pattern)
- Aggregate query_store drill-down by collection_time + plan_id with weighted averages
- Remove broken 7-day DATEADD filter from PR #179 that prevented drill-downs from returning data
- Fix Download button: increase RowHeight 30->35, remove stale IsEnabled binding, async on-demand fetch
- Fix DefaultTraceContent/CurrentConfigContent StaticResource -> DynamicResource for theme support
- Add upgrade script for memory_stats columns (1.2.0 -> 1.3.0)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
 #183, #184) (#190)

RowHeight 25 -> 28 to match all other Dashboard DataGrids.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
AcknowledgeServerAlerts_Click only updated AlertStateService (tab badges)
but never hid alerts in EmailAlertService, so the sidebar Alerts button
badge count stayed unchanged. Now also hides alerts for the acknowledged
server and refreshes the badge. Alert History dismiss actions also
immediately update the sidebar badge via an AlertsDismissed event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix sidebar alert badge not clearing on acknowledge (fixes #186)
…187)

Extended Events timestamps are UTC but the NOC queries used SYSDATETIME()
(local time). On a UTC-8 server, DATEDIFF returned -480 for recent events,
which FormatMinutesAgo treated as "just now". Changed to SYSUTCDATETIME().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mp-187

Fix NOC deadlock/blocking timestamps showing 'just now' for stale events (fixes #187)
RESOURCE_SEMAPHORE and RESOURCE_SEMAPHORE_QUERY_COMPILE were missing from
the wait type picker because they had no collected data. Poison waits are
now always injected into the picker list so they appear and get pre-selected
by default, ready to show data if/when those waits occur.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-188

Add missing poison wait defaults to wait stats picker (fixes #188)
…#199)

LandingPage created its own ServerManager instance, so its in-memory
server list was stale after MainWindow added a server. Now MainWindow
passes its ServerManager to LandingPage so they share the same instance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sync-199

Fix newly added servers not appearing on Overview until restart (fixes #199)
Added IsDoubleClickOnRow() helper that walks the visual tree to distinguish
DataGridRow clicks from DataGridColumnHeader clicks. Applied the guard to
all five MouseDoubleClick handlers across QueryPerformanceContent, ServerTab,
and ManageServersWindow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
erikdarlingdata and others added 19 commits February 20, 2026 07:45
Fix double-click on column header triggering row drill-down (fixes #195)
…xpress (fixes #192)

All GetBoolean() and Convert.ToInt32() calls now check IsDBNull first,
defaulting to false/0. Prevents "Object cannot be cast from DBNull to
other types" on editions where certain sys.databases columns return NULL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-192

Fix DBNull cast error in Lite database_config collector (fixes #192)
…ixes #194)

Plan cache execution_count is a cumulative counter — MAX is the correct
aggregation, matching the main grid. The "Total Executions" label implied
a sum, confusing users. Renamed to "Executions" in QueryStatsHistory and
ProcedureHistory windows. Query Store window keeps "Total Executions"
since it correctly sums interval deltas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename misleading Total Executions label in drill-down summaries (fixes #194)
NumericFilterHelper: Replace naive Split('-') range parsing with TryParseRange
that finds the separator dash by looking for a '-' preceded by a digit. Correctly
handles -100-200, -100--50, and .. syntax. Previously, negative-start ranges were
silently ignored due to the StartsWith('-') guard.

MainWindow: Wrap MCP StopAsync in Task.Run to avoid sync-over-async deadlock on
the WPF UI thread during app close.

Addresses items from #113 and #112.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CDC capture agents use `waitfor delay @waittime` and run indefinitely by design.
Service Broker listeners use `waitfor receive`. Neither should trigger long-running
query alerts.

Adds NOT LIKE filters for both patterns in Dashboard (live DMV query) and Lite
(DuckDB alert query).

Fixes #151.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrates 5 DataGrids from the old TextBox-in-header filter style to the
standard popup filter pattern used by the rest of the Dashboard:

- DefaultTraceContent: DefaultTraceEventsDataGrid (7 cols), TraceAnalysisDataGrid (9 cols)
- CurrentConfigContent: ServerConfigDataGrid (9 cols), DatabaseConfigDataGrid (5 cols),
  TraceFlagsDataGrid (5 cols — previously had no filters at all)

All columns now have consistent popup filter buttons with operator support
and gold active-filter indicators.

Fixes #200.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix numeric filter negative range parsing and MCP shutdown deadlock risk
…ning-151

Exclude WAITFOR sessions from long-running query alerts
Replace TextBox-in-header filters with popup filter buttons
…210)

Settings dropdown had 1h/6h/24h/7d but dashboard buttons offer
1h/4h/8h/12h/24h/7d/30d. Aligned the dropdown to match exactly.
Fixed fallback index for 24-hour default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix settings time range dropdown to match dashboard buttons
…ows (#206)

All three drill-down windows (QueryStatsHistory, QueryExecutionHistory,
ProcedureHistory) now have popup filter buttons on every column and
right-click context menus for copy cell/row/all rows and CSV export,
matching the behavior of all other DataGrids in the application.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rs-206

Add column filters and context menus to drill-down history windows
When dismissing alerts from inside a server tab (right-clicking Locking,
Memory, etc.), the sidebar Alerts badge count was not updated because
ServerTab only called AlertStateService.AcknowledgeAlert (tab badges)
without touching the EmailAlertService alert log (sidebar badge source).

Added AlertAcknowledged event on ServerTab, fired after sub-tab dismiss.
MainWindow subscribes and hides email alerts + refreshes sidebar badge,
matching the landing page dismiss flow.

Note: the sidebar badge re-notification follows a 5-minute cooldown per
alert type per server — this is by design to avoid notification spam.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix sidebar alert badge not syncing with sub-tab dismissals
- Version bump all 4 csproj files from 1.2.0 to 1.3.0
- Fix UTC timezone mismatch in process_deadlock_xml: sp_BlitzLock expects
  local time dates but was receiving UTC, causing 0 parsed results on any
  server not in UTC. Convert dates to local time before passing to sp_BlitzLock.
- Add CHANGELOG entries for both 1.2.0 (previously missing) and 1.3.0
- 1.3.0 includes schema upgrade warning for large memory_stats tables

Tested: deployed fix to sql2016/2017/2019/2022, verified 192 stuck
unprocessed deadlock XML events parsed successfully on sql2022.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit 1de2414 into main Feb 20, 2026
7 checks passed
erikdarlingdata added a commit that referenced this pull request Apr 16, 2026
Port PS PRs #216, #217, #219, #224, #229, #230, #231 to PM.

PlanAnalyzer changes:
- Rule 5: Suppress for Key Lookups (point lookups mislead per-execution estimates)
- Rule 8: Enhanced parallel skew with batch mode sort detection and practical context
- Rule 9: Large memory grant shows top 3 consumers sorted by row count
- Rule 10: Key lookup overhaul — show output columns, check predicate filtering, softer advice
- Rules 11/12/29: Suppress on 0-execution nodes (operator never ran)
- Rule 11: I/O wait severity elevation when scan hits disk
- Rule 24: FormatNodeRef helper includes object name for data access operators
- Rule 26: Suppress when row goal prediction was correct, specific cause detection
- Wait stats: DescribeWaitType with full wait type coverage, multi-wait summary
- New helpers: GetWaitLabel, HasSignificantIoWaits, IdentifyRowGoalCause, FormatNodeRef
- GetOperatorOwnElapsedMs changed to internal for BenefitScorer access

BenefitScorer (new file):
- Stage 1: MaxBenefitPercent for operator-level rules (filter, spill, lookup, etc.)
- Stage 2: Wait stats benefit scoring with parallel allocation (Joe's formula)

PlanModels additions:
- MaxBenefitPercent and ActionableFix on PlanWarning
- WaitBenefit class and WaitBenefits list on PlanStatement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
erikdarlingdata added a commit that referenced this pull request Apr 22, 2026
* Implements #843 in Lite

* Implements #843 for Full Dashboard

* Add trailing newlines to ScrollPanBehavior files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Harden DuckDB queries: parameterize values, escape paths, fix IsArchiving race

Addresses security findings from #840:
- #846: Escape single quotes in file paths interpolated into read_parquet() and COPY TO
- #847: Use DuckDB $1 parameters for DateTime values instead of string interpolation
- #849: Make IsArchiving volatile-backed to prevent stale reads across threads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Encrypt webhook URLs with DPAPI via Windows Credential Manager

Moves Teams and Slack webhook URLs from plaintext settings.json/preferences.json
to Windows Credential Manager (DPAPI-encrypted), matching the existing pattern
used for SMTP passwords and SQL Server credentials.

Includes automatic migration: on first settings load, any plaintext URLs are
moved to Credential Manager and removed from the JSON file.

Closes #848

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Lazy-load server tabs: only load visible tab on open, full-load on first visit

Initial tab open and Refresh button now only load the currently visible tab.
First switch to any tab triggers a full refresh of that tab (all sub-tabs).
Subsequent refreshes only hit the active sub-tab.

Ctrl+Click on Refresh Tab (or Ctrl+F5) refreshes all tabs at once.
Apply to All Tabs retains existing full-refresh behavior.

Fixes #835 — prevents heavy queries (e.g. GetQueryStatsAsync) from running
on tab open when the user is only viewing Overview.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Cap query/procedure/query store grid results to TOP 500

GetQueryStatsAsync, GetProcedureStatsAsync, and GetQueryStoreDataAsync
were returning unbounded result sets. With 49 databases and 742K rows
in query_stats over 3 days, the GROUP BY with plan XML could produce
thousands of rows and timeout after 120 seconds.

TOP 500 ordered by avg CPU desc is plenty for a grid view and prevents
the query from consuming unbounded memory on large installations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove pointless WAITFOR DECOMPRESS filters from stats/store queries

The CAST(DECOMPRESS(...)) NOT LIKE N'WAITFOR%' filter was decompressing
query text on every row in query_stats and query_store_data just to skip
WAITFOR queries. WAITFOR has no plan and no meaningful stats — it only
matters in query snapshots (active sessions), where the filter remains.

On a 742K-row query_stats table, this was a significant contributor to
the 120-second query timeouts reported in #835.

The snapshot filters (report.query_snapshots) and MCP phased queries
are untouched — they filter after TOP on already-hydrated text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor query/procedure/query store stats to phased DECOMPRESS approach

All three grid queries now use a 3-phase pattern:
1. Aggregate numerics into temp table (no DECOMPRESS)
2. Sum across lifetimes, rank TOP 500
3. OUTER APPLY to decompress text/plan for only the 500 winners

On a 742K-row query_stats table, this reduces DECOMPRESS calls from
742K to 500 — eliminating the 16+ minute query times reported in #835.

Matches the existing phased pattern used by the MCP query tools.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix FinOps TDE recommendation on SQL Server 2019+ (#854)

TDE moved to Standard Edition in SQL 2019, so dm_db_persisted_sku_features
no longer reports it as Enterprise-only. Add version check to give
version-appropriate licensing guidance instead of falsely claiming no
databases use TDE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Sync PlanAnalyzer and BenefitScorer from PerformanceStudio (Apr 9-16)

Port PS PRs #216, #217, #219, #224, #229, #230, #231 to PM.

PlanAnalyzer changes:
- Rule 5: Suppress for Key Lookups (point lookups mislead per-execution estimates)
- Rule 8: Enhanced parallel skew with batch mode sort detection and practical context
- Rule 9: Large memory grant shows top 3 consumers sorted by row count
- Rule 10: Key lookup overhaul — show output columns, check predicate filtering, softer advice
- Rules 11/12/29: Suppress on 0-execution nodes (operator never ran)
- Rule 11: I/O wait severity elevation when scan hits disk
- Rule 24: FormatNodeRef helper includes object name for data access operators
- Rule 26: Suppress when row goal prediction was correct, specific cause detection
- Wait stats: DescribeWaitType with full wait type coverage, multi-wait summary
- New helpers: GetWaitLabel, HasSignificantIoWaits, IdentifyRowGoalCause, FormatNodeRef
- GetOperatorOwnElapsedMs changed to internal for BenefitScorer access

BenefitScorer (new file):
- Stage 1: MaxBenefitPercent for operator-level rules (filter, spill, lookup, etc.)
- Stage 2: Wait stats benefit scoring with parallel allocation (Joe's formula)

PlanModels additions:
- MaxBenefitPercent and ActionableFix on PlanWarning
- WaitBenefit class and WaitBenefits list on PlanStatement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fall back to single-database mode when Azure master is inaccessible (#857)

On Azure SQL DB, some logins (e.g. Microsoft Dynamics 365 FO) are granted
access only to a specific user database and not to master. The three
collectors that enumerate databases via master — query_stats,
database_size_stats, file_io_stats — would fail the first time and
produce an empty screen.

GetAzureDatabaseListAsync now catches known access-denied/login-failed
errors from the master connection, caches the per-server decision, and
returns the connection's InitialCatalog as a single-element list. The
three callers already loop per-database, so single-DB mode works without
further changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add nonclustered indexes for query/procedure/query store lookups

Phase 3 OUTER APPLY hydration of compressed query_text/plan_text was forcing
an Eager Index Spool over the full collect.query_stats table (and similar
for procedure_stats / query_store_data), which took 104 seconds on a
742K-row table in #835.

Changes:
- Remove CONVERT(binary(8), nvarchar-hash, 1) anti-pattern from OUTER APPLY
  WHERE clauses by keeping query_hash as native binary(8) in temp tables.
  query_hash is only converted to nvarchar(20) in the final output projection.
- Add three nonclustered indexes (install script and upgrade script):
    IX_query_stats_hash_lookup (query_hash, database_name, collection_time DESC)
    IX_procedure_stats_name_lookup (database_name, schema_name, object_name, collection_time DESC)
    IX_query_store_data_id_lookup (database_name, query_id, collection_time DESC)
- Indexes use SORT_IN_TEMPDB = ON and DATA_COMPRESSION = PAGE.
- ONLINE = ON is applied conditionally via dynamic SQL based on
  SERVERPROPERTY('EngineEdition') — Enterprise/Developer/Azure only, since
  Standard/Web/Express don't support online index operations.

Tested against CADelete's 742K-row table: Phase 3 went from 104s to
well under 1s (5s total for the full three-phase query).

Fixes #835

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Scope query snapshots to current database on Azure SQL DB (#857)

On Azure SQL Database, logins without access to master can't resolve
cross-database rows returned by sys.dm_exec_requests, which caused the
Live Snapshot button and the query snapshots collector to error in
D365FO-style environments (reported by @TrudAX in #857 after PR #858).

BuildQuerySnapshotsQuery now takes an isAzureSqlDatabase flag and emits
AND der.database_id = DB_ID() only when true. Boxed SQL Server, MI, and
elastic pool behavior is unchanged. The Live Snapshot button path gets
the flag through a new ServerTab constructor parameter wired from the
cached ServerConnectionStatus.SqlEngineEdition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Polish Lite chart axes and sub-tab styling

- Chart X-axis prints the date line only on the first tick and on ticks
  where the date changes; all other ticks show time only. Format respects
  current culture (en-GB → dd/MM, de-DE → dd.MM, 24h clocks, etc.).
  Implemented as a DateTimeTicksBottomDateChange() extension in
  Lite/Helpers/AxesExtensions.cs and applied to every DateTimeTicksBottom
  call site in ServerTab and CorrelatedTimelineLanesControl.
- Server name no longer duplicated in the ServerTab header status line;
  ConnectionStatusText now shows just "Connecting..." / "Last refresh: ...".
- Chart tick label font bumped from 12 to 13 for readability.
- New SubTabItemStyle (thin accent underline, transparent background) in
  all three themes, applied to Queries / Memory / File I/O / Blocking /
  Perfmon / Running Jobs sub-TabControls so sub-tab selection no longer
  looks identical to main-tab selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Port Lite chart/tab polish to Dashboard + LSP diagnostics cleanup

Dashboard polish (ports the same items merged to Lite in #862):
- New Dashboard/Helpers/AxesExtensions.cs with DateTimeTicksBottomDateChange(),
  culture-aware (dd/MM for en-GB, dd.MM for de-DE, 24h clocks, etc.). All 52
  call sites of DateTimeTicksBottom() across 10 files swapped to use it.
- TabHelpers.ApplyTheme + ReapplyAxisColors bump chart tick label font from
  12 to 13 so numbers read cleaner on wide charts.
- SubTabItemStyle added to Dark / Light / CoolBreeze themes: thin accent
  underline + transparent background instead of filled cyan, so sub-tabs
  don't look identical to main tabs when selected. Wired via
  ItemContainerStyle on 11 sub-TabControls (Overview's inner tabs,
  Collection Health's inner tabs, Locking, ConfigChanges, CurrentConfig,
  FinOps, Memory, ResourceMetrics ×2, SystemEvents, QueryPerformance).

LSP diagnostics cleanup (tracked work from chore/lsp-diagnostics-cleanup):
- Small nullability/warning fixes across Dashboard and Lite services,
  analysis helpers, and BenefitScorer / PlanAnalyzer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix Overview crosshair disappearing after tab switches / layout passes

Root cause: the control wired `Unloaded += ...Dispose()` on the crosshair
manager, and WPF fires Unloaded for transient reasons (tab virtualization,
layout rebuilds, etc.), not just when the control is actually going away.
Dispose() clears the manager's lane list, after which ReattachVLines runs
over an empty list and the crosshair is gone permanently.

Changes:
- Remove the Unloaded → Dispose() handler in both Lite and Dashboard copies.
  The manager holds only managed state (a Popup + lane references) — GC
  will clean it up with the control.
- Remove the now-redundant `_isRefreshing` flag from CorrelatedCrosshairManager.
  The `lane.VLine == null` check in OnMouseMove is a sufficient "not ready"
  guard and is self-healing once VLines are recreated.
- Wrap ReattachVLines in a try/finally on the control side, with a new
  idempotent EnsureVLinesAttached() safety net that only creates VLines
  for lanes where they're still null.
- Make CreateVLine catch per-lane exceptions so one failing chart can't
  prevent the others from recovering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix Memory Pressure Events chart filter; add MCP interpretation (#865)

Chart previously filtered to HIGH severity only (indicator>=3), which on most
servers never fires, producing an empty chart even when sp_pressuredetector-
level medium pressure (indicator=2) was occurring constantly. Switch to stacked
bars per hour, split by SQL Server (process) vs Operating System (system), with
severe events capped on top of medium in a darker shade. Extend ChartHoverHelper
to support BarPlot tooltips. Add MCP guidance for interpreting indicator values
and routing to the right follow-up tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Port Memory Pressure Events feature to Lite (#865)

Lite was missing the RING_BUFFER_RESOURCE_MONITOR collector entirely — no
collector, no table, no chart, no MCP tool. This adds the full feature:

- Schema: new memory_pressure_events table + index, schema v25, added to
  ArchivableTables, server-id-fix list, and ArchiveService.
- Collector: CollectMemoryPressureEventsAsync queries the ring buffer and
  client-side-dedupes against DuckDB's MAX(sample_time). Azure SQL DB returns
  zero rows (ring buffer not exposed there). Scheduled every 5 min (Aggressive
  and Balanced presets) or 15 min (Low-Impact).
- UI: new 'Memory Pressure Events' sub-tab on the Memory tab with the same
  stacked-bar chart as Dashboard (SQL Server medium/severe, Operating System
  medium/severe). Wired into full-load and sub-tab-switch refresh paths.
- Hover: ported the BarPlot support from Dashboard's ChartHoverHelper so bar
  tooltips work and report the correct segment height for stacked bars.
- MCP: new get_memory_pressure_events tool + the 'Interpreting Memory
  Pressure Events' guidance section in McpInstructions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bump schema table count test to 30 for memory_pressure_events

Companion update to the new memory_pressure_events table added in this PR.
SchemaStatements_MatchTableCount asserts the total table count; needs to
move from 29 to 30 to reflect the new table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix blocked process report plan lookup (#867) (#868)

Right-click > View Plan on a Blocked Process Reports row silently fell
through (no handler case) and Get Actual Plan erred with "no query text."

- Split the grid onto its own BlockedProcessContextMenu with separate
  View Blocked Plan / View Blocking Plan actions; drop Get Actual Plan
  (re-executing a mid-transaction blocked query is a foot-gun).
- Parse all <frame> entries from the BPR XML's executionStack, filter
  the 42-byte all-zero sql_handle placeholder (dynamic SQL / system
  context), default stmtstart=0 / stmtend=-1 per the dm_exec_text_query_plan
  convention. Matches sp_HumanEventsBlockViewer's XPath and join shape.
- Add FetchPlanBySqlHandleAsync keyed on sql_handle + statement offsets
  against sys.dm_exec_query_stats. Caller iterates frames until one
  resolves; falls back to a clear "plan no longer in cache" message.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pre-filter query snapshot requests into #temp on Azure SQL DB (#857) (#869)

Follow-up to #861. The DB_ID() predicate in the WHERE clause wasn't
enough — the OUTER APPLYs to sys.dm_exec_sql_text and
sys.dm_exec_text_query_plan were still being evaluated against
master-scoped rows from sys.dm_exec_requests before the filter was
applied, tripping VIEW SERVER PERFORMANCE STATE errors for DB-scoped
logins (D365FO). A CTE or derived table wouldn't guarantee the
filter order, so materialise the filtered request rows into #req
first and drive the DMFs off that.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Stop retrying collectors after non-transient permission denial (#857) (#870)

The collector loop already classifies SQL errors 229 / 297 / 300 as
PERMISSIONS status and excludes them from the failure rate, but it
keeps re-running the collector every interval and logging an
identical denial each time. For DB-scoped logins on Azure SQL DB
(e.g. D365FO) this churns the collection log and gives no new
information — the permission won't change mid-session.

Flag the collector on first denial and short-circuit RunCollectorAsync
so we don't make the round-trip or the log entry. Flag is in-memory
per (server, collector) — cleared on app restart so newly granted
permissions are picked up on the next launch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Skip live query plans on Azure SQL DB (#857) (#871)

sys.dm_exec_query_statistics_xml requires VIEW SERVER PERFORMANCE
STATE on Azure SQL Database regardless of scope, so DB-scoped logins
(e.g. D365FO) still hit error 300 even after the #temp pre-filter
landed in #869. The OUTER APPLY evaluates the DMF for every session
in #req and fails on the permission check before returning rows.

Force supportsLiveQueryPlan=false for SqlEngineEdition=5 in both the
collector and the Live Snapshot button paths. Boxed SQL Server and
Azure MI (edition 8) still get live plans as before.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix FinOps recommendation severity sort order (#872)

Sort recommendations by severity rank (High=1, Medium=2, Low=3)
instead of alphabetically. Adds SeveritySort property to
RecommendationRow and uses it as SortMemberPath for the Severity
column. Display strings are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix FinOps severity sort order in Dashboard (#872)

Severity column was sorting alphabetically (High, Low, Medium) instead
of by severity ranking. Added SeveritySort computed property on
FinOpsRecommendation, ordered results by it, and wired the DataGrid
column's SortMemberPath so click-sort matches the default order.

Mirrors the Lite fix in PR #874.

* Drop sys.dm_os_schedulers from memory_stats on Azure SQL DB (#857) (#876)

Azure SQL Database DBs hosted in an elastic pool (notably D365FO
customer tenants) enforce VIEW SERVER PERFORMANCE STATE on
sys.dm_os_schedulers regardless of the login's DB-scoped grants —
VIEW DATABASE STATE + VIEW DATABASE PERFORMANCE STATE on the user DB
are not sufficient. Verified by reproducing the failure in a
standard Azure SQL DB elastic pool with a contained DB user; bare
sys.dm_exec_requests/sys.dm_os_sys_info/sys.dm_os_performance_counters
succeed but sys.dm_os_memory_clerks / sys.dm_os_schedulers /
sys.dm_os_waiting_tasks fail with error 300.

The other failing collectors (memory_clerks, waiting_tasks,
tempdb_stats) have no DB-scoped alternative and will stay skip-gated
via #870 for these users.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Release v2.8.0: version bumps and changelog (#877)

Adds nonclustered indexes to collect.query_stats, procedure_stats, and
query_store_data for Dashboard grid lookups (#835). Ports Memory Pressure
Events to Lite (#865). Multiple Azure SQL DB collector fixes (#857).
FinOps severity sort order fix (#872). Grid auto-scrolling (#843).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Scope v2.8.0 webhook DPAPI note to Dashboard (#879)

Lite webhook URLs still read from plaintext settings — avoids implying
the security hardening shipped to both editions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ClaudioESSilva <claudiosil100@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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