Repository navigation
Time range picker: more presets, calendar periods and typed ranges - #5570
Merged
Merged
Conversation
…by the fleet-name guard (#5562)
…icker reads (#5562) ValidateWindow gets an overload that takes the read's own ceiling and refuses beyond it. The query, procedure and Query Store duration trends opt in at 90 days; every other read stays at 168 hours. WebReadReach is the one table of reach and main collector per windowed read, served on the catalog's hours param as max_hours, shape and the collector's interval.
…page's use of it (#5562) time-range.js is the web twin of the shared model and runs all 100 rows of the shared fixture under Node. time-range-picker.js is the control: presets, calendar periods, a typed range that previews before Apply, a date and time pick, reach greying and the sample-interval and data-start notes. The server page swaps its preset list and datetime-local boxes for it; ranges under an hour are fetched as the whole hour and trimmed to the exact pair.
The server tab toolbar, the five FinOps lists and the object heatmap, Alert History, Job History and the Settings default range now use TimeRangePicker. A rolling range of whole hours still reaches the readers as (hours, null, null); sub-hour spans, calendar periods and typed ranges carry their instants. default_time_range_hours keeps working through FromLegacyHours and an optional default_time_range holds the other presets and periods. The picker's data-start note takes the Queries probe the tab already awaits and its sample-interval note the wait stats schedule. Lite's ApplyThemeToCalendar is gone.
…s LiteTimeRange (#5562)
…es how often a tab's data is collected (#5562)
…ts cover the rolling-only rule, the job and sweep ends and the server page note (#5562)
…nished range in the SQL, every banner feeds the data-start note, Alert History's All is its retention (#5562)
…bounds, Alert History longest span (#5562)
…nd data start gaps (#5562)
…nd SetLongestChoice (#5562)
…torage Growth reach from the catalog (#5562)
H1: an id or legacy-hours value past the 100-year limit no longer overflows TimeSpan (startup/tab crash from a corrupt settings file). M1: Lite Alert History, Job History and FinOps pickers read typed times and calendar periods in the zone their grid uses. M2: Apply to All resolves a calendar period once (ForBroadcast now lives in PerformanceMonitor.Ui, used by Lite and the Viewer). M3/L1 (R8): Job History "Last Year" (365d) on both desktops; Lite Alert History "All" is 365d. L2: Lite clears probed data-start floors when the range moves. L3: the Viewer's sample-interval note follows the sub-tab on screen and updates inside the reload loop. L4: removed the unreachable AtLeastMinimumSpan. L5 (ruling): calendar periods are exempt from the 5-minute floor. L6: behaviour tests for H1 and M1; dropped two text pins that duplicate live tests. Guard: stacked summaries, CommentFilterAdoption and KnownTruncatedRanges pins.
…CommentFilterAdoptionTests now lists (#5562)
…orage Growth floor, live end, perfmon and heatmap back to 168 h, reach descriptions pinned (#5562)
…load, harness clocks follow the range trim (#5562)
24 of 30 tasks
…; it is refused only at zero length (#5562)
…hat held Node open for 300 s per harness run; pin Today's floor exemption and the cache lifetime (#5562)
…onger than the tab reads is cut with a notice (#5562 review r1 M4)
… on the web and in the desktop picker; the two read-name scans skip reachReads and the interval pin follows the null guard (#5562 review r1 finish)
…tart ranges refused, one redraw per catalog wait, Storage Growth 90 days, midnight comment (#5562)
erikdarlingdata
marked this pull request as ready for review
October 8, 2026 19:25
…oves behind the guide marker (#5562)
…cker # Conflicts: # Darling/PerformanceMonitor.Darling.Viewer/AlertsHistoryTab.xaml.cs # Darling/PerformanceMonitor.Darling.Viewer/JobHistoryTab.xaml.cs # Lite/Controls/AlertsHistoryTab.xaml.cs # Lite/Controls/JobHistoryTab.xaml.cs
…de tail, with the reach sentence right after it (#5562)
…s entries in the darling path filter)
erikdarlingdata
enabled auto-merge (squash)
October 8, 2026 23:45
This was referenced Oct 9, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Oct 9, 2026
…kDB memory limit Release checklist step 2b. README.md already covered everything up to #5491. This adds the three user-facing 3.10.0 changes merged after it: the one time range picker (#5570), the Excel-style text column filters (#5577) and Lite's DuckDB memory limit setting (#5553). Collector count, tab lists, edition table, platform notes, permissions and the deprecation callout needed no change.
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.
One time range picker for Lite, the Darling Viewer and the Darling web: more presets, calendar periods and typed ranges. The design is in the last comment on the issue. This PR is built in parts by several lanes on one branch; each part adds its own section below.
Shared model and grammar (lane L1)
The pure model lives in
PerformanceMonitor.Ui/TimeRange/and has no WPF in it. Lite and the Viewer both reference it. The web module (time-range.js) has to produce the same results.Shared test cases:
Darling/Darling.Tests/Fixtures/time-range-cases.json(100 cases). Zone ids are IANA names. The file's_docblock says how to read it: instants are ISO UTC strings,echois the exact label text, anderroris a stable code (the message text is not compared). The C# tests inDarling/Darling.Tests/TimeRangeTests.csrun every case. They live in Darling.Tests only, because a Darling file that Lite.Tests compiles has to be named in the Lite path filter in build.yml. The expected values were produced by an independent Python implementation (zoneinfo), not by the C# code.Grammar (case-insensitive; commas, extra spaces and en or em dashes do not matter)
45m,12 hours,10d,2 weeks,last 3 days,past 90 min,1.5 hours. Units m/min/minute(s), h/hr/hour(s), d/day(s), w/wk/week(s), mo/month(s). A month is 30 real days. Real elapsed time, so a day is 24 hours across a daylight saving change. There is no year unit.today,yesterday,this week/week to date/wtd,last week/previous week,this month/mtd,last month,this year/ytd,last year. Wall-clock days in the display zone, weeks start Monday. Today and the to-date periods end at now; the previous periods end at the next local midnight (23 and 25 hour days happen). A boundary is the earliest instant labelled at or after the wall midnight, so neighbouring periods tile with no gap.-,to,until(or a single unspaced dash). A point is a date, a time, or both.Oct 1,October 1st 2025,1 Oct,10/1,10/1/26,2026-10-01,today,yesterday(month first for slashes).1:00 am,12pm,13:00,13:00:30,noon,midnight.Oct 1 - Oct 2ends at Oct 3 00:00).22:00 - 02:00).Dec 30 - Jan 2works in January;Oct 2 - Oct 1is refused).DisplayZone.ToUtcBound: the start takes the earliest instant, the end the latest, a repeated hour widens, a skipped time maps to the change instant.since 10/1,Oct 2 12pm to now,10/1 - now. The end slides with now.since 9amis the latest 9am not in the future.1790852543 - 1791457343orsince 1790852543. Ten digits are seconds, thirteen are milliseconds.too_short), an end before the start, nonsense, impossible dates and times, a start in the future forsince. Codes:empty,unrecognized,bad_date,bad_time,bad_range,needs_end,too_short,end_before_start,start_in_future,too_far_back(a relative count over 100 years). There is no upper cap in the model.Echo / label format
<length> <start> - <end> (<zone>), for example3d Oct 5, 12:00 am - Oct 8, 7:01 am (UTC-04:00). Length isNd(floored) for a day or more, elseHh Mmwith zero parts dropped. A bound isMon D, h:mm am|pm(English month abbreviations, lower-case am/pm),, YYYYafter the day when either end is not in now's year,:ssfor fixed ranges when the seconds are not zero, and+hh:mmafter a wall time that happens twice. The zone isUTC-04:00, orUTC-05:00 to UTC-04:00when a change falls between the ends.C# API (all in
PerformanceMonitor.Ui)TimeRangeParser.Parse(text, nowUtc, zone)returns{ Range, Echo, Error, ErrorCode, Spec, Ok }. Now is passed in, never read.TimeRangeSpec(Relative / Calendar / Fixed / Since):TryResolve(nowUtc, zone, out ResolvedTimeRange, out TimeRangeError), a stableId(5m,4h,1w,1mo,today,previous-week,fixed:...,since:...) withTryFromIdfor settings,WholeHoursfor readers that still take hours back.ResolvedTimeRange:StartUtc,EndUtc(naive UTC),IsLive,Span,Length,StartText,EndText,ZoneText,Label.TimeRangePresets:Rolling(5m 15m 30m 1h 4h 1d 2d 1w 1mo),CalendarPeriods,FromLegacyHours(int)(1/4/12/24/168 and any other whole hours; 24 is1d, 168 is1w),CurrentLength(spec, now, zone),Find(id).TimeRangeNotes.DataStartNote(range, dataStartUtc)("Data starts Oct 3, 2:00 pm" when the range starts more than 90 minutes before the data) andSampleIntervalNote(span, interval)("Data here is collected every 5 minutes." when the span holds fewer than 3 samples).WPF control (
PerformanceMonitor.Ui/TimeRangePicker.xamland.cs)A button names the range ("Past 4 hours", "Previous Week", "Custom (2d)"). The resolved start, end and zone sit beside it. The popup holds the nine short presets, the eight calendar periods (each with its current length, greyed while under 5 minutes), a text box that previews the parsed range and notes before Apply (or the reason it failed), and a Monday-first calendar for picking days. Enter applies, Esc closes.
Properties for the tab call sites:
ZoneProvider(Func<TimeZoneInfo>, default the machine zone) andNowProvider(for tests).Value(aTimeRangeSpec; setting it redraws without raising the event) andResolve()(the window to read now, so a live range keeps sliding). CallRefresh()when the zone changes or on a refresh tick.Select(spec)holds a range and raises the event, as if the user picked it.DataStartUtc: shows "Data starts Oct 3, 2:00 pm" when the range starts more than 90 minutes before it.SampleInterval: a tab sets its main collector's actual interval. A range with fewer than 3 samples shows "Data here is collected every N minutes."Compact: hides the detail beside the button and moves it into the tooltip (for FinOps).RangeChangedcarriesSpec(hold or persistSpec.Id) andRange.It is themed through
BackgroundBrush,ForegroundBrush,ForegroundDimBrush,BorderBrush,AccentBrush,AccentForegroundBrush,ErrorBrushandWarningTextBrush, which both apps define in all three themes. The calendar chrome is recoloured from those brushes when the popup opens. This replaces Lite'sApplyThemeToCalendarand the Viewer'sApplyDarkThemeToCalendar, which L3 can delete. PerformanceCalendar is a month heatmap and did not fit, so the stock WPF Calendar is used.Not done here, by design: no call site, web or MCP file is touched. One test edit outside the new files:
FleetIdentifierScrubTestsnow allowsutcas an ordinal role word, because its short-name-and-ordinal pattern read the label text "UTC-04:00" as a server name.Test plan
TimeRangeFixtureTests(100 shared cases plus a zone-id check),TimeRangeModelTestsandTimeRangePickerControlTests(STA: construction, events, zone, notes, compact, typed preview, popup open with calendar theming and a picked range): 144 passed together withFleetIdentifierScrubTests.Stage=Guardclass in Darling.Tests (1409 passed) and in Lite.Tests (441 passed, after dropping the Lite link that trippedDarlingReadsTraitGuardTests), and the Census and Ratchet classes (393 passed).PerformanceMonitor.Ui: ClipboardWriteCensusTests, SerialLoopStoreSizeSourceTests, StatementScrubViewerPlanWithheldTests, TsqlConventionGuardTests, Viewer4577Tests: 86 passed.Web and MCP reach (lane L4)
The server side of the picker: how far back each web or MCP read may be asked to go, and what the web needs to grey out a period a tab cannot read.
What changes
McpHelpers.ValidateWindow(hoursBack, asOf, maxHours, out end)andValidateHoursBack(hoursBack, maxHours)take a ceiling the read declares. A window past it is refused with the same error shape as today, never shortened.MaxHoursBackstays 168, and the old two-argument calls now delegate to the new ones, so every read that does not opt in answers byte for byte as before.WebReadReach(Darling service) is the one table of every windowed web read: its shape (bucketed trend, list or ranking, latest snapshot), its reach in hours, and its main collector. A read's validator takes its ceiling from this table./api/catalognow putsmax_hours,shape,collector,collector_interval_minutesandcollector_default_interval_minuteson thehoursparam of each windowed read. The interval is the schedule in force: the fleet-wide row over the shipped default, or, with?server=NAME, that server's own row first. The web keeps sending wholehoursandas_of; there is no minute-grain parameter.hours_backdescription that they go up to 2160.Decisions to check
ClampToTextHorizon) the brief describes; none of the three opted-in trends carries query text or plans, so no clamp note was needed here.get_query_trend(one query's series by hash) is not bucketed, so it stays at 168 hours.CollectorScheduleDefaults. Reads no collector feeds (analysis, alerts, the monitoring tool's own reads) andget_server_trend(whose collector depends on the metric) have no collector id.Test plan
WebReadReachTests(15 passed with the live class): every windowed web read has a row, and no row names a missing read (the census that fails when a read is added without one); every collector id exists; only bucketed trends reach past 168 and exactly the three opted-in reads do; each opted-in validator reads its ceiling from the table and no other does; the catalog servesmax_hoursand the collector interval, including per-server over fleet over default; the overload accepts up to its ceiling, refuses beyond it, and the 168 path is unchanged.WebReadReachLiveTestsagainst a TimescaleDB scratch store: with one busy hour 60 days back and its raw rows purged, the query and procedure duration trends asked for 2160 hours through the web dispatch answer from the hourly rollup; 2161 is refused; the Query Store trend takes the same ceiling; Top Queries, the tempdb trend, the heatmap and CPU utilization still refuse 169.Stage=Guardclass in Darling.Tests (1409 passed), the Census (475) and Ratchet (15) classes, and the source-scan classes that readDarlingWebEndpoints,McpHelpersorDarlingMcpTrendTools, all green.McpHelpersis shared.Not done
get_server_trendmetrics (memory clerks, latch, spinlock, collector duration, tempdb file I/O, file I/O throughput) andget_pg_query_duration_trend.Fixes #5562
Viewer (lane L3b and L3b-fix)
Every time range control in the Darling Viewer uses the shared picker. The desktop has no reach limit: a long period reads what the data holds, through the routers that already serve long windows.
What changed:
TimeRangeSpecheld by the picker and drawn in the tab's own display zone (Server, Local or UTC).GetWindowUtc(),GetWindowHoursBack()andTimeRangeIndexToHourskeep their shapes, so the readers below them do not change. The newViewerTimeRangeWindowclass holds the arithmetic (window, whole hours, legacy index, Apply to All, collector cadence), so tests can pin it without a window.GetWindowHoursBack()rounds up to one hour for any integer reader left.GetWindowUtc()in the display zone. A finished range is a past range, and the CPU chart bounds its end.SampleIntervalcomes from the main collector of the tab on screen. It uses the schedule overrides (read at most every five minutes), else the shipped cadence. A tab with no single main collector shows no note.GetAlertHistoryWindowAsyncandDismissAllVisibleAlertsWindowAsync. The old methods delegate.DefaultTimeRangeIndex(0 to 4) stays as the legacy field. The newDefaultTimeRangestring holds a rolling length or a calendar period.Normalizemaps between them and writes the nearest legacy index back for older builds. Custom stays unpersisted.ApplyDarkThemeToCalendarand the Viewer's own From/To code.90d) and 30 days is1mo.What L3b-fix added (the four gaps L3b left):
RollingUnitproperty (PerformanceMonitor.Ui/TimeRangePicker.xaml.cs) and a pure rule,RollingUnitRule(PerformanceMonitor.Ui/TimeRange/RollingUnitRule.cs). With a unit set, the picker hides the calendar periods and the calendar. It greys presets under one unit, and it refuses calendar, fixed and "since" ranges and any length that is not a whole number of units. A typed range shows the reason ("This read takes whole hours; try 2h."). The lists use one hour (FinOpsTab.xaml.cs), the heatmap one day. A picker no longer offers a range its read cannot honor.GetJobHistoryAsynctakesuntilUtc(ViewerDataService.JobHistory.cs) and hands it to the shared reader's existingJobHistoryFilter.UntilUtc. The SQL applies the bound before the per-server top-N and the row cap. The reader's end is inclusive and the picker's end is exclusive, so the Viewer takes one microsecond off it.JobHistoryTab.xaml.cspasses the end for a finished range, and the client trim is gone. A live test on a seeded store (10 row cap, 40 runs after the end) fails on the old shape and passes now.ViewerServerTab.UpdateTruncationBanner. The 29 event-surface sites reach it throughShowEventDataStartAsync. That method hands its floor toViewerDataStartNote.Feed. The feed sets the note of the tab the banner sits in, and only while the banner is on screen. The same floor feeds Job History's own picker. No new query. The six sites that used a feeding helper went back to the plain probe await, so their older source pins hold again. A source-scan test pins 35UpdateTruncationBannersites and 29ShowEventDataStartAsyncsites, so a new site must be counted.SetLongestChoice), set toViewerTimeRangeWindow.AlertHistoryLongestChoice. That is 90 days, fromDarlingRetentionHorizons.AlertHistoryRetentionDays(DailySummaryHorizon.cs:66), capped at 365 days if the retention is raised.<summary>inViewerServerTab.TimeRange.cs.Viewer test plan
ViewerTimeRangeGapFixTestspins the hour and day rules and the rolling-only picker on an STA thread. It also pins the five FinOps pickers, the banner site counts and the feed, the Alert History choice and the Job History tab. A live end-bound test runs on a seeded store. Passed.ViewerTimeRangeWindowTests,ViewerTypedRangeZoneTests,ViewerApplyToAllClockTests,ViewerAlertClockCacheTests,TimeRangePickerControlTests: passed.Stage=Guardtrait: 1,409 run, two failures (a live-class collection attribute and a member-scan list) fixed, then re-run green.Not changed, for the Lite lane: Lite has the same FinOps, Job History and Alert History readers. The Lite lane can reuse
RollingUnitandSetLongestChoiceon the shared control.Web picker (lane L2a)
The web grammar and picker are in
wwwroot/js/time-range.jsandwwwroot/js/time-range-picker.js, withcss/time-range.css. The server page uses them in place of its preset list and its two date-time boxes.Module API for the other web pages
time-range.jsis pure: no DOM, no clock, no imports. Every function is given the now (epoch milliseconds) and the zone, which is an IANA name.browserZone()gives the browser's. A spec is a plain object:{kind:"relative", spanMs},{kind:"calendar", period},{kind:"fixed", startMs, endMs}or{kind:"since", startMs}.parseRange(text, nowMs, zone)returns{ok, range, spec, echo, error, errorCode}. The error codes are the fixture's.resolveSpec(spec, nowMs, zone)returns{ok, range}or{ok:false, error:{code,message}}. A range hasstartMs,endMs,live,spanMs,length,startText,endText,zoneTextandlabel.ROLLING_PRESETS,CALENDAR_PRESETS,specId,specFromId,specName,fromLegacyHours,wholeHours,currentLength,relativeSpec,fixedSpec.readWindow(range)gives{hours, asOf, live, startMs, endMs}. That is the whole hours (at least 1, rounded up) and theas_ofend the reads take.asOfis null for a live range.reachHours(catalogEntry)readsmax_hoursfrom the entry or itshoursparam and falls back to 168.collectorIntervalMs(catalogEntry)readscollector_interval_minutesthe same way. The other helpers arereachRefusal(spanMs, hours),sampleIntervalNote(spanMs, intervalMs)anddataStartNote(range, dataStartMs).time-range-picker.jsexportstimeRangePicker(opts). A page mounts it like this:Options:
spec,zone,now,reachHours,sampleIntervalMs,dataStartMs,compact,label,onChange. Methods:window(),resolve(),spec()andsetSpec(specOrId), which holds without raisingonChange.select(spec), which acts as if the reader picked it and returns the refusal or null.refresh()(a live range slides),setReach,setSampleInterval,setDataStart,openPopupandclose.To persist a choice, store
specId(spec)and read it back withspecFromId.The popup lists the nine short presets and eight calendar periods, each with its current length. It also has a text box that shows the parsed range or the reason before Apply, and two date-time boxes that write the typed form into the text box. Enter applies, and Esc closes and returns focus to the button. A range longer than the reach, or a period under 5 minutes, is greyed out with the reason. A typed range gets the same checks. The button and the boxes carry accessible names. The document listener exists only while the popup is open.
What the server page does
hours, as before.RANGE_OPTIONSis derived fromROLLING_PRESETS, not listed by hand.as_offor a fixed range, none for a live one). The existingsetActiveRangepath trims it to the exact pair. The label says where totals begin when the span is not whole hours ("totals and rankings aggregate from ...").resolveCustomRange,applyCustomRangeandrangeContextkeep their shapes, so the tabs and the chart menu are unchanged. The 1 hour floor became the 5 minute floor.PAGE_REACH_HOURSis 168. 1mo, month to date, previous month, year to date and previous year are greyed out with the reason. The page shows many reads, so it keeps the common reach. A page that shows one read can passreachHours(entry).collectorIntervalMs(entry)andsampleIntervalMswere ready for the single-read pages. Lane L2b wired the note (see its section below).Tests
Fixtures/time-range-cases.jsonpass intime-range.js: start, end, live, echo text and error code. I also ran the harness with the process zone set to two other zones and got 100 of 100. So the module does not depend on the machine's zone. No fixture value was edited, and no JS versus .NET difference turned up.WebTimeRangeTestsis new, with 9 tests, skipped without Node. They cover the fixture run and the picker DOM harness. The harness checks closed, open, preview before Apply, Enter, Esc and focus, outside click and listener cleanup, presets, date boxes, compact, notes, reach from the catalog, and a sliding live range. Two server-page scenarios check a sub-hour range fetched as the whole hour and trimmed, and every preset and period held or refused.WebServerPageRangeTests. Its source pin now readsPAGE_REACH_HOURS, and the 1 hour refusal became hours=1 withas_ofplus a 5 minute refusal. Also updated:web-kept-history-harness.mjs(two new scenarios and an exportedholdSpecin its scratch copy),DarlingWebAssetsTests(the new CSS file) andindex.html(links the CSS).WebTimeRangeTests,WebServerPageRangeTests,WebRangeKeptHistoryBehaviourTests,DarlingWebAssetsTests,WebHarnessJsTreeCopyTestsandWebReadReach*gave 122 passed, 1 skipped. EveryWeb*,Chart*,AlertHistory*, Census and Ratchet class gave 2257 passed, 80 skipped, 0 failed. EveryStage=Guardclass gave 1409 passed. Darling.Tests builds with 0 warnings.For the other lanes
No server change is needed from this lane. The catalog fields the module reads (
max_hoursandcollector_interval_minuteson thehoursparam) match what the catalog now serves. The other nine pages can mount the picker the same way. Pages that takehoursas a number can usepicker.window().hoursandpicker.window().asOf.Web pages (lane L2b)
Head at push: 814a50f. Nine pages now use the shared web picker. The server page shows the "collected every N minutes" note.
Pages moved
pages/alerts.js), Fleet Sweeps (pages/sweeps.js) and Job History (pages/job-history.js) mount the full picker. Each reads its reach from the catalog (max_hours, 168 for all three).database-resources,high-impact,optimization,storage-growth) use one helper,pages/finops/window.js.pages/views.js) and the view editor's default range (js/editor.js, notpages/editor.js) userollingHoursPicker. The old Custom number box clamped to 90 days. The picker greys out a longer length and says why.Module changes (a real gap)
compactonly hid the detail text, so R5 had no base to build on. I addedrollingOnly,minSpanMsandstepMsto the picker, androllingOnlyRefusalandminSpanRefusaltotime-range.js. Newpage-range.jsholds the shared mount helper,windowOfSpecand the per-server catalog fetch. No grammar rule changed.Brief items 2 to 5
finops/window.js:35-44mounts the picker Compact and rolling-only. It offers no calendar period and no start-and-end boxes. These are greyed out or refused with the reason: a length under 1 hour, a length that is not whole hours, a since-range and a finished range. Storage Growth is whole days with a 1 day floor, because its heatmap has one column per day.storage-growth.js:25-29gives it its own 2160 hour reach: the tool takes that for this view (DarlingMcpFinOpsTools.cs:97), while the catalog entry forget_finopssays 168.as_of(job-history.jsreadParams). The tool passes the end intoJobHistoryFilter.UntilUtc(DarlingMcpJobTools.cs:131-135). The reader applies it in SQL inside the per-server top-N (DarlingJobHistoryReader.cs:74-88) and again before the cap (:164). No server change was needed. The page cuts only the start (for a range under an hour). The runs are newest first, so a cap never hides a run in range.AlertHistoryRetentionDays,DailySummaryHorizon.cs:66, used byDarlingRetention.cs:115). The page has no "All". Its longest choice is the read's reach of 7 days, because the catalog says 168 and R2 says lists stay at 168. Offering 90 days needs a one-row change inWebReadReachplus the validator call. I did not make it because it contradicts R2 for a list read. It needs a decision.collectorinserver-tabs.js(9 tabs, same names as the Viewer's table for the 7 shared SQL Server tabs).server.jsapplySampleNoteasks/api/catalog?server=for that collector's actual interval and callssetSampleInterval. Below 3 samples the picker shows "Data here is collected every N minutes." The range is never widened. The single-read pages show no note, because no collector feeds them as one read (alerts, sweeps, FinOps) or the page has no single read (views).Tests
WebPageRangeTests(7 tests) andpage-range-harness.mjs. They cover the interval lookup, the note thresholds, the rolling-only refusals and the whole-hours window. They also cover the sweep and job-run start cuts, and source pins for items 2 to 5.web-picker-driver.mjs. Job history covers rolling, finished, over-reach and 30 minutes trimmed. FinOps covers options, refusals and popup shape. Storage growth covers days and reach 2160. Alert history covers the greyed-out list and a finished range. I updatedJobHistoryPageTests,AlertHistoryRangePageTests, the FinOps page tests andFinOpsPageShellTeststo match.DocCommentHygieneTestsonViewerServerTab.TimeRange.cs:428, a Viewer-lane file I did not touch.Stage=Guardtrait, a live PostgreSQL class, and a browser check of the popup's look.Open points
ViewerServerTab.TimeRange.cs:428has two opening summary tags and one close. The Viewer lane owns that fix.Lite (lanes L3a and L3a-fix)
Every Lite time range control uses the shared picker. That covers the server tab toolbar, the four FinOps lists, the object heatmap window, Alert History, Job History and the Settings default range. No reach cap was added (R1). Apply to All, the Compare box and the display-mode box still sit beside the picker.
How a range reaches the readers
The readers keep their shapes (
GetHoursBack,GetCurrentWindowUtc,GetChartWindow).LiteTimeRange.WindowForturns the picker's range into the old triple:36h) is still(hours, null, null).since.hoursBackis then the whole hours from the start to now, rounded up, at least 1.Settings
default_time_range_hoursis still read and written. An optionaldefault_time_rangeholds presets and periods the hours key cannot spell (30m,1mo,previous-week). A whole-hour choice writes the hours key and removes the new key. Any other preset or period writes the new key. When both exist the new key wins. A typed range is never saved.What the L3a-fix lane changed (R3, R5, R6, R7, R8)
LiteTimeRange.ConfigureFinOpsPickermakes each one Compact. It sets the shared control'sRollingUnitto one hour for the four lists and one day for the heatmap (FinOpsTab.xaml.cs). No calendar period, no custom end, no span under the unit. This is the same shared API the Viewer's FinOps tab uses.GetJobHistoryAsyncandGetJobHistoryWithClocksAsynctake an optional exclusivewindowEndUtc. The SQL applies it in both the base and the per-job stats CTE, before the row cap. The bound is the server-local end widened by one hour, like the start. The exact instant check follows. If the margin hour pushes in-window runs out of the cap, the read repeats with more room until the cap holds only in-window runs.JobHistoryTabpasses the end for a finished range (LiteTimeRange.BoundsOf) and null for a rolling or live range. Microseconds and the sort order are unchanged.GetAlertHistoryWindowAsyncandDismissAllVisibleAlertsWindowAsync, the same shape as the Viewer's. Dismiss All covers exactly the rows the grid shows, in the live table, the archive sidecar and the post-check.GetAlertHistoryAsync(the MCP read) keeps its inclusive as-of end.DismissAllVisibleAlertsAsync(hoursBack)delegates.RangePicker.SetLongestChoice(LiteTimeRange.AlertHistoryLongestChoice(now), "All"), a span equal to how long Lite keeps the alert log. The archive is deleted 3 months back (RetentionService.ArchiveRetentionMonths, RetentionService.cs:34), so the span is 89 to 92 days, capped at 365. The Viewer uses 90 days for its store.LiteTimeRange.MainCollectorFor(tab, subTab)names the main collector of the page on screen.ServerTab.CurrentMainCollectorreads the selected headers and refreshes the note on every tab change. MainWindow hands it the server's actual schedule, falling back to the shipped default (SampleIntervalForCollector). Pages with no single sampled source (Overview, Plan Viewer, on-load configuration, Daily Summary, Collection Health) show no note. The range is never widened.RefreshWindowTruncatedBannerAsyncis Lite's funnel and now feedsServerTab.FeedDataStartfor every relation, keyed by the relation's collector. The XE blocking banner, the Overview blocking lane (through a newDataStartFoundevent) and the Job History tab (its own picker) feed it too. The picker shows the floor for the page on screen, else the archive's retention edge. No new query. The Collection Log window has its own window and no picker, so it does not feed. A source-scan test pins the five call sites ofApplyWindowFloorToBannerand what each feeds.Notes
servertab-eventwires.baseline.txtlost two wires for the deleted combo and hour/minute handlers.ServerTabHeldRangeTests,JobHistoryDataStartTestsandQueriesTabWindowRoundTripTestsfollow the new shapes.DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocksandEveryDocRunClosesTheSummariesItOpenswere red onViewerServerTabTimeRange(the Viewer lane's file) when this lane last ran them.Lite test plan
LiteTimeRangeGapTests(29 tests). They cover the rolling-unit rule, FinOps wiring and Job History with a range that ended before more rows than the cap (all inside the margin hour). They also cover Alert History end and Dismiss All scope, the longest choice, the collector map, the sample interval and the banner-site pin.Stage=Guard: 441 run, one failure (CiSelectReplayGuardTests) that passed when re-run alone.Reach from the timing run (lane L4b)
Raw-table trend reads now reach 30 days (720 hours) on the web and in MCP, through the per-read opt-in. Alert History reaches 90 days. Every ceiling is the
WebReadReachrow, and every validator reads it. A window past it is refused, never shortened.get_blocking_trendget_deadlock_trendget_lock_wait_trendget_blocking_statsget_cpu_utilizationget_query_heatmapget_tempdb_trendget_wait_trendget_file_io_trendget_memory_trendget_perfmon_trendget_pg_cpu_utilizationget_pg_io_trendget_pg_database_trendget_alert_historyLeft at 168 hours, and why
get_current_waits_trend: its waiting-task series readswaiting_tasks, which keeps 7 days by default. A longer reach would read empty past a week. The shortest retention wins.get_server_trend: five of its eleven metrics were timed. Six (memory clerks, latch, spinlock, collector duration, tempdb file I/O, file I/O throughput) never reached their series SQL on the timing store. The read has one ceiling for all metrics, so it stays until those six are timed. Raising it is a one-line row change plus one validator call.get_pg_wait_trend: its timing ran on an empty table (0.1 ms), so no measurement.get_pg_query_duration_trend: not timed.get_blocking_statsis raised in the table only. Its MCP tool takes any window length by design (ValidateUncappedWindow), so the page now offers 30 days and the tool is unchanged.get_perfmon_trendright(object_name, 16) = ':Wait Statistics'. On a compressed chunk that call is a plain filter, so the arm decompressed every row of the counter and dropped it. I reproduced it on a local TimescaleDB 2.30.1 hypertable with the same compression settings (segmentbyserver_idonly, so no equality onobject_namecan use compression).object_name LIKE '%:Wait Statistics'. EXPLAIN shows it inside the columnar scan's Vectorized Filter, in both arms. The match is the same: the suffix has no wildcard, and NULL still lands in the second arm only. Only the Darling reader changed; the shared helper that Lite and the Viewer use is untouched.get_perfmon_trendthere before relying on it.Alert History (item 6)
ORDER BY alert_time DESC LIMIT(DarlingAlertReader.cs,AlertHistorySql), and the tool validateslimitand over-fetches by one, so a long window cannot return more than the row limit. The only index onconfig_alert_logleads withserver_id(PgMigrations.cs:3364), so the fleet-wide read is a scan and top-N sort over a table that keeps 90 days. It is small; I did not time it.Storage Growth (item 7)
storage_growthitself: 24 hours, or 7 to 90 whole days (MaxStorageGrowthHoursBack, 2160). The catalog said 168 forget_finopsbecause its other views take 168.WebReadReach.ViewMaxHourstakes the server's constant, the catalog serves it asview_max_hours.storage_growthon thehoursparam, andstorage-growth.jsasks the picker forview: "storage_growth"instead of carrying its own 2160. The catalog'smax_hoursforget_finopsstays 168.Descriptions and tests
hours_backdescription of each raised tool says "up to 720 (30 days)", and Alert History says "up to 2160 (90 days...)".WebReadReachTests(the raised set, a new test that no read reaches past its collector's default retention, the uncapped read kept on its validator),WebReadReachLiveTests(720 answered, 721 refused),AlertHistoryRangePageTestsand its harness (catalog stub at 2160, options now include 30 days),WebPageRangeTestsand the storage growth harness (reach from the catalog).Stage=Guard: 1409 run, 3 failures.DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks:Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs:371has two opening summary tags. The Viewer file named in item 8 (ViewerServerTab.TimeRange.cs:428) no longer fails; its tags are balanced on this branch, so I did not edit it.CommentFilterAdoptionTests.EveryLinePrefixCommentFilter_StatesTheBoundItRestsOn:Lite.Tests/LiteTimeRangeGapTests.csis a new file the pin does not list.TsqlConventionGuardTests.TheMemberScan_ReadsEveryDeclarationWhole:Lite/Services/LocalDataService.AlertHistory.cs GetAlertHistoryWindowAsyncis a new truncated-range member the pin does not list.Lite.Tests, a browser check, and a re-time on the large store.Review round 1 (desktop)
Commits f190aaa and 254a8b5 (head) on the branch. 254a8b5 adds
Lite.Tests/LiteTimeRangeGapTests.csto thedarlingpath filter in.github/workflows/build.yml, becauseCommentFilterAdoptionTestsnow lists it (CrossAppGuardCiGateTestsrequires that). One line per finding in the desktop review.High:
TimeSpan.TimeRangeSpec.TryFromIdnow works out the span in minutes and returns false past the 100-year limit (PerformanceMonitor.Ui/TimeRange/TimeRangeSpec.cs, the unit switch near line 262), andTimeRangePresets.FromLegacyHoursreturns null past it (TimeRangePresets.cs:51). Lite (LiteTimeRange.FromSettings) and the Viewer (ViewerPreferences.Normalize) then fall back to their defaults. Tests:TimeRangeTests(ids past the limit, the id at the limit, legacy hours up toint.MaxValue),ServerTabHeldRangeTests.ACorruptRangeId_FallsBackWithoutThrowingand theint.MaxValuerow ofANonsenseLegacyValue_OpensOnFourHours,ViewerTimeRangeWindowTests.ACorruptPreferenceRange_FallsBackToTheIndexWithoutThrowing.Medium:
ServerTimeHelper.CurrentDisplayZoneasZoneProvider(AlertsHistoryTab.xaml.cs:68,LiteTimeRange.ConfigureFinOpsPicker). Job History usesJobHistoryTab.JobHistoryZone(JobHistoryTab.xaml.cs): UTC for All Servers, the server's clock for one server (its last read's clock, else its open tab's, else the machine's). Tests:LiteTimeRangeGapTests.JobHistoryZone_IsTheZoneTheGridWordsItsRowsIn_SoYesterdayMeansTheGridsYesterdayresolves "Yesterday" in each zone and checks the instants.TheAlertAndFinOpsPickers_ReadInTheDisplayModesZonechecks the three display modes.ForBroadcastmoved toTimeRangePresets.ForBroadcastinPerformanceMonitor.Uiand removed from the Viewer. Lite'sApplyTimeRangeToAll_Click(ServerTab.TimeRange.cs) calls it with the tab's picker zone, as the Viewer does. The existingApplyToAll_SendsACalendarPeriod...test now calls the shared helper.SetLongestChoicewith 365 days, labelled "Last Year" (Lite/Controls/JobHistoryTab.xaml.cs,Viewer/JobHistoryTab.xaml.cs. The spec lives inLiteTimeRange.JobHistoryLongestChoiceandViewerTimeRangeWindow.JobHistoryLongestChoice). The Lite comment that said "1y can be typed" is gone, and the Viewer XAML comment now says 90 days is typed. 90 days stays a typed value (the control supports one extra choice).Low:
LiteTimeRange.AlertHistoryLongest). The Viewer stays at 90 days, the Darling retention. Test replaced byAlertHistoryLongest_IsAYear_BecauseLiteKeepsRowsPastThreeMonths._probedFloors.Clear()inRangePicker_RangeChanged,SetTimeRangeandSetDrillDownTimeRange(ServerTab.TimeRange.cs,ServerTab.DrillDown.cs).ViewerTimeRangeWindow.MainCollectorFor(tab, subTab)mirrors Lite's sub-tab map (Queries, CPU, Memory, Blocking).ViewerServerTab.CurrentMainCollector()reads the selected sub-tab, andUpdateSampleIntervalAsyncnow runs inside the reload loop before each tab load instead of once at the top. Every collector name is checked againstCollectorScheduleDefaults.LiteTimeRange.AtLeastMinimumSpan, its call inSetDrillDownTimeRangeand its test.TimeRangeSpec.TryResolve. Only a zero-length period (exactly midnight) is still refused. Lite'sCurrentRangeand the Viewer'sWindowkeep their fallbacks for a "since" start in the future. Tests updated:TimeRangeTests,TimeRangePickerControlTests,ViewerTimeRangeWindowTests.JobHistoryTab_PassesTheRangeEndToTheRead_AndTheEndIsExclusive,TheJobHistoryTab_HandsAFinishedRangesEndToTheRead_AndNoLongerTrimsOnTheClient). The remaining pins on tab wiring stay: aServerTabcannot be built in a test.Guard failures:
CorrelatedTimelineLanesControl.xaml.cs:371: the event's own summary now sits above the event, and the "Showing since" doc sits on its method.CommentFilterAdoptionTests:Lite.Tests/LiteTimeRangeGapTests.csis listed as a stated bound (itsIsDocOrDefinitionfilter countsApplyWindowFloorToBanner(sites line by line. The bound is that a call inside a block comment on a line that does not start with*or/*counts). The class summary count went from Two to Three.KnownTruncatedRanges:LocalDataService.AlertHistory.cs GetAlertHistoryWindowAsyncis listed. It is an expression-bodied method that opens withis { } end, the same shape asIsStale. The text outside the range is two calls with no string literal and no T-SQL.Shared fixture, which needed the web lane (now done in the web finish round):
Darling.Tests/Fixtures/time-range-cases.json: the row "today just after midnight" now expects 04:00Z to 04:02Z, live, echo2m Oct 8, 12:00 am - Oct 8, 12:02 am (UTC-04:00), no error.TimeRangeFixtureTestspasses.WebTimeRangeTests.TheWebModule_RunsEveryRowOfTheSharedFixturefailed untilwwwroot/js/time-range.jsdid the same. The web finish round did it, and the test passes.Test plan:
LiteTimeRangeGapTests,ServerTabHeldRangeTests,QueriesTabWindowRoundTripTests77 passed.Stage=Guard441 passed. Census, Ratchet, DocCommentHygiene, McpPayload and WebReadReach classes 58 passed.Stage=Guard1409 passed. Census, Ratchet, DocCommentHygiene, McpPayload, WebReadReach, TimeRange and Viewer classes 4452 run, 1 failed (the shared fixture row, above), 338 skipped (live PostgreSQL).Lite.Testssuite, a visual check of the Lite tabs.Review round 1 (web)
Head 9a127c2. One line per finding, High to Low.
pages/finops/storage-growth.jsnow passesminSpanMs: 7 * DAY_MS. So "Past day" and 2 to 6 days are no longer offered. The read answers 24 as its 30-day default and refuses 2 to 6 days. The pin inFinOpsStorageGrowthWindowBehaviourTestsnow lists "Past week,Past 30 days", and the harness checks that 1d and 3d are refused with "at least 7d".pages/alerts.jsbuilds one request (readRequest) and cuts the reply to the exact start withtrimAlertsToStart. A 5 or 30 minute range, or Today at 10:30, now lists only alerts inside it. Dismiss All acts on that list. New harness scenariotrim(rows 1, 20 and 90 minutes old) is pinned inAlertHistoryRangePageTests.get_query_heatmapgoes back to 168 h (rawquery_statsis dropped at four days on a TimescaleDB store). New censusNoRawTierTableRead_ReachesPastTheTimescaleRawRetentionfails when a non-rollup read of one of the three raw-tier tables is raised. Every read raised past 168 h was checked on both store types. The other raw tables are listed here: (blocked_process_report, deadlocks, wait_stats, dmv_blocking_snapshot, cpu_utilization, tempdb_stats, file_io_stats, memory_stats, pg_cpu_utilization, pg_io_stats, pg_database_stats) keep 30 days on both (the existing collector-retention census) and are not in the TimescaleDB raw tier. The three duration trends reach the rollups' 90 days, alert history the alert table's 90 days. So 720 h stays for those and nothing else needed lowering.readWindow(range, nowMs)intime-range.jstreats a fixed range ending at or after now as a live range ending now. It sends noas_of, setslive: trueand counts hours to now. The picker,windowOfSpecand the server page'sholdSpecuse it. The shared fixture row ("same date today counts as not in the future") is untouched, so the parser parity holds. Harness casefutureEnd.time-range-picker.jsremembers the end box value it filled in for a live range. Moving only the start writes " - now" (a since-range), and typing an end still gives a fixed pair. Harness caseliveEnd.extraPresetsandsetExtraPresets, and exposesreachHours().pageRangePicker({ offerReach: true })(Alert History) androllingHoursPicker(views, view editor) add the read's reach as the longest quick choice when it is past 30 days. So Alert History lists "Past 90 days".AlertHistoryRangePageTestsexpects it.EveryRaisedTool_HoursBackDescription_NamesItsRowsNumber_AndNoOtherToolClaimsAHigherOnereflects each tool'shours_backdescription. A read past 168 h needs the words "up to ". No tool claims more than its row.get_blocking_statsis skipped because it never capped.dataStartMsandsetDataStartfrom the picker (no web page used them).dataStartNotestays intime-range.js.serverCataloginpage-range.jsdrops a failed fetch from the cache and expires a good one after 5 minutes. The finish round makes that expiry a check on lookup, see below.applySampleNoteinserver.jshas a.catch.alerts.jsshows "This range cannot be read yet: ..." instead of reading 24 h under a "today" label.server.jsrangeContextkeeps the held range and labels the shown window, instead of deleting the range.McpHelpers.ValidateWindowdoc now namesget_alert_historyas the list that passes its retention.WebPageRangeTestsasserts theapplySampleNote();call exists before comparing positions.WebReadReachLiveTestsasserts there is noerrorkey at 720 h (instead of "no exceeds-maximum text").job-history.jsbuilds the window once (currentWindow()), and the request and the trim share it./api/sweeps(DarlingFleetSweepEndpoints.cs) now takes its ceiling from theget_sweep_reportsrow, the number the sweeps page reads from the catalog.Brief items:
get_perfmon_trendstays at 7 days: 11.2 s cold at 30 days on a large store. #5574 tracks the fix. Its row, validator comment, description and tests are back at 168. The comment cites 11.2 s cold, the LIKE saving of 1.3 s, grouping by server only and #5574.get_alert_historykeeps 2160 h (the re-time read 148 ms. Cited in the alert tool and theWebReadReachtable).Test runs for this round are listed in "Review round 1 (web, finish)" below.
Review round 1 (web, finish)
Head c0c2191 (the commits before it are the first web round). Four changes and the runs the first round did not finish.
Changes:
resolveSpecinwwwroot/js/time-range.jsnow exempts a calendar period from the 5 minute floor. It refuses one only when it has zero length (exactly midnight), the same rule asTimeRangeSpec.TryResolve. Typed spans, "since" and custom start and end keep the floor. The shared fixture row "today just after midnight" now passes in the web module, so the desktop section's "needs the web lane" item is closed. The doc comments ofcurrentLength,resolveSpec,windowOfSpec,windowResultOfSpec, the picker and Alert History no longer say "first five minutes".windowResultOfSpecreturns the refusal and the page shows it. A new check inWebPageRangeTestspins three things. Today two minutes in reads 1 hour. Exactly midnight is refused with the 5 minute message. A typed 3 minute span still getstoo_short.WebTimeRangeTeststook 10 minutes. Two of its nine tests drive the kept-history harness, and that harness exited only after 300 seconds each. The cause was in the page code, not the harness.serverCataloginpage-range.jsset a 5 minutesetTimeoutto expire a cached catalog, and a pending timer keeps Node alive. The cache now stores when the catalog was read and drops it on the next lookup after 5 minutes, with no timer. The class now runs in about 10 seconds (9 tests), down from 606 seconds, with no assert changed. A new check inWebPageRangeTestsdrives a fake clock. Two lookups read once. A lookup 4 minutes later still reads once. A lookup 6 minutes later reads twice. The source has nosetTimeout(() => serverCatalogsleft.JobHistoryPageTests.ThePageReadsGetJobHistoryThroughTheSharedReadWithASignalfailed on the branch: the page now callsreadParams(w)and the pin still looked forreadParams(). The pin follows the page.Test plan (Darling.Tests, after the rebase onto the branch head, all on a throwaway PostgreSQL store with TimescaleDB on a local port, now stopped)
WebTimeRangeTests,WebPageRangeTests,WebReadReach*(the live class ran, not skipped),WebServerPageRange*,TimeRangePicker*,PageRange*: 53 passed.AlertHistory*,JobHistory*,Sweep*,Views*,FinOpsStorageGrowthWindow*,FinOpsWindowPicker*,McpPayload*: 849 run, 1 failed (the Job History pin above), fixed andJobHistoryPageTestsre-run clean.Census*,Ratchet*,DocCommentHygiene*andJobHistoryPageTests: 588 passed.Stage=Guard: 1409 passed.DocCommentHygieneis clean now (the first round saw one failure in a Lite file that the desktop round fixed).Lite.Tests(nothing in Lite changed in this round), a browser check of the picker.origin/devwas not possible: the picker harness andWebTimeRangeTestsdo not exist there.Review round 1 (M4)
Head 3b2d246. The code for M4 is pushed. The three tests that failed on it are fixed. See "Review round 1 (finish)".
What changed
reachReads(read names only, inserver-tabs.js, and inanalysis-findings.jsfor the Recommendations tab).readsReachHoursinpage-range.jsturns that list into the tab's reach: the smallest catalogmax_hoursof its reads. A read with no catalog row, a tab with no list and an unreadable catalog all give 168 hours.server.jsno longer hasPAGE_REACH_HOURS. The reach comes from the catalog throughtabReach. The picker gets the tab's reach once the catalog answers, and longer choices are greyed out with "This tab reads up to N days."What this delivers
Most tabs mix a list read (168 hours) with trend reads, so the smallest wins and they stay at 7 days.
Only the SQL Server I/O tab (file I/O and tempdb trends, both 720 hours) offers 30 days. The Activity tab (perfmon trend) and Queries tab (query heatmap) stay at 7 days. This follows the brief's "smallest of its reads" rule. On the Waits tab "Past 30 days" is still greyed out, because Latch Stats and Spinlock Stats take only 7 days. This is the intended reading. A tab's reach is the smallest
max_hoursamong its reads, so Waits stays at 7 days because Latch Stats and Spinlock Stats read 168 hours.Tests added or changed
WebServerPageRangeTestsnow holds these:WebReadReachfor its declared reads.analysis-findings.jsorserver.jstypes a reach as a number, and every declared read name is a read the web serves.offeredRangesharness scenario now serves the real catalog, refuses per read, and skips presets longer than a tab's reach.ServerPageTabsTestspin followstabNote(tab, tabReach()).WebServerPageRangeTests,ServerPageTabsTests,PageRange*,TimeRangePicker*on the rebased head: 50 tests, 3 failed (below). Not run: Census, Ratchet, DocCommentHygiene,Stage=Guard, the other classes in the brief.Still failing
None. The three failures are fixed in the finish round below.
Review round 1 (finish)
Head 0be679e.
NoTab_FetchesTheSameReadTwiceandTheQueryStoreTopGrid_DrawsTheApproximationNote_AsAFurtherNotenow readserver-tabs.jsthroughServerTabsJsWithoutReachReads(ServerPageTabsTests.cs:60, used at:143and:1053). It blanks only thereachReads: [...]lists, so a read a tab really makes still counts twice. The M4 test that fails on an undeclared read is unchanged.nullfor a tab with no collector is right.collectorIntervalFromCatalogalready answers null for a missing collector, and a null interval gives no sample note and never widens the range. The pin atWebPageRangeTests.cs:156now names the wholetab.collector ? ... : nullcall, so a rename or a move still fails it.max_hoursof its reads, and Latch Stats and Spinlock Stats read 168 hours). Said in one line in the M4 section above.time-range.js:442,TimeRangeSpec.cs:208), and the desktop tooltip uses the same words (TimeRangePicker.xaml.cs:373). Typed spans, "since" and custom ranges keep their text. Two fixture rows hold both front ends to the words (time-range-cases.json:981and:993, new optionalerrorMessagefield read bytime-range-harness.mjsandTimeRangeFixtureTests). Lite and the Viewer project build with 0 warnings.resolveSpecfailsTheWebModule_RunsEveryRowOfTheSharedFixture("today just after midnight") andThePureParts_RunUnderNode. I restored it. Old midnight text on both sides fails both new fixture rows on the C# side and the shared-fixture test on the web side, then restored.ServerPageTitlePinTests.TheRouteKey_StaysTheIdentity_OnlyTheHeadingChanges(aStage=Guardtest) pinned the oldmount(main, [...])list, and M4 addedreachSlot(ServerPageTitlePinTests.cs:85).ServerPageTabs*,WebServerPageRange*,PageRange*,TimeRangePicker*,WebTimeRange*andTimeRangeSpec*(the fixture and model classes). It also coveredAlertHistory*,JobHistory*,Sweep*,Views*, the two FinOps window classes,McpPayload*andWebReadReach*(live class too, on a throwaway cluster). The Viewer time-range classes, every Census and Ratchet class andDocCommentHygieneran with it. Result: 1643 passed, 0 failed.Stage=Guard: 1409 passed, 0 failed. Lite time-range classes: 89 passed, 0 failed.Review round 2
Head
a02545a9b. The branch now holds a real merge of the current dev (two XAML conflicts, inLite/Controls/ServerTab.xamlandDarling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml). Dev only added a screen-reader name to the old time range combo. The shared picker replaces that combo, so the picker keeps the same name (AutomationProperties.Name="Time range"), andServerTabToolbarAndTooltipsTestsnow checksRangePickerfor it.wwwroot/js/pages/alerts.js:446.trimAlertsToStartread the server'salert_timewithDate.parse. That time has no zone, so the browser read it as local time and every alert was shifted by the browser's offset. It now usesparseUtcfromutil.js. Test:AlertHistoryRangePageTests.ARangeShorterThanTheWholeHoursItFetches_ListsOnlyTheAlertsInsideIt, which now runs in UTC, Europe/Berlin, America/New_York and Asia/Tokyo with rows in the server's real shape (seven fraction digits, noZ). Node on Windows ignores aTZvariable set before it starts, so the harness takes the zone as a fourth argument and setsprocess.env.TZitself. The test also checks the offset's sign, so it cannot pass by running in UTC four times. I checked the other pages this PR touches. Job History sendsrun_timethroughFormatEffectiveStart. Sweeps readswept_atwithAsUtc. Both carry aZ, so they were already right.wwwroot/js/time-range.js(resolveSpec) andPerformanceMonitor.Ui/TimeRange/TimeRangeSpec.cs(TryResolve). A fixed range that starts after now is refused with "That range has not started yet." (coderange_not_started). The review said typed text cannot reach this, but a typed range with a year ahead does ("Dec 25 2030 - Dec 26 2030"). The desktop also accepted it, so both sides now refuse it. The refusal sits in the one resolver, so the picker's preview and Apply (refusalFor), the server page'sholdSpecand both desktop hosts all see it. Tests: a new row inFixtures/time-range-cases.json(run byWebTimeRangeTests.TheWebModule_RunsEveryRowOfTheSharedFixtureandTimeRangeFixtureTests), the picker harness casefutureStart(preview text, Apply off, nothing held), andWebServerPageRangeTests.AFixedRangeThatStartsInTheFuture_IsRefusedByTheHold_AndTheHeldRangeStays.pages/server.js:198-202.rangeContextpasses itsnowMstoreadWindowand works out the "aggregate from" note fromw.endMs(the end that is read). The change is covered by the existing server page range tests, which pass on the fixed clock.pages/server.js:382. A set of servers being waited on (catalogWaits) means a redraw during the catalog wait keeps the loading strip and adds no second wait. The one callback redraws whichever tab is current when the catalog arrives. Test:WebServerPageRangeTests.ARedrawDuringTheCatalogWait_ReadsEachPanelOnceholds the catalog back, renders the same server twice, releases it, and counts panel builds (1). Identical reads in flight are already joined byapiGetJoined, so the URLs alone cannot show the double redraw. The build count does.pages/finops/window.js:31-46andpages/finops/storage-growth.js:195.finopsWindowControlpassesofferReach, and Storage Growth asks for it, so "Past 90 days" is one click again (the page always offered 7, 30 and 90).FinOpsStorageGrowthWindowBehaviourTestsnow pins "Past week,Past 30 days,Past 90 days".PerformanceMonitor.Ui/TimeRange/TimeRangePresets.cs:70. TheCurrentLengthcomment now says it is null only at zero length (exactly midnight for Today).TimeRangePickerControlTests.csandViewerTimeRangeGapFixTests.csstart STA threads, which dev's newWpfStaGateCensusTests(Lite test flake: ScreenReaderNamesFollowUpTests hits a WPF dictionary from two threads #5559) requires to takeWpfStaGate.Enter()first. Both helpers now do.Red-plants: each test fails with the fix removed and passes with it back.
Date.parserestored fails Berlin, New York and Tokyo. UTC still passes, which is why the zones matter.Runs on the final head, against a throwaway PostgreSQL on port 55548 (stopped afterwards):
TimeRangeFixture*,ViewerTimeRange*andServerTabToolbar*. The set coversAlertHistory*,ServerPageTabs*,WebServerPageRange*,PageRange*, the time range classes,JobHistory*,Sweep*,Views*, the two FinOps window classes,McpPayload*andWebReadReach*with the live class: 1193 run. The first pass had one failure, the new fixture row, because that build still held the planted desktop change. After a rebuild the fixture and model classes passed (121 tests).LiteTimeRangeGap*,ServerTabHeldRange*,TimeRangeSlicerLabels*, anything namedTimeRange): 81 run, 0 failed.Stage=Guard: 1411 run, 0 failed.Review round 3 (the 8 Darling tests that failed in CI)
Head 3f313e4, merged with origin/dev (the merge conflicted in four tab constructors,
AlertsHistoryTabandJobHistoryTabin Lite and in the Darling viewer: dev addedColumnFilterScope.SetServer, this branch added the picker setup; both kept).What each failure was, one line each:
TheWebServerPage_DrawsNoChartFromTheCollectionLog(expected 2, got 4): real regression of the PR's shape, not a pin to move. ThereachReadslists on the tab objects inserver-tabs.jsname"get_collection_log"twice more. Fixed in the page (see below).TheWebSection_ReadsTheToolOnce_...(get_query_store_clutter named twice): same cause, the Queries tab'sreachReadslist. Fixed in the page.TheWebPanels_DeclareTheNoteKey_AndTheHostsPassTheRegistry: same cause. The test takes the first"get_blocking"in the file, and that was now the name in the Blocking tab'sreachReads, not the panel. Every panel still declaresseparately_monitored_note. Fixed in the page.hours_backparameter lines over their budget ceilings, and 5. the 197,994 byte total: real growth the budget forbids. The parameter lines are back toHours of history. Default N.(the pinned ceilings are unchanged, no ceiling raised). For the 7 tools that already have a reading guide (get_alert_history, get_pg_cpu_utilization, get_pg_io_trend, get_pg_database_trend, get_query_duration_trend, get_procedure_duration_trend, get_query_store_duration_trend) the reach sentence is now the first sentence after the tool's<<GUIDE>>marker, soget_tool_guideserves it. The other 8 (get_blocking_trend, get_deadlock_trend, get_lock_wait_trend, get_cpu_utilization, get_wait_trend, get_tempdb_trend, get_memory_trend, get_file_io_trend) have no guide; adding a marker would put the pointer sentence " Reading guide: get_tool_guide." (31 bytes each) into everytools/listanswer, which leaves the total over the ceiling (about 197,711 against 197,466). So they state no reach; a request past it is still accepted up to the row's number, and a request past that is refused with a sentence naming the ceiling. Total now 197,463 bytes, ceiling 197,466.TheActivityTab_...andTheWaitStatsTab_...: theBetweenmarkers needlabel:andbuild:to be neighbours. Same cause as 1 to 3, fixed in the page. The markers are untouched.NoPostgresWindowPanel_OptsOutOfTheNote: same cause (the first"get_pg_..."name in the PostgreSQL registry was in areachReadslist).The fix in the page: the
reachReadsandcollectordeclarations moved out of the tab objects intowwwroot/js/pages/server-tab-reach.js, keyed by tab id, andserver-tabs.jsapplies them once at load (withTabReach(SERVER_TABS, SQL_TAB_REACH)). The tab objects still carry both at run time, soserver.jsis unchanged.server-tabs.jsnow differs from dev only by the import and a six-line block with two calls. Ruling R2 (names, never hours) is unchanged: the file holds names only.Why the lane runs missed these: they ran the time-range classes, but the pins that read
server-tabs.jsas text and the MCP budget pins live in classes with other names. Step 3: I took every Darling.Tests class whose file mentions a file this PR changed in the web pages, the web endpoints, the MCP tool sources orMcpHelpers(218 classes) and ran them. That found two more failures nobody had listed, both mine:WebPageRangeTests.EveryServerTabThatDeclaresACollectorNamesARealOne_...matchedcollector:on the tab objects. It now reads the collector fromserver-tab-reach.js(still 9 tabs, still the Viewer agreeing on the first 7).WebReadReachTests.EveryRaisedTool_HoursBackDescription_...required the number in the parameter line, which contradicts item 4. Renamed toEveryRaisedTool_StatesItsRowsNumberInTheGuideTail_NeverInTheHead_AndNoToolClaimsMoreThanItsRow: the head states no reach above 168, the 7 guided raised tools state exactly their row's number after the marker, and no tool claims more than its row.Also updated:
WebServerPageRangeTests(reads the lists from the new module; assertsserver-tabs.jshas noreachReads:),ServerPageTabsTests(its strip-the-lists helper is no longer needed).Test plan (r3)
*TimeRange*,*WebTimeRange*,*Census*,*Ratchet*,*DocCommentHygiene*, on the final head: 910 passed, 0 failed, 4 skipped (live PostgreSQL, no rig).Stage=Guardon Darling.Tests: 1432 passed, 0 failed, 0 skipped.*TimeRange*,ServerTabHeldRange,JobHistoryDataStart,QueriesTabWindowRoundTrip,RefreshCoordinator,McpToolGuide: 207 passed, 0 failed.git merge-base --is-ancestor origin/dev HEADtrue before the push.What to double-check: the 8 trend tools with no guide no longer state their 30-day reach anywhere in the MCP descriptions; the choice was that or a larger
tools/list. If you would rather give them a guide, the total ceiling has to move by about 250 bytes.Review round 4 (the guide-tail pin)
Cause: round 3 moved each guided tool's reach sentence ("hours_back reaches N ...") to the very start of the text after the
<<GUIDE>>marker.McpToolGuideHeadsTrendTests.QueryStoreDurationTrend_TailKeepsItsOwnRoutingDetail_DarlingSiderequires that tail to start with the tool's own first sentence, so it failed in CI. The pin is right, so the code changed.Fix: in all 7 guided tools (get_alert_history, get_pg_cpu_utilization, get_pg_io_trend, get_pg_database_trend, get_query_duration_trend, get_procedure_duration_trend, get_query_store_duration_trend) the reach sentence now comes right after the tool's first sentence. The text only moved, so the byte count is unchanged and the
tools/listtotal stays at 197,463 of 197,466.WebReadReachTestsstill finds each number after the marker and none in the head. The tails still end with the baseline sentence.Checks on head b92e3ed: the
*McpTool*,*WebReadReach*,*ToolGuide*,*Census*,*Ratchet*and*DocCommentHygiene*classes ran 786 tests, 0 failed, 3 skipped.Stage=Guardran 1432, 0 failed, no FATAL ERROR. Build: 0 warnings. Lite is unaffected (no Lite or shared guide text changed). The[FATAL ERROR] Foreground threadsfailure in the guards job is #5596, left alone.