Skip to content

Make the admin org page useful, and let admin types come from the server - #768

Merged
InfinityBowman merged 3 commits into
mainfrom
feat/admin-org-detail
Sep 12, 2026
Merged

Make the admin org page useful, and let admin types come from the server#768
InfinityBowman merged 3 commits into
mainfrom
feat/admin-org-detail

Conversation

@InfinityBowman

@InfinityBowman InfinityBowman commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Org detail page

getAdminOrgDetails returned member and project counts and nothing else, so the page could tell you an org had nine members and name none of them.

  • Members and Projects panels under the header, linking through to the user and project pages. Members are ordered owner first, and a banned member is marked.
  • Rows are capped at 50 while the counts stay true, and the panel says so when it is showing a subset.
  • The counts move into the page description, so the two stat cards that only repeated the panel titles are gone.
  • Billing Reconciliation collapses to a single row. It is a diagnostic tool with four threshold inputs and a Stripe toggle, and it was firing its stuck-state query on every visit to an org page. It now loads only when opened.

Types

Every admin page cast its query result to a hand-written interface - data as UserData | undefined, and in one case data as unknown as UserData. The hooks were already typed end to end: useAdminOrgDetails().data comes back as the server function's exact return type. So the cast added no safety; it replaced a correct type with a drifting one and switched the compiler off.

The casts are gone and inference reaches the components. Where a table or a prop needs a name for the row type, it is derived from the server function rather than restated: AdminUserListItem, AdminOrgListItem, AdminProjectListItem, AdminOrgSubscription, AdminOrgGrant, AdminLedgerEntry, AdminTableColumn, and the detail types.

Net effect on the admin surface: -284 lines, +74.

One cast stays, in the database viewer: the table is chosen at runtime, so its rows genuinely are Record<string, unknown>. It now sits on that one line instead of over the whole query result.

Bugs this uncovered

Every one of these had been invisible because the hand-written interface was only ever checked against itself.

  • Every invitation showed as expired. ProjectInvitationsSection computed expiresAt * 1000, but expiresAt is a Drizzle timestamp arriving as a Date. Date * 1000 is NaN, and new Date(NaN) > new Date() is false.
  • Active subscription (undefined). OrgBillingSummary read billing.subscription.plan; the resolver's subscription object has no plan field. It now uses the effective plan's name.
  • Free orgs were styled as an error. The access badge tested for 'limited', not in the union ('full' | 'readOnly' | 'free'), so read-only never got the warning style and free fell through to destructive.
  • The projects "Created by" column never sorted, because its accessorKey was creatorDisplayName - a field nothing produces.
  • Every *DisplayName read was permanently undefined (userDisplayName, creatorDisplayName, uploaderDisplayName, inviterDisplayName). All declared, none produced anywhere; every render silently took the fallback.

Verification

Typecheck, lint, format, 308 web tests and the build all pass, before and after the rebase onto main. Every admin page loaded in the browser against the seeded data - directories, both detail pages, database viewer, storage, ledger - plus the reconciliation row expanding and collapsing, and a free org's access badge now neutral rather than red.

Noted, not changed

The server still selects creatorGivenName, userGivenName, uploaderGivenName and inviterGivenName, which no caller reads.

https://claude.ai/code/session_01LqxkXwhjRDsJ1N9cBYpU1n

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 21 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 78c9393b-6183-4d71-8025-3639ac9e6629

📥 Commits

Reviewing files that changed from the base of the PR and between 88e6e58 and d8b5a89.

