Repository navigation
feat(admin): add DevPass subscribers page - #2085
Conversation
Adds /devpass admin view (list + detail) showing current Lite/Pro/Max
subscribers, their cycle utilization, real provider cost, and margin.
- New API: GET /admin/devpass and /admin/devpass/{orgId}
- KPI strip: active by tier, gross MRR, net new this month, weighted
utilization, total margin
- Filters: tier, status, utilization band, negative-margin only,
show-churned toggle
- Detail page: cycle stats, subscription event timeline, payment failures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WalkthroughAdds backend DevPass admin API endpoints and two Next.js admin pages (list and detail), computes per-subscriber billing metrics and global KPIs, and integrates a DevPass item into the admin sidebar navigation. Changes
Sequence DiagramsequenceDiagram
participant AdminUser as Admin User
participant UI as DevPass UI
participant SA as Server Action
participant API as Admin API
participant DB as Metrics Store
AdminUser->>UI: adjust filters / request page
UI->>SA: submit filter/search (server action)
SA->>API: GET /admin/devpass (filters, sort, page)
API->>DB: aggregate hourly stats & subscriber data
DB-->>API: derived subscriber metrics + KPIs
API-->>SA: paged rows + global KPIs
SA->>UI: redirect / render with params
UI-->>AdminUser: display KPI cards + table
AdminUser->>UI: open subscriber detail
UI->>API: GET /admin/devpass/{orgId}
API->>DB: compute detail metrics, fetch transactions/failures
DB-->>API: subscriber detail payload
API-->>UI: detail response
UI-->>AdminUser: render subscriber dashboard + history tabs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/routes/admin.ts`:
- Around line 7504-7525: totalRealCostCycle and totalMrrCycle are being computed
from the paginated rows (rows) which causes KPIs to change with limit/offset
while totalMargin is computed from the unpaginated universe; instead, derive all
three KPIs from the same unpaginated query used for margin. Replace the loop
that sums rows with assignments that parse Number(marginRow.totalCost) and
Number(marginRow.totalMrr) (the values selected using realCostSub.realCost and
tierPriceExpr), so totalRealCostCycle = Number(marginRow?.totalCost ?? 0) and
totalMrrCycle = Number(marginRow?.totalMrr ?? 0), keeping totalMargin =
universeMrr - universeRealCost; ensure the SQL selection still uses COALESCE/SUM
and casts as needed and that you reference realCostSub, tierPriceExpr and
tables.organization used in the existing margin query.
- Around line 7614-7760: The handler currently returns a fabricated subscriber
for orgs that never had DevPass; after fetching org and firstStartRow, add a
guard that if org.devPlan === "none" and firstStartRow?.firstStart is
missing/null, throw new HTTPException(404, { message: "Subscriber not found" })
so only organizations with an active/previous DevPass (or a dev_plan_start
record) produce a subscriber payload; locate symbols org, firstStartRow, and the
existing HTTPException usage to implement this check.
In `@ee/admin/src/app/devpass/page.tsx`:
- Around line 287-320: URL params are used unchecked before calling
createServerApiClient and can produce NaN or invalid enums; validate and
sanitize searchParams (e.g., parseInt with fallback for page, clamp offset using
limit, ensure page is >=1), enforce whitelists for sortBy/status/utilization by
mapping incoming strings to allowed SortBy/Status/UtilFilter values (fall back
to defaults when invalid), and coerce booleans (marginNegative/showChurned)
explicitly; then call $api.GET with the sanitized values and handle API errors
by checking response status (only render SignInPrompt on 401/unauthenticated)
while surfacing or handling other errors/fallbacks instead of treating every
failure as auth.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a5342ede-1309-4123-b6d8-1f3de2ca2f95
⛔ Files ignored due to path filters (1)
ee/admin/src/lib/api/v1.d.tsis excluded by!**/v1.d.ts
📒 Files selected for processing (4)
apps/api/src/routes/admin.tsee/admin/src/app/devpass/[orgId]/page.tsxee/admin/src/app/devpass/page.tsxee/admin/src/components/admin-shell.tsx
- KPIs now read from the unpaginated subscriber universe instead of mixing paginated row sums with the universe-based margin total, so the strip is consistent regardless of page size - Detail handler 404s when the org has no current or prior DevPass (devPlan === 'none' && no dev_plan_start) instead of returning a fabricated empty subscriber payload - List page sanitizes URL params: NaN-safe page parsing and explicit enum whitelists for sortBy/sortOrder/tier/status/utilization to keep invalid query strings from reaching the API as 400s Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
ee/admin/src/app/devpass/page.tsx (2)
383-416:handleSearchserver action: minor robustness nits.Two small things worth tightening:
formData.get(...) as stringwill benullif any of the hidden inputs is ever removed from the form; the current code then writes the literal string"null"into the URL viasp.set("sortBy", sortByValue)forsortBy/sortOrder(which are unconditionally set). A?? ""guard plus the samepickEnumwhitelist used at the top of the page would prevent a malformed FormData payload from poisoning the redirect target.marginNegative/showChurnedhidden inputs encode the boolean as"true"or""and the action then re-checksif (marginValue) sp.set(..., "true")— works, but it means a future change that usesvalue="false"would silently flip semantics. Consider normalizing once (e.g.,marginValue === "true").Non-blocking; current happy path is fine because the hidden inputs are always rendered.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/app/devpass/page.tsx` around lines 383 - 416, In handleSearch: avoid casting FormData values directly to string and writing literal "null" into the query by normalizing inputs and whitelisting enums; replace occurrences like const sortByValue = formData.get("sortBy") as string with a guarded value (e.g. (formData.get("sortBy') ?? "") and run it through the same pickEnum whitelist used elsewhere on the page before sp.set); likewise normalize boolean-like hidden inputs once (e.g. const marginValue = (formData.get("marginNegative") === "true") and const churnValue = (formData.get("showChurned") === "true") ) and only call sp.set("marginNegative","true") or sp.set("showChurned","true") when those normalized booleans are true so future value="false" won’t invert semantics.
318-330: Drop redundant||fallbacks and casts afterpickEnum.
pickEnumalready returns either a valid enum member or the supplied fallback, so|| "subscribedSince"/|| "desc"are dead code, and theas SortBy/as TierFilter/as StatusFilter/as UtilFiltercasts can be eliminated by typingpickEnumso callers get the precise return type back.Proposed simplification
-function pickEnum<T extends readonly string[]>( - allowed: T, - raw: string | undefined, - fallback: T[number] | "", -): T[number] | "" { +function pickEnum<T extends readonly string[], F extends T[number] | "">( + allowed: T, + raw: string | undefined, + fallback: F, +): T[number] | F { if (!raw) { return fallback; } return (allowed as readonly string[]).includes(raw) ? (raw as T[number]) : fallback; } @@ - const sortBy = - (pickEnum(SORT_BY_VALUES, params?.sortBy, "subscribedSince") as SortBy) || - "subscribedSince"; - const sortOrder = - (pickEnum(SORT_ORDER_VALUES, params?.sortOrder, "desc") as SortOrder) || - "desc"; - const tier = pickEnum(TIER_VALUES, params?.tier, "") as TierFilter; - const status = pickEnum(STATUS_VALUES, params?.status, "") as StatusFilter; - const utilization = pickEnum( - UTIL_VALUES, - params?.utilization, - "", - ) as UtilFilter; + const sortBy = pickEnum(SORT_BY_VALUES, params?.sortBy, "subscribedSince"); + const sortOrder = pickEnum(SORT_ORDER_VALUES, params?.sortOrder, "desc"); + const tier = pickEnum(TIER_VALUES, params?.tier, ""); + const status = pickEnum(STATUS_VALUES, params?.status, ""); + const utilization = pickEnum(UTIL_VALUES, params?.utilization, "");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/app/devpass/page.tsx` around lines 318 - 330, Remove the redundant "||" fallbacks and unnecessary type assertions when assigning sortBy, sortOrder, tier, status, and utilization: call pickEnum(SORT_BY_VALUES, params?.sortBy, "subscribedSince") and pickEnum(SORT_ORDER_VALUES, params?.sortOrder, "desc") etc. without trailing || defaults or "as" casts, and update pickEnum's TypeScript signature so it returns the precise enum/union type based on the provided values and fallback (so callers receive correctly typed results and no runtime fallback is needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ee/admin/src/app/devpass/page.tsx`:
- Around line 781-784: The current rendering uses
currencyFormatter.format(parseFloat(sub.creditsUsed)) and
parseFloat(sub.creditsLimit) which can produce NaN and render as "$NaN" when
sub.creditsUsed/creditsLimit are null/undefined/empty; update the display logic
around currencyFormatter to first coerce and validate the numeric value (e.g.,
const used = parseFloat(sub.creditsUsed); const limit =
parseFloat(sub.creditsLimit); if (!Number.isFinite(used)) show a safe fallback
(0 or "—") and likewise for limit) so that currencyFormatter.format is only
called on finite numbers; reference the existing currencyFormatter call and the
sub.creditsUsed / sub.creditsLimit properties when implementing the guard.
---
Nitpick comments:
In `@ee/admin/src/app/devpass/page.tsx`:
- Around line 383-416: In handleSearch: avoid casting FormData values directly
to string and writing literal "null" into the query by normalizing inputs and
whitelisting enums; replace occurrences like const sortByValue =
formData.get("sortBy") as string with a guarded value (e.g.
(formData.get("sortBy') ?? "") and run it through the same pickEnum whitelist
used elsewhere on the page before sp.set); likewise normalize boolean-like
hidden inputs once (e.g. const marginValue = (formData.get("marginNegative") ===
"true") and const churnValue = (formData.get("showChurned") === "true") ) and
only call sp.set("marginNegative","true") or sp.set("showChurned","true") when
those normalized booleans are true so future value="false" won’t invert
semantics.
- Around line 318-330: Remove the redundant "||" fallbacks and unnecessary type
assertions when assigning sortBy, sortOrder, tier, status, and utilization: call
pickEnum(SORT_BY_VALUES, params?.sortBy, "subscribedSince") and
pickEnum(SORT_ORDER_VALUES, params?.sortOrder, "desc") etc. without trailing ||
defaults or "as" casts, and update pickEnum's TypeScript signature so it returns
the precise enum/union type based on the provided values and fallback (so
callers receive correctly typed results and no runtime fallback is needed).
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 5a8ae1d5-ec23-4ee9-a7eb-98e20ba853ba
⛔ Files ignored due to path filters (3)
apps/code/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/src/lib/api/v1.d.tsis excluded by!**/v1.d.ts
📒 Files selected for processing (2)
apps/api/src/routes/admin.tsee/admin/src/app/devpass/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/api/src/routes/admin.ts
| <p className="mt-1 text-xs tabular-nums text-muted-foreground"> | ||
| {currencyFormatter.format(parseFloat(sub.creditsUsed))} /{" "} | ||
| {currencyFormatter.format(parseFloat(sub.creditsLimit))} | ||
| </p> |
There was a problem hiding this comment.
parseFloat on credit fields can render $NaN.
If the API ever returns null/undefined (or an empty string) for creditsUsed / creditsLimit — e.g., in degraded states or for non-active orgs surfaced via "show churned" — parseFloat yields NaN and currencyFormatter.format(NaN) prints $NaN. A small guard (Number.isFinite check, or using the typed numeric value the API exposes for mrr/realCost/margin) avoids the visual glitch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ee/admin/src/app/devpass/page.tsx` around lines 781 - 784, The current
rendering uses currencyFormatter.format(parseFloat(sub.creditsUsed)) and
parseFloat(sub.creditsLimit) which can produce NaN and render as "$NaN" when
sub.creditsUsed/creditsLimit are null/undefined/empty; update the display logic
around currencyFormatter to first coerce and validate the numeric value (e.g.,
const used = parseFloat(sub.creditsUsed); const limit =
parseFloat(sub.creditsLimit); if (!Number.isFinite(used)) show a safe fallback
(0 or "—") and likewise for limit) so that currencyFormatter.format is only
called on finite numbers; reference the existing currencyFormatter call and the
sub.creditsUsed / sub.creditsLimit properties when implementing the guard.
Summary
Adds a
/devpassadmin view (list + detail) for monitoring current Dev Plans subscribers (Lite/Pro/Max). The page is tuned to the three things the existing/organizationsview doesn't surface for a flat-rate subscription product:What's in it
API (
apps/api/src/routes/admin.ts):GET /admin/devpass— paginated, filterable, sortable subscriber list with KPI stripGET /admin/devpass/{orgId}— subscriber detail with subscription event timeline + payment failuresAdmin UI (
ee/admin/src/app/devpass/):Data sources:
organization.devPlan*columns,transaction(dev_plan_*types),paymentFailure,projectHourlyStats.costwindowed to the currentdevPlanBillingCycleStart.Test plan
/devpassas admin — list renders, KPI strip populateddevPlan = 'none'but had a pastdev_plan_start/devpass/{orgId}loads with utilization bar + cycle statsdev_plan_*transactions chronologicallypnpm --filter api buildandpnpm --filter admin buildsucceed🤖 Generated with Claude Code
Summary by CodeRabbit