Auto-indexing service and GraphQL API for AT Protocol Records
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: correct activity chart bucket count off-by-one error

The recursive CTE was generating one extra bucket extending into the
future, causing the rightmost bar to always be empty. Fixed by adjusting
the WHERE clause to generate exactly the expected number of buckets.

+314 -2
+252
docs/plans/2025-11-24-fix-activity-chart-bucket-count.md
··· 1 + # Fix Activity Chart Bucket Count Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Fix the off-by-one error causing the rightmost bar in the activity chart to always be empty for 1hr, 3hr, 6hr, and 1 day time ranges. 6 + 7 + **Architecture:** The `get_activity_bucketed()` function generates time buckets via a recursive SQL CTE. The condition `WHERE n < expected_buckets` creates one extra bucket extending into the future, which always has zero data. 8 + 9 + **Tech Stack:** Gleam, SQLite, gleeunit 10 + 11 + --- 12 + 13 + ### Task 1: Write Failing Test for Bucket Count 14 + 15 + **Files:** 16 + - Create: `server/test/jetstream_activity_bucket_test.gleam` 17 + 18 + **Step 1: Create the test file with bucket count test** 19 + 20 + ```gleam 21 + import database/repositories/jetstream_activity 22 + import database/schema/tables 23 + import gleam/list 24 + import gleeunit 25 + import gleeunit/should 26 + import sqlight 27 + 28 + pub fn main() { 29 + gleeunit.main() 30 + } 31 + 32 + fn setup_test_db() -> sqlight.Connection { 33 + let assert Ok(conn) = sqlight.open(":memory:") 34 + let assert Ok(_) = tables.create_jetstream_activity_table(conn) 35 + conn 36 + } 37 + 38 + pub fn test_1hr_returns_exactly_12_buckets() { 39 + let conn = setup_test_db() 40 + 41 + let assert Ok(buckets) = jetstream_activity.get_activity_1hr(conn) 42 + 43 + list.length(buckets) 44 + |> should.equal(12) 45 + } 46 + 47 + pub fn test_3hr_returns_exactly_12_buckets() { 48 + let conn = setup_test_db() 49 + 50 + let assert Ok(buckets) = jetstream_activity.get_activity_3hr(conn) 51 + 52 + list.length(buckets) 53 + |> should.equal(12) 54 + } 55 + 56 + pub fn test_6hr_returns_exactly_12_buckets() { 57 + let conn = setup_test_db() 58 + 59 + let assert Ok(buckets) = jetstream_activity.get_activity_6hr(conn) 60 + 61 + list.length(buckets) 62 + |> should.equal(12) 63 + } 64 + 65 + pub fn test_1day_returns_exactly_24_buckets() { 66 + let conn = setup_test_db() 67 + 68 + let assert Ok(buckets) = jetstream_activity.get_activity_1day(conn) 69 + 70 + list.length(buckets) 71 + |> should.equal(24) 72 + } 73 + 74 + pub fn test_7day_returns_exactly_7_buckets() { 75 + let conn = setup_test_db() 76 + 77 + let assert Ok(buckets) = jetstream_activity.get_activity_7day(conn) 78 + 79 + list.length(buckets) 80 + |> should.equal(7) 81 + } 82 + ``` 83 + 84 + **Step 2: Run test to verify it fails** 85 + 86 + Run from `/Users/chadmiller/code/quickslice/server`: 87 + ```bash 88 + gleam test -- --only jetstream_activity_bucket_test 89 + ``` 90 + 91 + Expected: Tests for 1hr, 3hr, 6hr, 1day should FAIL with `left: 13, right: 12` (or similar for 1day: `left: 25, right: 24`). The 7day test may also fail with `left: 6, right: 7`. 92 + 93 + --- 94 + 95 + ### Task 2: Fix the Bucket Count in get_activity_bucketed 96 + 97 + **Files:** 98 + - Modify: `server/src/database/repositories/jetstream_activity.gleam:238-249` 99 + 100 + **Step 1: Fix the WHERE clause** 101 + 102 + Change line 249 from: 103 + ```gleam 104 + WHERE n < " <> max_n <> " 105 + ``` 106 + 107 + To: 108 + ```gleam 109 + WHERE n < " <> int.to_string(expected_buckets - 1) <> " 110 + ``` 111 + 112 + The full function after the fix (lines 232-279): 113 + ```gleam 114 + /// Helper function to get activity bucketed by a specific interval 115 + fn get_activity_bucketed( 116 + conn: sqlight.Connection, 117 + hours: Int, 118 + interval: String, 119 + expected_buckets: Int, 120 + ) -> Result(List(ActivityBucket), sqlight.Error) { 121 + let hours_str = int.to_string(hours) 122 + 123 + // Build the SQL dynamically based on interval 124 + let sql = " 125 + WITH RECURSIVE time_series(bucket, n) AS ( 126 + SELECT datetime('now', '-" <> hours_str <> " hours'), 0 127 + UNION ALL 128 + SELECT datetime(bucket, '+" <> interval <> "'), n + 1 129 + FROM time_series 130 + WHERE n < " <> int.to_string(expected_buckets - 1) <> " 131 + ) 132 + SELECT 133 + strftime('%Y-%m-%dT%H:%M:00Z', ts.bucket) as timestamp, 134 + COALESCE(SUM(CASE WHEN a.operation = 'create' THEN 1 ELSE 0 END), 0) as create_count, 135 + COALESCE(SUM(CASE WHEN a.operation = 'update' THEN 1 ELSE 0 END), 0) as update_count, 136 + COALESCE(SUM(CASE WHEN a.operation = 'delete' THEN 1 ELSE 0 END), 0) as delete_count 137 + FROM time_series ts 138 + LEFT JOIN jetstream_activity a ON 139 + datetime(a.timestamp) >= ts.bucket 140 + AND datetime(a.timestamp) < datetime(ts.bucket, '+" <> interval <> "') 141 + AND a.status = 'success' 142 + GROUP BY ts.bucket 143 + ORDER BY ts.bucket 144 + " 145 + 146 + let decoder = { 147 + use timestamp <- decode.field(0, decode.string) 148 + use create_count <- decode.field(1, decode.int) 149 + use update_count <- decode.field(2, decode.int) 150 + use delete_count <- decode.field(3, decode.int) 151 + decode.success(ActivityBucket( 152 + timestamp:, 153 + create_count:, 154 + update_count:, 155 + delete_count:, 156 + )) 157 + } 158 + 159 + sqlight.query(sql, on: conn, with: [], expecting: decoder) 160 + } 161 + ``` 162 + 163 + Note: We also need to remove the now-unused `max_n` variable from line 240. 164 + 165 + **Step 2: Run tests to verify they pass** 166 + 167 + Run from `/Users/chadmiller/code/quickslice/server`: 168 + ```bash 169 + gleam test -- --only jetstream_activity_bucket_test 170 + ``` 171 + 172 + Expected: Tests for 1hr, 3hr, 6hr, 1day should PASS. 173 + 174 + --- 175 + 176 + ### Task 3: Fix the 7-Day Bucket Count 177 + 178 + **Files:** 179 + - Modify: `server/src/database/repositories/jetstream_activity.gleam:190-214` 180 + 181 + **Step 1: Fix the 7-day WHERE clause** 182 + 183 + The 7-day function has its own SQL. Change line 201 from: 184 + ```gleam 185 + WHERE n <= 5 186 + ``` 187 + 188 + To: 189 + ```gleam 190 + WHERE n < 7 191 + ``` 192 + 193 + This changes the CTE from generating 6 buckets (n=0 through n=5) to 7 buckets (n=0 through n=6). 194 + 195 + **Step 2: Run all bucket tests** 196 + 197 + Run from `/Users/chadmiller/code/quickslice/server`: 198 + ```bash 199 + gleam test -- --only jetstream_activity_bucket_test 200 + ``` 201 + 202 + Expected: All 5 tests PASS. 203 + 204 + --- 205 + 206 + ### Task 4: Run Full Test Suite 207 + 208 + **Step 1: Run all server tests** 209 + 210 + Run from `/Users/chadmiller/code/quickslice/server`: 211 + ```bash 212 + gleam test 213 + ``` 214 + 215 + Expected: All 179 tests PASS. 216 + 217 + --- 218 + 219 + ### Task 5: Manual Verification 220 + 221 + **Step 1: Start the server and verify in browser** 222 + 223 + Run from `/Users/chadmiller/code/quickslice`: 224 + ```bash 225 + make run 226 + ``` 227 + 228 + **Step 2: Check the activity chart** 229 + 230 + Open the client UI and: 231 + 1. Click the "1hr" tab - rightmost bar should now show data (if there's recent activity) 232 + 2. Click the "3hr" tab - verify data appears correctly 233 + 3. Click the "6hr" tab - verify data appears correctly 234 + 4. Click the "1 day" tab - verify data appears correctly 235 + 5. Click the "7 day" tab - verify 7 bars are shown 236 + 237 + --- 238 + 239 + ### Task 6: Commit 240 + 241 + **Step 1: Stage and commit** 242 + 243 + ```bash 244 + git add server/test/jetstream_activity_bucket_test.gleam server/src/database/repositories/jetstream_activity.gleam 245 + git commit -m "fix: correct activity chart bucket count off-by-one error 246 + 247 + The recursive CTE was generating one extra bucket extending into the 248 + future, causing the rightmost bar to always be empty. Fixed by adjusting 249 + the WHERE clause to generate exactly the expected number of buckets. 250 + 251 + Also fixed 7-day view which was generating 6 buckets instead of 7." 252 + ```
+1 -2
server/src/database/repositories/jetstream_activity.gleam
··· 237 237 expected_buckets: Int, 238 238 ) -> Result(List(ActivityBucket), sqlight.Error) { 239 239 let hours_str = int.to_string(hours) 240 - let max_n = int.to_string(expected_buckets) 241 240 242 241 // Build the SQL dynamically based on interval 243 242 let sql = " ··· 246 245 UNION ALL 247 246 SELECT datetime(bucket, '+" <> interval <> "'), n + 1 248 247 FROM time_series 249 - WHERE n < " <> max_n <> " 248 + WHERE n < " <> int.to_string(expected_buckets - 1) <> " 250 249 ) 251 250 SELECT 252 251 strftime('%Y-%m-%dT%H:%M:00Z', ts.bucket) as timestamp,
+61
server/test/jetstream_activity_bucket_test.gleam
··· 1 + import database/repositories/jetstream_activity 2 + import database/schema/tables 3 + import gleam/list 4 + import gleeunit 5 + import gleeunit/should 6 + import sqlight 7 + 8 + pub fn main() { 9 + gleeunit.main() 10 + } 11 + 12 + fn setup_test_db() -> sqlight.Connection { 13 + let assert Ok(conn) = sqlight.open(":memory:") 14 + let assert Ok(_) = tables.create_jetstream_activity_table(conn) 15 + conn 16 + } 17 + 18 + pub fn bucket_1hr_returns_exactly_12_buckets_test() { 19 + let conn = setup_test_db() 20 + 21 + let assert Ok(buckets) = jetstream_activity.get_activity_1hr(conn) 22 + 23 + list.length(buckets) 24 + |> should.equal(12) 25 + } 26 + 27 + pub fn bucket_3hr_returns_exactly_12_buckets_test() { 28 + let conn = setup_test_db() 29 + 30 + let assert Ok(buckets) = jetstream_activity.get_activity_3hr(conn) 31 + 32 + list.length(buckets) 33 + |> should.equal(12) 34 + } 35 + 36 + pub fn bucket_6hr_returns_exactly_12_buckets_test() { 37 + let conn = setup_test_db() 38 + 39 + let assert Ok(buckets) = jetstream_activity.get_activity_6hr(conn) 40 + 41 + list.length(buckets) 42 + |> should.equal(12) 43 + } 44 + 45 + pub fn bucket_1day_returns_exactly_24_buckets_test() { 46 + let conn = setup_test_db() 47 + 48 + let assert Ok(buckets) = jetstream_activity.get_activity_1day(conn) 49 + 50 + list.length(buckets) 51 + |> should.equal(24) 52 + } 53 + 54 + pub fn bucket_7day_returns_exactly_7_buckets_test() { 55 + let conn = setup_test_db() 56 + 57 + let assert Ok(buckets) = jetstream_activity.get_activity_7day(conn) 58 + 59 + list.length(buckets) 60 + |> should.equal(7) 61 + }