Skip to content

Add admin usage dashboard - #627

Merged
kody-bot merged 1 commit into
mainfrom
cursor/admin-usage-dashboard-98ba
Jul 5, 2026
Merged

kody-bot merged 1 commit into
mainfrom
cursor/admin-usage-dashboard-98ba

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Added a read-only /admin/usage dashboard for admins with per-user current-month usage rollups, daily entitlement counters, live resource counts, and per-user drill-down details.
  • Added the admin_usage_overview admin MCP capability with the same aggregate data and existing admin audit logging.
  • Reused entitlement counting through an exported readEntitlementResourceUsage helper; after rebasing over Entitlements follow-up: don't let a service's own stale running row block its restart #623, package-service usage delegates to countRunningPackageServices so dashboard counts match enforcement while service restarts can still exclude their own stale running row.

Walkthrough

admin_usage_dashboard_walkthrough.mp4

Validation

  • npx vitest run packages/worker/src/entitlements/entitlements.node.test.ts packages/worker/src/mcp/capabilities/services/service-start.node.test.ts packages/worker/src/app/admin-usage-data.node.test.ts packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
  • npm run validate (passed after rebase onto main @ f47f785)
  • Push hook E2E: 6 Playwright tests passed
  • Manual browser walkthrough on local dev server at /admin/usage, recorded above
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ f47f785 · Head: 3fb604f

Classification: extends — this PR adds a new admin app route and a new admin MCP capability while reusing existing RBAC, usage metering, entitlement counters, and D1 storage primitives.

Primitives touched

Primitive Group Impact
app-ui surfaces extends — adds /admin/usage browser route and client dashboard
mcp-server surfaces composes — exposes usage overview through the existing capability execution surface
rbac auth composes — uses existing admin role guard and admin capability access
entitlements auth extends — exports a shared read helper for existing entitlement counter queries, now delegating package-service counts to countRunningPackageServices
capability-registry assistant extends — registers admin_usage_overview in the admin domain
usage-metering storage composes — reads existing usage_rollups counters
d1-app-db storage composes — reads existing users, usage, entitlement, and resource-count tables without schema changes

System map

flowchart LR
	appUi["app-ui"]:::extended --> rbac["rbac"]:::touched
	appUi --> adminUsageData["admin usage aggregation"]:::extended
	mcpServer["mcp-server"]:::touched --> capabilityRegistry["capability-registry"]:::extended --> adminUsageData
	adminUsageData --> usageMetering["usage-metering"]:::touched
	adminUsageData --> entitlements["entitlements"]:::extended
	adminUsageData --> d1AppDb["d1-app-db"]:::touched
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Change flow

sequenceDiagram
	participant Admin as Admin user / MCP caller
	participant RBAC as RBAC guard
	participant Aggregator as admin-usage-data
	participant Usage as usage_rollups
	participant Entitlements as entitlement counters
	participant D1 as D1 resource tables
	Admin->>RBAC: request /admin/usage or admin_usage_overview
	RBAC->>Aggregator: admin authorized
	Aggregator->>Usage: current/monthly metric rollups by derived usage user id
	Aggregator->>Entitlements: shared resource usage reader
	Entitlements->>D1: package-service counts through countRunningPackageServices
	Aggregator->>D1: users metadata and live count source tables
	Aggregator-->>Admin: metadata-only counters and limits
Loading

Invariants

  • per-user-isolation: the dashboard crosses users only through the existing admin/RBAC boundary, returns account metadata counters only, derives usage user ids internally from account email, and never returns package code, secret names/values, memory text, email bodies, or other user content.
  • No migration: the change reads existing usage_rollups, entitlement_daily_counters, users.plan, and existing resource tables.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added a new Admin usage page with per-user usage summaries, drill-down details, and quota indicators.
    • Added Usage navigation links throughout admin pages for quicker access.
    • Exposed usage overview data through admin tools and the API.
  • Bug Fixes

    • Strengthened admin access checks on usage pages and ensured non-admins see a forbidden state.
    • Improved privacy protections so sensitive values are not shown in the UI or API responses.
  • Tests

    • Expanded end-to-end and unit coverage for access control, usage data, and month boundary behavior.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an admin-only usage dashboard feature: new data types and a data-loading module aggregating per-user usage rollups, daily counters, live resource counts, and entitlement consumption; SSR/API route handlers and routing entries; a client route/UI; an MCP capability exposing the same data; navigation links across admin pages; a refactored entitlement usage reader; and associated tests.

