The Badge That Needed a Click: Undoing Host Blacklisting in vast-manager

In the middle of a sprawling coding session to build a GPU proving infrastructure for the Curio/cuzk Filecoin proving system, a single user message cut straight to a UX design question that exposed the relationship between two independent data systems. The message, at index 1371 in the conversation, read:

Is the 'known perf' 'bad' marker from db? if so make clicking the marker undo the mark. Also why is it 'bad' with no proofs per hr number?

This short question — part clarification request, part feature specification — arrived immediately after the assistant had implemented an "Ignore" button that allowed users to blacklist GPU hosts from the Vast.ai marketplace. The user was now looking at the result and asking for two things: a symmetric undo mechanism, and an explanation for a visual anomaly they had spotted.

The Context: A Management UI Takes Shape

To understand why this message was written, we need to step back into the broader arc of the session. The vast-manager was evolving from a basic monitoring dashboard into a full deployment platform. Over the preceding messages, the assistant had added an Offers panel — a searchable, filterable table of available GPU instances from Vast.ai, color-coded by hardware quality. GPU generations were highlighted in green (RTX 4090, RTX 6000 Ada), amber (RTX 3090, A-series), or red (older generations). CPU architectures received similar treatment. PCIe bandwidth, RAM size, and network speed all got visual indicators.

Into this rich visual system, the assistant had just added two new features at the user's request. First, a min-rate auto-calculation that derived the required proofs-per-hour from the instance price using a configurable cost-per-proof threshold (defaulting to $0.008/proof after a 10× correction). Second, an "Ignore" button on each offer row that would add the host to a bad_hosts database table, causing it to be visually struck through and optionally hidden from the list.

The "Ignore" feature was implemented in messages 1365–1370. The assistant added a red button to each offer row, a confirmation dialog showing the host ID, GPU name, and location, and a backend handler that inserted a row into the bad_hosts table with the reason "Not good enough (GPU, location)". The assistant then built and deployed the updated binary to the controller host at 10.1.2.104.

What the User Saw

After the deployment, the user was presumably looking at the Offers panel in their browser. The UI rendered each offer row with a series of visual badges. Two badge types were relevant:

  1. The "BAD" badge — a red rounded pill showing the text "BAD", with a tooltip containing the reason from the bad_hosts table. This appeared when o.is_bad_host was true.
  2. The performance badge — a colored pill showing a proofs-per-hour number (e.g., "45.2 p/h"), colored green (≥40), amber (25–40), or red (<25). This appeared when o.known_perf was populated from the host_perf table. The user noticed two things. First, the BAD badge was not interactive — clicking it did nothing. This felt asymmetrical: there was a way to add a host to the bad list (the Ignore button), but no way to remove one without some hidden mechanism. Second, some offers showed a BAD badge with no accompanying proofs-per-hour number, which looked odd and raised the question of whether the data was incomplete or the rendering was buggy.

The Reasoning Behind the Question

The user's first question — "Is the 'known perf' 'bad' marker from db?" — reveals an assumption about data provenance. The user was trying to understand whether the BAD badge was computed on the fly from some ephemeral state or persisted in the database. This matters because persistence determines behavior: if the marker is from the database, undoing it requires a database write (a DELETE operation), not just a UI toggle. The user was essentially asking, "If this is persistent data, can I get a UI action to remove it?"

The second question — "Also why is it 'bad' with no proofs per hr number?" — shows the user was reading the visual layout carefully. They had noticed that some rows displayed a BAD badge but no performance badge, and they wanted to understand whether this was a bug, a missing feature, or a legitimate state. This is a classic signal of a user who is both technically sophisticated and attentive to detail — they're not just using the tool, they're reasoning about its internal consistency.

Input Knowledge Required

To fully understand this message, a reader would need several pieces of context:

The Assistant's Analysis and Response

