feat(frontend): per-device usage breakdown on profile and settings - #702
Merged
Conversation
Surface the per-device aggregates that already exist server-side:
- Public profile (/u/[username]) gains a "Devices" section rendered from
GET /api/users/[username]/devices, fetched server-side in page.tsx in
parallel with the existing profile fetch. Hidden entirely when a user
has no recorded devices; tolerates fetch failure by omitting the
section instead of failing the page.
- Settings gains a "Devices" section listing each device with a usage
summary (tokens, cost, active days, last submit) and an inline rename
flow wired to PATCH /api/settings/devices/[deviceId]. Client-side
validation mirrors the server's RenameBodySchema (<=120 chars, no
control characters); empty input clears the custom name back to the
deviceDisplayLabel fallback.
- New formatRelativeTime helper in lib/format.ts (with tests) for the
"last submit" timestamps on both surfaces.
Constraint: No new API routes; settings reuses the public devices
endpoint with the session user's username since /api/me/stats is
bearer-token-only (CLI) and unusable from a cookie session.
Constraint: Public devices endpoint returns the fallback display label,
not the raw null name, so the rename input detects fallback labels and
pre-fills empty instead.
Rejected: GET /api/settings/devices listing route | public endpoint
already returns everything the settings UI needs.
Rejected: date-fns formatDistanceToNow for relative time | verbose
output ("about 3 hours ago") and untestable without injection; a 20-line
helper with an injectable clock fits the existing lib/format.ts pattern.
Confidence: high
Scope-risk: narrow
Not-tested: hydration of relative timestamps when the 60s ISR cache
serves a stale page (suppressHydrationWarning covers the text node).
Replace the profile page's hardcoded #10121C background with var(--color-bg-default) (same value, token form) and the settings token icon's stale #737373 neutral gray with var(--color-fg-muted), matching the design tokens both pages already use everywhere else. Deliberately not changed: the landing page uses its own bespoke navy palette (#01070f / #10233e / #0073ff accent) that is disjoint from the globals.css token system the app pages share; retheming profile and settings onto it would be a site-wide restructure, not a surgical fix. Confidence: high Scope-risk: narrow
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b03aa70a9f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Contributor
There was a problem hiding this comment.
2 issues found across 7 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Three review findings on the device breakdown UI: 1. Stale rename on public profile: the profile page's devices fetch now carries the same `user:<lowercased-username>` cache tag the rename PATCH revalidates, and revalidateUsernamePaths additionally flushes /api/users/[username]/devices (called from the PATCH route too) so a rename is visible immediately instead of after the 60s ISR window. 2. Style duplication: the card/header/row/metric-cell styled primitives shared by ProfileModels and ProfileDevices now live in components/profile/listStyles.ts; both tables consume them so the layouts cannot drift. Rendered output is unchanged. 3. Rename prefill clearing legitimate names: the public devices API now exposes the raw nullable `customName` alongside the resolved `displayName`, and the settings rename input prefills from `customName ?? ""` instead of comparing the resolved label against the fallback string. Constraint: dedup must keep profile tables pixel-identical Rejected: sharing MetricText/CostText too | ProfileModels has an extra 390px breakpoint the devices table lacks Rejected: styled(ListMetricCell).attrs for fixed widths | styled-components v6 attrs typing rejects omitted required props Confidence: high Scope-risk: narrow Not-tested: end-to-end ISR invalidation on Vercel (verified via unit-level revalidate assertions only)
pinion05
added a commit
to pinion05/tokscale
that referenced
this pull request
Jun 23, 2026
…unhoyeo#702) * feat(frontend): show per-device usage on profile and settings Surface the per-device aggregates that already exist server-side: - Public profile (/u/[username]) gains a "Devices" section rendered from GET /api/users/[username]/devices, fetched server-side in page.tsx in parallel with the existing profile fetch. Hidden entirely when a user has no recorded devices; tolerates fetch failure by omitting the section instead of failing the page. - Settings gains a "Devices" section listing each device with a usage summary (tokens, cost, active days, last submit) and an inline rename flow wired to PATCH /api/settings/devices/[deviceId]. Client-side validation mirrors the server's RenameBodySchema (<=120 chars, no control characters); empty input clears the custom name back to the deviceDisplayLabel fallback. - New formatRelativeTime helper in lib/format.ts (with tests) for the "last submit" timestamps on both surfaces. Constraint: No new API routes; settings reuses the public devices endpoint with the session user's username since /api/me/stats is bearer-token-only (CLI) and unusable from a cookie session. Constraint: Public devices endpoint returns the fallback display label, not the raw null name, so the rename input detects fallback labels and pre-fills empty instead. Rejected: GET /api/settings/devices listing route | public endpoint already returns everything the settings UI needs. Rejected: date-fns formatDistanceToNow for relative time | verbose output ("about 3 hours ago") and untestable without injection; a 20-line helper with an injectable clock fits the existing lib/format.ts pattern. Confidence: high Scope-risk: narrow Not-tested: hydration of relative timestamps when the 60s ISR cache serves a stale page (suppressHydrationWarning covers the text node). * style(frontend): tokenize stray hardcoded colors on profile and settings Replace the profile page's hardcoded #10121C background with var(--color-bg-default) (same value, token form) and the settings token icon's stale #737373 neutral gray with var(--color-fg-muted), matching the design tokens both pages already use everywhere else. Deliberately not changed: the landing page uses its own bespoke navy palette (#01070f / #10233e / #0073ff accent) that is disjoint from the globals.css token system the app pages share; retheming profile and settings onto it would be a site-wide restructure, not a surgical fix. Confidence: high Scope-risk: narrow * fix(frontend): address device UI review findings Three review findings on the device breakdown UI: 1. Stale rename on public profile: the profile page's devices fetch now carries the same `user:<lowercased-username>` cache tag the rename PATCH revalidates, and revalidateUsernamePaths additionally flushes /api/users/[username]/devices (called from the PATCH route too) so a rename is visible immediately instead of after the 60s ISR window. 2. Style duplication: the card/header/row/metric-cell styled primitives shared by ProfileModels and ProfileDevices now live in components/profile/listStyles.ts; both tables consume them so the layouts cannot drift. Rendered output is unchanged. 3. Rename prefill clearing legitimate names: the public devices API now exposes the raw nullable `customName` alongside the resolved `displayName`, and the settings rename input prefills from `customName ?? ""` instead of comparing the resolved label against the fallback string. Constraint: dedup must keep profile tables pixel-identical Rejected: sharing MetricText/CostText too | ProfileModels has an extra 390px breakpoint the devices table lacks Rejected: styled(ListMetricCell).attrs for fixed widths | styled-components v6 attrs typing rejects omitted required props Confidence: high Scope-risk: narrow Not-tested: end-to-end ISR invalidation on Vercel (verified via unit-level revalidate assertions only)
t1000040
pushed a commit
to tmobi-internal/tokscale
that referenced
this pull request
Jun 30, 2026
…unhoyeo#702) * feat(frontend): show per-device usage on profile and settings Surface the per-device aggregates that already exist server-side: - Public profile (/u/[username]) gains a "Devices" section rendered from GET /api/users/[username]/devices, fetched server-side in page.tsx in parallel with the existing profile fetch. Hidden entirely when a user has no recorded devices; tolerates fetch failure by omitting the section instead of failing the page. - Settings gains a "Devices" section listing each device with a usage summary (tokens, cost, active days, last submit) and an inline rename flow wired to PATCH /api/settings/devices/[deviceId]. Client-side validation mirrors the server's RenameBodySchema (<=120 chars, no control characters); empty input clears the custom name back to the deviceDisplayLabel fallback. - New formatRelativeTime helper in lib/format.ts (with tests) for the "last submit" timestamps on both surfaces. Constraint: No new API routes; settings reuses the public devices endpoint with the session user's username since /api/me/stats is bearer-token-only (CLI) and unusable from a cookie session. Constraint: Public devices endpoint returns the fallback display label, not the raw null name, so the rename input detects fallback labels and pre-fills empty instead. Rejected: GET /api/settings/devices listing route | public endpoint already returns everything the settings UI needs. Rejected: date-fns formatDistanceToNow for relative time | verbose output ("about 3 hours ago") and untestable without injection; a 20-line helper with an injectable clock fits the existing lib/format.ts pattern. Confidence: high Scope-risk: narrow Not-tested: hydration of relative timestamps when the 60s ISR cache serves a stale page (suppressHydrationWarning covers the text node). * style(frontend): tokenize stray hardcoded colors on profile and settings Replace the profile page's hardcoded #10121C background with var(--color-bg-default) (same value, token form) and the settings token icon's stale #737373 neutral gray with var(--color-fg-muted), matching the design tokens both pages already use everywhere else. Deliberately not changed: the landing page uses its own bespoke navy palette (#01070f / #10233e / #0073ff accent) that is disjoint from the globals.css token system the app pages share; retheming profile and settings onto it would be a site-wide restructure, not a surgical fix. Confidence: high Scope-risk: narrow * fix(frontend): address device UI review findings Three review findings on the device breakdown UI: 1. Stale rename on public profile: the profile page's devices fetch now carries the same `user:<lowercased-username>` cache tag the rename PATCH revalidates, and revalidateUsernamePaths additionally flushes /api/users/[username]/devices (called from the PATCH route too) so a rename is visible immediately instead of after the 60s ISR window. 2. Style duplication: the card/header/row/metric-cell styled primitives shared by ProfileModels and ProfileDevices now live in components/profile/listStyles.ts; both tables consume them so the layouts cannot drift. Rendered output is unchanged. 3. Rename prefill clearing legitimate names: the public devices API now exposes the raw nullable `customName` alongside the resolved `displayName`, and the settings rename input prefills from `customName ?? ""` instead of comparing the resolved label against the fallback string. Constraint: dedup must keep profile tables pixel-identical Rejected: sharing MetricText/CostText too | ProfileModels has an extra 390px breakpoint the devices table lacks Rejected: styled(ListMetricCell).attrs for fixed widths | styled-components v6 attrs typing rejects omitted required props Confidence: high Scope-risk: narrow Not-tested: end-to-end ISR invalidation on Vercel (verified via unit-level revalidate assertions only)
junhoyeo
added a commit
that referenced
this pull request
Jul 6, 2026
Resolves the single conflicted file, packages/frontend/src/app/settings/SettingsClient.tsx: 5 hunks, all pure calendar-drift interleaves between this PR's Danger Zone blocks (styled-components, DangerConfirmationModal, dangerAction state, handleDangerSuccess, <DangerSection> JSX) and main's #702 Devices blocks (CreatedApiToken/SettingsDevice types, token/device state, device-rename handlers, <Section>Devices</Section> JSX). Both sides kept in every hunk; no shared logic reconciled. Deliberate ordering: the Devices section renders before the Danger Zone so destructive actions sit at the bottom of the Settings page, rather than accepting the merge-tool default order at the </MainContent> insertion point. Constraint: No shared line was edited by both sides; resolution is keep-both Rejected: Rebase onto main | 30+ commits to replay, merge keeps PR history reviewable Confidence: high Scope-risk: narrow
This was referenced Jul 6, 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
Surfaces the per-device usage data that already exists server-side on two pages:
Profile (
/u/[username])GET /api/users/[username]/devices.page.tsx(internal fetch withrevalidate: 60, inPromise.allwith the existing profile fetch) — same pattern the page already uses for its core data. A devices fetch failure omits the section instead of failing the page.ProfileDevicescomponent (src/components/profile/ProfileDevices.tsx) styled to match the existingProfileModelstable card (same radius/border/header tokens). Shows device name, total tokens (formatNumber), cost (formatCurrency), active days, and relative last-submit time.legacy-defaultnaming handled by the API viadeviceDisplayLabel.Settings (
/settings)SettingsClientlisting each device with a usage summary and an inline rename flow (Rename → input + Save/Cancel, Enter/Escape shortcuts) wired toPATCH /api/settings/devices/[deviceId].GET /api/users/[username]/deviceswith the session user's username — no new API routes./api/me/statswas considered but is bearer-token-only (CLI), unusable from a cookie session, and lacks per-device usage totals.RenameBodySchema: ≤120 chars, no Unicode control characters; empty input clears the name back to the fallback label. Since the public endpoint returns the fallback label (not the rawnull), the edit input detects fallback labels and pre-fills empty.deviceDisplayLabel; errors render through the section's existingErrorTextpattern.Shared
formatRelativeTimeinsrc/lib/format.ts(injectable clock, "just now" / "5m ago" / "3h ago" / "12d ago" / "2mo ago" / "1y ago", "never" for null) + unit tests following theformatTokenCount.test.tspattern.Palette consistency check (vs landing)
src/components/landing/) uses its own bespoke hardcoded navy palette (#01070fbg,#10233eborders,#0073ffaccent) and does not consume theglobals.cssdesign tokens. Profile and settings consistently use the token system (--color-bg-default,--color-border-default,--color-fg-*, etc.), and the new device sections use those same tokens from the start.#10121Cbackground →var(--color-bg-default)(same value, token form); settings key-icon#737373stale neutral gray →var(--color-fg-muted).ProfileHeaderuses a one-off#141A21card background that matches no token; settings danger red#F85149has no danger token inglobals.css.Screenshots
N/A — authored in a headless environment; no DB/session available for a live render.
Test evidence
bun run test: 52 files, 425 tests passed (includes 8 newformatRelativeTimetests).bunx tsc --noEmit: only the 3 pre-existing errors (groupMemberRoleRoute.test.ts×2,BlackholeHero.tsx) — zero new.bun run build: compiles; route table includes/u/[username]and/settings.bun run lint: identical findings to cleanorigin/main(1 pre-existing error inViewSelector.tsx, 9 pre-existing warnings) — zero new.Refs #593 (devices API), #329 (/api/me/stats)
Summary by cubic
Adds a per‑device usage breakdown to the public profile and settings pages, plus an inline device rename flow. Renames now update the profile immediately via tagged fetches and path revalidation.
New Features
GET /api/users/[username]/devices, fetched server‑side inpage.tsxwithrevalidate: 60and cache taguser:<normalized-username>. Shows name, tokens, cost, active days, and last submit; hidden on 0 devices; fetch failures omit the section.PATCH /api/settings/devices/[deviceId]. Reuses the public devices endpoint; client validation: ≤120 chars, no control chars; empty clears the name. Prefills fromcustomNameand updates from the PATCH response.formatRelativeTimehelper (“just now”, “5m ago”, … “1y ago”) with tests.Refactors
displayName(fallback‑resolved) andcustomName(raw nullable) to support correct rename prefill. Profile devices fetch is tagged andrevalidateUsernamePathsnow also flushes/api/users/[username]/devicesso renames appear instantly.components/profile/listStyles.tsand adopted in both Models and Devices. Also replaced stray hardcoded colors with tokens.Written for commit 17bb534. Summary will update on new commits.