Admin Dashboard
Platform administration interface for managing users, solvers, tickets, sessions, and viewing metrics
Admin Dashboard
The admin dashboard provides administrators and owners with tools to monitor platform health, manage users and solvers, and oversee all tickets and sessions. Access requires the admin role or higher.
Accessing the Admin Dashboard
The admin section appears in the main sidebar for users with admin or owner roles. Navigation items include:
- Overview - Platform metrics and KPIs
- Users - User management and role assignment
- Tickets - All customer support requests
- Sessions - All support sessions
- Solvers - Solver management and approval
The admin layout enforces role requirements via server-side guards:
import { roleGuard } from "@/lib/auth";
export default async function AdminLayout({ children }) {
await roleGuard("admin", "/dashboard");
return <>{children}</>;
}Overview Page
The overview page displays real-time platform metrics through the AdminMetrics component.
Key Metrics
| Metric | Description |
|---|---|
| Active Sessions | Currently ongoing support sessions |
| Pending Tickets | Tickets awaiting solver claim |
| Total Users | All registered platform users |
| Total Solvers | Registered solvers (available/busy/offline breakdown) |
| Revenue Today | Earnings from completed sessions today |
| Revenue This Week | Rolling 7-day revenue |
| Revenue This Month | Rolling 30-day revenue |
| Average Rating | Mean customer rating across all sessions |
| Payment Rate | Percentage of completed sessions that are paid |
Metrics API
Metrics are fetched via the admin.getMetrics tRPC procedure:
getMetrics: adminProcedure.query(async ({ ctx }) => {
const [
activeSessions,
pendingTickets,
totalUsers,
solverStats,
revenueStats,
ratingStats,
] = await Promise.all([
db.select({ count: count() }).from(sessions)
.where(eq(sessions.status, "active")),
db.select({ count: count() }).from(tickets)
.where(eq(tickets.status, "pending")),
db.select({ count: count() }).from(users),
// ... additional queries
]);
return {
activeSessions: activeSessions[0]?.count ?? 0,
pendingTickets: pendingTickets[0]?.count ?? 0,
// ... formatted metrics
};
}),User Management
The users page provides full user administration capabilities with a list view and detailed individual user pages.
Features
- Search - Filter users by email or name
- Role Filter - View users by role (customer, solver, admin, owner)
- Pagination - 20 users per page
- User Detail Page - View comprehensive user information at
/admin/users/[id] - Role Assignment - Change user roles with notes for audit trail
- Suspension - Remove access from problematic users
- Audit Logging - All changes tracked with actor, timestamp, and notes
User Detail Page
Each user has a dedicated detail page showing:
- User Information - Email, role, account creation date
- Solver Information - If the user is a solver, shows status, employment type, and session count
- Recent Tickets - Customer tickets associated with this user
- Audit History - Complete log of all administrative actions taken on this user
Role Hierarchy and Permissions
The platform enforces a strict role hierarchy for permission boundaries:
| Role | Level | Can Assign | Can Modify |
|---|---|---|---|
| Customer | 1 | - | - |
| Solver | 2 | - | - |
| Admin | 3 | Customer, Solver | Customers, Solvers |
| Owner | 4 | All roles | All users |
Key restrictions:
- Users cannot change their own role
- Admins cannot assign admin or owner roles
- Admins cannot modify other admins
- Only owners can assign the admin or owner role
const ROLE_HIERARCHY: Record<UserRole, number> = {
customer: 1,
solver: 2,
admin: 3,
owner: 4,
};
function canAssignRole(actorRole: UserRole, targetRole: UserRole): boolean {
if (actorRole === "owner") return true;
if (actorRole === "admin") {
return targetRole === "customer" || targetRole === "solver";
}
return false;
}
function canModifyUser(
actorRole: UserRole,
targetRole: UserRole,
isSelf: boolean
): boolean {
if (isSelf) return false;
if (actorRole === "owner") return true;
return ROLE_HIERARCHY[actorRole] > ROLE_HIERARCHY[targetRole];
}Changing User Roles
Role changes include optional notes for the audit trail:
updateUserRole: adminProcedure
.input(z.object({
userId: z.string().uuid(),
role: userRoleSchema,
notes: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const actor = ctx.user;
const targetUser = await db.query.users.findFirst({
where: eq(users.id, input.userId),
});
// Permission checks
if (actor.id === targetUser.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "You cannot change your own role" });
}
if (!canModifyUser(actor.role, targetUser.role, false)) {
throw new TRPCError({ code: "FORBIDDEN", message: `You cannot modify users with ${targetUser.role} role` });
}
if (!canAssignRole(actor.role, input.role)) {
throw new TRPCError({ code: "FORBIDDEN", message: `You cannot assign the ${input.role} role` });
}
// Update role
const [updated] = await db.update(users)
.set({ role: input.role })
.where(eq(users.id, input.userId))
.returning();
// Create audit log
await createAuditLog({
actorId: actor.id,
action: "role_change",
targetType: "user",
targetId: input.userId,
previousValue: { role: targetUser.role },
newValue: { role: input.role },
notes: input.notes,
});
return updated;
}),Suspending Users
Suspension follows the same permission boundaries and creates an audit log:
suspendUser: adminProcedure
.input(z.object({
userId: z.string().uuid(),
notes: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const actor = ctx.user;
const targetUser = await db.query.users.findFirst({
where: eq(users.id, input.userId),
});
// Permission checks
if (actor.id === targetUser.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "You cannot suspend yourself" });
}
if (!canModifyUser(actor.role, targetUser.role, false)) {
throw new TRPCError({ code: "FORBIDDEN", message: `You cannot suspend users with ${targetUser.role} role` });
}
// Create audit log
await createAuditLog({
actorId: actor.id,
action: "user_suspend",
targetType: "user",
targetId: input.userId,
previousValue: { role: targetUser.role },
notes: input.notes,
});
return { success: true };
}),Audit Logging
All administrative actions are logged to the audit_logs table for accountability and compliance.
Logged Actions
| Action | Description |
|---|---|
role_change | User role was modified |
user_suspend | User was suspended |
solver_approve | Solver was approved to go live |
solver_update | Solver settings were modified |
application_approve | Solver application was approved |
application_reject | Solver application was rejected |
Audit Log Schema
export const auditLogs = pgTable("audit_logs", {
id: uuid("id").primaryKey().defaultRandom(),
actorId: uuid("actor_id").notNull().references(() => users.id),
action: auditActionEnum("action").notNull(),
targetType: varchar("target_type", { length: 50 }).notNull(),
targetId: uuid("target_id").notNull(),
previousValue: jsonb("previous_value"),
newValue: jsonb("new_value"),
notes: text("notes"),
ipAddress: varchar("ip_address", { length: 45 }),
userAgent: text("user_agent"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});Viewing Audit Logs
Audit logs appear in two places:
- User Detail Page - Shows logs related to that specific user
- Global Audit Log - Available via
admin.getAuditLogsprocedure (paginated)
Ticket Management
The tickets page provides a read-only view of all support requests.
Filtering Options
- All - Every ticket regardless of status
- Pending - Awaiting solver claim
- Claimed - Assigned to a solver but session not started
- In Progress - Active session underway
- Completed - Successfully resolved
- Cancelled - Abandoned by customer or solver
- Refunded - Payment refunded
Displayed Information
| Column | Description |
|---|---|
| Customer | Name and phone number |
| Description | Truncated problem description |
| Device | Device type (computer, phone, tablet, other) |
| Urgency | Priority level (low, normal, high, urgent) |
| Status | Current ticket status with color-coded badge |
| Created | Timestamp of ticket creation |
Session Management
The sessions page shows all support sessions with their details.
Filtering Options
- All - Every session
- Not Started - Ticket claimed but session not begun
- Active - Currently in progress
- Paused - Temporarily on hold
- Completed - Successfully finished
- Cancelled - Abandoned or terminated early
Displayed Information
| Column | Description |
|---|---|
| Customer | Customer name from associated ticket |
| Solver | Assigned solver's name |
| Duration | Session length in MM:SS format |
| Tier | Pricing tier (standard, premium, enterprise) |
| Earnings | Solver's earnings in USD |
| Status | Session status with color-coded badge |
| Started | Session start timestamp |
Solver Management
The solvers page enables solver oversight and approval workflows.
Pending Approvals Section
New solvers start in "onboarding" status and appear in a dedicated section at the top of the page. Each pending solver shows:
- Name and email
- Application timestamp
- Approve button to activate the solver
approveSolver: adminProcedure
.input(z.object({ solverId: z.string().uuid() }))
.mutation(async ({ input }) => {
const solver = await db.query.solvers.findFirst({
where: eq(solvers.id, input.solverId),
});
if (!solver) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Solver not found",
});
}
if (solver.status !== "onboarding") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Solver is not in onboarding status",
});
}
await db.update(solvers)
.set({ status: "available" })
.where(eq(solvers.id, input.solverId));
return { success: true };
}),Solver Table
The main table shows all active solvers with:
| Column | Description |
|---|---|
| Name | Solver's display name |
| Employment | W-2 (employed) or 1099 (contract) |
| Status | Current availability status |
| Sessions | Total completed sessions |
| Earnings | Lifetime earnings |
| Rating | Average customer rating |
| Actions | Edit button for solver settings |
Editing Solver Settings
Clicking edit opens a dialog to modify:
- Employment Type - Switch between employed and contract
- Google Email - Workspace email for Meet integration (employed only)
updateSolver: adminProcedure
.input(z.object({
solverId: z.string().uuid(),
employment: z.enum(["employed", "contract"]).optional(),
status: solverStatusSchema.optional(),
googleEmail: z.string().email().optional().nullable(),
revenueSharePercent: z.number().min(0).max(100).optional(),
callPreference: z.enum(["in_app", "bridge"]).optional(),
}))
.mutation(async ({ input }) => {
const { solverId, ...updates } = input;
// Only include provided fields
const updateData: Partial<typeof solvers.$inferInsert> = {};
if (updates.employment !== undefined) updateData.employment = updates.employment;
if (updates.status !== undefined) updateData.status = updates.status;
if (updates.googleEmail !== undefined) updateData.googleEmail = updates.googleEmail;
if (updates.revenueSharePercent !== undefined) {
updateData.revenueSharePercent = updates.revenueSharePercent;
}
if (updates.callPreference !== undefined) {
updateData.callPreference = updates.callPreference;
}
await db.update(solvers)
.set(updateData)
.where(eq(solvers.id, solverId));
return { success: true };
}),Filtering Options
- Status - Available, busy, offline, onboarding
- Employment - Employed (W-2) or Contract (1099)
File Structure
app/(authenticated)/admin/
├── layout.tsx # Role guard and layout wrapper
├── page.tsx # Overview with metrics
├── users/
│ ├── page.tsx # User list with search/filter
│ └── [id]/
│ └── page.tsx # User detail with role management
├── tickets/
│ └── page.tsx # Ticket list view
├── sessions/
│ └── page.tsx # Session list view
├── solvers/
│ └── page.tsx # Solver management
└── applications/
└── page.tsx # Solver application review
components/admin/
├── AdminMetrics.tsx # Metrics display component
└── index.ts # Component exports
lib/db/schema/
└── audit-logs.ts # Audit log table and enum
server/routers/
└── admin.ts # All admin tRPC proceduresRelated Documentation
- User Role System - RBAC and permissions
- Solver Onboarding - New solver flow
- Database Architecture - Schema reference