bradtraversy.dev — 2026-05-02-mc-network-tab-feels-live.md
home.md projects/ tools/ devlog/ × articles/ now.md about.md
2026-05-02 · #mission-control · #devlog #architecture

# making the network tab feel live (per-tab, not globally)

mission control’s network tab shows scheduled-task health: who runs on what cadence, when each last fired, whether the last run was green or red. the data is regenerated by a cron every fifteen minutes through the day. but the ui never knew. i had to refresh the page to see the latest.

the existing global watcher (chokidar + sse) has an exclusion list that skips Network/data/* because that data churns on every cron tick and would refresh every tab in the browser, mostly for no reason. the right thing was not to remove the exclusion globally.

per-tab refresh instead

the cleaner version is: each tab that needs near-live freshness mounts its own tiny “refresh while i’m here” component:

'use client';
 
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
 
export function NetworkLiveRefresh() {
  const router = useRouter();
  useEffect(() => {
    const id = setInterval(() => router.refresh(), 60_000);
    return () => clearInterval(id);
  }, [router]);
  return null;
}

mounted at the top of /network, ticks every sixty seconds, cleans up when you leave the tab. no other tab pays any cost. if i ever want the same behavior on the home tab or a future “live deploys” page, i drop the same component in. the global watcher continues to do what it was originally designed for: vault edits that humans care about seeing immediately.

the rule that fell out of this: don’t centralize freshness. let the tabs that need it ask for it. the central infrastructure is for the class of changes that affect everything.

sortable columns, while i was in there

the automations table grew enough rows that scanning for “what failed” was annoying. extracted the table into a 'use client' component, added sort state on each header. status sorts by traffic- light severity (red → yellow → unknown → green) so failures rise to the top automatically; everything else is alphabetical or chronological.

clicking a header cycles asc → desc → unsorted. unsorted falls back to the page’s source order. the third click is the one nobody remembers exists, but it’s the only way to undo without a refresh.

the bite that keeps biting

third project in a row hit the same next.js error: passing a function as a prop into a client component fails at runtime with “Functions cannot be passed directly to Client Components.” the fix is consistent: pre-compute the strings server-side and ship plain data:

// server page
const rows: AutomationRowView[] = automations.map((a) => ({
  data: a,
  displayStatus: STATUS_LABEL[a.status],
  lastRunRelative: formatRelativeTime(a.lastRun),
}));
 
return <AutomationsTable rows={rows} />;

the client gets displayStatus: "ok" already as a string, not a formatter that can produce one. once you’re disciplined about it, client components only care about display, not derivation.

none of this changes what the dashboard does. it changes how the data feels. going from “this number is from when i loaded the page” to “this number is from a minute ago, and it’ll be a minute newer in a minute.” that’s most of the difference between a dashboard you check and one you trust.

// EOF 2026-05-02-mc-network-tab-feels-live.md
main
2026-05-02-mc-network-tab-feels-live.md
UTF-8
LF
Markdown
Ln 1, Col 1