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
and the browser console logged:
Failed to load resource: the server responded with a status of 404 (Not Found) /condominios/incidencias
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;
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
+ );
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([
// …
]);
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),
};
}
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
],
},
];
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>
);
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
Then executed the stats endpoint:
curl http://localhost:3000/api/stats/condos | jq .
Result:
{
"complexes": 12,
"units": 342,
"feesPaid": 298,
"feesTotal": 320,
"assemblies": 4,
"incidents": 7,
"maintenance": 15
}
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)