Changes

Admin Usage Feature

Layer / File(s) Summary
Usage data types and loader
packages/worker/src/app/loader-data.ts, packages/worker/src/app/admin-usage-data.ts, packages/worker/src/app/admin-usage-data.node.test.ts
New AdminUsage* types and loadAdminUsageData compute paginated per-user summaries, monthly rollups, daily counters, live resource counts, and entitlement consumption with over-80% warnings; covered by unit tests including UTC month-boundary handling.
Entitlement usage reader refactor
packages/worker/src/entitlements/service.ts
Replaces internal countEntitlementUsage with exported readEntitlementResourceUsage, used by assertWithinEntitlement.
SSR/API handlers and routing
packages/worker/src/app/handlers/admin-usage.ts, packages/worker/src/app/router.ts, packages/worker/src/app/routes.ts
Adds admin-role-gated page and JSON API handlers for /admin/usage and /admin/usage.json, wired into the router and route map.
Client route, UI, and navigation
packages/worker/client/routes/admin-usage.tsx, packages/worker/client/routes/index.tsx, packages/worker/client/routes/admin-community-reports.tsx, packages/worker/client/routes/admin-invites.tsx, packages/worker/client/routes/admin-roles.tsx, packages/worker/client/routes/admin-users.tsx
New AdminUsageRoute/adminUsageRouteLoader render usage tables and drill-down panels; registered in client route maps; “Usage” links added to admin page headers.
MCP admin_usage_overview capability
packages/worker/src/mcp/capabilities/admin/admin-usage-overview.ts, packages/worker/src/mcp/capabilities/admin/domain.ts, packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
New admin-only MCP capability with Zod schemas fetches admin usage overview data via an audited handler, registered in the admin domain and covered by extended tests.
E2E RBAC coverage
e2e/admin-rbac.spec.ts
Verifies non-admins see “Forbidden” on /admin/usage, and admins see usage data without seeded secrets appearing in page or JSON payload.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AdminUsageHandler
  participant LoadAdminUsageData
  participant D1Database
  Browser->>AdminUsageHandler: GET /admin/usage or /admin/usage.json
  AdminUsageHandler->>AdminUsageHandler: requireUserWithRole(admin)
  AdminUsageHandler->>LoadAdminUsageData: loadAdminUsageData(env, url)
  LoadAdminUsageData->>D1Database: query users, rollups, counters
  D1Database-->>LoadAdminUsageData: rows
  LoadAdminUsageData-->>AdminUsageHandler: AdminUsageLoaderData
  AdminUsageHandler-->>Browser: SSR page or JSON response
Loading

Possibly related PRs

  • kentcdodds/kody#601: Both PRs modify the same E2E spec (e2e/admin-rbac.spec.ts) for admin navigation checks.
  • kentcdodds/kody#608: The new /admin/usage route/loader registration relies on the same clientRouteLoaders/clientRoutes wiring framework introduced there.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main addition: a new admin usage dashboard.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/admin-usage-dashboard-98ba

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2b02e71. Configure here.

month.usage,
metric as AdminUsageRollup['metric'],
)?.eventCount ?? 0,
)}

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.

Month table shows runtime events

Medium Severity

The month-over-month table formats every metric with eventCount, but service_runtime rollups accumulate wall-clock time in totalDurationMs. The summary table already uses formatDuration for that column, so historical “Service runtime” cells show event counts instead of runtime.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2b02e71. Configure here.

Comment thread packages/worker/src/app/admin-usage-data.ts
Comment thread packages/worker/src/app/admin-usage-data.ts
<p mix={css({ margin: 0, color: colors.textMuted })}>
Page {data.page} of {totalPages}
</p>
) : null}

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.