📒 Files selected for processing (34)
  • packages/web/src/components/admin/GrantList.tsx
  • packages/web/src/components/admin/OrgBillingReconcilePanel.tsx
  • packages/web/src/components/admin/OrgBillingSummary.tsx
  • packages/web/src/components/admin/SubscriptionList.tsx
  • packages/web/src/components/admin/UserTable.tsx
  • packages/web/src/components/admin/orgs/OrgMembersSection.tsx
  • packages/web/src/components/admin/orgs/OrgProjectsSection.tsx
  • packages/web/src/components/admin/projects/ProjectFilesSection.tsx
  • packages/web/src/components/admin/projects/ProjectInfoSection.tsx
  • packages/web/src/components/admin/projects/ProjectInvitationsSection.tsx
  • packages/web/src/components/admin/projects/ProjectMembersSection.tsx
  • packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx
  • packages/web/src/components/admin/projects/types.ts
  • packages/web/src/components/admin/users/UserActions.tsx
  • packages/web/src/components/admin/users/UserLinkedAccounts.tsx
  • packages/web/src/components/admin/users/UserOrganizations.tsx
  • packages/web/src/components/admin/users/UserProfileSection.tsx
  • packages/web/src/components/admin/users/UserProjects.tsx
  • packages/web/src/components/admin/users/UserSessions.tsx
  • packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx
  • packages/web/src/routes/_app/_protected/admin/database.tsx
  • packages/web/src/routes/_app/_protected/admin/index.tsx
  • packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx
  • packages/web/src/routes/_app/_protected/admin/orgs.index.tsx
  • packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx
  • packages/web/src/routes/_app/_protected/admin/projects.index.tsx
  • packages/web/src/routes/_app/_protected/admin/storage.tsx
  • packages/web/src/routes/_app/_protected/admin/users.$userId.tsx
  • packages/web/src/routes/_app/_protected/admin/users.index.tsx
  • packages/web/src/server/functions/admin-billing.server.ts
  • packages/web/src/server/functions/admin-database.server.ts
  • packages/web/src/server/functions/admin-orgs.server.ts
  • packages/web/src/server/functions/admin-projects.server.ts
  • packages/web/src/server/functions/admin-users.server.ts

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.

@InfinityBowman InfinityBowman changed the title Make the admin org page useful, and stop restating server types by hand Make the admin org page useful, and let admin types come from the server Sep 12, 2026
getAdminOrgDetails returned member and project counts and nothing else, so
the page could tell you an org had nine members and name none of them.

- Members and Projects panels sit directly under the header, both linking
  through to the user and project pages. Members are ordered owner first;
  a banned member is marked as such.
- Rows are capped at 50 while the counts stay true, and the panel says so
  when it is showing a subset.
- The counts move into the page description, so the two stat cards that only
  repeated the panel titles are gone.
- Billing Reconciliation collapses to a single row. It is a diagnostic tool
  with four thresholds and a Stripe toggle, and it was running its stuck-state
  query on every visit to an org page; it now loads only when opened.

Claude-Session: https://claude.ai/code/session_01LqxkXwhjRDsJ1N9cBYpU1n
The admin detail pages typed their data with hand-written interfaces that had
drifted from what the server actually returns, and nothing could catch it: the
components cast the query result to the interface, so TypeScript only ever
checked the interface against itself.

Three bugs were hiding behind that:

- Every `*DisplayName` field the UI read - userDisplayName, creatorDisplayName,
  uploaderDisplayName, inviterDisplayName - was declared in the interface and
  produced by no server code anywhere, so each one was permanently undefined
  and every render silently took the fallback branch.
- Project invitations computed expiry as `expiresAt * 1000`, but expiresAt is a
  Drizzle timestamp and arrives as a Date, so the arithmetic gave NaN and every
  invitation rendered as expired.
- A user's org access badge tested for 'limited', which is not in the union
  ('full' | 'readOnly' | 'free'), so read-only orgs never got the warning style
  and free orgs were styled as an error.

The interfaces are replaced with types inferred from the server functions, so
the compiler now checks the shape the server really returns. WorkspaceStats
stays hand-written - it describes the sync engine's API, not ours - but moves
next to the function that returns it, which removes an inline
import('@/components/...') in server code.

Left alone: the server still selects creatorGivenName, userGivenName,
uploaderGivenName and inviterGivenName, which no caller reads.

Claude-Session: https://claude.ai/code/session_01LqxkXwhjRDsJ1N9cBYpU1n
Every admin page cast its query result to a hand-written interface. The hooks
were already typed end to end - useAdminOrgDetails().data comes back as the
server function's exact return type - so the cast added nothing and replaced a
correct type with a drifting one. Dropping the casts lets inference reach the
components, and the compiler immediately found more of the same drift:

- OrgBillingSummary read billing.subscription.plan for its summary line, but
  the resolver's subscription carries no plan field; the line always rendered
  "Active subscription (undefined)". It now uses the effective plan's name.
- Its accessMode type omitted 'free', so the union was wrong in the same way
  the org badge was.
- The projects directory sorted its "Created by" column on creatorDisplayName,
  a field that does not exist, so the column never sorted.

The row types the tables need are exported from the server functions and
derived, not restated: AdminUserListItem, AdminOrgListItem,
AdminProjectListItem, AdminOrgSubscription, AdminOrgGrant, AdminLedgerEntry,
AdminTableColumn.

One cast stays, in the database viewer: the table is chosen at runtime, so its
rows really are Record<string, unknown>, and the cast sits at that one line
rather than over the whole query result.

Claude-Session: https://claude.ai/code/session_01LqxkXwhjRDsJ1N9cBYpU1n
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