feat(analytics): sticky range + sortable per-model table columns - #490
Open
jasnoorgill wants to merge 2 commits into
Open
feat(analytics): sticky range + sortable per-model table columns#490jasnoorgill wants to merge 2 commits into
jasnoorgill wants to merge 2 commits into
Conversation
Remembers the last-selected 24h/7d/30d range across reloads and new sessions so the user lands back on the window they were inspecting. Same localStorage shape as the theme toggle (key: 'theme') and the locale picker (key: 'freellmapi.locale'). Storage key: 'freellmapi.analytics.range'. Invalid or missing values fall back to the 7d default; storage errors (quota, private mode) are swallowed so the page keeps working.
3-state cycle per column: unsorted (API order) → asc → desc → unsorted. Clicking a different column resets to asc on the new column. Last selection persists in localStorage so reloads land on the user's preferred sort (key: 'freellmapi.analytics.byModelSort'). Sortable columns: model, provider, requests, success%, latency, inTokens, outTokens, saved ($). The 'pinned' column stays static because it renders '—' for zero-pinned rows and the unsortable 0/>0 distinction would confuse the indicator. Null values sort last in both directions (Excel/Sheets convention). Same persistence pattern as the range selector (tashfeenahmed#462) and the existing 'theme' / 'freellmapi.locale' keys. Right-aligned columns flip the sort-indicator with flex-row-reverse so the label stays closest to the data. aria-sort attributes set on the buttons for screen readers.
This was referenced Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two small but related client-side improvements to the analytics page,
shipped together because they share infrastructure and would otherwise
conflict on the same lines of
AnalyticsPage.tsx:in
localStorageso reloads and new sessions land on the user'slast choice.
breakdown" table (except
pinned, which renders—) is nowclickable to sort. 3-state cycle:
unsorted (API order) → asc → desc → unsorted. Last selection persists inlocalStoragetoo.Both use the same persistence shape as the existing
themetoggleand
freellmapi.localepreference. SameuseState+useEffectpattern, same try/catch on every storage access.
Why combined
Originally shipped as PR #462 (sticky range only). Adding the sort
work on top of #462's branch required a rebase that would have
re-written #462's commits — at that point the cleanest move was a
single PR covering both. The two features are thematically adjacent
(analytics-page UX) and small individually, so combining avoids
two PRs reviewing the same file.
Rebase onto current
origin/main(9858eee)This is a re-submission of #463 (which was based on a stale
origin/mainsnapshot @
9048dd6). The analytics page changed significantly upstreamsince the original PR — main now uses a
<SegmentedControl>sharedcomponent for the range selector,
TimeRangewidens to include90d,stats load via skeletons, and several i18n keys changed. The
localStorage pattern itself is unchanged.
Two conflicts during cherry-pick, both resolved in favor of keeping
both changes:
useState<TimeRange>('7d')line — kept the PR'sloadStoredRangeinit and added upstream's
const [now]capture alongside it.byModel.map((m, i) => (vs PR'ssortedByModel.map(...)—took the PR's sort-applied version.
Also: added
'90d'to theTIME_RANGESallowlist (the PR pre-datedthat range option; without it, a user who selected 90d would have
their sticky selection silently dropped to
'7d'on the next reload).Changes
client/src/pages/AnalyticsPage.tsxuseMemoto imports,STORAGE_KEY/loadStoredRange/sort types,SortableHeaderhelper component,sortstate + persistence effect,onHeaderClickreducer,sortedByModelmemoized sort, replaced 8 of 9 column headers with<SortableHeader>(pinned stays static), addedlucide-reactArrowUp/ArrowDown/ChevronsUpDown imports.1 file, +189 / -11 across 2 commits.
Sort behavior
key removed.
flex-row-reversesothe label stays closest to the data.
aria-sortattribute is set on each header button for screen readers.pinnedis intentionally not sortable: it renders—for zero-pinnedrows, and the 0/>0 distinction is meaningless to sort on.
localStorage keys
freellmapi.analytics.range'24h'|'7d'|'30d'|'90d'freellmapi.analytics.byModelSort{"column": "...", "direction": "asc"|"desc"}or absentBoth keys are validated against an allowlist on read so a corrupted
entry falls back to the default rather than crashing the page. Both
writes are wrapped in try/catch for SSR / private-mode safety.
Test plan
cd client && npx tsc -b— clean, 0 errors.cd client && npm run build—✓ built in 8.72s.grep -oE 'freellmapi\.analytics\.[a-zA-Z]+' client/dist/assets/index-*.jsreturns both
freellmapi.analytics.rangeandfreellmapi.analytics.byModelSort.Manual smoke (assumes the user runs locally):
/analytics, click 30d. Hard-reload — opens on 30d.shows ↑, header text darkens.
removed from localStorage, the page loads in API order.
Compatibility
No breaking changes. Defaults are unchanged (7d range, API-natural
row order). The persistence is additive. New
localStoragekeysare namespaced under
freellmapi.*per the existing convention.Supersedes
Closes #462 (the sticky-range half of this work is included here).
This PR supersedes #463, which was the original submission based on
a stale
mainsnapshot and is now CLOSED.References operator sessions 2026-07-04 / 2026-07-07. No upstream
issue number yet — happy to file one if requested.