DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Accurate Chromecast Disconnected Count to the TV Wallboard

Adding Accurate Chromecast Disconnected Count to the TV Wallboard

TL;DR: I extended the dashboard API and UI to expose a real‑time chromecastDisconnected metric, fixing the misleading “always‑zero” count. The change required a new field in DashboardData, a refactor of dashboard.service.ts to compute the value, and a small UI tweak in TVDashboard.tsx.


The Problem

The TV wallboard (/tv) displayed three Chromecast metrics:

online: number;
chromecastOnline: number;
chromecastTotal: number;
Enter fullscreen mode Exit fullscreen mode

chromecastDisconnected was hard‑coded to 0 in the UI, which made the wallboard lie whenever a device lost connection. The symptom was obvious in production: the UI showed “Disconnected: 0” even when the logs from the sync service reported dozens of offline Chromecasts. No error was thrown; the data simply wasn’t being calculated.

What I Tried First

My first instinct was to derive the disconnected count on the client side:

const disconnected = data.chromecastTotal - data.chromecastOnline;
Enter fullscreen mode Exit fullscreen mode

I added this line in src/features/tv/TVDashboard.tsx and displayed it. It worked for the happy path, but it broke when the backend started returning null for chromecastOnline (a recent edge case when the sync job fails partially). The subtraction produced NaN, and the UI threw a runtime warning:

Warning: Received NaN for prop `chromecastDisconnected` on <StatCard>.
Enter fullscreen mode Exit fullscreen mode

I also tried to patch the service by adding a fallback:

const online = chromecastOnline ?? 0;
Enter fullscreen mode Exit fullscreen mode

But this masked the underlying data quality issue and still left the UI responsible for a derived metric. The wallboard should reflect the source of truth, not compute it locally.

The Implementation

1. Extend the data contract

The first step was to make the API return the disconnected count explicitly. I added the field to the DashboardData interface in src/features/tv/TVDashboard.tsx:

@@ -24,6 +24,7 @@ interface DashboardData {
   online: number;
   chromecastOnline: number;
   chromecastTotal: number;
+  chromecastDisconnected: number;
   tvOnline: number;
   noMac: number;
   // … other fields
Enter fullscreen mode Exit fullscreen mode

Adding the field forces TypeScript to enforce its presence throughout the codebase, preventing accidental omission.

2. Compute the metric in the service layer

All device aggregation lives in src/services/dashboard.service.ts. I refactored getDeviceCounts to calculate chromecastDisconnected directly from the raw device list instead of relying on the UI:

@@ -5,26 +5,37 @@ export async function getLastSync() {
 }

 export async function getDeviceCounts() {
-  const [total, validMac, chromecast, chromecastValid, noMac, noTv, byLocation] = await Promise
-    .all([
-      deviceRepo.countAll(),
-      deviceRepo.countValidMac(),
-      deviceRepo.countByType('chromecast'),
-      deviceRepo.countValidByType('chromecast'),
-      deviceRepo.countNoMac(),
-      deviceRepo.countNoTv(),
-      deviceRepo.groupByLocation(),
-    ]);
-
-  return {
-    total,
-    validMac,
-    chromecastOnline: chromecastValid,
-    chromecastTotal: chromecast,
-    tvOnline: total - noTv,
-    noMac,
-    // …
-  };
+  // Fetch raw device rows once – we need the full list to compute the disconnected count.
+  const devices = await deviceRepo.getAll(); // [{id, type, status, …}, …]
+
+  const total = devices.length;
+  const validMac = devices.filter(d => !!d.mac).length;
+  const chromecastDevices = devices.filter(d => d.type === 'chromecast');
+  const chromecastTotal = chromecastDevices.length;
+  const chromecastOnline = chromecastDevices.filter(d => d.status === 'online').length;
+  const chromecastDisconnected = chromecastDevices.filter(d => d.status !== 'online').length;
+
+  const noMac = devices.filter(d => !d.mac).length;
+  const noTv = devices.filter(d => d.type !== 'tv').length;
+  const byLocation = aggregateByLocation(devices);
+
+  return {
+    total,
+    validMac,
+    chromecastOnline,
+    chromecastTotal,
+    chromecastDisconnected, // <-- new field
+    tvOnline: total - noTv,
+    noMac,
+    byLocation,
+  };
 }
Enter fullscreen mode Exit fullscreen mode

Why this change?

  • Single source of truth – The service now owns the calculation, guaranteeing consistency across any consumer (wallboard, API, admin UI).
  • Performance – We only hit the DB once (getAll) instead of multiple count queries, which reduces round‑trips from 7 to 1.
  • Resilience – If a device has an unexpected status value, it falls into the “disconnected” bucket, which is safer than silently treating it as online.

3. Wire the new field into the component

In TVDashboard.tsx I imported the updated type and displayed the metric:

import { DashboardData } from '../../services/dashboard.service';

const TVDashboard: React.FC = () => {
  const [data, setData] = useState<DashboardData | null>(null);

  useEffect(() => {
    async function fetchCounts() {
      const counts = await getDeviceCounts();
      setData(counts);
    }
    fetchCounts();
  }, []);

  if (!data) return <Spinner />;

  return (
    <section className="grid gap-4 md:grid-cols-3">
      <StatCard title="Chromecast Online" value={data.chromecastOnline} />
      <StatCard title="Chromecast Total" value={data.chromecastTotal} />
      <StatCard
        title="Chromecast Disconnected"
        value={data.chromecastDisconnected}
        color="red"
      />
      {/* other cards */}
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

No more derived calculations, no more NaN warnings. The UI now trusts the backend.

4. Update the API contract (if applicable)

If the wallboard consumes a REST endpoint (/api/dashboard), I added the new field to the response schema in src/api/dashboard.controller.ts:

router.get('/dashboard', async (req, res) => {
  const counts = await getDeviceCounts();
  res.json(counts); // counts now includes chromecastDisconnected
});
Enter fullscreen mode Exit fullscreen mode

I also bumped the OpenAPI spec (swagger.yaml) to keep documentation accurate.

5. Tests

Added a unit test in tests/services/dashboard.service.test.ts to assert the new field:

test('getDeviceCounts returns correct chromecastDisconnected', async () => {
  // Mock deviceRepo.getAll() to return a mix of online/offline chromecasts
  deviceRepo.getAll = jest.fn().mockResolvedValue([
    { id: 1, type: 'chromecast', status: 'online', mac: 'AA:BB' },
    { id: 2, type: 'chromecast', status: 'offline', mac: 'CC:DD' },
    { id: 3, type: 'tv', status: 'online', mac: 'EE:FF' },
  ]);

  const result = await getDeviceCounts();
  expect(result.chromecastDisconnected).toBe(1);
});
Enter fullscreen mode Exit fullscreen mode

All CI pipelines passed after the change.

Key Takeaway

Never let the UI derive a metric that the backend can compute reliably. Centralizing the calculation in the service layer gives you a single source of truth, reduces DB round‑trips, and eliminates client‑side edge‑case handling.

What's Next

The next step is to expose a **


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

Repo: zaerohell/tvview · 2026-10-03

#playadev #buildinpublic

Top comments (0)