Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/761-format-ms-carry.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Durations in the run report no longer overflow their own unit: a wait of 59.6 s reads `1m` instead of `60s`, and 1h59m50s reads `2h00m` instead of `1h60m` ([#761](https://github.com/ThinkFlowLab/FlyBot/issues/761)).
2 changes: 1 addition & 1 deletion docs/subsystems/timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Everything in `plugins/timeline/`: the recorder, the trace file, the read model
| `plugins/timeline/src/store.js` | The trace: single writer, write-tmp + fsync + rename, capped segments. |
| `plugins/timeline/src/window.js` | Pure read model: records to lanes, turns, tools, arrows, edges, utilization, the token spend per Bot (#357), per-Bot outcomes and recent failures (#471), the open requests waiting longest (#469) and the tools each Bot used (#359). |
| `plugins/timeline/src/health.js` | Team health (#357): pure `buildHealth(window)` turning the window into per-Bot signals and flags. |
| `plugins/timeline/src/digest.js` | Team run report (#582): pure `buildDigest(window)` turning the window into short Markdown, with the count and duration formatting (`formatCount`, `formatMs`). |
| `plugins/timeline/src/digest.js` | Team run report (#582): pure `buildDigest(window)` turning the window into short Markdown, with the count and duration formatting (`formatCount`, `formatMs`). A duration rounds once and carries into the next unit (#761), so the text says `1m` and `2h00m`, never `60s`, `60m` or `1h60m`. |
| `plugins/timeline/src/alerts.js` | Durable alerts (#584): pure `createAlertTracker()` folding a window across reads, so a condition opens one alert and resolves when it clears. |
| `plugins/timeline/client.js` | Browser entry (`@flybot/timeline`): the 2 s poller exposed as `ctx.flybot.timeline`, the geometry functions (`layoutSwimlanes`, `layoutMap`, `waitRows`, `utilizationRows`) and the panel (swimlanes, who waits on whom, utilization, and **Copy report**), opened from its `flybot.sidebar.section` row, the only entry (not in the + menu, #269). |
| `plugins/timeline/src/sanitize.js` | The cleaners for ids, labels, codes, times, counts. |
Expand Down
11 changes: 7 additions & 4 deletions plugins/timeline/src/digest.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ export function formatCount(value) {
/** A duration a person reads at a glance: `40s`, `12m`, `1h05m`. */
export function formatMs(ms) {
const value = Number.isFinite(ms) ? Math.max(0, Math.round(ms)) : 0
if (value < 60_000) return `${Math.round(value / 1000)}s`
if (value < 3_600_000) return `${Math.round(value / 60_000)}m`
const minutes = Math.round((value % 3_600_000) / 60_000)
return `${Math.floor(value / 3_600_000)}h${String(minutes).padStart(2, '0')}m`
// Round once and carry into the next unit, so the text never prints a count its own unit cannot
// hold (#761): picking the unit from the raw value and rounding inside it printed `60s` for
// 59.6 s, `60m` for 59m59.999s and `1h60m` for 1h59m50s.
if (Math.round(value / 1000) < 60) return `${Math.round(value / 1000)}s`
const minutes = Math.round(value / 60_000)
if (minutes < 60) return `${minutes}m`
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`
}

const percent = (part) => `${Math.round(part * 100)}%`
Expand Down
16 changes: 16 additions & 0 deletions plugins/timeline/test/digest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,25 @@ test('formatCount and formatMs read at a glance', () => {
assert.equal(formatMs(40_000), '40s')
assert.equal(formatMs(720_000), '12m')
assert.equal(formatMs(3_900_000), '1h05m')
assert.equal(formatMs(3_600_000), '1h00m')
assert.equal(formatMs(undefined), '0s')
})

// The carry must never overflow the unit the text prints (#761): rounding the raw milliseconds
// inside a unit chosen from the raw value printed `60s` for 59.6 s and `1h60m` for 1h59m50s, and a
// person pasting the report read those numbers in a standup.
test('#761: a duration rounds once and carries, instead of printing 60s, 60m or 1h60m', () => {
assert.equal(formatMs(59_600), '1m')
assert.equal(formatMs(3_599_999), '1h00m')
assert.equal(formatMs(7_190_000), '2h00m')
// The boundaries themselves stay where they were.
assert.equal(formatMs(59_400), '59s') // 59.4 s still reads in seconds
assert.equal(formatMs(59_999), '1m') // 59.999 s rounds to a minute, never to '60s'
assert.equal(formatMs(60_000), '1m')
assert.equal(formatMs(3_540_000), '59m')
assert.equal(formatMs(86_400_000), '24h00m')
})

test('#582: per-Bot sections, the team rollup and the longest wait', () => {
const digest = buildDigest(make(), { now: 100_000 })
assert.equal(digest.bots[0].name, 'Chief')
Expand Down
Loading