Public: anyone can read this; the crew can change it.
Observability 2: every panel's data as JSON/CSV, bucketed on the server - charts never stop at 1000 points #3713
Check · Review · Land · Live done — Landed and live in production.
Every board panel's data is now available as JSON or CSV at `GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv`. Charts are bucketed in SQL to the step and are no longer cut off at 1000 points, so `/boards/usage` over 24 h now runs right up to now.
shipped production is serving it deploy f9c1fcc
Every board panel's data is now available as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are bucketed in SQL to the step and are no longer cut off at 1000 points, so /boards/usage over 24 h now runs right up to now.
Cause. runQuery ended every query with LIMIT 1000 and sorted oldest first, so a day of per-minute buckets kept the first 1000 minutes and lost the newest ~7 hours.
What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, 1 s to a year). Each point is rounded down to its bucket in UTC. Minute, hour and day buckets give the same labels as before, so the existing query tests pass unchanged. A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step"). Queries grouped by a column (like by state) still stop at 1000 rows, as before.src/boards/panel_data.ts(new): reads one panel over a window and step.runFormoved here fromweb/pages/boards.ts.
- It parses and checks from, to and step (300, 15s, 5m, 1h, 1d).
- Without a step, a time panel picks the finest standard step that keeps it within 360 buckets: a 24 h board gets 5 minutes (288 buckets), 7 days gets 30 minutes. If an answer is still too large, it tries the next coarser step.
- A step you ask for is kept, or refused if too fine — never truncated. Panels grouped by a column ignore step and report "step": null.
- CSV output is group,value or group,series,value. A text cell starting with =, +, - or @ gets a leading ' so a spreadsheet won't run it as a formula.
src/http/api.ts: the new route, admin people only, like the other boards routes.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, signed out 401.- Board page and home's pinned panels (
boards-view.ts,studio/pinned.ts) read through the same module, so the charts on screen match the API. The pinned-panel cache now includes the step in its key. - Docs:
docs/APPS.mdandREADME.mddescribestep, the new route and the row limits.
Not done: the explore page (/boards/data) has no step field yet; I added no MCP tool for panel data; and buckets with no data are left out, not filled with zeros.
Tests: test/boards-panel-data.test.ts covers the three checks from the issue:
- 24 h of 1-minute data comes back up to the newest minute (1440 rows; the old limit would return 1000).
step=5mover an aligned day returns 288 buckets.- The CSV rows equal the JSON rows.
It also covers the coarser-step retry, refusing a step that is too fine, formula escaping, and the error statuses. The type check is clean, and the boards, pins, home, usage, OpenAPI and auth suites pass (650 tests).
To try it: as an admin, open /boards/usage; the CPU charts should end at the current time. Then fetch /api/v1/boards/usage/panels/0?step=5m and the same address with &format=csv.
🤖 Generated with Claude Code
Every board panel's data is now available as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are bucketed in SQL to the step and are no longer cut off at 1000 points, so /boards/usage over 24 h now runs right up to now.
Cause. runQuery ended every query with LIMIT 1000 and sorted oldest first, so a day of per-minute buckets kept the first 1000 minutes and lost the newest ~7 hours.
What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, 1 s to a year). Each point is rounded down to its bucket in UTC. Minute, hour and day buckets give the same labels as before, so the existing query tests pass unchanged. A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step"). Queries grouped by a column (like by state) still stop at 1000 rows, as before.src/boards/panel_data.ts(new): reads one panel over a window and step.runFormoved here fromweb/pages/boards.ts.
- It parses and checks from, to and step (300, 15s, 5m, 1h, 1d).
- Without a step, a time panel picks the finest standard step that keeps it within 360 buckets: a 24 h board gets 5 minutes (288 buckets), 7 days gets 30 minutes. If an answer is still too large, it tries the next coarser step.
- A step you ask for is kept, or refused if too fine — never truncated. Panels grouped by a column ignore step and report "step": null.
- CSV output is group,value or group,series,value. A text cell starting with =, +, - or @ gets a leading ' so a spreadsheet won't run it as a formula.
src/http/api.ts: the new route, admin people only, like the other boards routes.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, signed out 401.- Board page and home's pinned panels (
boards-view.ts,studio/pinned.ts) read through the same module, so the charts on screen match the API. The pinned-panel cache now includes the step in its key. - Docs:
docs/APPS.mdandREADME.mddescribestep, the new route and the row limits.
Fix for the failed check: the test itself was wrong, not the code. It seeded 1440 per-minute samples, then asked for a window that starts 24 h before the request. When a minute boundary passed in between, the oldest sample fell outside the window and 1439 rows came back. The 1440-minute test and the CSV-matches-JSON test now give an explicit from at the oldest sample, so they no longer depend on the clock.
Not done: the explore page (/boards/data) has no step field yet; I added no MCP tool for panel data; and buckets with no data are left out, not filled with zeros.
Tests: test/boards-panel-data.test.ts covers the three checks from the issue:
- 24 h of 1-minute data comes back up to the newest minute (1440 rows; the old limit would return 1000).
step=5mover an aligned day returns 288 buckets.- The CSV rows equal the JSON rows.
It also covers the coarser-step retry, refusing a step that is too fine, formula escaping, and the error statuses. After the fix, that file passes (13 tests) and the type check is clean. The wider boards, pins, home and API suites passed on the previous push and were not re-run here.
To try it: as an admin, open /boards/usage; the CPU charts should end at the current time. Then fetch /api/v1/boards/usage/panels/0?step=5m and the same address with &format=csv.
🤖 Generated with Claude Code
The server side is good. query.ts now buckets in SQL with a bound step (no SQL built from user text), a time-grouped answer is refused at 20 000 rows instead of being cut, parseWindow validates from, to and step at the boundary, the CSV escapes formulas and quotes cells, the route is admin-only through boardsAdmin, and it is private, no-store. The tests cover the issue's three checks (1440 minutes up to now, 288 buckets at 5m, CSV matches JSON) as well as the refusals. I found no security problems.
It is not done by the direction's bar yet. Done means reachable on the page, through MCP, in a test and by eye. Today the panel data exists only as a REST route:
- Page (fix this first): a person on
/boards/:slughas no way to reach a panel's JSON or CSV. The card head hasexploreand nothing else. Add a plain<a>(muted small, besideexplore,downloadattribute for CSV) to/api/v1/boards/:slug/panels/:i?format=csvfor each panel. It must work with JavaScript off. Add a test that the board page renders that link. - MCP: the
boardstool returns board definitions, not data. Add apanel(index) argument to it, or aboard_paneltool, with the samefrom/to/step. It should go throughpanelData+parseWindow, refuse non-admins and agents exactly asboardsdoes, and have a test. - Terminal:
src/tui/screens/boards.tsstill lists only panel definitions. At least show each time panel's last bucket and its step throughpanelData, or say in the README that the terminal shows definitions only.
I could not reach the preview from the review sandbox (egress to the lab host was denied), so this verdict is from the diff and the tests alone.
Every board panel's data is available as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are now grouped into time buckets in SQL, so they no longer stop at 1000 points, and /boards/usage over 24 h runs right up to now. Following review, the board page, MCP and the terminal now all lead to that data.
Cause. runQuery ended every query with LIMIT 1000 and sorted oldest first. A day of per-minute buckets therefore kept the first 1000 minutes and dropped the newest ~7 hours.
What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, from 1 s to a year), and each point is rounded down to its bucket in UTC.
- Minute, hour and day buckets give the same labels as before.
- A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step").
- Queries grouped by a column still stop at 1000 rows.
src/boards/panel_data.ts(new): reads one panel over a window and a step.runFormoved here fromweb/pages/boards.ts.
- It checks from, to and step (300, 15s, 5m, 1h, 1d).
- Without a step, a time panel picks the finest standard step that keeps it within 360 buckets (24 h gives 5 minutes). If the answer is still too large, it moves up to the next coarser step.
- A step that was asked for is kept, or refused if it is too fine; it is never truncated.
- CSV is group,value or group,series,value. A text cell starting with =, +, - or @ gets a leading ' so a spreadsheet won't run it as a formula.
src/http/api.ts: the route, for admin people only.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, and signed out 401.- Panel cards (from review): each card on the board page and on home's pinned panels links
jsonandcsvbesideexplore. These are plain links, andcsvdownloads a file named<slug>-<n>.csv.drawPanelnow takes the panel's{ slug, index }. - MCP (from review): instead of adding a separate
board_paneltool, the existingboardstool takespanel(with optionalfrom,to,step) next toslug. That reuses its admin-person gate. It returns{board, panel, title, chart, from, to, step, rows}frompanelData, the same code the API uses. - Terminal: the board screen shows each panel's latest reading: a time panel's last bucket and step, another panel's row count, or why it was refused.
- Docs:
docs/APPS.mdandREADME.mddescribestep, the route, the card links, the MCP option and the row limits.
Not done: the explore page (/boards/data) has no step field yet, and buckets with no data are left out rather than filled with zeros.
Tests:
- 24 h of 1-minute data reaches the newest minute (1440 rows).
step=5mover an aligned day returns 288 buckets.- The CSV rows equal the JSON rows.
- The board page links each panel's JSON and CSV.
- MCP
boardswithpanelreturns exactly whatpanelDatareturns, and refuses agents and non-admins. - The terminal shows the last-bucket line.
The type check is clean, and the boards, pins, MCP, TUI, home and studio suites pass.
To try it: as an admin, open /boards/usage; the CPU charts should end at the current time. Click a card's csv, or fetch /api/v1/boards/usage/panels/0?step=5m.
🤖 Generated with Claude Code
Closes #3691
Every board panel's data is available as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are grouped into time buckets in SQL, so they no longer stop at 1000 points, and /boards/usage over 24 h runs right up to now. The board page, MCP and the terminal all lead to that data.
Cause. runQuery ended every query with LIMIT 1000 and sorted oldest first. A day of per-minute buckets therefore kept the first 1000 minutes and dropped the newest ~7 hours.
What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, from 1 s to a year), and each point is rounded down to its bucket in UTC.
- Minute, hour and day buckets give the same labels as before.
- A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step").
- Queries grouped by a column still stop at 1000 rows.
src/boards/panel_data.ts(new): reads one panel over a window and a step.runFormoved here fromweb/pages/boards.ts.
- It checks from, to and step (300, 15s, 5m, 1h, 1d).
- Without a step, a time panel picks the finest standard step that keeps it within 360 buckets (24 h gives 5 minutes). If the answer is still too large, it moves up to the next coarser step.
- A step that was asked for is kept, or refused if it is too fine; it is never truncated.
- CSV is group,value or group,series,value. A text cell starting with =, +, - or @ gets a leading ' so a spreadsheet won't run it as a formula.
src/http/api.ts: the route, for admin people only.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, and signed out 401.- Panel cards: each card on the board page and on home's pinned panels links
jsonandcsvbesideexplore. These are plain links, andcsvdownloads a file named<slug>-<n>.csv.drawPanelnow takes the panel's{ slug, index }. - MCP: instead of adding a separate
board_paneltool as the review suggested, the existingboardstool takespanel(with optionalfrom,to,step) next toslug. That reuses its admin-person gate. It returns{board, panel, title, chart, from, to, step, rows}frompanelData, the same code the API uses. - Terminal: the board screen shows each panel's latest reading: a time panel's last bucket and step, another panel's row count, or why it was refused.
- Docs:
docs/APPS.mdandREADME.mddescribestep, the route, the card links, the MCP option and the row limits.
Test fix: the check failed because the test was wrong, not the code. The "aligned day" window in test/boards-panel-data.test.ts ended at the last 5-minute mark before the newest point. When the clock was 4 minutes past a mark, the window began a minute before the oldest point and returned 287 buckets. It now ends at the first 5-minute mark after the newest point. I checked all five possible minute positions and each gives 288 buckets.
Not done: the explore page (/boards/data) has no step field yet, and buckets with no data are left out rather than filled with zeros.
Tests:
- 24 h of 1-minute data reaches the newest minute (1440 rows).
step=5mover an aligned day returns 288 buckets.- The CSV rows equal the JSON rows.
- The board page links each panel's JSON and CSV.
- MCP
boardswithpanelreturns exactly whatpanelDatareturns, and refuses agents and non-admins. - The terminal shows the last-bucket line.
After the test fix I re-ran only the panel-data suite, which passes. Before it, the type check was clean and the boards, pins, MCP, TUI, home and studio suites passed.
To try it: as an admin, open /boards/usage; the CPU charts should end at the current time. Click a card's csv, or fetch /api/v1/boards/usage/panels/0?step=5m.
🤖 Generated with Claude Code
Closes #3691
The data layer is solid. step is bound as a SQL parameter and checked as an integer from 1 s to a year. parseWindow checks from/to/step at the edge. The new route uses the same admin-person gate as the other boards routes (boardsAdmin, showBoard → isSlug), so the slug in Content-Disposition is safe. The CSV blocks formula injection and quotes cells correctly. The MCP boards tool and the terminal's board screen both read through the same panelData, and the terminal escapes what it prints. Tests cover the issue's three checks, the coarser-step retry, the refusals and the access gates. I could not reach the preview or the screenshots from my sandbox, so this verdict is based on the code.
Fix first (design system, high): the new json and csv links break docs/DESIGN.md's density rule: "anything touchable is ≥44px under @media (pointer: coarse) — via padding". On a phone, each card head now has three 13px inline links a dot apart (explore · json · csv), each about 17px tall, inside a hand-rolled <span class="muted small">. On a thumb, json and csv are a mis-tap away from each other. Give the card head's links one class, for example .card-links > a, with display:inline-flex; align-items:center; min-height:44px; padding:0 8px under (pointer: coarse) in style/components.ts, the same way .topbar-tools > a.small does it. Or move json/csv into the card's .card-foot. Add a test that the card head carries that class.
Medium: on a board whose range is all, and whose panel names no from, panelData never fits the step to the span. It runs at the bucket's own width, so a minute panel can now draw up to 20000 SVG points on the board page and on home, where the old cap was 1000. Each retry after tooMany is another full scan, up to a dozen per panel per render. Base the span on the oldest row (SELECT min(time)), or treat all as a fixed span, so that stepFor applies and the page stays near 360 points.
Every board panel's data is available as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are grouped into time buckets in SQL, so they no longer stop at 1000 points, and /boards/usage over 24 h runs right up to now. The board page, MCP and the terminal all lead to that data.
Cause. runQuery ended every query with LIMIT 1000 and sorted oldest first. A day of per-minute buckets therefore kept the first 1000 minutes and dropped the newest ~7 hours.
What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, from 1 s to a year), and each point is rounded down to its bucket in UTC.
- Minute, hour and day buckets give the same labels as before.
- A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step").
- Queries grouped by a column still stop at 1000 rows.
- The source, scope, window and filter checks moved out of runQuery into rowsOf. The new oldestTime uses them to read MIN(time) over the same rows the query reads.
src/boards/panel_data.ts(new): reads one panel over a window and a step.runFormoved here fromweb/pages/boards.ts.
- It checks from, to and step (300, 15s, 5m, 1h, 1d).
- Without a step, a time panel picks the finest standard step that keeps it within 360 buckets (24 h gives 5 minutes). If the answer is still too large, it moves up to the next coarser step.
- When the window has no start (a board whose range is all), the span runs from the panel's oldest row, so the step still fits about 360 points. Before, such a panel ran at its own bucket width, up to 20 000 points, with a full re-scan for each coarser step it tried.
- A step that was asked for is kept, or refused if it is too fine; it is never truncated.
- CSV is group,value or group,series,value. A text cell starting with =, +, - or @ gets a leading ' so a spreadsheet won't run it as a formula.
src/http/api.ts: the route, for admin people only.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, and signed out 401.- Panel cards: each card on the board page and on home's pinned panels links
explore,jsonandcsv. Thecsvlink downloads a file named<slug>-<n>.csv.
- The three links sit in one .card-links class, defined in style/components.ts and named in docs/DESIGN.md §5.
- Under (pointer: coarse) each link is a 44 px target, as .topbar-tools > a.small is. They stay in the card head rather than moving to .card-foot.
- MCP: instead of adding a separate
board_paneltool, the existingboardstool takespanel(with optionalfrom,to,step) next toslug. That reuses its admin-person gate. It returns{board, panel, title, chart, from, to, step, rows}frompanelData, the same code the API uses. - Terminal: the board screen shows each panel's latest reading: a time panel's last bucket and step, another panel's row count, or why it was refused.
- Docs:
docs/APPS.mdandREADME.mddescribestep, the route, the card links, the MCP option, the row limits and how anallboard picks its step.
Not done: the explore page (/boards/data) has no step field yet, and buckets with no data are left out rather than filled with zeros.
Tests (test/boards-panel-data.test.ts unless noted):
- 24 h of 1-minute data reaches the newest minute (1440 rows).
step=5mover an aligned day returns 288 buckets. An earlier version of this test depended on the clock and failed one minute in five. That was the test's fault, not the code's; it now holds on any minute.- The CSV rows equal the JSON rows.
- An
allboard picks a 5-minute step for a day of data. This test fails without the fix, which stayed at the 60 s bucket. oldestTimerespects the query's filters and window, and returns nothing for no rows, a refused query or an empty scope.- The card head carries
.card-links, and the stylesheet gives it the 44 px coarse-pointer rule. - MCP
boardswithpanelreturns exactly whatpanelDatareturns, and refuses agents and non-admins (test/mcp.test.ts). - The terminal shows each panel's latest reading (
test/tui-boards.test.ts).
The type check is clean, and the boards, pins, MCP, TUI, home, components, usage and design-check suites pass.
To try it: as an admin, open /boards/usage; the CPU charts should end at the current time. On a phone, each of explore, json and csv should be a full-height tap target. Then fetch /api/v1/boards/usage/panels/0?step=5m, or click a card's csv.
🤖 Generated with Claude Code
Closes #3691
Every board panel's data is available as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are grouped into time buckets in SQL, so they no longer stop at 1000 points, and /boards/usage over 24 h runs right up to now. The board page, MCP and the terminal all lead to that data.
Cause. runQuery ended every query with LIMIT 1000 and sorted oldest first. A day of per-minute buckets therefore kept the first 1000 minutes and dropped the newest ~7 hours.
What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, from 1 s to a year), and each point is rounded down to its bucket in UTC.
- Minute, hour and day buckets give the same labels as before.
- A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step").
- Queries grouped by a column still stop at 1000 rows.
- The source, scope, window and filter checks moved out of runQuery into rowsOf. The new oldestTime uses them to read MIN(time) over the same rows the query reads.
src/boards/panel_data.ts(new): reads one panel over a window and a step.runFormoved here fromweb/pages/boards.ts.
- It checks from, to and step (300, 15s, 5m, 1h, 1d).
- Without a step, a time panel picks the finest standard step that keeps it within 360 buckets (24 h gives 5 minutes). If the answer is still too large, it moves up to the next coarser step.
- When the window has no start (a board whose range is all), the span runs from the panel's oldest row, so the step still fits about 360 points.
- A step that was asked for is kept, or refused if it is too fine; it is never truncated.
- CSV is group,value or group,series,value. A text cell starting with =, +, - or @ gets a leading ' so a spreadsheet won't run it as a formula.
src/http/api.ts: the route, for admin people only.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, and signed out 401.- Panel cards: each card on the board page and on home's pinned panels links
explore,jsonandcsv. Thecsvlink downloads a file named<slug>-<n>.csv.
- The three links sit in one .card-links class, defined in style/components.ts and named in docs/DESIGN.md §5.
- Under (pointer: coarse) each link is a 44 px target.
- MCP: instead of adding a separate
board_paneltool, the existingboardstool takespanel(with optionalfrom,to,step) next toslug. That reuses its admin-person gate. It returns{board, panel, title, chart, from, to, step, rows}frompanelData, the same code the API uses. - Terminal: the board screen shows each panel's latest reading: a time panel's last bucket and step, another panel's row count, or why it was refused.
- Docs:
docs/APPS.mdandREADME.mddescribestep, the route, the card links, the MCP option, the row limits and how anallboard picks its step. - Merge with
main: the only conflict wasdocs/DESIGN.md§5. This branch added.card-linksto the card row, andmainaddedfoldto the list row on the line below; both are kept.
Not done: the explore page (/boards/data) has no step field yet, and buckets with no data are left out rather than filled with zeros.
Tests (test/boards-panel-data.test.ts unless noted):
- 24 h of 1-minute data reaches the newest minute (1440 rows).
step=5mover an aligned day returns 288 buckets on any minute of the clock.- The CSV rows equal the JSON rows.
- An
allboard picks a 5-minute step for a day of data, not its 60 s bucket. oldestTimerespects the query's filters and window, and returns nothing for no rows, a refused query or an empty scope.- The card head carries
.card-links, and the stylesheet gives it the 44 px coarse-pointer rule. - MCP
boardswithpanelreturns exactly whatpanelDatareturns, and refuses agents and non-admins (test/mcp.test.ts). - The terminal shows each panel's latest reading (
test/tui-boards.test.ts).
After the merge, the type check is clean and the design-check, boards, MCP, components and TUI-boards suites pass. The home, pins and usage suites last passed before the merge.
To try it: as an admin, open /boards/usage; the CPU charts should end at the current time. On a phone, each of explore, json and csv should be a full-height tap target. Then fetch /api/v1/boards/usage/panels/0?step=5m, or click a card's csv.
🤖 Generated with Claude Code
Closes #3691
Approved. Every point in #3691 is met and tested.
- Data reaches now: a time-grouped query is bucketed in SQL and never truncated. Past 20 000 rows it is refused with
tooMany, andpanelDataretries at the next coarser step. A step the caller asked for is kept or refused, never cut. - Issue tests: 1440 per-minute points reach the newest minute;
step=5mover an aligned day gives 288 buckets; CSV equals JSON. All three are intest/boards-panel-data.test.ts. - Security: the route reuses
boardsAdmin(admin people only; agents get 403). Identity comes from the session, bearer token or edge header thatresolveAuthalready verifies, so the card links work for a signed-in browser.:idis checked against^\d{1,2}$.from/toare capped at 64 characters and parsed; the step is bounded. The step is a bound parameter and no SQL is built from user text. The slug incontent-dispositionbelongs to an existing board, so it already matched the slug format. CSV cells starting with= + - @ \t \rare neutralised and quoted. - Every door leads to the data:
- board page and home's pinned cards: plain json and csv links that work with JavaScript off;
- MCP: boards takes slug plus panel, behind the same admin-person gate;
- terminal: the board screen shows each panel's last bucket and step;
- README and docs/APPS.md describe it.
- Design system:
.card-linksis a named part of the card in DESIGN.md §5, adds no new accent, and gives a 44 px target under a coarse pointer.
I ran these at ba9db42: the panel-data, tui-boards and MCP boards tests, plus the board-pins, boards-page, boards-query, boards-machine, boards-starting, boards-data-timing and design-audit suites. 110 tests, all green.
I did not check the preview by eye: my sandbox refuses outbound connections to the preview host.
For later, not blocking: the explore page has no step field yet, and empty buckets are left out rather than filled with zero. The pull says both.
Every board panel's data can now be fetched as JSON or CSV at GET /api/v1/boards/:slug/panels/:id?from=&to=&step=. Charts are bucketed in SQL to the requested step, so they no longer stop at 1000 rows. A 24-hour panel of per-minute data now runs right up to the present, where before it ended about 7 hours early.
- Queries (
3b801290): board queries take a step and bucket in SQL. If a time-grouped query would still be too large, it is refused rather than cut off. - Endpoint (
ee27c889,f6ee2c00): the panel picks the step unless the request names one. CSV holds the same rows as JSON. The tests use a fixed time window and check three things: 24 hours of 1-minute data reaches the newest point,step=5mgives 288 buckets, and CSV matches JSON. - Panels with no start (
82c1dc8e): an "all time" panel sizes its step from its oldest row, so it stays near 360 points. - Cards (
81147e21,be0ff9ee): each panel card links its JSON and a CSV download next to explore. The links share.card-links, and each is a 44 px target on touch screens. - MCP and terminal (
0203c606,695c76af,3756c7a3): the MCPboardstool with a slug and a panel returns that panel's data throughpanelData. The terminal board screen shows each panel's last bucket and step, and the docs list the new endpoint.
This push only merges four new commits from main. It merged cleanly, and nothing in #3691 changed. On the merged tree the type check passes, and the three test files this branch changes pass (54 tests). The issue still has the Later label, and the automatic triage note says it waits until someone names a pillar for it; that is the owner's call, so I left it alone.
To try it:
curl -H "authorization: Bearer $TOKEN" "$URL/api/v1/boards/usage/panels/<id>?step=5m"
Add format=csv, or use the CSV link on a panel card.
🤖 Generated with Claude Code
Approved. This does what #3691 asks. Time-grouped queries are bucketed in SQL to a step that is bound as a parameter, never spliced into the SQL. They are never truncated: past 20 000 rows the query is refused, and an auto-picked step steps coarser. Each panel has a door at GET /api/v1/boards/:slug/panels/:id with JSON and CSV, and the tests cover the issue's three checks: 1440 minutes up to now, 288 buckets at 5m, and CSV equal to JSON.
Security: the route sits behind boardsAdmin like its siblings, and the tests show 401, 403 and agent refusal. :id is capped to two digits. from, to and step are validated, with a 64-char date cap, and the step must be 1 s to a year. The CSV escapes formula cells. Content-Disposition uses a slug that showBoard already resolved. Responses are private, no-store.
Both doors: MCP boards takes panel, from, to and step and goes through the same panelData, with the same gate. The terminal board screen says each panel's last bucket, step and row count, and the error text is passed through plain().
Design: the card head's explore, json and csv are plain <a> links that work without JavaScript. .card-links is added to DESIGN.md's card row, has a 44 px coarse-pointer target, and adds no new accent. Pinned cards get the right index because listPinned returns the board's own panel object.
What I ran: on the PR head, bun test over the boards*, panel-data and tui-boards files passes (151 tests), and mcp.test.ts passes (34). The preview could not be reached from my sandbox, so I judged it from the code and the tests.
Not blocking: on an all board every render now runs one extra MIN(time) scan per time panel. That is fine at today's sizes; worth caching with the panel if boards grow.
Every board panel's data is now available as JSON or CSV at
GET /api/v1/boards/:slug/panels/:id?from=&to=&step=&format=csv. Charts are bucketed in SQL to the step and are no longer cut off at 1000 points, so/boards/usageover 24 h now runs right up to now.Cause.
runQueryended every query withLIMIT 1000and sorted oldest first, so a day of per-minute buckets kept the first 1000 minutes and lost the newest ~7 hours.What changed
src/boards/query.ts: a query grouped by a time bucket can take astep(seconds, 1 s to a year). Each point is rounded down to its bucket in UTC. Minute, hour and day buckets give the same labels as before, so the existing query tests pass unchanged. A time-grouped answer is never cut short: past 20 000 rows it is refused (tooMany, "ask for a coarser step"). Queries grouped by a column (like by state) still stop at 1000 rows, as before.src/boards/panel_data.ts(new): reads one panel over a window and step.runFormoved here fromweb/pages/boards.ts.- It parses and checks
from,toandstep(300,15s,5m,1h,1d). - Without a step, a time panel picks the finest standard step that keeps it within 360 buckets: a 24 h board gets 5 minutes (288 buckets), 7 days gets 30 minutes. If an answer is still too large, it tries the next coarser step. - A step you ask for is kept, or refused if too fine — never truncated. Panels grouped by a column ignorestepand report"step": null. - CSV output isgroup,valueorgroup,series,value. A text cell starting with=,+,-or@gets a leading'so a spreadsheet won't run it as a formula.src/http/api.ts: the new route, admin people only, like the other boards routes.:idis the panel's position on the board, counting from 0. A bad window or format gets 422, a missing board or panel 404, a non-admin 403, signed out 401.boards-view.ts,studio/pinned.ts) read through the same module, so the charts on screen match the API. The pinned-panel cache now includes the step in its key.docs/APPS.mdandREADME.mddescribestep, the new route and the row limits.Not done: the explore page (
/boards/data) has no step field yet; I added no MCP tool for panel data; and buckets with no data are left out, not filled with zeros.Tests:
test/boards-panel-data.test.tscovers the three checks from the issue:step=5mover an aligned day returns 288 buckets.It also covers the coarser-step retry, refusing a step that is too fine, formula escaping, and the error statuses. The type check is clean, and the boards, pins, home, usage, OpenAPI and auth suites pass (650 tests).
To try it: as an admin, open
/boards/usage; the CPU charts should end at the current time. Then fetch/api/v1/boards/usage/panels/0?step=5mand the same address with&format=csv.🤖 Generated with Claude Code
Closes #3691