Unleashing the Data: Building Our Analytics Command Center with Recharts & tRPC
From static placeholders to a dynamic data powerhouse: dive into the dev session where we built a comprehensive analytics dashboard, complete with real-time insights and a few unexpected hurdles.
Ever stared at a blank dashboard, knowing the data is there, just waiting to be unleashed? That was us, metaphorically speaking, until a recent dev session set out to transform our application's static placeholder into a vibrant, data-rich analytics command center. The goal was ambitious: replace a dormant UI with actionable insights, powered by Recharts and backed by a robust tRPC API.
This post chronicles the journey, from data modeling to visualization, and even the "gotchas" we hit along the way.
The Mission: From Placeholder to Powerhouse
Our existing dashboard was, well, a placeholder. It looked nice, but offered zero actual data. The task was clear: populate it with meaningful metrics, giving users a holistic view of their platform usage, costs, performance, and more. This wasn't just about pretty charts; it was about empowering users to make informed decisions.
By the end of the session, we had a feature-complete module, humming along on localhost:3000 (commit cd6ac09 on main).
Architecting the Data Flow: Types, Themes, and tRPC
Any data-intensive feature starts with understanding the data itself.
1. Data Definition: Our Blueprint for Insights
First, we laid the groundwork with a comprehensive TypeScript interface: src/types/analytics.ts. This AnalyticsDashboardData interface became our contract between the frontend and backend, meticulously defining every piece of data required across various sections:
- Hero metrics (spend, tokens, energy, etc.)
- Activity timelines
- Provider-specific intelligence
- Insights, projects, workflows, discussions, and knowledge base statistics
// src/types/analytics.ts (simplified)
export interface AnalyticsDashboardData {
heroMetrics: {
totalSpend: number;
totalTokens: number;
estimatedEnergySaved: number;
estimatedTimeSaved: number;
totalWorkflows: number;
successRate: number;
};
activityTimeline: { date: string; workflows: number; discussions: number; insights: number }[];
providerIntelligence: { provider: string; totalTokens: number; totalCost: number }[];
insightPulse: { typeDistribution: { type: string; count: number }[]; /* ... */ };
// ... many more sections
}
This upfront data modeling was crucial for maintaining type safety and clarity throughout the development process.
2. Styling with Purpose: chart-theme.ts
To ensure visual consistency and leverage our existing design system, we created src/lib/chart-theme.ts. This file centralized Recharts color constants, mapping them to our nyx-* CSS variables and defining specific colors for AI providers (Anthropic, OpenAI, Google, Ollama, Kimi). This makes future branding updates a breeze.
3. The Backend Engine: getAnalytics tRPC Procedure
The true heavy lifting happened in src/server/trpc/routers/dashboard.ts. We introduced a new tRPC procedure, getAnalytics, designed to fetch all the necessary data in a single, efficient request.
This procedure orchestrates roughly 12 distinct Prisma queries, fetching data from various tables. To optimize performance and avoid waterfall requests, we wrapped these queries in a Promise.all call:
// src/server/trpc/routers/dashboard.ts (simplified)
import { computeWorkflowAggregates } from 'src/lib/workflow-metrics';
export const dashboardRouter = t.router({
getAnalytics: publicProcedure.query(async ({ ctx }) => {
const [
heroMetricsData,
activityTimelineData,
providerData,
insightData,
workflowData,
knowledgeBaseData,
projectData,
discussionStatsData, // Data computed, but not yet rendered in UI
] = await Promise.all([
ctx.db.workflow.aggregate({ /* ... */ }),
ctx.db.activityLog.findMany({ /* ... */ }),
ctx.db.providerCall.groupBy({ /* ... */ }),
// ... more Prisma queries
]);
// Post-processing and aggregation
const computedWorkflowAggregates = computeWorkflowAggregates(workflowData);
return {
heroMetrics: { /* map heroMetricsData */ },
activityTimeline: activityTimelineData,
providerIntelligence: providerData,
insightPulse: insightData,
workflowPerformance: computedWorkflowAggregates,
knowledgeBaseStats: knowledgeBaseData,
projectPortfolio: projectData,
// discussionStats: discussionStatsData, // Ready for UI integration!
} satisfies AnalyticsDashboardData; // Ensure type safety
}),
});
We also incorporated computeWorkflowAggregates() from src/lib/workflow-metrics.ts to perform complex calculations like success rates, average durations, and retry rates, ensuring our raw database data was transformed into meaningful business metrics. The satisfies AnalyticsDashboardData at the end is a small but mighty TypeScript helper, ensuring our returned object perfectly matches our frontend's expectations.
Bringing Data to Life: The Visual Layer
With the data plumbing in place, it was time to build the user interface. We spun up nine new, highly-focused components within src/components/dashboard/analytics/:
analytics-skeleton.tsx: A custom loading skeleton that precisely mirrors the final layout, providing a smooth user experience during data fetch.hero-metrics.tsx: Six prominent cards displaying key performance indicators at a glance: total spend, tokens, estimated energy saved, time saved, total workflows, and success rate.activity-timeline-chart.tsx: A Recharts stackedAreaChartvisualizing 30 days of activity, breaking down workflows, discussions, and insights.provider-intelligence-chart.tsx: A RechartsComposedChartoffering a dual view: token usage as bars and cost as a line, segmented by AI provider. Critical for cost optimization!insight-pulse.tsx: Displays the distribution of insight types, severity badges, paired ratio, and top categories.workflow-performance.tsx: A deep dive into workflow efficiency with status breakdowns, average duration, retry rates, and identification of top errors.knowledge-base-stats.tsx: Totals for memory fragments, insights, and patterns, complemented by weekly growth indicators.project-portfolio.tsx: Individual cards for each project, summarizing their workflow, insight, cost, and activity metrics.analytics-dashboard.tsx: The orchestrator. This component fetches theAnalyticsDashboardDatavia tRPC and intelligently renders all the above sections, handling its own loading states.
Finally, src/app/(dashboard)/dashboard/page.tsx was updated to integrate our new "Analytics" view. We introduced a tabbed interface, allowing users to switch between the new "Analytics" command center (now the default) and a legacy "Widgets" grid. The old page-level loading skeletons and SSE hooks were removed, as analytics-dashboard.tsx now manages its own lifecycle.
Hurdles & Hard-Learned Lessons
No development session is complete without a few bumps in the road. These "pain log" entries, while frustrating at the time, offered valuable lessons:
1. ESLint's Silent Killer: @typescript-eslint/no-unused-vars
Attempting a standard npm run build immediately failed. The culprit? A pre-existing, project-wide ESLint configuration issue: the @typescript-eslint/no-unused-vars rule definition couldn't be found. This wasn't introduced by our changes but affected all files.
Lesson Learned: Don't ignore linting errors, even if they're "pre-existing." A broken linting setup can mask real issues and prevent successful builds. Our workaround (next build --no-lint) confirmed our code was valid and compiled cleanly, but a proper fix for the ESLint config is paramount for long-term project health. It's a technical debt that needs immediate attention.
2. SSG & Suspense Boundaries: useSearchParams() Pitfall
Another pre-existing issue reared its head: the /dashboard/consolidation/new page was failing Static Site Generation (SSG). The root cause was the direct use of useSearchParams() within a component without an appropriate Suspense boundary.
Lesson Learned: When working with Next.js (or any React framework that combines server-side and client-side rendering), be acutely aware of where client-side hooks like useSearchParams() can be called. They require a client environment, and attempting to use them during SSG will cause failures. Proper Suspense boundaries or ensuring such components are only rendered on the client are crucial for robust applications.
What's Next?
With the core analytics dashboard feature-complete and committed, the immediate next steps are clear:
- Verify: A thorough browser check at
/dashboardto ensure everything renders as expected. - Fix ESLint: Prioritize resolving the
@typescript-eslint/no-unused-varsconfiguration issue project-wide. - Address SSG Error: Implement a Suspense boundary or refactor the
/dashboard/consolidation/newpage to correctly handleuseSearchParams(). - Enhance: Consider adding a dedicated UI section for
discussionStats. The data is already computed on the backend; it's just waiting for its moment in the spotlight! - Push: Once verified and critical issues addressed, push the commit to origin.
This session was a significant leap forward, transforming a static concept into a dynamic, data-driven reality. The journey highlighted the importance of meticulous planning, robust backend architecture, thoughtful UI design, and, crucially, addressing technical debt as it appears. Onwards to a more insightful future!
{
"thingsDone": [
"Created AnalyticsDashboardData interface (src/types/analytics.ts)",
"Created chart-theme.ts for Recharts colors and provider mapping",
"Implemented getAnalytics tRPC procedure in dashboard.ts, leveraging Promise.all and Prisma",
"Developed 9 new dashboard components (analytics-skeleton, hero-metrics, activity-timeline-chart, provider-intelligence-chart, insight-pulse, workflow-performance, knowledge-base-stats, project-portfolio, analytics-dashboard)",
"Modified dashboard/page.tsx to integrate new analytics view with tabbed navigation",
"Removed old page-level loading skeletons and SSE hooks, delegating loading to analytics-dashboard"
],
"pains": [
"Pre-existing ESLint config issue: @typescript-eslint/no-unused-vars rule definition not found, preventing npm run build",
"Pre-existing SSG failure on /dashboard/consolidation/new due to useSearchParams() without Suspense boundary"
],
"successes": [
"Successfully replaced static dashboard with data-rich analytics command center",
"Achieved feature completeness for the core analytics dashboard",
"Verified clean compilation with next build --no-lint despite linting issues",
"Established a clear data contract between frontend and backend with TypeScript interfaces",
"Optimized backend data fetching with Promise.all for multiple Prisma queries",
"Created a modular and reusable component structure for dashboard visualizations"
],
"techStack": [
"Next.js",
"tRPC",
"Recharts",
"Prisma",
"TypeScript",
"ESLint",
"PostgreSQL",
"React"
]
}