Command Center
Unified dashboard workspace for solvers to monitor queues, manage sessions, and track system health
Command Center
The Command Center is a unified workspace that transforms the solver dashboard from a simple stats page into an operational hub. It addresses a core insight from the PRD: solvers need to live in "flow state" while helping customers. Rather than navigating between separate pages for queue monitoring, session management, and status updates, everything lives in one view.
This feature directly supports the DoorDash-style experience described in the PRD—solvers can claim tickets, start ad-hoc sessions, and resume active work without context switching.
Why a Command Center?
Traditional dashboards separate concerns into distinct pages: a queue page, a sessions page, an earnings page. For on-demand support work, this creates friction:
- Claim → Navigate → Work becomes a multi-step dance
- Active sessions get forgotten when reviewing the queue
- System health issues (disconnected WebSocket) go unnoticed
The Command Center consolidates these into a single view where solvers can:
- See pending tickets and claim them instantly
- Monitor their active session with one-click resume
- Start ad-hoc sessions without needing a ticket first
- Verify their real-time connection is healthy
- Review recent completed sessions and earnings
Architecture
The Command Center is implemented as the main dashboard page with several specialized components:
app/(authenticated)/dashboard/page.tsx # Main Command Center layout
components/dashboard/
├── InlineQueue.tsx # Compact queue widget
├── ActiveSessionCard.tsx # Current session panel
├── SystemHealth.tsx # Connection status
├── RecentSessions.tsx # Completed sessions list
└── StartSessionButton.tsx # Ad-hoc session creationThe page uses a responsive grid layout that adapts to screen size:
return (
<div className="space-y-6">
{/* Stats row */}
<div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
{statCards.map((stat) => (
<Card key={stat.title} className="p-3">
{/* Compact stat display */}
</Card>
))}
</div>
{/* Main content grid */}
<div className="grid gap-6 lg:grid-cols-3">
<div className="space-y-6">
<ActiveSessionCard />
<SystemHealth />
</div>
<div className="lg:col-span-1">
<InlineQueue />
</div>
<div>
<RecentSessions />
</div>
</div>
</div>
);On desktop, this renders as a three-column layout. On mobile, columns stack vertically with the active session and queue taking priority.
Inline Queue Widget
The InlineQueue component displays the five most recent pending tickets with quick claim actions. Unlike the full queue page, it's optimized for speed—claim a ticket without leaving the dashboard.
export function InlineQueue() {
const router = useRouter();
const { data: tickets, refetch } = trpc.tickets.list.useQuery({
limit: 5,
});
const claimMutation = trpc.tickets.claim.useMutation({
onSuccess: () => {
refetch();
router.push("/queue");
},
});
const handleClaim = (ticketId: string) => {
claimMutation.mutate({ ticketId });
};
// Renders ticket list with claim buttons
}The widget shows customer name, urgency level, time in queue, and a claim button for each ticket. Urgency is color-coded:
- Low: Slate
- Medium: Yellow
- High: Orange
- Critical: Red
After claiming, the solver is directed to the queue page where they can start the session.
Active Session Card
When a solver has an in-progress session, the ActiveSessionCard appears prominently, ensuring they don't forget about ongoing work:
export function ActiveSessionCard() {
const router = useRouter();
const { data: activeSession } = trpc.sessions.getActive.useQuery();
if (!activeSession) {
return (
<Card className="border-dashed">
<CardContent className="flex items-center justify-center">
<p>No active session</p>
</CardContent>
</Card>
);
}
return (
<Card className="border-primary/50 bg-primary/5">
<CardHeader>
<CardTitle>Active Session</CardTitle>
<Badge variant="default" className="bg-green-600">
In Progress
</Badge>
</CardHeader>
<CardContent>
<Button onClick={() => router.push(`/session/${activeSession.id}`)}>
Resume Session
</Button>
</CardContent>
</Card>
);
}The sessions.getActive procedure queries for sessions with status active, paused, or not_started belonging to the current solver:
getActive: protectedProcedure.query(async ({ ctx }) => {
const { solver } = await getSolverForUser(ctx.auth.userId);
const session = await db.query.sessions.findFirst({
where: and(
eq(sessions.solverId, solver.id),
sql`${sessions.status} IN ('active', 'paused', 'not_started')`
),
with: { ticket: true },
orderBy: [desc(sessions.createdAt)],
});
if (!session) return null;
return {
id: session.id,
status: session.status,
customerName: session.ticket?.customerInfo?.name ?? null,
description: session.ticket?.description ?? null,
startedAt: session.startedAt,
createdAt: session.createdAt,
};
}),Ad-Hoc Session Creation
Not all sessions start from the queue. A solver might need to help a friend, run a demo, or assist a customer who contacted them through other channels. The StartSessionButton handles this:
export function StartSessionButton() {
const [open, setOpen] = useState(false);
const [customerName, setCustomerName] = useState("");
const [customerPhone, setCustomerPhone] = useState("");
const [customerEmail, setCustomerEmail] = useState("");
const [description, setDescription] = useState("");
const [inviteLink, setInviteLink] = useState<string | null>(null);
const createAdHoc = trpc.sessions.createAdHoc.useMutation({
onSuccess: (data) => {
const link = `${window.location.origin}/join/${data.sessionId}?token=${data.accessToken}`;
setInviteLink(link);
},
});
const handleCreate = () => {
createAdHoc.mutate({
customerName: customerName.trim(),
customerPhone: customerPhone.trim() || undefined,
customerEmail: customerEmail.trim() || undefined,
description: description.trim() || undefined,
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4" />
Start Session
</Button>
</DialogTrigger>
<DialogContent>
{!inviteLink ? (
<div className="space-y-4">
<Input placeholder="Customer Name *" value={customerName} onChange={...} />
<Input type="tel" placeholder="Phone Number" value={customerPhone} onChange={...} />
<Input type="email" placeholder="Email" value={customerEmail} onChange={...} />
<Input placeholder="Issue Description" value={description} onChange={...} />
<Button onClick={handleCreate}>Create Session</Button>
</div>
) : (
// Invite link display with copy button
)}
</DialogContent>
</Dialog>
);
}The sessions.createAdHoc procedure creates both a ticket and session in one transaction, returning an invite link the solver can share:
createAdHoc: protectedProcedure
.input(z.object({
customerName: z.string().min(1).max(100),
customerPhone: z.string().optional(),
customerEmail: z.string().email().optional(),
description: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
const { solver } = await getSolverForUser(ctx.auth.userId);
// Create ad-hoc ticket
const [ticket] = await db
.insert(tickets)
.values({
customerInfo: {
name: input.customerName,
phone: input.customerPhone ?? "",
email: input.customerEmail,
},
description: input.description ?? "Ad-hoc support session",
deviceType: "other",
status: "in_progress",
solverId: solver.id,
})
.returning();
// Generate access token
const accessToken = generateShortSessionToken();
// Create session
const [session] = await db
.insert(sessions)
.values({
ticketId: ticket.id,
solverId: solver.id,
accessToken,
status: "not_started",
tier: "standard",
})
.returning();
return {
sessionId: session.id,
ticketId: ticket.id,
accessToken,
};
}),The invite link format is /join/[sessionId]?token=[accessToken], which allows customers to join without authentication.
System Health Indicators
The SystemHealth component monitors the solver's real-time connection status using the Ably WebSocket:
export function SystemHealth() {
const { isConnected, connectionState } = useAblyConnection();
const status = isConnected
? "connected"
: connectionState === "connecting"
? "connecting"
: "disconnected";
return (
<Card>
<CardHeader>
<CardTitle>System Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<span>Real-time</span>
<Badge variant="outline">
<span className={`h-2 w-2 rounded-full ${statusColor}`} />
{statusLabel}
</Badge>
</div>
</CardContent>
</Card>
);
}This helps solvers identify connection issues before they affect customer interactions.
Recent Sessions
The RecentSessions component shows the last five completed sessions with earnings, using the existing solvers.getEarnings procedure:
export function RecentSessions() {
const { data } = trpc.solvers.getEarnings.useQuery({ limit: 5 });
return (
<Card>
<CardHeader>
<CardTitle>Recent Sessions</CardTitle>
</CardHeader>
<CardContent>
{data?.sessions.map((session) => (
<div key={session.id} className="flex items-center justify-between">
<div>
<span>{session.customer.name}</span>
<Badge>{session.tier}</Badge>
<span>{Math.ceil(session.duration / 60)} min</span>
</div>
<span className="text-green-600">
{formatCents(session.earnings)}
</span>
</div>
))}
</CardContent>
</Card>
);
}Development Bypass
For development and testing, a bypass mechanism allows accessing the Command Center without completing onboarding. This is controlled by environment variables:
# .env.local
DEV_BYPASS_ONBOARDING=true
NEXT_PUBLIC_DEV_BYPASS_ONBOARDING=trueThe bypass is implemented in lib/auth/dev-bypass.ts:
export function isOnboardingBypassed(): boolean {
return process.env.DEV_BYPASS_ONBOARDING === "true";
}
export function hasCompletedOnboarding(solverStatus: string): boolean {
if (isOnboardingBypassed()) {
return true;
}
return solverStatus !== "onboarding";
}This function is used in the solvers and tickets routers to gate features behind onboarding completion while allowing developers to bypass the check.
Testing Strategy
The Command Center components are tested with a combination of unit and integration tests:
| Component | Test File | Coverage |
|---|---|---|
| InlineQueue | InlineQueue.test.tsx | 11 tests |
| ActiveSessionCard | ActiveSessionCard.test.tsx | 8 tests |
| SystemHealth | SystemHealth.test.tsx | 9 tests |
| RecentSessions | RecentSessions.test.tsx | 9 tests |
| StartSessionButton | StartSessionButton.test.tsx | 14 tests |
| Dev Bypass | dev-bypass.test.ts | 10 tests |
Tests verify:
- Loading and error states
- Empty state rendering
- Data display accuracy
- User interactions (claim, resume, create)
- Mutation behavior (onSuccess callbacks, navigation)
Related Documentation
- Solver Dashboard - Original dashboard architecture
- Queue System - Full queue page and claim workflow
- Ably Integration - Real-time connection details
- Session Interface - What happens after claiming