nyxcore-systems
5 min read

Leveling Up Our Project Dashboard & Taming the CI Beast

A deep dive into enriching our project dashboard with vital stats and a comprehensive fix for our failing CI pipeline, covering linting, unit tests, and E2E infrastructure.

Next.jsTypeScripttRPCCI/CDPrismaPostgreSQLLintingFrontendBackendRefactoring

Every now and then, a development session comes along that perfectly marries feature enhancement with critical infrastructure maintenance. This past week was one such time. Our mission: to transform a functional but sparse project list into an insightful, data-rich overview, and simultaneously, to wrestle our stubbornly failing CI pipeline back into submission.

It was a classic two-pronged attack – pushing forward with new capabilities while shoring up our foundational quality gates. By the end of the session, both objectives were not just met, but exceeded, leaving us with a more robust application and a much happier CI.

The Quest for Project Insights: A Data-Driven Dashboard

Our /dashboard/projects page, while serving its purpose, was a bit... minimalist. It listed projects, but offered no immediate context on their health or activity. We wanted to see key stats at a glance: how many workflows were running, how many discussions were active, total spend, and perhaps most crucially, a project's success rate.

Backend Brilliance: Aggregating the Data

The journey began deep in the backend, specifically within our src/server/trpc/routers/projects.ts list query. To get the rich data we needed, we implemented a multi-faceted approach:

  1. Counting Relations: We leveraged Prisma's _count includes for several related models like workflows, discussions, projectNotes, and actionPoints. This gave us immediate counts of associated records.
  2. Parallel Aggregation Power: For more complex metrics, we introduced a series of parallel aggregation queries. This included:
    • Counting drafts and open actions for a quick activity pulse.
    • Calculating totalSpend by summing costs across workflow steps, discussion messages, reports, and blog posts. This required careful aggregation to avoid floating point inaccuracies.
    • Deriving a successRate for terminal workflows: completed / (completed + failed). If no terminal workflows existed, we gracefully returned null.
  3. Refinements for Robustness: We added small but mighty optimizations:
    • Stripping the raw _count objects from the final response to keep the payload clean.
    • Rounding totalSpend to prevent float drift issues.
    • An early return for empty projectIds to prevent unnecessary database queries, a simple but effective performance gain.

Frontend Flourish: Bringing Data to Life

With the backend serving up a feast of data, the frontend's job was to present it elegantly. Our src/app/(dashboard)/dashboard/projects/page.tsx underwent a significant redesign:

  • Compact Stat Rows: Each project card now features a compact row of statistics, each adorned with a relevant lucide icon (think GitBranch for workflows, MessageSquare for discussions, DollarSign for spend). This provides visual cues without clutter.
  • Semantic Badges: We introduced badges for key metrics. The successRate badge dynamically changes its variant (success, warning, danger) based on the rate, offering immediate visual feedback on project health. Other badges highlight open action counts, drafts, and post counts.
  • Accessibility & Helpers: Icons were marked aria-hidden="true" for screen reader users, and a formatCost(usd) helper ensured consistent, readable currency display.

Taming the CI Beast: A Pipeline Comeback

While the dashboard was getting its glow-up, our CI pipeline was struggling, with three distinct job failures. A broken CI is a developer's nemesis, so this was a top priority.

1. Linting with a Vengeance

The lint job was failing consistently. The culprit? An upgrade of eslint-config-next to 14.2.35, which brought @typescript-eslint up to v8.x. This new version requires explicit plugin registration.

  • The Fix: We updated .eslintrc.json to explicitly include "plugins": ["@typescript-eslint"].
  • Beyond the Fix: This also presented an opportunity to clean house. We added varsIgnorePattern and destructuredArrayIgnorePattern for the common _ prefix convention for unused variables. More importantly, this allowed us to finally tackle 40+ pre-existing lint errors across approximately 25 files! This included removing unused imports and fixing a conditional React Hook call in markdown-renderer.tsx by moving a useCallback before an early return. This wasn't just a fix; it was a significant improvement in our codebase's health.

2. The Elusive Unit Test

A single unit test in tests/unit/services/llm/kimi.test.ts was failing.

  • The Fix: A quick inspection revealed that an external LLM adapter had changed its expected model name from kimi-k2-0711 to kimi-k2-0711-preview. Updating the test's expected value brought it back in line. A reminder that even small external changes can ripple through our tests.

3. E2E's Database Dilemma

Our End-to-End (E2E) tests were failing due to a database issue.

  • The Fix: The E2E tests' PostgreSQL service in .github/workflows/ci.yml was using postgres:16-alpine. However, our application schema requires the vector type, which is provided by the pgvector extension. Changing the service image to pgvector/pgvector:pg16 resolved the issue, allowing our E2E tests to finally connect and run against a compatible database.

Lessons Learned & Overcoming Hurdles

No development session is without its minor skirmishes. Here are a couple of insights gained:

  • Styling Component Variants: We initially tried to override the Badge component's color using a className directly. This proved fragile, as tailwind-merge struggled to correctly resolve CSS variable names, leading to inconsistent styling.
    • Lesson: When a component offers a variant prop for semantic styling (e.g., success, warning, danger), it's almost always best to use it directly rather than trying to force overrides with className. It's more robust and aligns with the component's intended API.
  • Destructuring Unused Props in TypeScript: We attempted to prefix a destructured prop name directly (e.g., _teamId) in a function signature to mark it as unused. TypeScript, however, correctly flagged this as an error because the prop name must match the type definition.
    • Lesson: For marking unused destructured props in TypeScript, use the rename syntax: { teamId: _teamId }. This allows you to match the type definition while still using the _ prefix convention for unused variables within the function body.

Looking Ahead

With all work complete, linting, type-checking, and unit tests passing locally, this session concluded with a successful commit and push. The CI pipeline is now green, and our project dashboard is brimming with useful data.

Our immediate next steps include a final visual verification of the new dashboard in the browser and considering future enhancements like tooltips for the success rate badge (a product decision) or even denormalizing some stats into the Project model if the list query becomes a performance bottleneck at scale.

It was a productive session, demonstrating the power of tackling both feature development and critical maintenance in parallel. A richer user experience and a more stable development environment – a win-win!