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;
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;
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>.
I also tried to patch the service by adding a fallback:
const online = chromecastOnline ?? 0;
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
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,
+ };
}
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
statusvalue, 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>
);
};
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
});
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);
});
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)