feat promote timeframe at natural boundaries

24 hours → 1 day, 7 days → 1 week, 4 weeks → 1 month, 12 months → 1 year. Demoting lands at threshold-1 of the previous unit (e.g. week→6 days).

Commit
c6960d3711de17af1339a32bc3bd3ceb6be71e8b
Author
gpt-5.6-sol high <agent@openai.com>
Author date
Committer
gpt-5.6-sol high <agent@openai.com>
Committer date
Changed files
roles/dashboard/public/js/dashboard/timeframe.js
index 72d19d77..ac14f1a0 100644..100644
@@ -7,6 +7,9 @@
7 7 const ranges = Object.fromEntries(Object.keys(TIMEFRAMES).map(timeframe => [timeframe, 1]));
8 8 const order = Object.keys(TIMEFRAMES);
9 9
10 Added: // Natural promotion thresholds: when range of current == ratio, promote to next
11 Added: const promotionAt = { hour: 24, day: 7, week: 4, month: 12 };
12 Added:
10 13 function selected() {
11 14 return TIMEFRAMES[active] ? active : DEFAULT_TIMEFRAME;
12 15 }
@@ -30,17 +33,25 @@
30 33 active = timeframe;
31 34 const current = ranges[timeframe] || 1;
32 35 const next = current + adjustment;
36 Added: const index = order.indexOf(active);
33 37
34 38 if (next < 1) {
35 Removed: // Step down to previous timeframe at range 1
36 Removed: const index = order.indexOf(active);
39 Added: // Step down to previous timeframe at its promotion threshold minus one
37 40 if (index > 0) {
38 41 active = order[index - 1];
42 Added: const threshold = promotionAt[active];
43 Added: ranges[active] = threshold ? threshold - 1 : 1;
44 Added: }
45 Added: } else if (promotionAt[active] && next >= promotionAt[active]) {
46 Added: // Promote to next timeframe at range 1
47 Added: if (index < order.length - 1) {
48 Added: active = order[index + 1];
39 49 ranges[active] = 1;
50 Added: } else {
51 Added: ranges[timeframe] = Math.min(next, MAX_TIMEFRAME_RANGE);
40 52 }
41 53 } else if (next > MAX_TIMEFRAME_RANGE) {
42 Removed: // Step up to next timeframe at range 1
43 Removed: const index = order.indexOf(active);
54 Added: // At absolute max without a promotion path
44 55 if (index < order.length - 1) {
45 56 active = order[index + 1];
46 57 ranges[active] = 1;
roles/dashboard/t/js/frontend.test.cjs
index 5cf4589d..ba7ef8d7 100644..100644
@@ -75,27 +75,50 @@
75 75 assert.equal(state.selected(), "day");
76 76 assert.equal(state.range(), 1);
77 77
78 Removed: // Decrease below range 1 should step down to hour
78 Added: // Decrease below range 1 should step down to hour at threshold-1 (23)
79 79 state.adjust("day", -1);
80 80 assert.equal(state.selected(), "hour");
81 Added: assert.equal(state.range(), 23);
82 Added:
83 Added: // Increase from 23 hours to 24 should promote to day
84 Added: state.adjust("hour", 1);
85 Added: assert.equal(state.selected(), "day");
81 86 assert.equal(state.range(), 1);
82 87
83 88 // Decrease at hour/1 should stay at hour (lowest)
89 Added: state.select("hour");
90 Added: state.ranges.hour = 1;
84 91 state.adjust("hour", -1);
85 92 assert.equal(state.selected(), "hour");
86 93 assert.equal(state.range(), 1);
87 94
88 Removed: // Increase from hour should increment range
89 Removed: state.adjust("hour", 1);
90 Removed: assert.equal(state.selected(), "hour");
91 Removed: assert.equal(state.range(), 2);
92 Removed:
93 Removed: // Select day, then increase past MAX should step to week
95 Added: // 6 days + 1 = 7 days → promotes to week
94 96 state.select("day");
95 Removed: state.ranges.day = 24;
97 Added: state.ranges.day = 6;
96 98 state.adjust("day", 1);
97 99 assert.equal(state.selected(), "week");
98 100 assert.equal(state.range(), 1);
101 Added:
102 Added: // 3 weeks + 1 = 4 weeks → promotes to month
103 Added: state.select("week");
104 Added: state.ranges.week = 3;
105 Added: state.adjust("week", 1);
106 Added: assert.equal(state.selected(), "month");
107 Added: assert.equal(state.range(), 1);
108 Added:
109 Added: // 11 months + 1 = 12 → promotes to year
110 Added: state.select("month");
111 Added: state.ranges.month = 11;
112 Added: state.adjust("month", 1);
113 Added: assert.equal(state.selected(), "year");
114 Added: assert.equal(state.range(), 1);
115 Added:
116 Added: // Demoting from week should land at day/6
117 Added: state.select("week");
118 Added: state.ranges.week = 1;
119 Added: state.adjust("week", -1);
120 Added: assert.equal(state.selected(), "day");
121 Added: assert.equal(state.range(), 6);
99 122
100 123 delete globalThis.document;
101 124 });