Usage dashboard lacks pagination

Medium Severity

The UI renders “Page X of Y” but provides no controls to change page or pageSize, even though loadAdminUsageData paginates with default page size 20. Admins with more than twenty accounts cannot reach later pages from the browser without manually editing the URL.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2b02e71. Configure here.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/worker/src/entitlements/service.ts (1)

138-141: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Stubbed persistent_package_services count is now surfaced as real usage data in the admin dashboard.

The hardcoded return 0 here is correct for assertWithinEntitlement's boolean-allowance gating (per the comment), but readEntitlementResourceUsage is now also called directly by admin-usage-data.ts's readEntitlementConsumption (via adminUsageEntitlementResources, which includes 'persistent_package_services') to populate the admin drill-down's "current" usage column. That dashboard is meant to show Read-only account metadata for usage, quota counters, and resource counts per the PR description, but for this resource it will always display current: 0 regardless of the account's actual persistent-service state, misrepresenting live data to admins.

Consider either excluding persistent_package_services from adminUsageEntitlementResources in admin-usage-data.ts (since it has no meaningful count, only an allow/disallow state), or adding a real counting query for it if such visibility is actually needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/entitlements/service.ts` around lines 138 - 141, The
hardcoded zero in readEntitlementResourceUsage for persistent_package_services
is fine for entitlement gating, but it makes adminUsageEntitlementResources in
admin-usage-data.ts report fake current usage. Update the admin drill-down path
so persistent_package_services is excluded from adminUsageEntitlementResources,
or replace the stub in readEntitlementConsumption/readEntitlementResourceUsage
with a real count if admins need actual live usage; use the
persistent_package_services case in service.ts and the
adminUsageEntitlementResources list to locate the affected flow.
🧹 Nitpick comments (5)
packages/worker/src/app/admin-usage-data.ts (3)

294-323: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

percentOfLimit treats a hard limit === 0 the same as "unlimited".

Line 310-311 maps both limit == null and limit === 0 to percentOfLimit = null, which the client renders as "Unlimited" (per formatLimit/formatPercent in admin-usage.tsx). For a resource whose plan limit is genuinely 0 (fully disallowed), any nonzero current should register as a violation/warning, not "Unlimited". Today this is masked because the only resource with a possible limit === 0 (persistent_package_services) always reports current = 0 (see companion comment in entitlements/service.ts), but the logic itself is a latent edge case if a future zero-limit resource is added here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/admin-usage-data.ts` around lines 294 - 323, The
percentOfLimit calculation in readEntitlementConsumption is incorrectly treating
a real zero limit the same as no limit, which makes fully disallowed resources
look “Unlimited.” Update the logic around resolvePlanLimit so that only a
null/undefined limit maps to percentOfLimit = null, while limit === 0 is handled
as a valid limit and yields an appropriate ratio/violation signal; keep the rest
of the AdminUsageEntitlementConsumption shape and overEightyPercent calculation
consistent with this change.

218-254: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant rollup re-fetch for the selected user.

buildSelectedUser re-runs loadCurrentMonthRollups for input.user.usageUserId (line 228-233) even though loadAdminUsageData already fetched current-month rollups for every user on the page into usageByUserId (lines 125-130). When the selected user is on the current page (the common case, e.g. default selection at line 182), this is a fully redundant D1 round trip.

