@@ -53,7 +53,7 @@ export function ProjectMembersSection({ members, loading, onRemove }: ProjectMem
params={{ userId: member.userId } as Record
}
className='text-foreground hover:text-primary font-medium transition-colors'
>
- {member.userDisplayName || member.userName}
+ {member.userName}
{member.userEmail}
@@ -74,7 +74,7 @@ export function ProjectMembersSection({ members, loading, onRemove }: ProjectMem
className='text-muted-foreground/70 hover:text-destructive'
onClick={() => onRemove(member)}
disabled={loading}
- aria-label={`Remove ${member.userDisplayName || member.userName}`}
+ aria-label={`Remove ${member.userName}`}
>
diff --git a/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx b/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx
index b901b292..eda97255 100644
--- a/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx
+++ b/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx
@@ -2,7 +2,7 @@ import { RefreshCwIcon } from 'lucide-react';
import { AdminPanel, AdminField, AdminFieldGrid, AdminStat } from '@/components/admin/ui';
import { Button } from '@/components/ui/button';
import { formatFileSize } from '@corates/shared';
-import type { WorkspaceStats } from './types';
+import type { WorkspaceStats } from '@/server/functions/admin-projects.server';
interface WorkspaceStorageSectionProps {
stats?: WorkspaceStats;
diff --git a/packages/web/src/components/admin/projects/types.ts b/packages/web/src/components/admin/projects/types.ts
deleted file mode 100644
index 995ca9f3..00000000
--- a/packages/web/src/components/admin/projects/types.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-export interface ProjectMember {
- id: string;
- userId: string;
- role: string;
- userAvatar?: string;
- userDisplayName?: string;
- userName?: string;
- userEmail?: string;
- joinedAt?: string | number | Date;
-}
-
-export interface ProjectFile {
- id: string;
- originalName?: string;
- filename?: string;
- fileType?: string;
- fileSize?: number;
- uploadedBy?: string;
- uploaderDisplayName?: string;
- uploaderName?: string;
- createdAt?: string | number | Date;
-}
-
-export interface ProjectInvitation {
- id: string;
- email: string;
- role: string;
- grantOrgMembership?: boolean;
- acceptedAt?: string | number | Date | null;
- expiresAt?: number;
- invitedBy: string;
- inviterDisplayName?: string;
- inviterName?: string;
- createdAt?: string | number | Date;
-}
-
-export interface ProjectData {
- project: {
- id: string;
- name: string;
- orgId: string;
- orgName: string;
- orgSlug: string;
- createdBy: string;
- creatorDisplayName?: string;
- creatorName?: string;
- creatorEmail?: string;
- createdAt?: string | number | Date;
- updatedAt?: string | number | Date;
- };
- stats: {
- memberCount: number;
- fileCount: number;
- totalStorageBytes: number;
- };
- members?: ProjectMember[];
- files?: ProjectFile[];
- invitations?: ProjectInvitation[];
-}
-
-/** The sync-engine workspace's admin stats (`workspaceAdmin(...).stats()`). */
-export interface WorkspaceStats {
- workspaceId: string;
- backendId: string;
- schemaVersion: number;
- currentVersion: number;
- rows: { live: number; tombstones: number };
- mutationLogEntries: number;
- knownClients: number;
- databaseSizeBytes: number;
- connections: { total: number; ready: number; presence: number };
- /** The Yjs fields add-on's stats, when mounted. */
- extension?: {
- fields: number;
- frozenFields: number;
- fieldBytes: number;
- pendingUpdates: number;
- cachedDocs: number;
- };
-}
diff --git a/packages/web/src/components/admin/users/UserActions.tsx b/packages/web/src/components/admin/users/UserActions.tsx
index a4293379..97a116b2 100644
--- a/packages/web/src/components/admin/users/UserActions.tsx
+++ b/packages/web/src/components/admin/users/UserActions.tsx
@@ -1,9 +1,9 @@
import { LogInIcon, UserCheckIcon, UserXIcon, Trash2Icon } from 'lucide-react';
import { Button } from '@/components/ui/button';
-import type { UserData } from './types';
+import type { AdminUserDetails } from '@/server/functions/admin-users.server';
interface UserActionsProps {
- user: UserData['user'];
+ user: AdminUserDetails['user'];
loading: boolean;
onImpersonate: () => void;
onUnban: () => void;
diff --git a/packages/web/src/components/admin/users/UserLinkedAccounts.tsx b/packages/web/src/components/admin/users/UserLinkedAccounts.tsx
index f459d637..91260158 100644
--- a/packages/web/src/components/admin/users/UserLinkedAccounts.tsx
+++ b/packages/web/src/components/admin/users/UserLinkedAccounts.tsx
@@ -1,7 +1,7 @@
import { MailIcon } from 'lucide-react';
import { AdminEmpty, AdminPanel } from '@/components/admin/ui';
import { formatDate } from '@/lib/formatDate';
-import type { UserAccount } from './types';
+import type { AdminUserAccount } from '@/server/functions/admin-users.server';
const PROVIDER_LABEL: Record
= {
google: 'Google',
@@ -14,7 +14,7 @@ const PROVIDER_LOGO: Record = {
orcid: '/logos/orcid.svg',
};
-export function UserLinkedAccounts({ accounts }: { accounts?: UserAccount[] }) {
+export function UserLinkedAccounts({ accounts }: { accounts?: AdminUserAccount[] }) {
const rows = accounts ?? [];
return (
diff --git a/packages/web/src/components/admin/users/UserOrganizations.tsx b/packages/web/src/components/admin/users/UserOrganizations.tsx
index 8e15c685..6f07c091 100644
--- a/packages/web/src/components/admin/users/UserOrganizations.tsx
+++ b/packages/web/src/components/admin/users/UserOrganizations.tsx
@@ -10,9 +10,9 @@ import {
TableCell,
} from '@/components/ui/table';
import { formatDate } from '@/lib/formatDate';
-import type { UserOrg } from './types';
+import type { AdminUserOrg } from '@/server/functions/admin-users.server';
-export function UserOrganizations({ orgs }: { orgs?: UserOrg[] }) {
+export function UserOrganizations({ orgs }: { orgs?: AdminUserOrg[] }) {
const rows = orgs ?? [];
return (
@@ -50,9 +50,9 @@ export function UserOrganizations({ orgs }: { orgs?: UserOrg[] }) {
{org.billing.accessMode}
diff --git a/packages/web/src/components/admin/users/UserProfileSection.tsx b/packages/web/src/components/admin/users/UserProfileSection.tsx
index 4c83ac7b..833651d0 100644
--- a/packages/web/src/components/admin/users/UserProfileSection.tsx
+++ b/packages/web/src/components/admin/users/UserProfileSection.tsx
@@ -1,9 +1,9 @@
import { ExternalLinkIcon } from 'lucide-react';
import { AdminPanel, AdminField, AdminFieldGrid, CopyButton } from '@/components/admin/ui';
import { formatDateTime } from '@/lib/formatDate';
-import type { UserData } from './types';
+import type { AdminUserDetails } from '@/server/functions/admin-users.server';
-export function UserProfileSection({ user }: { user: UserData['user'] }) {
+export function UserProfileSection({ user }: { user: AdminUserDetails['user'] }) {
return (
diff --git a/packages/web/src/components/admin/users/UserProjects.tsx b/packages/web/src/components/admin/users/UserProjects.tsx
index e0a21a4a..1d436378 100644
--- a/packages/web/src/components/admin/users/UserProjects.tsx
+++ b/packages/web/src/components/admin/users/UserProjects.tsx
@@ -10,9 +10,9 @@ import {
TableCell,
} from '@/components/ui/table';
import { formatDate } from '@/lib/formatDate';
-import type { UserProject } from './types';
+import type { AdminUserProject } from '@/server/functions/admin-users.server';
-export function UserProjects({ projects }: { projects?: UserProject[] }) {
+export function UserProjects({ projects }: { projects?: AdminUserProject[] }) {
const rows = projects ?? [];
return (
diff --git a/packages/web/src/components/admin/users/UserSessions.tsx b/packages/web/src/components/admin/users/UserSessions.tsx
index 721d234e..1d9fabad 100644
--- a/packages/web/src/components/admin/users/UserSessions.tsx
+++ b/packages/web/src/components/admin/users/UserSessions.tsx
@@ -2,7 +2,7 @@ import { MonitorIcon, LogOutIcon } from 'lucide-react';
import { AdminEmpty, AdminPanel } from '@/components/admin/ui';
import { Button } from '@/components/ui/button';
import { formatDateTime } from '@/lib/formatDate';
-import type { UserSession } from './types';
+import type { AdminUserSession } from '@/server/functions/admin-users.server';
const parseUserAgent = (ua: string | undefined): { browser: string; os: string } => {
if (!ua) return { browser: 'Unknown', os: 'Unknown' };
@@ -24,7 +24,7 @@ const parseUserAgent = (ua: string | undefined): { browser: string; os: string }
};
interface UserSessionsProps {
- sessions?: UserSession[];
+ sessions?: AdminUserSession[];
loading: boolean;
onRevoke: (sessionId: string) => void;
onRevokeAll: () => void;
@@ -55,7 +55,7 @@ export function UserSessions({ sessions, loading, onRevoke, onRevokeAll }: UserS
{rows.length === 0 ?
: rows.map(session => {
- const { browser, os } = parseUserAgent(session.userAgent);
+ const { browser, os } = parseUserAgent(session.userAgent ?? undefined);
return (
diff --git a/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx b/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx
index 10297b56..91069609 100644
--- a/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx
@@ -24,26 +24,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { formatDateTime } from '@/lib/formatDate';
-
-interface LedgerEntry {
- receivedAt?: string | number;
- processedAt?: string | number;
- status: string;
- type?: string;
- stripeEventId?: string;
- orgId?: string;
- stripeCustomerId?: string;
- stripeSubscriptionId?: string;
- stripeCheckoutSessionId?: string;
- requestId?: string;
- error?: string;
- httpStatus?: number;
-}
-
-interface LedgerStats {
- total?: number;
- byStatus?: Record;
-}
+import type { AdminLedgerEntry } from '@/server/functions/admin-billing.server';
const STATUS_OPTIONS = [
{ value: '', label: 'All Statuses' },
@@ -110,11 +91,11 @@ function AdminBillingLedgerPage() {
type: debouncedTypeFilter || undefined,
});
- const data = ledgerQuery.data as { entries: LedgerEntry[]; stats: LedgerStats } | undefined;
+ const data = ledgerQuery.data;
const entries = data?.entries || [];
- const stats = data?.stats || {};
+ const stats = data?.stats;
- const columns = useMemo[]>(
+ const columns = useMemo[]>(
() => [
{
accessorKey: 'receivedAt',
@@ -318,12 +299,12 @@ function AdminBillingLedgerPage() {
}
>
-
+
{STATUS_OPTIONS.slice(1, 5).map(option => (
))}
diff --git a/packages/web/src/routes/_app/_protected/admin/database.tsx b/packages/web/src/routes/_app/_protected/admin/database.tsx
index 1168a595..989e341c 100644
--- a/packages/web/src/routes/_app/_protected/admin/database.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/database.tsx
@@ -15,6 +15,7 @@ import {
useAdminTableRows,
useAdminTableSchema,
} from '@/hooks/useAdminQueries';
+import type { AdminTableColumn } from '@/server/functions/admin-database.server';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -43,28 +44,6 @@ export const Route = createFileRoute('/_app/_protected/admin/database')({
const LIMIT_OPTIONS = [25, 50, 100];
-interface TableInfo {
- name: string;
- rowCount?: number;
-}
-
-interface ColumnSchema {
- name: string;
- type?: string;
- primaryKey?: boolean;
- foreignKey?: { table: string; column: string } | null;
-}
-
-interface TableRowsData {
- rows: Array>;
- pagination: {
- page: number;
- limit: number;
- totalRows: number;
- totalPages: number;
- };
-}
-
const formatCellValue = (value: unknown): string => {
if (value === null || value === undefined) return '-';
if (typeof value === 'boolean') return value ? 'true' : 'false';
@@ -83,14 +62,10 @@ function DatabaseViewerPage() {
const [filterValue, setFilterValue] = useState(null);
const tablesQuery = useAdminDatabaseTables();
- const tables = ((tablesQuery.data as { tables: TableInfo[] } | undefined)?.tables ??
- []) as TableInfo[];
+ const tables = tablesQuery.data?.tables ?? [];
const schemaQuery = useAdminTableSchema(selectedTable);
- const schemaColumns = useMemo(
- () => (schemaQuery.data as { columns: ColumnSchema[] } | undefined)?.columns ?? [],
- [schemaQuery.data],
- );
+ const schemaColumns = useMemo(() => schemaQuery.data?.columns ?? [], [schemaQuery.data]);
const rowsQuery = useAdminTableRows({
tableName: selectedTable ?? undefined,
@@ -102,8 +77,10 @@ function DatabaseViewerPage() {
filterValue,
});
- const rowsData = rowsQuery.data as TableRowsData | undefined;
- const rows = useMemo(() => rowsData?.rows ?? [], [rowsData]);
+ const rowsData = rowsQuery.data;
+ // The viewer renders whatever table was picked at runtime, so the rows are
+ // read by column name rather than as one known shape.
+ const rows = useMemo(() => (rowsData?.rows ?? []) as Record[], [rowsData]);
const pagination = rowsData?.pagination ?? { page: 1, limit, totalRows: 0, totalPages: 0 };
const columns = useMemo(() => {
@@ -112,7 +89,7 @@ function DatabaseViewerPage() {
}, [rows]);
const columnSchemaMap = useMemo(() => {
- const map: Record = {};
+ const map: Record = {};
for (const col of schemaColumns) {
map[col.name] = col;
}
diff --git a/packages/web/src/routes/_app/_protected/admin/index.tsx b/packages/web/src/routes/_app/_protected/admin/index.tsx
index deabbedc..86b52743 100644
--- a/packages/web/src/routes/_app/_protected/admin/index.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/index.tsx
@@ -9,7 +9,7 @@ export const Route = createFileRoute('/_app/_protected/admin/')({
function AdminDashboard() {
const statsQuery = useAdminStats();
- const stats = statsQuery.data as Record | undefined;
+ const stats = statsQuery.data;
return (
diff --git a/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx b/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx
index c264a8f5..86e877a3 100644
--- a/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx
@@ -23,7 +23,7 @@ import {
AlertDialogAction,
} from '@/components/ui/alert-dialog';
import { handleError } from '@/lib/error-utils';
-import { AdminError, AdminPage, AdminStat, AdminStatRow } from '@/components/admin/ui';
+import { AdminError, AdminPage } from '@/components/admin/ui';
import { formatDateInput } from '@/lib/formatDate';
import { OrgBillingSummary } from '@/components/admin/OrgBillingSummary';
import { OrgQuickActions } from '@/components/admin/OrgQuickActions';
@@ -32,6 +32,9 @@ import { SubscriptionDialog } from '@/components/admin/SubscriptionDialog';
import { GrantList } from '@/components/admin/GrantList';
import { GrantDialog } from '@/components/admin/GrantDialog';
import { OrgBillingReconcilePanel } from '@/components/admin/OrgBillingReconcilePanel';
+import { OrgMembersSection } from '@/components/admin/orgs/OrgMembersSection';
+import { OrgProjectsSection } from '@/components/admin/orgs/OrgProjectsSection';
+import type { AdminOrgSubscription } from '@/server/functions/admin-orgs.server';
import { queryKeys } from '@/lib/queryKeys';
const BACK_TO_ORGS = { to: '/admin/orgs', label: 'Back to Organizations' };
@@ -40,57 +43,19 @@ export const Route = createFileRoute('/_app/_protected/admin/orgs/$orgId')({
component: OrgDetailPage,
});
-interface OrgDetails {
- org?: { name?: string; slug?: string };
- stats?: { memberCount?: number; projectCount?: number };
-}
-
-interface SubscriptionRecord {
- id: string;
- plan: string;
- status: string;
- periodStart?: string | number | Date;
- periodEnd?: string | number | Date;
- cancelAtPeriodEnd?: boolean;
- canceledAt?: string | number | Date | null;
- endedAt?: string | number | Date | null;
- stripeCustomerId?: string;
- stripeSubscriptionId?: string;
-}
-
-interface BillingData {
- billing?: {
- plan?: {
- name?: string;
- entitlements?: Record;
- quotas?: Record;
- };
- effectivePlanId?: string;
- accessMode?: 'full' | 'readOnly';
- source?: 'free' | 'subscription' | 'grant';
- subscription?: { id?: string; plan?: string } | null;
- grant?: { type?: string } | null;
- };
- subscriptions?: SubscriptionRecord[];
- grants?: Array<{
- id: string;
- type: string;
- startsAt?: string | number | Date;
- expiresAt?: string | number | Date;
- createdAt?: string | number | Date;
- revokedAt?: string | number | Date | null;
- }>;
-}
-
function OrgDetailPage() {
const { orgId } = Route.useParams();
const queryClient = useQueryClient();
const orgDetailsQuery = useAdminOrgDetails(orgId);
const billingQuery = useAdminOrgBilling(orgId);
- const orgDetails = orgDetailsQuery.data as OrgDetails | undefined;
- const billing = billingQuery.data as BillingData | undefined;
+ const orgDetails = orgDetailsQuery.data;
+ const billing = billingQuery.data;
+
+ const memberCount = orgDetails?.stats?.memberCount ?? 0;
+ const projectCount = orgDetails?.stats?.projectCount ?? 0;
+ const [reconcileOpen, setReconcileOpen] = useState(false);
const [subscriptionDialogOpen, setSubscriptionDialogOpen] = useState(false);
const [grantDialogOpen, setGrantDialogOpen] = useState(false);
const [confirmDialog, setConfirmDialog] = useState<{
@@ -99,7 +64,7 @@ function OrgDetailPage() {
grantId?: string;
} | null>(null);
const [loading, setLoading] = useState(false);
- const [editingSubscription, setEditingSubscription] = useState(null);
+ const [editingSubscription, setEditingSubscription] = useState(null);
// Subscription form state
const [subPlan, setSubPlan] = useState('team');
@@ -235,7 +200,7 @@ function OrgDetailPage() {
}
};
- const handleEditSubscription = (subscription: SubscriptionRecord) => {
+ const handleEditSubscription = (subscription: AdminOrgSubscription) => {
setEditingSubscription(subscription);
setSubPlan(subscription.plan);
setSubStatus(subscription.status);
@@ -342,21 +307,24 @@ function OrgDetailPage() {
-
-
-
-
+
+
+
@@ -388,7 +356,17 @@ function OrgDetailPage() {
}
/>
-
+ {reconcileOpen ?
+ setReconcileOpen(false)} />
+ :
+ }
{/* Subscription Dialog */}
{
setSearch(value);
setPage(1);
};
- const columns = useMemo[]>(
+ const columns = useMemo[]>(
() => [
{
accessorKey: 'name',
@@ -143,7 +128,7 @@ function AdminOrgList() {
/>
}
enableSorting
- onRowClick={(row: OrgRow) =>
+ onRowClick={(row: AdminOrgListItem) =>
navigate({
to: '/admin/orgs/$orgId' as string,
params: { orgId: row.id } as Record,
diff --git a/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx b/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx
index 108cdae6..c5d5cf8d 100644
--- a/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx
@@ -10,7 +10,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { handleError } from '@/lib/error-utils';
import { queryKeys } from '@/lib/queryKeys';
import { AdminError, AdminPage, AdminPanel } from '@/components/admin/ui';
-import type { ProjectData, WorkspaceStats, ProjectMember } from '@/components/admin/projects/types';
+import type { AdminProjectMember } from '@/server/functions/admin-projects.server';
import { ProjectInfoSection } from '@/components/admin/projects/ProjectInfoSection';
import { WorkspaceStorageSection } from '@/components/admin/projects/WorkspaceStorageSection';
import { ProjectMembersSection } from '@/components/admin/projects/ProjectMembersSection';
@@ -33,17 +33,17 @@ function ProjectDetailPage() {
const queryClient = useQueryClient();
const projectQuery = useAdminProjectDetails(projectId);
- const projectData = projectQuery.data as ProjectData | undefined;
+ const projectData = projectQuery.data;
// DO storage stats are fetched separately because they route through the
// ProjectDoc DO and are slower than the D1 details query. Loading them as
// a sibling query lets the rest of the page render immediately.
const statsQuery = useAdminWorkspaceStats(projectId);
- const workspaceStats = statsQuery.data as WorkspaceStats | undefined;
+ const workspaceStats = statsQuery.data;
const [confirmDialog, setConfirmDialog] = useState<{
type: 'delete-project' | 'remove-member';
- member?: ProjectMember;
+ member?: AdminProjectMember;
} | null>(null);
const [loading, setLoading] = useState(false);
@@ -156,9 +156,7 @@ function ProjectDetailPage() {
open={confirmDialog?.type === 'remove-member'}
onOpenChange={open => !open && setConfirmDialog(null)}
memberName={
- confirmDialog?.member?.userDisplayName ||
- confirmDialog?.member?.userName ||
- confirmDialog?.member?.userEmail
+ confirmDialog?.member?.userName || confirmDialog?.member?.userEmail || undefined
}
onConfirm={() => {
if (confirmDialog?.member?.id) {
diff --git a/packages/web/src/routes/_app/_protected/admin/projects.index.tsx b/packages/web/src/routes/_app/_protected/admin/projects.index.tsx
index 6f8b4022..9a45f7de 100644
--- a/packages/web/src/routes/_app/_protected/admin/projects.index.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/projects.index.tsx
@@ -3,6 +3,7 @@ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
import { FolderIcon } from 'lucide-react';
import { useAdminProjects, useAdminOrgs } from '@/hooks/useAdminQueries';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
+import type { AdminProjectListItem } from '@/server/functions/admin-projects.server';
import { formatDate } from '@/lib/formatDate';
import {
AdminDataTable,
@@ -23,26 +24,6 @@ import {
SelectValue,
} from '@/components/ui/select';
-interface ProjectRow {
- id: string;
- name: string;
- orgId: string;
- orgName: string;
- orgSlug: string;
- createdBy: string;
- creatorDisplayName?: string;
- creatorName?: string;
- creatorEmail?: string;
- memberCount: number;
- fileCount: number;
- createdAt?: string | number;
-}
-
-interface OrgOption {
- id: string;
- name: string;
-}
-
const PAGE_SIZE = 25;
const ALL_ORGS_VALUE = 'all';
@@ -63,15 +44,10 @@ function AdminProjectList() {
search: debouncedSearch,
orgId: selectedOrgId,
});
- const projectsData = projectsQuery.data as
- | {
- projects: ProjectRow[];
- pagination: { page: number; total: number; totalPages: number };
- }
- | undefined;
+ const projectsData = projectsQuery.data;
const orgsQuery = useAdminOrgs({ page: 1, limit: 100, search: '' });
- const orgsData = orgsQuery.data as { orgs: OrgOption[] } | undefined;
+ const orgsData = orgsQuery.data;
const projects = projectsData?.projects || [];
const pagination = projectsData?.pagination;
@@ -87,7 +63,7 @@ function AdminProjectList() {
setPage(1);
};
- const columns = useMemo[]>(
+ const columns = useMemo[]>(
() => [
{
accessorKey: 'name',
@@ -112,11 +88,11 @@ function AdminProjectList() {
},
},
{
- accessorKey: 'creatorDisplayName',
+ accessorKey: 'creatorName',
header: 'Created by',
cell: info => {
const project = info.row.original;
- const name = project.creatorDisplayName || project.creatorName;
+ const name = project.creatorName;
if (!name && !project.creatorEmail) {
return -;
}
@@ -244,7 +220,7 @@ function AdminProjectList() {
/>
}
enableSorting
- onRowClick={(row: ProjectRow) =>
+ onRowClick={(row: AdminProjectListItem) =>
navigate({
to: '/admin/projects/$projectId' as string,
params: { projectId: row.id } as Record,
diff --git a/packages/web/src/routes/_app/_protected/admin/storage.tsx b/packages/web/src/routes/_app/_protected/admin/storage.tsx
index 490ddb0d..664ca4a6 100644
--- a/packages/web/src/routes/_app/_protected/admin/storage.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/storage.tsx
@@ -44,22 +44,6 @@ export const Route = createFileRoute('/_app/_protected/admin/storage')({
component: StorageManagementPage,
});
-interface StorageDocument {
- key: string;
- fileName: string;
- size?: number;
- projectId?: string;
- studyId?: string;
- uploaded?: string;
- orphaned?: boolean;
-}
-
-interface StorageDocumentsData {
- documents: StorageDocument[];
- nextCursor?: string | null;
- truncated?: boolean;
-}
-
const PAGE_SIZE = 50;
function StorageManagementPage() {
@@ -78,7 +62,7 @@ function StorageManagementPage() {
prefix,
search: debouncedSearch,
});
- const documentsData = documentsDataQuery.data as StorageDocumentsData | undefined;
+ const documentsData = documentsDataQuery.data;
const documents = documentsData?.documents ?? [];
const resetPaging = () => {
diff --git a/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx b/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx
index bea3002f..6a19df44 100644
--- a/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx
@@ -19,7 +19,6 @@ import { handleError } from '@/lib/error-utils';
import { queryKeys } from '@/lib/queryKeys';
import { AdminError, AdminPage, AdminPanel } from '@/components/admin/ui';
import { Skeleton } from '@/components/ui/skeleton';
-import type { UserData } from '@/components/admin/users/types';
import { UserActions } from '@/components/admin/users/UserActions';
import { UserProfileSection } from '@/components/admin/users/UserProfileSection';
import { UserLinkedAccounts } from '@/components/admin/users/UserLinkedAccounts';
@@ -76,7 +75,7 @@ function UserDetailContent() {
const qc = useQueryClient();
const { data } = useSuspenseQuery(adminUserDetailsQueryOptions(userId));
- const userData = data as unknown as UserData;
+ const userData = data;
const user = userData.user;
const [confirmDialog, setConfirmDialog] = useState<{
@@ -172,7 +171,11 @@ function UserDetailContent() {
back={BACK_TO_USERS}
title={
-
+
{user.name}
}
diff --git a/packages/web/src/routes/_app/_protected/admin/users.index.tsx b/packages/web/src/routes/_app/_protected/admin/users.index.tsx
index f426cfcd..05feff8f 100644
--- a/packages/web/src/routes/_app/_protected/admin/users.index.tsx
+++ b/packages/web/src/routes/_app/_protected/admin/users.index.tsx
@@ -22,12 +22,7 @@ function AdminUserList() {
limit: PAGE_SIZE,
search: debouncedSearch,
});
- const usersData = usersDataQuery.data as
- | {
- users: Array<{ id: string; [key: string]: unknown }>;
- pagination: { limit: number; total: number; totalPages: number };
- }
- | undefined;
+ const usersData = usersDataQuery.data;
const handleSearchChange = (value: string) => {
setSearch(value);
diff --git a/packages/web/src/server/functions/admin-billing.server.ts b/packages/web/src/server/functions/admin-billing.server.ts
index 8d529627..2e31cc2d 100644
--- a/packages/web/src/server/functions/admin-billing.server.ts
+++ b/packages/web/src/server/functions/admin-billing.server.ts
@@ -215,3 +215,5 @@ export async function getAdminBillingStuckStates(
stuckOrgs,
};
}
+
+export type AdminLedgerEntry = Awaited>['entries'][number];
diff --git a/packages/web/src/server/functions/admin-database.server.ts b/packages/web/src/server/functions/admin-database.server.ts
index 4d975c13..5dd3b0e1 100644
--- a/packages/web/src/server/functions/admin-database.server.ts
+++ b/packages/web/src/server/functions/admin-database.server.ts
@@ -307,3 +307,5 @@ export async function getAdminTableRows(
},
};
}
+
+export type AdminTableColumn = Awaited>['columns'][number];
diff --git a/packages/web/src/server/functions/admin-orgs.server.ts b/packages/web/src/server/functions/admin-orgs.server.ts
index 6a0d7cfc..1251a811 100644
--- a/packages/web/src/server/functions/admin-orgs.server.ts
+++ b/packages/web/src/server/functions/admin-orgs.server.ts
@@ -1,7 +1,7 @@
import { captureError, info } from '@corates/workers/logger';
import { env } from 'cloudflare:workers';
import type { Database } from '@corates/db/client';
-import { organization, member, projects, subscription } from '@corates/db/schema';
+import { organization, member, projects, subscription, user } from '@corates/db/schema';
import { and, count, desc, eq, or, sql } from 'drizzle-orm';
import { containsInsensitive } from '@/server/lib/sqlSearch';
import {
@@ -175,6 +175,10 @@ export async function listAdminOrgs(
};
}
+// The detail page shows counts from the real totals, so the rows themselves can
+// stay capped without the page reporting a smaller org than it is.
+const ORG_DETAIL_ROW_LIMIT = 50;
+
export async function getAdminOrgDetails(session: Session, db: Database, orgId: OrgId) {
assertAdmin(session);
@@ -197,6 +201,43 @@ export async function getAdminOrgDetails(session: Session, db: Database, orgId:
.all();
const projectCount = projectCountResult?.count || 0;
+ const members = await db
+ .select({
+ id: member.id,
+ userId: member.userId,
+ role: member.role,
+ joinedAt: member.createdAt,
+ userName: user.name,
+ userEmail: user.email,
+ userAvatar: user.avatarUrl,
+ userBanned: user.banned,
+ })
+ .from(member)
+ .leftJoin(user, eq(member.userId, user.id))
+ .where(eq(member.organizationId, orgId))
+ .orderBy(
+ sql`CASE ${member.role} WHEN 'owner' THEN 0 WHEN 'admin' THEN 1 ELSE 2 END`,
+ desc(member.createdAt),
+ )
+ .limit(ORG_DETAIL_ROW_LIMIT)
+ .all();
+
+ const orgProjects = await db
+ .select({
+ id: projects.id,
+ name: projects.name,
+ createdBy: projects.createdBy,
+ creatorName: user.name,
+ creatorEmail: user.email,
+ createdAt: projects.createdAt,
+ })
+ .from(projects)
+ .leftJoin(user, eq(projects.createdBy, user.id))
+ .where(eq(projects.orgId, orgId))
+ .orderBy(desc(projects.createdAt))
+ .limit(ORG_DETAIL_ROW_LIMIT)
+ .all();
+
const orgBilling = await resolveOrgAccess(db, orgId);
const effectivePlan =
orgBilling.source === 'grant' ?
@@ -206,6 +247,8 @@ export async function getAdminOrgDetails(session: Session, db: Database, orgId:
return {
org,
stats: { memberCount, projectCount },
+ members,
+ projects: orgProjects,
billing: {
effectivePlanId: orgBilling.effectivePlanId,
source: orgBilling.source,
@@ -221,6 +264,15 @@ export async function getAdminOrgDetails(session: Session, db: Database, orgId:
};
}
+export type AdminOrgBilling = Awaited>;
+export type AdminOrgBillingState = AdminOrgBilling['billing'];
+export type AdminOrgSubscription = AdminOrgBilling['subscriptions'][number];
+export type AdminOrgGrant = AdminOrgBilling['grants'][number];
+export type AdminOrgListItem = Awaited>['orgs'][number];
+export type AdminOrgDetails = Awaited>;
+export type AdminOrgMember = AdminOrgDetails['members'][number];
+export type AdminOrgProject = AdminOrgDetails['projects'][number];
+
export async function getAdminOrgBilling(session: Session, db: Database, orgId: OrgId) {
assertAdmin(session);
diff --git a/packages/web/src/server/functions/admin-projects.server.ts b/packages/web/src/server/functions/admin-projects.server.ts
index bfdd300a..c67cd6b9 100644
--- a/packages/web/src/server/functions/admin-projects.server.ts
+++ b/packages/web/src/server/functions/admin-projects.server.ts
@@ -220,6 +220,95 @@ export async function getAdminProjectDetails(session: Session, db: Database, pro
};
}
+export interface ProjectMember {
+ id: string;
+ userId: string;
+ role: string;
+ userAvatar?: string;
+ userDisplayName?: string;
+ userName?: string;
+ userEmail?: string;
+ joinedAt?: string | number | Date;
+}
+
+export interface ProjectFile {
+ id: string;
+ originalName?: string;
+ filename?: string;
+ fileType?: string;
+ fileSize?: number;
+ uploadedBy?: string;
+ uploaderDisplayName?: string;
+ uploaderName?: string;
+ createdAt?: string | number | Date;
+}
+
+export interface ProjectInvitation {
+ id: string;
+ email: string;
+ role: string;
+ grantOrgMembership?: boolean;
+ acceptedAt?: string | number | Date | null;
+ expiresAt?: number;
+ invitedBy: string;
+ inviterDisplayName?: string;
+ inviterName?: string;
+ createdAt?: string | number | Date;
+}
+
+export interface ProjectData {
+ project: {
+ id: string;
+ name: string;
+ orgId: string;
+ orgName: string;
+ orgSlug: string;
+ createdBy: string;
+ creatorDisplayName?: string;
+ creatorName?: string;
+ creatorEmail?: string;
+ createdAt?: string | number | Date;
+ updatedAt?: string | number | Date;
+ };
+ stats: {
+ memberCount: number;
+ fileCount: number;
+ totalStorageBytes: number;
+ };
+ members?: ProjectMember[];
+ files?: ProjectFile[];
+ invitations?: ProjectInvitation[];
+}
+
+/** The sync-engine workspace's admin stats (`workspaceAdmin(...).stats()`). */
+export interface WorkspaceStats {
+ workspaceId: string;
+ backendId: string;
+ schemaVersion: number;
+ currentVersion: number;
+ rows: { live: number; tombstones: number };
+ mutationLogEntries: number;
+ knownClients: number;
+ databaseSizeBytes: number;
+ connections: { total: number; ready: number; presence: number };
+ /** The Yjs fields add-on's stats, when mounted. */
+ extension?: {
+ fields: number;
+ frozenFields: number;
+ fieldBytes: number;
+ pendingUpdates: number;
+ cachedDocs: number;
+ };
+}
+
+export type AdminProjectListItem = Awaited<
+ ReturnType
+>['projects'][number];
+export type AdminProjectDetails = Awaited>;
+export type AdminProjectMember = AdminProjectDetails['members'][number];
+export type AdminProjectFile = AdminProjectDetails['files'][number];
+export type AdminProjectInvitation = AdminProjectDetails['invitations'][number];
+
export async function getAdminWorkspaceStats(session: Session, db: Database, projectId: string) {
assertAdmin(session);
@@ -234,7 +323,7 @@ export async function getAdminWorkspaceStats(session: Session, db: Database, pro
}
const stats = await projectWorkspace(env, projectId).stats();
- return stats as import('@/components/admin/projects/types').WorkspaceStats;
+ return stats as WorkspaceStats;
}
export async function removeAdminProjectMember(
diff --git a/packages/web/src/server/functions/admin-users.server.ts b/packages/web/src/server/functions/admin-users.server.ts
index cb4ffa21..ef11c185 100644
--- a/packages/web/src/server/functions/admin-users.server.ts
+++ b/packages/web/src/server/functions/admin-users.server.ts
@@ -238,6 +238,13 @@ export async function getAdminUserDetails(session: Session, db: Database, userId
};
}
+export type AdminUserListItem = Awaited>['users'][number];
+export type AdminUserDetails = Awaited>;
+export type AdminUserProject = AdminUserDetails['projects'][number];
+export type AdminUserSession = AdminUserDetails['sessions'][number];
+export type AdminUserAccount = AdminUserDetails['accounts'][number];
+export type AdminUserOrg = AdminUserDetails['orgs'][number];
+
export async function deleteAdminUser(session: Session, db: Database, userId: string) {
assertAdmin(session);