feat(api,ui): wire Download CSV into the Runtime page#6500
Conversation
/runtime was the one Explorer list page with no CSV export, despite rendering a plain newest-first table structurally identical to every sibling feed (blocks, extrinsics, events, sudo, validators, providers, subnets, ...). It is the last unshipped installment of the "wire Download CSV into <page>" series (JSONbored#3403/JSONbored#3404/JSONbored#3408/JSONbored#5562/JSONbored#5665). The frontend alone could not close it: /api/v1/runtime rejected every query param (validateQueryParams(url, [])), so ?format=csv returned 400 before it could reach the handler -- verified against the live route before changing anything. Backend: - handleRuntime accepts the one param, via the same validateEntityQuery(url, ["format"]) idiom its neighbours in this file already use, so ?format=bogus and any unknown param are still rejected. - ?format=csv exports data.transitions with the three columns the page's table renders (Spec Version | Block | Observed -> spec_version, block_number, observed_at), satisfying the issue's "CSV must match the on-screen table". The rollup fields (current_spec_version, coverage_from_block/at) describe the series rather than any row, so they stay JSON-only -- mirroring how chain-signers/chain-fees export their leaderboard and keep totals out of the CSV. A cold store yields transitions: [] -> a header-only CSV, never an error. - The route contract gains csvResponse + the format parameter, so the generated OpenAPI documents both the param and the text/csv response. Frontend: - DownloadCsvButton in runtime.index.tsx's PageHero.actions, ahead of ShareButton, matching every sibling page's ActionBar order. /api/v1/runtime is a single whole-window aggregate with no filters to carry, so the export URL needs no params beyond format -- unlike the filtered feeds, which spread their <page>QueryParams(search). The 6 regression tests fail on the unfixed handler (verified by reverting it), covering the CSV body/headers/filename, the JSON default, ?format=json, the cold header-only CSV, ?format=bogus, and that unknown params are still refused.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6500 +/- ##
=======================================
Coverage 97.85% 97.86%
=======================================
Files 174 174
Lines 22196 22199 +3
Branches 8798 8799 +1
=======================================
+ Hits 21721 21724 +3
Misses 81 81
Partials 394 394
🚀 New features to boost your workflow:
|
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-17 17:30:18 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|

Closes #6392
/runtimewas the one Explorer list page with no CSV export — the last unshipped installment of the "wire Download CSV into " series (#3403/#3404/#3408/#5562/#5665).Screenshots
Download CSV now sits beside Share view, the same slot and order every sibling page's
ActionBaruses. Fixed-viewport captures (never fullPage), theme forced viamg-theme+ reload; before =main, after = this branch.before
after
before
after
before
after
before
after
before
after
before
after
This was not a frontend-only job
The issue asks to "confirm (and add if missing)
format=csvsupport". It was missing —/api/v1/runtimerejected every query param (validateQueryParams(url, [])), so?format=csv400'd before it could reach the handler. Verified against the live route before changing anything:Backend
handleRuntimeaccepts the one param via the samevalidateEntityQuery(url, ["format"])idiom its neighbours in this file already use — so?format=bogusand any unknown param are still rejected (?limit=5→ 400).data.transitionswith the three columns the page's table actually renders (Spec Version | Block | Observed →spec_version,block_number,observed_at), satisfying the issue's "CSV must match the on-screen table's columns". The rollup fields (current_spec_version,coverage_from_block/at) describe the series rather than any row, so they stay JSON-only — mirroring howchain-signers/chain-feesexport their leaderboard and keep totals out of the CSV.transitions: []→ a header-only CSV, never an error.csvResponse+ theformatparameter, so the generated OpenAPI documents both (200now advertisesapplication/jsonandtext/csv).Frontend
DownloadCsvButtoninruntime.index.tsx'sPageHero.actions./api/v1/runtimeis a single whole-window aggregate with no filters to carry, so the export URL needs no params beyondformat— unlike the filtered feeds, which spread their<page>QueryParams(search).Tests
6 new cases in
tests/request-handlers-entities.test.mjs. They are real regression tests — I reverted the handler and confirmed they fail without it: the CSV body/headers/filename, the JSON default (asserting the rollup stays JSON-only),?format=json, the cold header-only CSV,?format=bogus→ 400, and that unknown params are still refused.The Playwright capture asserts the control exists rather than just photographing it:
Derived artifacts (
openapi.json,types.d.ts,api-index.json,packages/contract/index.d.ts) regenerated and verified byte-identical to a fresh build after rebasing onto a main that moved 3 commits mid-work.eslintclean,prettier --checkclean, pushed atbehind: 0.