♻️ Reuse already-loaded rollups when available
 async function buildSelectedUser(input: {
 	db: D1Database
 	user: UserWithUsageId
 	currentMonth: string
 	now: Date
+	currentMonthUsage?: Array<AdminUsageRollupRow>
 }): Promise<AdminUsageSelectedUser> {
 	const [summary, monthRows, entitlementConsumption] = await Promise.all([
 		buildUserSummary({
 			db: input.db,
 			user: input.user,
-			usage: await loadCurrentMonthRollups({
-				db: input.db,
-				userIds: [input.user.usageUserId],
-				month: input.currentMonth,
-			}),
+			usage:
+				input.currentMonthUsage ??
+				(await loadCurrentMonthRollups({
+					db: input.db,
+					userIds: [input.user.usageUserId],
+					month: input.currentMonth,
+				})),
 			now: input.now,
 		}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/admin-usage-data.ts` around lines 218 - 254,
buildSelectedUser is redundantly reloading current-month rollups instead of
reusing the data already fetched by loadAdminUsageData. Update buildSelectedUser
to accept and prefer the existing usageByUserId entry for
input.user.usageUserId, and only fall back to loadCurrentMonthRollups when the
selected user is not already in the preloaded set. Keep the existing
buildUserSummary and toMonthUsage flow unchanged, but thread the preloaded
rollups from loadAdminUsageData into buildSelectedUser to avoid the extra D1
round trip.

344-356: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unbounded history fetch before JS-side slicing.

loadUserMonthRollups has no LIMIT/date filter, fetching a user's entire usage_rollups history; toMonthUsage then slices to the last 12 months in JS (line 383). Push the bound into SQL (e.g. LIMIT on a reasonable row count, or filter month >= ?) to avoid transferring/growing an unbounded result set as history accumulates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/admin-usage-data.ts` around lines 344 - 356, The
loadUserMonthRollups query is fetching the full usage_rollups history for a user
and relying on toMonthUsage to trim it later, which can grow unbounded over
time. Update loadUserMonthRollups to apply the retention bound directly in SQL,
using a month filter or a reasonable LIMIT before the ORDER BY results are
returned, and keep toMonthUsage focused on shaping already-bounded data.
packages/worker/src/app/loader-data.ts (1)

116-116: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reuse PlanName for admin usage summaries. AdminUsagePlanName duplicates the canonical plan union; aliasing it to PlanName keeps AdminUsageUserSummary.plan in sync automatically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/loader-data.ts` at line 116, The admin usage plan
union is duplicated here, which can drift from the canonical plan set. Update
the AdminUsagePlanName type in loader-data.ts to alias the existing PlanName
definition so AdminUsageUserSummary.plan always stays in sync with the shared
plan union. Keep the change localized to the type declaration and preserve all
existing usages of AdminUsagePlanName.
packages/worker/src/mcp/capabilities/admin/admin-usage-overview.ts (1)

10-32: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Derive these enums from the canonical exports.
usageMetricSchema, entitlementResourceSchema, and planSchema duplicate adminUsageMetrics, entitlementResources, and planNames; importing those values here keeps the output schema aligned when new metrics/resources/plans are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/mcp/capabilities/admin/admin-usage-overview.ts` around
lines 10 - 32, The enum schemas in admin-usage-overview are duplicating
canonical values, so update usageMetricSchema, entitlementResourceSchema, and
planSchema to derive from the existing adminUsageMetrics, entitlementResources,
and planNames exports instead of hardcoding strings. Use the imported symbols
directly in this module so the output schema stays aligned automatically when
new metrics, resources, or plans are added.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/admin-rbac.spec.ts`:
- Around line 32-34: Add the missing non-admin forbidden assertion for the
`/admin/usage.json` endpoint in the `admin-rbac.spec.ts` non-admin access block.
Extend the existing checks around `page.goto('/admin/usage')` so the same
unauthorized user flow also requests `/admin/usage.json` and verifies it is
rejected with the expected forbidden response, alongside the current HTML page
assertion. Use the existing non-admin test section and related `page`/`expect`
calls to keep the coverage aligned with the RBAC behavior.

In `@packages/worker/client/routes/admin-usage.tsx`:
- Around line 212-214: The users table in admin-usage.tsx calculates totalPages
and renders the current page, but it has no navigation controls to move between
pages. Add Previous/Next pagination buttons and a helper like buildUsageHref,
mirroring the pattern used in admin-users.tsx with buildUsersHref, so the UI can
update the page query params returned by the loader (page, pageSize, total).
Wire the controls into the table/footer area where Page {data.page} of
{totalPages} is shown and disable them appropriately at the first and last page.

---

Outside diff comments:
In `@packages/worker/src/entitlements/service.ts`:
- Around line 138-141: The hardcoded zero in readEntitlementResourceUsage for
persistent_package_services is fine for entitlement gating, but it makes
adminUsageEntitlementResources in admin-usage-data.ts report fake current usage.
Update the admin drill-down path so persistent_package_services is excluded from
adminUsageEntitlementResources, or replace the stub in
readEntitlementConsumption/readEntitlementResourceUsage with a real count if
admins need actual live usage; use the persistent_package_services case in
service.ts and the adminUsageEntitlementResources list to locate the affected
flow.

---

Nitpick comments:
In `@packages/worker/src/app/admin-usage-data.ts`:
- Around line 294-323: The percentOfLimit calculation in
readEntitlementConsumption is incorrectly treating a real zero limit the same as
no limit, which makes fully disallowed resources look “Unlimited.” Update the
logic around resolvePlanLimit so that only a null/undefined limit maps to
percentOfLimit = null, while limit === 0 is handled as a valid limit and yields
an appropriate ratio/violation signal; keep the rest of the
AdminUsageEntitlementConsumption shape and overEightyPercent calculation
consistent with this change.
- Around line 218-254: buildSelectedUser is redundantly reloading current-month
rollups instead of reusing the data already fetched by loadAdminUsageData.
Update buildSelectedUser to accept and prefer the existing usageByUserId entry
for input.user.usageUserId, and only fall back to loadCurrentMonthRollups when
the selected user is not already in the preloaded set. Keep the existing
buildUserSummary and toMonthUsage flow unchanged, but thread the preloaded
rollups from loadAdminUsageData into buildSelectedUser to avoid the extra D1
round trip.
- Around line 344-356: The loadUserMonthRollups query is fetching the full
usage_rollups history for a user and relying on toMonthUsage to trim it later,
which can grow unbounded over time. Update loadUserMonthRollups to apply the
retention bound directly in SQL, using a month filter or a reasonable LIMIT
before the ORDER BY results are returned, and keep toMonthUsage focused on
shaping already-bounded data.

In `@packages/worker/src/app/loader-data.ts`:
- Line 116: The admin usage plan union is duplicated here, which can drift from
the canonical plan set. Update the AdminUsagePlanName type in loader-data.ts to
alias the existing PlanName definition so AdminUsageUserSummary.plan always
stays in sync with the shared plan union. Keep the change localized to the type
declaration and preserve all existing usages of AdminUsagePlanName.

In `@packages/worker/src/mcp/capabilities/admin/admin-usage-overview.ts`:
- Around line 10-32: The enum schemas in admin-usage-overview are duplicating
canonical values, so update usageMetricSchema, entitlementResourceSchema, and
planSchema to derive from the existing adminUsageMetrics, entitlementResources,
and planNames exports instead of hardcoding strings. Use the imported symbols
directly in this module so the output schema stays aligned automatically when
new metrics, resources, or plans are added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 557900b8-dafd-4e92-b41d-b879f52ac2ea

📥 Commits

Reviewing files that changed from the base of the PR and between 4794eed and 2b02e71.

📒 Files selected for processing (17)
  • e2e/admin-rbac.spec.ts
  • packages/worker/client/routes/admin-community-reports.tsx
  • packages/worker/client/routes/admin-invites.tsx
  • packages/worker/client/routes/admin-roles.tsx
  • packages/worker/client/routes/admin-usage.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/client/routes/index.tsx
  • packages/worker/src/app/admin-usage-data.node.test.ts
  • packages/worker/src/app/admin-usage-data.ts
  • packages/worker/src/app/handlers/admin-usage.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-usage-overview.ts
  • packages/worker/src/mcp/capabilities/admin/domain.ts

Comment thread e2e/admin-rbac.spec.ts
Comment on lines +32 to +34
await page.goto('/admin/usage')
await expect(page.getByRole('heading', { name: 'Admin usage' })).toBeHidden()
await expect(page.getByText('Forbidden')).toBeVisible()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing non-admin forbidden check for /admin/usage.json.

Only the HTML page (Lines 32-34) is verified to be forbidden for non-admins; there's no assertion that /admin/usage.json also rejects non-admin requests. The stack description for this layer calls out verifying "non-admin access is forbidden ... on both the usage page and JSON API," so this coverage looks incomplete.

✅ Suggested addition near the non-admin block
 	await page.goto('/admin/usage')
 	await expect(page.getByRole('heading', { name: 'Admin usage' })).toBeHidden()
 	await expect(page.getByText('Forbidden')).toBeVisible()
+	const usageApiForbidden = await page.request.get('/admin/usage.json')
+	expect(usageApiForbidden.ok()).toBe(false)

Also applies to: 123-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/admin-rbac.spec.ts` around lines 32 - 34, Add the missing non-admin
forbidden assertion for the `/admin/usage.json` endpoint in the
`admin-rbac.spec.ts` non-admin access block. Extend the existing checks around
`page.goto('/admin/usage')` so the same unauthorized user flow also requests
`/admin/usage.json` and verifies it is rejected with the expected forbidden
response, alongside the current HTML page assertion. Use the existing non-admin
test section and related `page`/`expect` calls to keep the coverage aligned with
the RBAC behavior.

Comment on lines +212 to +214
const totalPages = data
? Math.max(1, Math.ceil(data.total / data.pageSize))
: 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing pagination controls for the users table.

totalPages is computed and Page {data.page} of {totalPages} is rendered, but there is no way to actually navigate to another page — no buildUsageHref/Previous-Next buttons like admin-users.tsx provides (see buildUsersHref and the Previous/Next buttons in that file). Since the loader returns page/pageSize/total (implying server-side pagination is expected), admins with more users than one page cannot reach the remaining accounts or drill into them through the UI.

🔧 Suggested fix (mirrors admin-users.tsx pattern)
+function buildUsageHref(handle: Handle, page: number) {
+	const url = new URL(readCurrentRouterHref(handle), 'http://localhost')
+	if (page <= 1) url.searchParams.delete('page')
+	else url.searchParams.set('page', String(page))
+	return `${url.pathname}${url.search}`
+}
+
 ...
-						{totalPages > 1 ? (
-							<p mix={css({ margin: 0, color: colors.textMuted })}>
-								Page {data.page} of {totalPages}
-							</p>
-						) : null}
+						{totalPages > 1 ? (
+							<div mix={css({ display: 'flex', gap: spacing.sm, alignItems: 'center' })}>
+								<a
+									href={buildUsageHref(handle, data.page - 1)}
+									aria-disabled={data.page <= 1}
+									mix={css({ ...secondaryButtonCss, textDecoration: 'none' })}
+								>
+									Previous
+								</a>
+								<span mix={css({ color: colors.textMuted })}>
+									Page {data.page} of {totalPages}
+								</span>
+								<a
+									href={buildUsageHref(handle, data.page + 1)}
+									aria-disabled={data.page >= totalPages}
+									mix={css({ ...secondaryButtonCss, textDecoration: 'none' })}
+								>
+									Next
+								</a>
+							</div>
+						) : null}

Also applies to: 387-391

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-usage.tsx` around lines 212 - 214, The
users table in admin-usage.tsx calculates totalPages and renders the current
page, but it has no navigation controls to move between pages. Add Previous/Next
pagination buttons and a helper like buildUsageHref, mirroring the pattern used
in admin-users.tsx with buildUsersHref, so the UI can update the page query
params returned by the loader (page, pageSize, total). Wire the controls into
the table/footer area where Page {data.page} of {totalPages} is shown and
disable them appropriately at the first and last page.

@cursor
cursor Bot force-pushed the cursor/admin-usage-dashboard-98ba branch from 2b02e71 to 3fb604f Compare July 5, 2026 20:25
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-627.kentcdodds.workers.dev

Worker: kody-pr-627
D1: kody-pr-627-db
KV: kody-pr-627-oauth-kv

Mocks:

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.

3 participants