DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Real‑Time IP Communication Status to the TV Inventory Dashboard

Adding Real‑Time IP Communication Status to the TV Inventory Dashboard

TL;DR: I extended the Prisma schema and React UI to surface a boolean “IP Comm” flag for each TV device, letting the inventory table show live connectivity status. The change required a new column, type updates, a badge component, and a small store tweak—all done without breaking existing queries.


The Problem

Our inventory page lists hundreds of TV devices, but we had no way to tell if a TV was currently reachable over the network. The backend periodically ingests a CSV report (sis_report.csv) that contains a terminalAddress (MAC) and a boolean ip_comm indicating whether the TV responded to a ping test. The UI displayed a “—” for that column because the Prisma model TvDevice didn’t have a matching field, resulting in a runtime error:

TypeError: Cannot read property 'ipComm' of undefined
    at InventoryTable (src/features/inventory/InventoryTable.tsx:42:27)
Enter fullscreen mode Exit fullscreen mode

The missing field also meant we couldn’t filter or sort by connectivity status, limiting our ability to quickly isolate offline units.


What I Tried First

My first attempt was to compute the IP status on the client side by joining the CSV data with the fetched devices in useDevicesStore. I added a helper calcIpComm(devices, csvRows) that returned a map of MAC → boolean. Then I patched each TvDeviceRow with ipComm before rendering.

Problems:

  1. Performance hit – The CSV can be >10 k rows; parsing it on every page load caused a noticeable UI lag (>500 ms).
  2. Stale data – The CSV is refreshed by a separate cron job; the client‑side merge never updated until a full page reload.
  3. Type mismatch – The TvDeviceRow interface in use-devices-store.ts didn’t have an ipComm field, so TypeScript threw errors across the codebase.

I reverted that approach and decided to push the IP flag into the database so the UI could treat it like any other column.


The Implementation

1. Extend the Prisma schema

File: prisma/schema.prisma

@@ -49,6 +49,13 @@ model TvDevice {
   tvPowerOns7Days  Int?
   stateModified    DateTime?

+  // IP communication status – derived from the SIS report (MAC ↔ ip_comm)
+  // Null means we haven’t processed a report for this device yet.
+  ipComm           Boolean?   @default(null)
+  ipCommCheckedAt  DateTime?  @default(null)
 }
Enter fullscreen mode Exit fullscreen mode

Why Boolean? The SIS report only tells us “reachable” (true) or “not reachable” (false). Using null signals “unknown” – useful for newly added TVs that haven’t been scanned yet.

After the schema change I ran:

npx prisma migrate dev --name add-ip-comm-status
Enter fullscreen mode Exit fullscreen mode

Prisma generated the migration SQL:

ALTER TABLE "TvDevice" ADD COLUMN "ipComm" BOOLEAN;
ALTER TABLE "TvDevice" ADD COLUMN "ipCommCheckedAt" TIMESTAMP;
Enter fullscreen mode Exit fullscreen mode

2. Update the TypeScript store

File: src/hooks/use-devices-store.ts

@@ -21,6 +21,8 @@ export interface TvDeviceRow {
   chromecastState: string | null;
   stays60Days: number | null;
   tvPowerOns7Days: number | null;
+  ipComm: boolean | null;
+  ipCommCheckedAt: string | null;
 }
Enter fullscreen mode Exit fullscreen mode

The store pulls rows via prisma.tvDevice.findMany(). Adding the fields required no extra code because Prisma automatically includes them in the generated type.

3. Create a badge component for IP status

File: src/components/inventory/status-badge.tsx

export function IpCommBadge({ ipComm }: { ipComm: boolean | null }) {
  if (ipComm === null) {
    return <Badge variant="neutral">—</Badge>;
  }
  return ipComm ? (
    <Badge variant="success">Online</Badge>
  ) : (
    <Badge variant="danger">Offline</Badge>
  );
}
Enter fullscreen mode Exit fullscreen mode

I kept the UI consistent with existing badges (OccupiedBadge, OutOfServiceBadge). The component returns a neutral dash for unknown status, mirroring the rest of the table.

4. Wire the badge into the inventory table

File: src/features/inventory/InventoryTable.tsx

@@ -18,6 +18,7 @@ import {
   SerialBadge,
   OutOfServiceBadge,
   OccupiedBadge,
+  IpCommBadge,
 } from "@/components/inventory/status-badge";
Enter fullscreen mode Exit fullscreen mode

Then inside the row rendering loop:

<td className="px-4 py-2">
  <IpCommBadge ipComm={row.ipComm} />
</td>
Enter fullscreen mode Exit fullscreen mode

I also added a column header:

<th className="px-4 py-2 text-left">IP Comm</th>
Enter fullscreen mode Exit fullscreen mode

5. Populate the new columns from the SIS report

The backend job that processes sis_report.csv already runs nightly. I extended its parser (src/jobs/process-sis-report.ts) to upsert the ipComm and ipCommCheckedAt fields:

await prisma.tvDevice.upsert({
  where: { terminalAddress: mac },
  update: {
    ipComm: ipCommFlag,
    ipCommCheckedAt: new Date(),
  },
  create: {
    terminalAddress: mac,
    // other required fields...
    ipComm: ipCommFlag,
    ipCommCheckedAt: new Date(),
  },
});
Enter fullscreen mode Exit fullscreen mode

Because the fields are optional, existing rows are updated without a migration lock, and newly discovered devices receive the flag immediately.

6. Adjust the API response shape

Our GraphQL resolver (src/graphql/resolvers/tvDevice.ts) now includes ipComm and ipCommCheckedAt in the selection set:

return prisma.tvDevice.findMany({
  select: {
    id: true,
    name: true,
    ipComm: true,
    ipCommCheckedAt: true,
    // …other fields
  },
});
Enter fullscreen mode Exit fullscreen mode

No client‑side changes were needed beyond the badge import.


Key Takeaway

Never treat a missing column as a UI‑only problem; push the data into the source of truth. Adding a boolean flag to the Prisma model gave us a single source of truth, eliminated client‑side merging, and let us reuse existing type‑generation pipelines. The pattern—extend the schema, run a migration, update the store interface, and add a small UI component—scales cleanly for any new telemetry field.


What's Next

I plan to add a real‑time WebSocket subscription that pushes ipComm updates as soon as the SIS job finishes, so the badge flips from “—” to “Online/Offline” without a page refresh. This will involve:

  1. Adding a pubsub.publish('IP_COMM_UPDATED', device) call in the job.
  2. Exposing a GraphQL subscription resolver.
  3. Updating InventoryTable to listen via useSubscription.

That will close the loop between data ingestion and UI responsiveness.


Tags: #vibecoding #buildinpublic #typescript #react #prisma #nextjs #graphql #webdev


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-12

#playadev #buildinpublic

Top comments (0)