Architecture
The widget does not fetch anything. It reads one file and draws it.
That’s the core of the structure. The widget reads one file and draws it. A separate script walks the sheets and the issue tracker and writes that file.
A frozen UI gets closed
The first reason for the split. A remote query takes seconds, and if the window locks up during them, people close the widget.
So the refresh is fired asynchronously and a timer checks for completion. The window keeps moving; values change late when they arrive late.
This isn’t performance tuning, it’s survival. The only value this tool has is that it stays on screen — and if freezing makes someone close it, that value goes to zero. A five-second stall is equivalent to deleting a feature.
Failure leaves the previous values
The second reason. When the fetch script fails, the widget still holds the previous values.
A slightly stale number beats a blank panel or a row of zeroes. Zero is a lie; a stale number is an old truth.
This works precisely because the widget doesn’t fetch. Had it called the APIs directly, a failure would mean the display collapses. Reading a file means an un-refreshed file simply shows yesterday’s numbers.
Which then makes “how old is this” important. So the refresh timestamp is on screen.
A milestone is configuration, not code
Rolling to a new milestone had to require no code changes. So everything that differs per milestone became a config key — sheet identifier, report URL, sprint name, notification channel, deploy folder, template path.
Opening a new milestone means copying a template and filling in the values, and a separate desktop shortcut appears for it. There is one copy of the code.
One thing mattered here. Put the milestone in a code constant and you can never look at a previous one. Reasons to revisit an old milestone’s results keep appearing long after it ends, and with a constant that means reverting code each time. Now it’s a double-click on a shortcut.