nyxcore-systems
9 min read

Leveling Up Our Project Dashboard: Stats, Fixes, and the CI Gauntlet

A deep dive into a recent development sprint, covering how we enriched our project dashboard, wrestled with a failing CI pipeline, and extracted valuable lessons along the way.

TypeScriptReacttRPCCI/CDLintingDatabaseFrontendBackendLessons LearnedPostgreSQLPrisma

Every developer knows the satisfaction of a productive session – the kind where you tackle a major feature, squash a few nasty bugs, and leave the codebase cleaner than you found it. I recently had one such session, focused on enhancing our /dashboard/projects page and untangling a stubborn CI pipeline. This post is a recount of that journey, sharing the technical decisions, the challenges, and the lessons learned.

The Mission: A Smarter Dashboard & A Smoother CI

Our primary goal was two-fold:

  1. Enrich the Project List: The existing project cards on the dashboard were a bit sparse. We wanted to add key statistics at a glance: workflow counts, discussion threads, notes, action points, total spend, and a crucial success rate for terminal workflows.
  2. Fix the Failing CI: Our continuous integration pipeline was red, with three distinct jobs failing. A critical blocker that needed immediate attention.

By the end of the session, all goals were met, local checks passed, and the codebase was ready for a commit and push. Let's break down how we got there.

The Dashboard Glow-Up: From Data to Insights

Our project list lives at /dashboard/projects, powered by a tRPC endpoint. To get the rich data we needed for each project card, the src/server/trpc/routers/projects.ts list query was the first stop.

Backend Brilliance: Aggregating Project Data

The existing list query was extended to pull in more granular data efficiently.

  • Counting Relations: We leveraged Prisma's _count includes for seven different relations: workflows, discussions, projectNotes, actionPoints, repositories, projectDocuments, and blogPosts. This immediately gave us raw counts for each category.

    typescript
    // Simplified example in projects.ts
    const projects = await ctx.db.project.findMany({
      where: { id: { in: projectIds } },
      include: {
        _count: {
          select: {
            workflows: true,
            discussions: true,
            projectNotes: true,
            actionPoints: true,
            // ... more relations
          },
        },
      },
      // ...
    });
    
  • Parallel Aggregations: Beyond simple counts, we needed more complex aggregations like the number of draft items, open actions, workflow statuses, and total spend across various activities. To keep the query performant, especially for multiple projects, these aggregations were fetched in parallel. This involved nine separate queries for things like:

    • Counting draft workflows, discussions, and blog posts.
    • Determining the status distribution of workflows (completed, failed, in progress).
    • Summing step costs for workflows, message costs for discussions, and report/blog post costs.
    • Performing lookups for specific workflow and discussion states.
  • Calculating Success Rate: A key metric was the successRate. This was computed as (completed terminal workflows / (completed + failed terminal workflows)). If no terminal workflows existed, the rate was null. This gives a clear indicator of project health.

  • Total Spend Aggregation: We summed totalSpend across all relevant entities: individual workflow steps, discussion messages, and costs associated with reports and blog posts. To prevent floating-point inaccuracies, the final totalSpend was rounded.

  • Optimizations:

    • _count was stripped from the final response, as the frontend only needed the aggregated numbers, not the raw Prisma _count object.
    • An early return was implemented for empty projectIds to avoid unnecessary database hits.

Frontend Polish: Bringing Stats to Life

With the backend serving up rich data, the src/app/(dashboard)/dashboard/projects/page.tsx was redesigned to display these insights elegantly.

  • Compact Stat Row: Each project card now features a concise row of statistics, each paired with a semantic Lucide icon:

    • GitBranch for workflows
    • MessageSquare for discussions
    • FileText for project notes
    • ListChecks for action points
    • BookOpen for blog posts
    • Database for documents
    • DollarSign for total spend
  • Badges for Key Metrics:

    • Success Rate Badge: This dynamically changes color using our Badge component's variant prop (success, warning, danger) based on the calculated success rate, providing an immediate visual cue.
    • Open Action Count, Draft Count, Post Count Badges: Small, informative badges highlight critical numbers.
  • Accessibility: All stat icons were marked with aria-hidden="true" since their meaning is conveyed by the accompanying text, preventing screen readers from announcing redundant information.

  • Cost Formatting: A formatCost(usd) helper function ensures currency values are displayed consistently and readably.

The result is a dashboard that provides a wealth of information at a glance, allowing users to quickly gauge the status and activity of each project.

Taming the CI Beast: Fixing the Pipeline

A failing CI pipeline is a productivity killer. Our CI had three distinct issues, each requiring a different approach.

1. The Linting Labyrinth

The biggest culprit was a massive number of linting errors (over 40!), primarily due to a recent upgrade of eslint-config-next to version 14.2.35, which internally upgraded to @typescript-eslint@8.x. This introduced new, stricter rules and configuration requirements.

  • Explicit Plugin Registration: The new @typescript-eslint version now requires explicit plugin registration. The fix was simple but crucial: adding "plugins": ["@typescript-eslint"] to our .eslintrc.json.

  • Ignoring Unused Variables: We follow a convention of prefixing unused variables or arguments with _. The linter, however, was flagging these. We updated .eslintrc.json to include:

    json
    // .eslintrc.json
    {
      "rules": {
        "@typescript-eslint/no-unused-vars": [
          "warn",
          {
            "varsIgnorePattern": "^_",
            "argsIgnorePattern": "^_",
            "destructuredArrayIgnorePattern": "^_"
          }
        ]
      }
    }
    

    This allowed us to maintain our convention without lint errors.

  • Mass Cleanup: With the configuration fixed, I went through approximately 25 files, addressing every single lint error. This involved:

    • Removing unused imports.
    • Prefixing unused variables/arguments with _.
    • Crucially, fixing a conditional React Hook call in markdown-renderer.tsx. A useCallback hook was being called after an early return, violating the "Rules of Hooks." Moving it before the early return resolved the issue.

