Admin Dashboard Verification
Overview
A systematic approach to verifying admin dashboard enhancements — covering error tracking, simulation testing, database integrity, mobile responsiveness, and edge case handling. Built from real-world verification of org↔user↔agent relationship views in a Next.js + Express + MongoDB stack.
Prerequisites
- Admin dashboard: Next.js with TailwindCSS
- API: Express.js with Mongoose
- Test runner:
bun test - Error tracking: Sentry
Verification Checklist
Phase 1: Error Tracking Coverage
For every changed backend endpoint:
- Identify all
catch (err)blocks - Verify each has
captureExceptionwith a descriptive context string - Run file-level test to count catches vs Sentry calls
# Quick audit: find catch blocks without Sentry
grep -n "catch (err)" routes/admin/*.ts | while read line; do
file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2)
# Check if captureException appears within 3 lines after the catch
sed -n "$((lineno)),$(( lineno + 3 ))p" "$file" | grep -q "captureException" || echo "MISSING: $line"
done
Phase 2: Simulation Tests
Write tests that verify endpoint behavior without hitting the database:
describe("Org Detail Relationship View", () => {
// Verify endpoint code contains expected queries
test("endpoint returns agents, users, messageCount, recentMessages", async () => {
const file = await Bun.file("routes/admin/organizations.ts").text();
expect(file).toContain("agents, users, messageCount, recentMessages");
expect(file).toContain("Message.countDocuments({ organizationId:");
});
// Verify response shape with mock data
test("response shape simulation", () => {
const mockResponse = {
organization: { _id: "org1", name: "Test", plan: "free" },
agents: [{ _id: "a1", name: "Agent 1" }],
users: [{ _id: "u1", firstName: "John" }],
messageCount: 42,
recentMessages: [{ _id: "m1", subject: "Hello", direction: "inbound" }],
};
expect(mockResponse.agents).toBeArray();
expect(mockResponse.messageCount).toBeNumber();
expect(mockResponse.recentMessages[0]).toHaveProperty("direction");
});
});
Phase 3: Database Integrity
- Index coverage — Every query pattern has a supporting index
- Relationship direction — Verify which schema owns the foreign key
- Schema field existence — Test that referenced fields actually exist
- Migration completeness — Historical data is updated and usable
Phase 4: Mobile Responsiveness
Verify CSS classes in the changed UI files:
describe("Mobile Responsiveness", () => {
test("responsive table columns", async () => {
const file = await Bun.file("customers/page.tsx").text();
expect(file).toContain("hidden sm:table-cell"); // Hide columns on mobile
expect(file).toContain("overflow-x-auto"); // Horizontal scroll
});
test("drawer is mobile-friendly", async () => {
const file = await Bun.file("customers/page.tsx").text();
expect(file).toContain("w-full max-w-md"); // Full width on mobile, capped on desktop
});
test("no hover-only interactions on touch", async () => {
const file = await Bun.file("customers/page.tsx").text();
// Should use sm:opacity-0 sm:group-hover:opacity-100, NOT opacity-0 group-hover:opacity-100
expect(file).not.toMatch(/(?<!sm:)opacity-0 group-hover:opacity-100/);
});
});
Phase 5: Edge Cases
- Empty states — Arrays with 0 items render gracefully (null, not crash)
- Loading states — Shimmer skeletons during fetch, not blank space
- Null safety — Use
?? "—"or?? 0for missing data - Error states — API failures show user-friendly message
- Cross-navigation — Closing one drawer and opening another works without stale state
Common Patterns
Cross-Navigation Between Drawers
When clicking an entity in one drawer opens another drawer:
<button => {
setSelectedOrg(null); // Close current drawer first
setSelectedUser(userObj); // Then open target drawer
}}>
Inline Editing Pattern
{editingField ? (
<div className="flex items-center gap-1.5">
<input value={input} => setInput(e.target.value)}
=> { if (e.key === "Enter") save(); if (e.key === "Escape") cancel(); }}
autoFocus className="..." />
<button
<button
</div>
) : (
<div className="flex items-center gap-1.5 group">
<p>{displayValue}</p>
<button
className="sm:opacity-0 sm:group-hover:opacity-100"> {/* Visible on mobile, hover on desktop */}
<PencilIcon />
</button>
</div>
)}
Enriched Detail Endpoint
When a list endpoint returns minimal data but a detail view needs relationships:
router.get("/:id", async (req, res) => {
const entity = await Model.findById(req.params.id).lean();
if (!entity) return res.status(404).json({ error: "Not found" });
const [related1, related2, count, recent] = await Promise.all([
Related1.find({ entityId: entity._id }).lean(),
Related2.find({ entityId: entity._id }).lean(),
Message.countDocuments({ entityId: entity._id }),
Message.find({ entityId: entity._id })
.sort({ createdAt: -1 }).limit(10)
.select("subject from direction createdAt").lean(),
]);
res.json({ entity, related1, related2, messageCount: count, recentMessages: recent });
});
Pitfalls
- Querying by non-existent field —
Inbox.findOne({ agentId })returns null silently when Inbox has noagentIdfield. Always verify the schema owns the field you're querying. - Hover-only UI on mobile —
opacity-0 group-hover:opacity-100is invisible on touch devices. Usesm:opacity-0 sm:group-hover:opacity-100so it's always visible on mobile. - Missing indexes on frequently-joined collections — Organization and User schemas often lack indexes because they're small initially but grow. Add indexes proactively.
- Stale drawer state — When cross-navigating, always close the source drawer before opening the target. Otherwise both drawers stack.
- Migration idempotency — Running "migrate default org names" twice should be safe. Check for the condition before updating.
Combining With Other Skills
- Sentry Instrumentation — Every endpoint verified here must have Sentry coverage
- MongoDB Schema Auditing — Index and relationship validation feeds into this verification
- UI/UX Audit — Mobile responsiveness checks overlap with the UI audit checklist
- Deployment Testing — Run all verification tests before deploy