Skip to content

feat(admin): add DevPass subscribers page - #2085

Merged
smakosh merged 3 commits into
mainfrom
feat/devpass-admin-page
Apr 26, 2026
Merged

smakosh merged 3 commits into
mainfrom
feat/devpass-admin-page

Conversation

@smakosh

@smakosh smakosh commented Apr 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a /devpass admin view (list + detail) for monitoring current Dev Plans subscribers (Lite/Pro/Max). The page is tuned to the three things the existing /organizations view doesn't surface for a flat-rate subscription product:

  • Cycle utilization — strongest churn / over-cap signal
  • Real provider cost vs MRR — unit-margin per subscriber
  • Subscription lifecycle state — active / cancel-pending / expired / churned, plus payment-failure overlay

What's in it

API (apps/api/src/routes/admin.ts):

  • GET /admin/devpass — paginated, filterable, sortable subscriber list with KPI strip
  • GET /admin/devpass/{orgId} — subscriber detail with subscription event timeline + payment failures

Admin UI (ee/admin/src/app/devpass/):

  • KPI strip: active by tier, gross MRR, net new this month, weighted utilization, total cycle margin
  • Filters: tier, status, utilization band (<20% / 20–80% / 80–100% / over cap), negative-margin only, show-churned toggle
  • Sortable columns including computed ones (utilization, real cost, margin, subscribed-since)
  • Detail page mirrors the data with a per-subscriber cycle utilization bar, MRR / real cost / margin cards, and tabs for subscription events and payment failures
  • Sidebar nav entry under Organizations

Data sources: organization.devPlan* columns, transaction (dev_plan_* types), paymentFailure, projectHourlyStats.cost windowed to the current devPlanBillingCycleStart.

Test plan

  • Visit /devpass as admin — list renders, KPI strip populated
  • Tier / status / utilization filters narrow the list correctly
  • Sort by utilization, real cost, margin, subscribed-since works asc/desc
  • "Show churned" surfaces orgs whose devPlan = 'none' but had a past dev_plan_start
  • "Negative margin only" only shows rows where real cost > MRR
  • Search matches name, billing email, owner email, org id
  • Click into a row → /devpass/{orgId} loads with utilization bar + cycle stats
  • Subscription history tab lists dev_plan_* transactions chronologically
  • Payment failures tab lists Stripe failures for the org
  • Sidebar "DevPass" item highlights when on the page
  • pnpm --filter api build and pnpm --filter admin build succeed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced DevPass analytics dashboard for comprehensive subscriber monitoring
    • Search, multi‑filter (tier/status/utilization/margin‑negative/churn), sort and paginated subscriber list
    • Global KPIs and per‑subscriber billing metrics (MRR, real cost, margin, utilization)
    • Detailed subscriber profiles with transaction and payment‑failure histories and indicators
    • Admin navigation updated with a dedicated DevPass section

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

coderabbitai Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds 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

Cohort / File(s) Summary
Backend Analytics API
apps/api/src/routes/admin.ts
Adds two DevPass admin endpoints: GET /admin/devpass (list with filtering, sorting, pagination) and GET /admin/devpass/{orgId} (detail). Implements zod schemas and derives subscriber-level metrics (status, utilization%, credits, MRR via DEV_PLAN_PRICES, real provider cost from hourly stats, margin, tier-change counts, payment-issue detection) plus global KPI aggregates.
Frontend DevPass List Page
ee/admin/src/app/devpass/page.tsx
New server page that requires session, reads query params for page/search/sort/filters, fetches KPI aggregates and paged subscriber list, renders KPI cards, persistent filter/search form (server action), sortable table, utilization/mrr/cost/margin displays, payment-issue indicators, and pagination UI.
Frontend DevPass Detail Page
ee/admin/src/app/devpass/[orgId]/page.tsx
New server page that requires session, fetches /admin/devpass/{orgId}, handles null/404 cases, and renders subscriber dashboard with identity/tier/status/payment badges, clamped utilization with tone, summary metrics (MRR/cost/margin), and tabbed transaction/payment-failure history tables.
Admin Navigation
ee/admin/src/components/admin-shell.tsx
Adds "DevPass" navigation item (Sparkles icon) to the admin sidebar, with active-state detection for /devpass routes.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and accurately summarizes the main change: adding a new DevPass subscribers admin page with list and detail views.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/devpass-admin-page

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 and usage tips.

@smakosh smakosh self-assigned this Apr 26, 2026

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between e50ef87 and bab8cc7.

⛔ Files ignored due to path filters (1)
  • ee/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (4)
  • apps/api/src/routes/admin.ts
  • ee/admin/src/app/devpass/[orgId]/page.tsx
  • ee/admin/src/app/devpass/page.tsx
  • ee/admin/src/components/admin-shell.tsx

Comment thread apps/api/src/routes/admin.ts
Comment thread apps/api/src/routes/admin.ts Outdated
Comment thread apps/api/src/routes/admin.ts
Comment thread ee/admin/src/app/devpass/page.tsx
steebchen and others added 2 commits April 26, 2026 11:34
- 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>

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
ee/admin/src/app/devpass/page.tsx (2)

383-416: handleSearch server action: minor robustness nits.

Two small things worth tightening:

  • formData.get(...) as string will be null if any of the hidden inputs is ever removed from the form; the current code then writes the literal string "null" into the URL via sp.set("sortBy", sortByValue) for sortBy/sortOrder (which are unconditionally set). A ?? "" guard plus the same pickEnum whitelist used at the top of the page would prevent a malformed FormData payload from poisoning the redirect target.
  • marginNegative / showChurned hidden inputs encode the boolean as "true" or "" and the action then re-checks if (marginValue) sp.set(..., "true") — works, but it means a future change that uses value="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 after pickEnum.

pickEnum already returns either a valid enum member or the supplied fallback, so || "subscribedSince" / || "desc" are dead code, and the as SortBy / as TierFilter / as StatusFilter / as UtilFilter casts can be eliminated by typing pickEnum so 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

📥 Commits

Reviewing files that changed from the base of the PR and between bab8cc7 and cc0623b.

⛔ Files ignored due to path filters (3)
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (2)
  • apps/api/src/routes/admin.ts
  • ee/admin/src/app/devpass/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/routes/admin.ts

Comment on lines +781 to +784
<p className="mt-1 text-xs tabular-nums text-muted-foreground">
{currencyFormatter.format(parseFloat(sub.creditsUsed))} /{" "}
{currencyFormatter.format(parseFloat(sub.creditsLimit))}
</p>

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.

⚠️ Potential issue | 🟡 Minor

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.

@smakosh
smakosh added this pull request to the merge queue Apr 26, 2026
Merged via the queue into main with commit 0b6b544 Apr 26, 2026
12 checks passed
@smakosh
smakosh deleted the feat/devpass-admin-page branch April 26, 2026 12:00
@coderabbitai coderabbitai Bot mentioned this pull request Jul 10, 2026
2 of 3 tasks
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.

2 participants