2. The Unit Test Blip

A single unit test (tests/unit/services/llm/kimi.test.ts:42) was failing. This was a straightforward fix: an adapter change meant the expected model name had shifted from kimi-k2-0711 to kimi-k2-0711-preview. Updating the test's expectation brought it back in line.

3. The E2E Database Dilemma

Our end-to-end tests were failing specifically because of a database issue. The problem? Our schema now requires the vector data type (likely for AI/embedding features), but the default postgres:16-alpine service image used in our .github/workflows/ci.yml didn't include the necessary pgvector extension.

The fix was to swap the Postgres service image to pgvector/pgvector:pg16, which comes pre-bundled with the pgvector extension. This ensures our E2E tests run against a database environment that fully supports our application's schema.

Lessons from the Trenches: My "Pain Log" Takeaways

Not every development decision goes smoothly. These moments of friction, initially frustrating, are often the most valuable learning opportunities.

Lesson 1: Prefer Component APIs Over Fragile Overrides

The Problem: I initially tried to override the Badge component's color for the success rate using a className with Tailwind CSS, like className="bg-green-500". Why it Failed: This approach was fragile. Our internal Badge component uses tailwind-merge to combine classes, and relying on className overrides for semantic styling (like success/warning/danger) can lead to unexpected behavior or require intricate knowledge of the component's internal CSS variable usage. It's an anti-pattern when a dedicated prop exists. The Takeaway: Always prefer using a component's explicit API (like the variant prop in this case) for styling and behavior control. It's more robust, readable, and less prone to breakage with future updates or tailwind-merge interactions. The variant prop abstracts away the styling details, making the code cleaner and more maintainable.

Lesson 2: Mastering TypeScript Destructuring for Unused Variables

The Problem: When destructuring props or objects, I often want to ignore certain fields that aren't used in the current scope. My initial attempt to signal this to TypeScript and the linter was to directly prefix the destructured prop name with _ (e.g., function MyComponent({ _teamId }) { ... }). Why it Failed: TypeScript correctly threw an error. The prop name in the function signature must match the type definition. You can't just rename it in the type signature like that. The Takeaway: To destructure a property and rename it to signal it's unused (and satisfy the linter's no-unused-vars rule with varsIgnorePattern: "^_"), use the JavaScript object destructuring rename syntax:

typescript
// Incorrect (TypeScript error)
function MyComponent({ _teamId }: { teamId: string }) {
  // ...
}

// Correct: Destructure `teamId` and rename it to `_teamId`
function MyComponent({ teamId: _teamId }: { teamId: string }) {
  console.log("Team ID is ignored:", _teamId); // Value is there, but linter knows it's intentionally unused
  // ... other logic
}

This preserves the type contract while allowing us to use our _ prefix convention for ignored variables.

What's Next?

With the dashboard enhanced and the CI pipeline green, the immediate next steps are to visually verify the new project list page in the browser and then monitor the CI for continued stability.

Looking ahead, we'll consider:

  • Adding a tooltip to the success rate badge for clarity (e.g., "of completed terminal workflows").
  • Investigating denormalizing some of these aggregated statistics directly into the Project model if the list query becomes a performance bottleneck at a larger scale.

This session was a great reminder that development is a continuous cycle of building, fixing, and learning. Each challenge overcome makes the system (and us) stronger.


json
{
  "thingsDone": [
    "Enriched project list page with key stats (workflows, discussions, notes, action points, spend, success rate)",
    "Enhanced tRPC list query with _count includes and parallel aggregations",
    "Implemented successRate calculation (completed / (completed + failed) terminal workflows)",
    "Aggregated totalSpend across various entities (workflow steps, discussion messages, reports, blog posts)",
    "Redesigned dashboard projects page with compact stat row and semantic badges",
    "Fixed linting issues: added @typescript-eslint plugin, configured varsIgnorePattern, resolved 40+ pre-existing errors including a React Hook rule violation",
    "Fixed unit test by updating expected model name (kimi-k2-0711 to kimi-k2-0711-preview)",
    "Fixed E2E test by changing postgres service image to pgvector/pgvector:pg16 for vector type support"
  ],
  "pains": [
    "Fragile className override on Badge component for success rate coloring due to tailwind-merge interactions",
    "TypeScript error when trying to directly prefix destructured prop name with _ (e.g., _teamId) in function signature"
  ],
  "successes": [
    "All work completed, lint/typecheck/unit tests pass locally",
    "Successful implementation of complex backend data aggregation for frontend display",
    "Resolved critical CI pipeline failures across linting, unit, and E2E tests",
    "Learned to prefer component variant props over className overrides for semantic styling",
    "Learned correct TypeScript destructuring syntax for ignoring unused variables (prop: _prop)"
  ],
  "techStack": [
    "TypeScript",
    "React",
    "Next.js",
    "tRPC",
    "Prisma",
    "PostgreSQL",
    "pgvector",
    "ESLint",
    "Prettier",
    "Tailwind CSS",
    "Lucide Icons",
    "GitHub Actions (CI/CD)"
  ]
}