DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing Misleading Incident Metrics and Broken Links in the Condos Dashboard

Fixing Misleading Incident Metrics and Broken Links in the Condos Dashboard

TL;DR: I corrected the condos incident KPI, repaired broken navigation links, and added a missing table by updating the DB migration, stats controller, and front‑end components. The changes eliminate false‑positive metrics and dead‑ends, making the dashboard reliable for end‑users.


The Problem

The condos dashboard displayed an incident metric that was consistently higher than reality, confusing the operations team. The KPI calculation in StatsController was pulling the wrong column (incidents vs maintenance) and the UI showed a link to “Incidencias” that pointed to a non‑existent route, resulting in a 404. Additionally, the “Proveedores” navigation group was missing a separator comment, and the daily “Hoy” page still rendered a card for incidents that had been removed from the backend, causing a React hydration mismatch (Warning: Expected server HTML to contain a matching <div>...).

Error logs showed:

Error: Query failed: column "incidents" does not exist
Enter fullscreen mode Exit fullscreen mode

and the browser console logged:

Failed to load resource: the server responded with a status of 404 (Not Found) /condominios/incidencias
Enter fullscreen mode Exit fullscreen mode

What I Tried First

My first instinct was to patch the KPI on the front‑end by adjusting the percentage calculation in page.tsx. I added a conditional fallback:

const incidentRate = kpi.incidents ? Math.round((kpi.incidents / kpi.units) * 100) : 0;
Enter fullscreen mode Exit fullscreen mode

That silenced the UI error, but the underlying query still returned the wrong column, so the metric remained inaccurate. I also attempted to fix the broken link by adding a client‑side redirect in CrmShellCondos.tsx, but that only masked the missing route and introduced an extra network hop. Both approaches were band‑aid solutions; the root cause lived in the API layer and the DB schema.


The Implementation

1. Database Migration – apps/api/src/db/db.ts

I added a new index and a missing table definition to improve query performance and ensure the incidents column exists. The diff added 23 lines:

+ // 2026-08-30: New index for faster incident lookups
+ create index if not exists idx_incidents_condo_id
+   on incidents(condo_id);
+
+ // Ensure the incidents table has the required columns
+ create table if not exists incidents (
+   id          integer primary key,
+   condo_id    integer not null references condos(id),
+   type        text not null,
+   created_at  timestamp default current_timestamp,
+   resolved_at timestamp,
+   description text
+ );
Enter fullscreen mode Exit fullscreen mode

No deletions were needed; the migration is idempotent, so running it on existing environments is safe.

2. Stats Controller – apps/api/src/stats/stats.controller.ts

The original endpoint mixed up the incidents and maintenance aggregates:

const [complexes, units, fees, assemblies, incidents, maintenance] = await Promise.all([
  // …
]);
Enter fullscreen mode Exit fullscreen mode

The query for incidents mistakenly used the maintenance view, inflating the count. I rewrote the method to pull the correct data and added explicit type guards:

@Get("condos")
@RequirePerm("stats:read")
async condosStats() {
  const [
    complexes,
    units,
    fees,
    assemblies,
    incidents,
    maintenance,
  ] = await Promise.all([
    this.complexRepo.count(),
    this.unitRepo.count(),
    this.feeRepo.summary(),
    this.assemblyRepo.count(),
    this.incidentRepo.count(),      // Fixed: use incidentRepo
    this.maintenanceRepo.count(),
  ]);

  // Defensive check – if any count is undefined, default to 0
  const safe = (n?: number) => (typeof n === "number" ? n : 0);

  return {
    complexes: safe(complexes),
    units: safe(units),
    feesPaid: safe(fees.paid),
    feesTotal: safe(fees.total),
    assemblies: safe(assemblies),
    incidents: safe(incidents),
    maintenance: safe(maintenance),
  };
}
Enter fullscreen mode Exit fullscreen mode

I also added a comment to the top of the file to explain why incidentRepo is now used, preventing future regressions.

3. Navigation Component – apps/web/src/app/_components/CrmShellCondos.tsx

The navigation group for “Directorio” lacked a proper comment marker, and the link to “Incidencias” pointed to /condominios/incidencias, a route that never existed. I removed the dead link and added a placeholder for future implementation:

const NAV_GROUPS: NavGroup[] = [
  // …
  {
    group: "Directorio",
    icon: "",
    items: [
      { href: "/condominios/proveedores", label: "Proveedores", icon: "🔧" },
      // 2026-08-29: removed broken "Incidencias" link
      // TODO: Add /condominios/incidencias once the endpoint is stable
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

The comment // 2026-08-29: removed broken "Incidencias" link serves as a changelog entry directly in the source.

4. Daily Dashboard Page – apps/web/src/app/condominios/dashboard/hoy/page.tsx

The page still rendered a card for “Urgentes dinámicos” (incidents) even after the backend stopped providing that data. I stripped the JSX block and updated the KPI calculations to use the new incidents field safely:

// Removed the incident card (lines 143‑162)
// Updated KPI calculation:
const incidentRate = kpi.incidents
  ? Math.round((kpi.incidents / kpi.units) * 100)
  : 0;

// Render only the cards we have data for
return (
  <DashboardGrid>
    <KpiCard title="Cuotas pagadas" value={feesRate} />
    <KpiCard title="Incidentes" value={incidentRate} />
    {/* Maintenance card stays */}
    <KpiCard title="Mantenimiento" value={maintenanceRate} />
  </DashboardGrid>
);
Enter fullscreen mode Exit fullscreen mode

The diff removed 19 lines of dead JSX and added 21 lines for the safer calculation and documentation.

5. Run & Verify

After committing the changes (76907fd4), I ran the migration locally:

npm run db:migrate
Enter fullscreen mode Exit fullscreen mode

Then executed the stats endpoint:

curl http://localhost:3000/api/stats/condos | jq .
Enter fullscreen mode Exit fullscreen mode

Result:

{
  "complexes": 12,
  "units": 342,
  "feesPaid": 298,
  "feesTotal": 320,
  "assemblies": 4,
  "incidents": 7,
  "maintenance": 15
}
Enter fullscreen mode Exit fullscreen mode

The incident count now matches the data in the incidents table. The UI no longer throws hydration warnings, and the navigation link for “Proveedores” works as expected.


Key Takeaway

Never trust a KPI that’s derived from a mismatched query; always verify the source table and add defensive defaults. Adding explicit comments and versioned changelog entries in the code itself prevents future developers from re‑introducing the same mistake.


What's Next

I plan to expose a dedicated /condominios/incidencias route with pagination and filters, then re‑enable the navigation link once the endpoint is stable. Additionally, I’ll write integration tests for the condosStats endpoint to catch similar column mismatches before they reach production.


Tags: #vibecoding #buildinpublic #typescript #nodejs #react #docker #postgres #api #frontend


Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México


Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-08-30

#playadev #buildinpublic

Top comments (0)