The assistant's thinking process, visible in the following messages (1372–1376), shows a methodical trace through the codebase. The assistant started by grepping for the relevant UI elements:

[grep] is_bad_host|bad_reason|known_perf|perf-badge
Found 10 matches

This revealed the two independent rendering paths. The assistant then reasoned aloud:

OK so the "BAD" badge and the "known perf" badge are separate — is_bad_host comes from the bad_hosts DB table, known_perf comes from the host_perf table. The BAD badge shows when the host is in the bad_hosts table, independently of whether there's perf data.
The reason BAD shows with no proofs/hr number: if a host was added via "Ignore" button (or manually), it's in bad_hosts but may not have an entry in host_perf (no benchmark was ever run). So is_bad_host=true but known_perf=null.

This analysis correctly identified that the two badges were backed by different database tables and that a host could be in one table without being in the other. The "Ignore" button added to bad_hosts without running a benchmark, so no host_perf entry existed. The BAD badge was therefore a legitimate standalone state — not a bug, but a natural consequence of the feature design.

The Implementation Decision

The assistant then made a straightforward implementation choice: make the BAD badge clickable, with the click calling DELETE /bad-host/{hostId} to remove the entry from bad_hosts. This was the symmetric counterpart to the Ignore button's POST /bad-host/{hostId}.

The edit changed the BAD badge from a static &lt;span&gt; to a clickable element with an onclick handler:

html += `<span class="perf-badge perf-bad" 
    onclick="unignoreHost(${o.host_id}, '${esc(o.gpu_name)}', '${esc(o.geolocation||'')}')"
    title="${esc(o.bad_reason||'Bad host')}">BAD</span>`;

The unignoreHost function was added alongside the existing ignoreHost function, calling the DELETE endpoint and refreshing both the offers list and the dashboard. The assistant then built and deployed the binary.

Assumptions Made

The user made several implicit assumptions in this message:

  1. The BAD marker was persistent database state, not ephemeral UI state. This was correct — the bad_hosts table persists across server restarts.
  2. Clicking the marker should undo it. This assumes a symmetric interaction model where any action that creates state should have a visible, in-place mechanism to reverse it. This is a good UX principle but not one the system was originally designed with.
  3. The absence of a proofs-per-hour number alongside BAD was noteworthy. The user assumed this was either a bug or had an explanation worth surfacing. They were right that it was the latter. The assistant made its own assumptions in responding:
  4. The user wanted the undo to happen via a DELETE API call, not a UI-only toggle. This was the correct architectural choice — since the data was persisted server-side, undoing required a server-side operation.
  5. The user's question about "no proofs per hr number" was a request for explanation, not a bug report. The assistant correctly interpreted this as confusion to be clarified rather than a defect to be fixed.
  6. The existing ignoreHost function structure could be mirrored for unignoreHost. This was a reasonable reuse pattern that kept the codebase consistent.

Output Knowledge Created

This message and its resolution produced several lasting artifacts:

The Deeper Design Lesson

This message is a small but perfect example of how real-world UI design unfolds in an interactive coding session. The user didn't file a bug report or write a formal spec — they looked at the screen, noticed something that felt off, and asked a question that combined "how does this work?" with "can you make it better?" The assistant had to answer both the explanatory and the engineering parts.

The interaction also reveals something about the trust and communication style in this session. The user felt comfortable asking "why is it bad with no proofs per hr number?" — a question that could have been interpreted as "is this a bug?" but was instead received as "help me understand the system." The assistant responded with a clear architectural explanation rather than defensiveness, and immediately translated the understanding into a code change.

This is the kind of micro-interaction that separates a well-tuned tool from a frustrating one. The user noticed an asymmetry (Ignore adds but doesn't remove) and a visual inconsistency (BAD without p/h). Both were addressed in a single deployment cycle. The result was not just a fix, but a shared understanding between builder and user about how the system's data layers relate — knowledge that would pay dividends in every future interaction with the vast-manager.