On October 1, the Sales Summary month chart showed 30 labels in a 31-day month.
That was the whole symptom on the first ticket. The x-axis stopped at day 30. An agent read the page source and found this date arithmetic:
var nextMonth = new DateObj(this.reportDateObj.T + MONTH);var firstOfNextMonth = new DateObj(nextMonth.M + '/1/' + nextMonth.Y);var lastDayOfThisMonth = new DateObj(firstOfNextMonth.T - DAY);var numDaysInThisMonth = Number(lastDayOfThisMonth.D);MONTH is a flat 30 days. On the 1st of a 31-day month, adding it lands in the same month. “First of next month” comes out as 10/1, minus a day is September 30, and the chart gets 30 labels.
The fix was a helper, since day 0 of the next month is the last day of this one:
Page.prototype.DaysInMonth = function(y, m) { return new Date(Number(y), Number(m), 0).getDate();};We used it in two places, the top chart and the projection in the annual chart below it. We also set pointRadius: 4 on the current-year series, which has exactly one point on the 1st.
The check that passed
We sent a browser agent into the signed-in Chrome on debug port 9222. The brief was verification only. It was to load the page, find the Chart.js instance behind the cumSales canvas, and report the label count, each dataset’s label and length, and any console errors. It would do this once on prod for the before state and once on a local copy of the working tree for the after state. The brief expected 31 labels, no new console errors, and a bottom chart that still rendered. The fix went out and the release feed posted “live now” with a line saying months with 31 days no longer get cut off a day early.
We treated that as the chart being fixed. It wasn’t.
Later we asked for another read of prod, this time with a hard reload to bypass cache. The top chart came back titled “October Cumulative Sales” with 31 labels, first 1, last 31. The first fix had worked. The 2026 series was one point at 1570. The 2025 and 2024 lines sat at zero, and the Projected line, which is built from last year, was empty.
So the first hotfix went to prod and was announced as done while the chart still drew zeros for two prior years and no Projected line. It cost a second hotfix and a second verification pass. The brief had asked for dataset lengths, and a series of 31 zeros has a length of 31.
Second bug: the prior-year windows
The prior-year windows used the same kind of arithmetic. Each year back was a flat 365 days, and the end of the window was a flat 30 days after that:
var dateObj1YrAgo = new DateObj(this.reportDateObj.T - YEAR);var dateObj1YrAgoNextMonth = new DateObj(dateObj1YrAgo.T + MONTH);On October 1, a year back is October 1 and thirty days after that is October 31. The query built both ends of its window from the month and year of those dates with a “/1/” day, so both ends read 10/1. The window started and ended on the same day, the query had nothing to sum, the running total stayed at 0, and the line drew flat at zero. Projected reads from last year’s line, so it went blank too. This was a different code path from the label count. Fixing DaysInMonth could never have touched it.
There was a third problem under it. With “sync day of week” on, each prior line is shifted by an offset from the report date, and the loop cut every prior line short by that offset.
The old file had four near-identical blocks, one each for 4, 3, 2 and 1 years ago, each with its own date objects, offset loop and query. The new code builds the window from the real calendar:
var monthStart = reportM + '/1/' + y;var windowEnd = new Date(y, reportM, 1 + offset); // day after the month ends, plus the offsetvar dayExpr = "DATEDIFF(DAY, '" + monthStart + "', t.tdate) + 1";Each year queries its own calendar month, plus the offset days past month end when syncing. That way day N of the chart lines up with day N plus the offset of that year. The projection also guards missing days and ratios now. The diff was 96 insertions and 234 deletions in one file.
A check that reads values
This time the brief asked for the last non-null value of each dataset and the count of non-null points, not just the length, and we compared them to the database. A series that ends at 0 when last year had sales that month is the failure. The signed-in browser read on prod and the database agreed, so we called it verified.
Both bugs came from treating a month as 30 days and a year as 365 days, and October 1 happened to expose both. The first fix passed because we asked a question the half-broken chart could answer correctly. The second pass asked for values instead of shapes, and the answer changed.
Two things are still unchecked: dates other than October 1, and the bottom chart’s 365-day year lookup on December 31 of a leap year.