Skip to content

feat(frontend): per-device usage breakdown on profile and settings - #702

Merged
junhoyeo merged 3 commits into
mainfrom
feat/device-breakdown-ui
Jun 10, 2026
Merged

feat(frontend): per-device usage breakdown on profile and settings#702
junhoyeo merged 3 commits into
mainfrom
feat/device-breakdown-ui

Conversation

@junhoyeo

@junhoyeo junhoyeo commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Surfaces the per-device usage data that already exists server-side on two pages:

Profile (/u/[username])

  • New Devices section below the tab panels, rendered from GET /api/users/[username]/devices.
  • Fetched server-side in page.tsx (internal fetch with revalidate: 60, in Promise.all with 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.
  • New ProfileDevices component (src/components/profile/ProfileDevices.tsx) styled to match the existing ProfileModels table card (same radius/border/header tokens). Shows device name, total tokens (formatNumber), cost (formatCurrency), active days, and relative last-submit time.
  • 0 devices → section hidden entirely; 1 device still renders; legacy-default naming handled by the API via deviceDisplayLabel.

Settings (/settings)

  • New Devices section in SettingsClient listing each device with a usage summary and an inline rename flow (Rename → input + Save/Cancel, Enter/Escape shortcuts) wired to PATCH /api/settings/devices/[deviceId].
  • Data source: reuses the public GET /api/users/[username]/devices with the session user's username — no new API routes. /api/me/stats was considered but is bearer-token-only (CLI), unusable from a cookie session, and lacks per-device usage totals.
  • Client-side validation mirrors the server's 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 raw null), the edit input detects fallback labels and pre-fills empty.
  • On success the list updates locally from the PATCH response via deviceDisplayLabel; errors render through the section's existing ErrorText pattern.

Shared

  • New formatRelativeTime in src/lib/format.ts (injectable clock, "just now" / "5m ago" / "3h ago" / "12d ago" / "2mo ago" / "1y ago", "never" for null) + unit tests following the formatTokenCount.test.ts pattern.

Palette consistency check (vs landing)

  • The landing page (src/components/landing/) uses its own bespoke hardcoded navy palette (#01070f bg, #10233e borders, #0073ff accent) and does not consume the globals.css design 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.
  • Surgical fixes in this PR (separate commit): profile page's hardcoded #10121C background → var(--color-bg-default) (same value, token form); settings key-icon #737373 stale neutral gray → var(--color-fg-muted).
  • Deliberately not fixed (reported instead): retheming profile/settings onto the landing's bespoke navy would be a site-wide restructure; ProfileHeader uses a one-off #141A21 card background that matches no token; settings danger red #F85149 has no danger token in globals.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 new formatRelativeTime tests).
  • 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 clean origin/main (1 pre-existing error in ViewSelector.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

    • Profile: new Devices section under the tabs from GET /api/users/[username]/devices, fetched server‑side in page.tsx with revalidate: 60 and cache tag user:<normalized-username>. Shows name, tokens, cost, active days, and last submit; hidden on 0 devices; fetch failures omit the section.
    • Settings: Devices list with usage summary and Rename via PATCH /api/settings/devices/[deviceId]. Reuses the public devices endpoint; client validation: ≤120 chars, no control chars; empty clears the name. Prefills from customName and updates from the PATCH response.
    • Shared: formatRelativeTime helper (“just now”, “5m ago”, … “1y ago”) with tests.
  • Refactors

    • Devices API: returns both displayName (fallback‑resolved) and customName (raw nullable) to support correct rename prefill. Profile devices fetch is tagged and revalidateUsernamePaths now also flushes /api/users/[username]/devices so renames appear instantly.
    • Profile tables: extracted shared card/header/row primitives to components/profile/listStyles.ts and adopted in both Models and Devices. Also replaced stray hardcoded colors with tokens.

Written for commit 17bb534. Summary will update on new commits.

Review in cubic

junhoyeo added 2 commits June 10, 2026 09:22
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
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tokscale Ready Ready Preview, Comment Jun 10, 2026 12:40am

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/frontend/src/app/u/[username]/page.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/frontend/src/components/profile/ProfileDevices.tsx Outdated
Comment thread packages/frontend/src/app/settings/SettingsClient.tsx Outdated
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
junhoyeo merged commit c9111b0 into main Jun 10, 2026
7 checks passed
@junhoyeo
junhoyeo deleted the feat/device-breakdown-ui branch June 10, 2026 01:39
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant