Return to notes
Engineering notes· 3 min read

What a telemetry dashboard taught me about resilient interfaces

Building live coding, GitHub, and Spotify signals into one portfolio changed how I think about loading states, partial failure, and honest interface design.

#Next.js#Resilience#API design#User experience

The first version of my portfolio treated external data as decoration. If Spotify, WakaTime, and GitHub responded, the dashboard felt alive. If one of them did not, the whole experience felt unfinished.

That was the wrong mental model. Live data is not decoration; it is a small distributed system embedded in a user interface. Once I treated it that way, the design became calmer and the code became easier to reason about.

A useful interface should explain uncertainty without making the user carry it.

The hidden cost of a live-looking interface

Every external signal has a different idea of success. Spotify may be configured but paused. WakaTime may have no activity today. GitHub may rate-limit a request. A slow response is also different from a failed response, even though both initially look like “nothing happened.”

I started by naming those states instead of hiding them:

  • Unconfigured: credentials are intentionally absent.
  • Loading: the request is still inside its expected response window.
  • Active: fresh data is available.
  • Inactive: the service responded, but there is nothing current to show.
  • Unavailable: the service or network failed.

That small vocabulary prevented one generic fallback from carrying five different meanings.

Model the state before styling it

The most useful change was moving uncertainty into the data contract. Components should receive a state they can render, not reverse-engineer one from missing strings.

type TelemetryState<T> =
  | { status: "loading" }
  | { status: "ready"; data: T }
  | { status: "inactive"; message: string }
  | { status: "unavailable"; message: string };

This makes the rendering branch deliberate:

if (telemetry.status === "unavailable") {
  return <QuietStatus label={telemetry.message} />;
}

if (telemetry.status === "ready") {
  return <LiveSignal data={telemetry.data} />;
}

The code is not sophisticated, but the boundary is valuable. The component no longer needs to know whether an empty track name means “paused,” “not configured,” or “request failed.”

Partial failure should stay partial

One failed integration should never take the page down with it. Each API route now catches upstream failures and returns a stable, non-sensitive fallback. On the client, each card owns its loading and inactive behavior.

SignalUseful fallbackWhat should remain visible
SpotifyHide the playerThe rest of the portfolio
WakaTimeShow an unavailable stateSkills and project history
GitHubKeep the activity summary quietProject case studies

This is graceful degradation in a very small form: preserve the primary experience and reduce only the feature that lost its dependency.

Motion must communicate state

Animation can make live data feel responsive, but permanent motion creates false urgency. I kept motion tied to meaning:

  1. Equalizer bars move only while music is playing.
  2. Loading indicators stop when a request resolves.
  3. Entry animations run once instead of repeatedly demanding attention.
  4. Reduced-motion preferences remove non-essential movement.

The result feels more alive precisely because fewer things move without a reason.

Security is part of the interface boundary

The browser never needs Spotify secrets or a WakaTime API key. Those credentials stay in server route handlers, which expose only the fields the interface needs.

  • Keep credentials in server-only environment variables.
  • Normalize third-party responses before returning them.
  • Avoid sending raw upstream errors to the browser.
  • Provide a stable response shape during failure.

This also makes the UI less coupled to a provider. If an upstream response changes, the normalization layer absorbs most of that change.

What I would carry into the next build

I used to think resilience mostly belonged to backend infrastructure. This project reminded me that users experience reliability through the interface first.

My current rule is simple: design the empty, delayed, inactive, and failed states while designing the successful one. It produces clearer APIs, quieter interfaces, and fewer surprises when the network behaves like a network.

Continue reading

Design tenant boundaries before automation writes data