Add admin usage dashboard - #627
Conversation
📝 WalkthroughWalkthroughThis 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. ChangesAdmin Usage Feature
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
❌ 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, | ||
| )} |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 2b02e71. Configure here.
| <p mix={css({ margin: 0, color: colors.textMuted })}> | ||
| Page {data.page} of {totalPages} | ||
| </p> | ||
| ) : null} |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 2b02e71. Configure here.
There was a problem hiding this comment.
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 liftStubbed
persistent_package_servicescount is now surfaced as real usage data in the admin dashboard.The hardcoded
return 0here is correct forassertWithinEntitlement's boolean-allowance gating (per the comment), butreadEntitlementResourceUsageis now also called directly byadmin-usage-data.ts'sreadEntitlementConsumption(viaadminUsageEntitlementResources, 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 displaycurrent: 0regardless of the account's actual persistent-service state, misrepresenting live data to admins.Consider either excluding
persistent_package_servicesfromadminUsageEntitlementResourcesinadmin-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
percentOfLimittreats a hardlimit === 0the same as "unlimited".Line 310-311 maps both
limit == nullandlimit === 0topercentOfLimit = null, which the client renders as "Unlimited" (performatLimit/formatPercentinadmin-usage.tsx). For a resource whose plan limit is genuinely 0 (fully disallowed), any nonzerocurrentshould register as a violation/warning, not "Unlimited". Today this is masked because the only resource with a possiblelimit === 0(persistent_package_services) always reportscurrent = 0(see companion comment inentitlements/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 winRedundant rollup re-fetch for the selected user.
buildSelectedUserre-runsloadCurrentMonthRollupsforinput.user.usageUserId(line 228-233) even thoughloadAdminUsageDataalready fetched current-month rollups for every user on the page intousageByUserId(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 winUnbounded history fetch before JS-side slicing.
loadUserMonthRollupshas noLIMIT/date filter, fetching a user's entireusage_rollupshistory;toMonthUsagethen slices to the last 12 months in JS (line 383). Push the bound into SQL (e.g.LIMITon a reasonable row count, or filtermonth >= ?) 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 winReuse
PlanNamefor admin usage summaries.AdminUsagePlanNameduplicates the canonical plan union; aliasing it toPlanNamekeepsAdminUsageUserSummary.planin 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 winDerive these enums from the canonical exports.
usageMetricSchema,entitlementResourceSchema, andplanSchemaduplicateadminUsageMetrics,entitlementResources, andplanNames; 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
📒 Files selected for processing (17)
e2e/admin-rbac.spec.tspackages/worker/client/routes/admin-community-reports.tsxpackages/worker/client/routes/admin-invites.tsxpackages/worker/client/routes/admin-roles.tsxpackages/worker/client/routes/admin-usage.tsxpackages/worker/client/routes/admin-users.tsxpackages/worker/client/routes/index.tsxpackages/worker/src/app/admin-usage-data.node.test.tspackages/worker/src/app/admin-usage-data.tspackages/worker/src/app/handlers/admin-usage.tspackages/worker/src/app/loader-data.tspackages/worker/src/app/router.tspackages/worker/src/app/routes.tspackages/worker/src/entitlements/service.tspackages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.tspackages/worker/src/mcp/capabilities/admin/admin-usage-overview.tspackages/worker/src/mcp/capabilities/admin/domain.ts
| await page.goto('/admin/usage') | ||
| await expect(page.getByRole('heading', { name: 'Admin usage' })).toBeHidden() | ||
| await expect(page.getByText('Forbidden')).toBeVisible() |
There was a problem hiding this comment.
🎯 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.
| const totalPages = data | ||
| ? Math.max(1, Math.ceil(data.total / data.pageSize)) | ||
| : 1 |
There was a problem hiding this comment.
🎯 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.
2b02e71 to
3fb604f
Compare
|
🔎 Preview deployed: https://kody-pr-627.kentcdodds.workers.dev Worker: Mocks:
|


Summary
/admin/usagedashboard for admins with per-user current-month usage rollups, daily entitlement counters, live resource counts, and per-user drill-down details.admin_usage_overviewadmin MCP capability with the same aggregate data and existing admin audit logging.readEntitlementResourceUsagehelper; after rebasing over Entitlements follow-up: don't let a service's own stale running row block its restart #623, package-service usage delegates tocountRunningPackageServicesso dashboard counts match enforcement while service restarts can still exclude their own stale running row.Walkthrough
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.tsnpm run validate(passed after rebase ontomain@f47f785)/admin/usage, recorded aboveSystem recap — extends existing primitives (medium risk)
Mode: recap · Base:
main@f47f785· Head:3fb604fClassification: 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
app-ui/admin/usagebrowser route and client dashboardmcp-serverrbacentitlementscountRunningPackageServicescapability-registryadmin_usage_overviewin the admin domainusage-meteringusage_rollupscountersd1-app-dbSystem map
Change flow
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.usage_rollups,entitlement_daily_counters,users.plan, and existing resource tables.Summary by CodeRabbit
New Features
Bug Fixes
Tests