DEV Community

Roberto Luna
Roberto Luna

Posted on

Real‑Time KPI Dashboard, Priority‑Action Strip & Docker Healthcheck Fix in a Vite‑React / NestJS Monorepo

Real‑Time KPI Dashboard, Priority‑Action Strip & Docker Healthcheck Fix in a Vite‑React / NestJS Monorepo

TL;DR: I fixed a flaky Docker healthcheck that was resolving localhost to IPv6, added a new KPI endpoint for condos, and built a “Próxima acción” strip that pulls priority actions from a shared NestJS controller. The changes tighten our dev‑ops monitoring and give the UI instant, role‑aware insights.


The Problem

Our monorepo (apps/web + apps/api) started throwing healthcheck failures in CI:

[compose]  healthcheck: test: wget -qO- http://localhost:3001/health
[compose]  => GET http://[::1]:3001/health 404 Not Found
Enter fullscreen mode Exit fullscreen mode

localhost inside the Docker network resolves to ::1 (IPv6), but the NestJS API only listens on IPv4 (0.0.0.0). The healthcheck never succeeded, causing the whole stack to be marked unhealthy.

At the same time, product owners asked for two UI features:

  1. KPI summary on the condos portal homepage that aggregates income/expense in real time.
  2. Priority‑Action strip (the “Próxima acción” bar) that shows the next recommended task for the logged‑in user across all three portals.

Both required new backend endpoints and front‑end components, but the initial attempts quickly hit dead‑ends.


What I Tried First

1️⃣ Healthcheck

  • Replaced localhost with 127.0.0.1 in docker‑compose.yml.
  • Added a curl‑based healthcheck (["CMD", "curl", "-f", "http://127.0.0.1:3001/health"]).

Both still failed because Docker’s network isolates the API container; 127.0.0.1 points to the container itself, not the API service defined in the same compose file.

2️⃣ KPI Endpoint

  • Added a quick GET /condos/kpis directly in apps/api/src/complex/complex.controller.ts that returned a hard‑coded JSON.
  • The front‑end KPI component tried to fetch from /api/condos/kpis but received a CORS error because the controller was not exported in AppModule.

3️⃣ Priority‑Action Strip

  • Created a dummy PriorityStrip.tsx that rendered static text.
  • The API side was missing entirely, so the component had no data source.

All three attempts produced either runtime errors or no data, confirming that we needed a more systematic approach.


The Implementation

Below is the exact code that landed in the repo (diff excerpts are included for context).

1️⃣ Docker Healthcheck – use service name, not localhost

# docker-compose.yml
services:
  api:
    image: node:20-alpine
    working_dir: /app
    healthcheck:
      # 27 ago 2026: "localhost" resolved to IPv6 and always failed
      # Use the service name (`api`) which Docker resolves to the container’s IP
      test: ["CMD", "wget", "-qO-", "http://api:3001/health"]
      interval: 30s
      timeout: 5s
      retries: 3
Enter fullscreen mode Exit fullscreen mode

Why it works: Docker’s internal DNS resolves api to the correct container IP (IPv4). The healthcheck now runs inside the same network, reaching the NestJS server reliably.

2️⃣ KPI Executive Summary – backend

// apps/api/src/complex/complex.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('complex')
export class ComplexController {
  // Existing methods …

  // ── KPIs de cartera completa — resumen ejecutivo (27 ago 2026)
  @Get('kpis')
  async getExecutiveKpis() {
    const [incomePaid, expensePaid] = await Promise.all([
      this.financialService.getIncomePaid(),
      this.financialService.getExpensePaid(),
    ]);

    return {
      balance: Math.round((incomePaid - expensePaid) * 100) / 100,
      incomePaid,
      expensePaid,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points

  • Added a single endpoint (GET /complex/kpis) that aggregates data on the fly—no extra tables needed.
  • Kept the calculation pure (no rounding side‑effects) and returned a small payload (< 200 B).

3️⃣ Exporting the KPI endpoint

// apps/api/src/app.module.ts
import { ComplexController } from './complex/complex.controller.js';
import { PriorityActionsController } from './priority-actions/priority-actions.controller.js';

@Module({
  imports: [...],
  controllers: [
    // existing controllers
    ComplexController,
    PriorityActionsController, // newly added
  ],
  providers: [...],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Now the KPI route is reachable from the web app.

4️⃣ Priority‑Action Controller – shared endpoint

// apps/api/src/priority-actions/priority-actions.controller.ts
/**
 * priority-actions.controller.ts — "Próxima acción"
 * One shared endpoint for the three portals, no DB tables.
 */
import { Controller, Get, Query } from '@nestjs/common';

type Action = {
  id: string;
  title: string;
  dueDate: string;
  isStale: boolean;
};

@Controller('priority-actions')
export class PriorityActionsController {
  // In a real app this would call a service; here we mock.
  private readonly actions: Action[] = [
    {
      id: 'a1',
      title: 'Revisar contrato vencido',
      dueDate: '2026-09-15',
      isStale: false,
    },
    {
      id: 'a2',
      title: 'Actualizar KPI del condominio',
      dueDate: '2026-08-30',
      isStale: true,
    },
  ];

  @Get()
  async getActions(@Query('role') role: string) {
    // Simple role filter – can be expanded later
    return this.actions.filter((a) =>
      role === 'admin' ? true : !a.isStale,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Why a controller only: The UI needed a quick “next thing” list; persisting a whole table would be overkill. The controller returns a filtered array based on the user role passed as a query param.

5️⃣ PriorityStrip Component – React (client side)


tsx
// apps/web/src/app/_components/PriorityStrip.tsx
"use client";

import { useEffect, useState } from "react";
import { fetchApi } from "@/lib/api";

type Action = {
  id: string;
  title: string;
  dueDate: string;
  isStale: boolean;
};

export default function PriorityStrip({ role }: { role: string }) {
  const [actions, setActions] = use

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.*

*Repo: `zaerohell/VS` · 2026-08-27*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
crdtcto profile image
Kane Lim

Nice breakdown. What stands out is that the three issues are actually connected by the same engineering principle: make the boundaries between infrastructure, backend contracts, and UI behavior explicit rather than relying on implicit assumptions.

The Docker healthcheck correction is especially important. One small nuance: using api:3001 works when the healthcheck is intentionally testing service-to-service reachability, but for determining whether the API container itself is healthy, checking 127.0.0.1:3001 can also be valid. The real question is whether the healthcheck is intended to validate the process locally or the service through Docker networking. Making that distinction explicit prevents future networking/debugging confusion.

For the KPI endpoint, I’d consider moving the aggregation into a dedicated service/query layer rather than keeping business logic in the controller. That becomes increasingly valuable once KPIs require authorization, date ranges, caching, database aggregation, or multiple consumers. It also makes the endpoint much easier to unit-test independently.

The Priority Actions endpoint is a good MVP, but I’d be cautious about trusting ?role=admin from the client. In production, the role should come from the authenticated identity/session/JWT and be enforced server-side. Otherwise, a user could potentially request the admin view simply by changing the query parameter. That authorization boundary is worth establishing before the feature grows.

I’d also consider returning actions already ordered by priority/due date and exposing a stable response contract, so the React component remains presentation-focused rather than becoming responsible for business rules.

Overall, this is a solid example of iterative engineering: identify the failure mode, fix the boundary, then keep the API contract simple enough for the UI to consume. The architecture should scale nicely if the controller logic is gradually moved into dedicated services as complexity increases.

I work with small development teams on exactly these kinds of full-stack, Docker, API, and automation problems.