The Anatomy of a Badge: Data Provenance and UX Design in the Vast-Manager Dashboard
Introduction
In the sprawling development of the vast-manager system—a comprehensive deployment and monitoring platform for GPU proving workers on Vast.ai—small UI details often reveal deep architectural truths. Message [msg 1376] appears, at first glance, to be a simple status update: the assistant has made the "BAD" badge clickable and explained why some hosts show a "BAD" marker without a proofs-per-hour number. But beneath this brief response lies a rich story about data modeling, user experience iteration, and the subtle complexity of building trust into a distributed system. This article examines that single message as a case study in how provenance-aware design emerges from practical debugging.
The Message
The assistant's response reads in full:
Done.
>
BAD badge is now clickable — clicking it calls DELETE /bad-host/{id} to remove the host from the bad_hosts table, immediately updates the offers list, and refreshes the dashboard.
>
Why BAD shows without a p/h number: They're two independent data sources.BAD= host is in thebad_hoststable (manually ignored). Thep/hbadge = host has an entry in thehost_perftable (benchmark was run). A host marked via "Ignore" was never benchmarked, so it has no perf data — only the BAD marker. If a host was benchmarked and also got bad-listed (e.g., benchmark failed min_rate), both badges would show.
This message closes a loop that began with the user's question in [msg 1371]: "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?"
The Context: Two Independent Data Sources
To understand why this message matters, one must understand the data architecture of the vast-manager system. The system tracks GPU hosts through two separate database tables, each serving a distinct purpose.
The bad_hosts table is a manual blacklist. It records hosts that a human operator has decided are "not good enough" for deployment. This table is populated through the "Ignore" button on offer rows (added in <msg id=1366-1370>) and can also accumulate entries when a benchmark fails to meet the minimum proofs-per-hour rate. The bad_hosts table stores a host_id (later refactored to machine_id in a subsequent chunk), a reason string, and a timestamp. It is fundamentally a human judgment: "I don't want to rent from this machine again."
The host_perf table is an empirical measurement store. It records the actual benchmark results from instances that were deployed and ran the proving benchmark. Each entry contains a machine_id, a bench_rate (proofs per hour), and a timestamp. This table is populated automatically when a deployed instance completes its benchmark cycle. It is fundamentally a measurement: "This machine achieved X proofs per hour."
These two tables are independent. A host can appear in bad_hosts without ever appearing in host_perf (if it was manually ignored before deployment). A host can appear in host_perf without appearing in bad_hosts (if it passed its benchmark). And a host can appear in both (if it was benchmarked but failed to meet the minimum rate, triggering an automatic bad-listing).
The User's Confusion and the Assistant's Investigation
The user's question in [msg 1371] reveals a natural assumption: that a "BAD" marker and a proofs-per-hour number are part of the same data system. If a host is "bad," the user expected to see a performance number explaining why it's bad. The absence of that number was confusing.
The assistant's investigation in <msg id=1372-1373> traced the UI rendering logic. The is_bad_host flag comes from the bad_hosts table via the dashboard API response. The known_perf data comes from the host_perf table via the offers search API. These are fetched through separate backend handlers and assembled into the offer row independently. The UI code in ui.html renders them as separate <span> elements:
if (o.is_bad_host) {
html += `<span class="perf-badge perf-bad" ...>BAD</span>`;
}
if (o.known_perf) {
const rate = o.known_perf.bench_rate;
const cls = rate >= 40 ? 'perf-good' : rate >= 25 ? 'perf-ok' : 'perf-bad';
html += `<span class="perf-badge ${cls}">${rate.toFixed(1)} p/h</span>`;
}
The perf-bad CSS class on the BAD badge gives it a red color, but the badge itself contains no numeric data. The confusion is understandable: the same CSS class name (perf-bad) is used for both the BAD marker and the low-performance numeric badge, suggesting a relationship that doesn't exist.
The Fix: Making the Badge Interactive
The assistant's response in [msg 1376] describes two outcomes. First, the BAD badge is now clickable. Clicking it calls DELETE /bad-host/{id}, which removes the host from the bad_hosts table. The UI then immediately updates the offers list (re-rendering the row without the BAD badge) and refreshes the dashboard (updating the bad hosts panel). This is a classic undo pattern: the user can reverse their earlier "Ignore" action directly from the badge that represents it.
The implementation involved adding an onclick handler to the BAD badge span and creating an unignoreHost(hostId) JavaScript function that sends the DELETE request and triggers the UI refresh. This follows the same pattern as the ignoreHost function added in [msg 1368], creating a symmetrical pair of actions: ignore and unignore.
The Explanation: Data Provenance as UX
The second part of the message is an explanation of why BAD can appear without a proofs-per-hour number. This explanation is essentially a lesson in data provenance—the origin and lineage of each piece of information displayed in the UI.
The assistant articulates a clear conceptual model:
- BAD badge = presence in
bad_hoststable (human judgment) - p/h badge = presence in
host_perftable (empirical measurement) - Both badges = host appears in both tables (judgment + measurement) This explanation serves multiple purposes. It educates the user about the system's data architecture. It validates the user's observation that something was confusing. And it establishes a design principle: the UI should make data provenance visible, so users can understand why they see what they see.
Assumptions and Design Decisions
The assistant made several assumptions in this interaction. It assumed that the user wanted a technical explanation rather than just a fix—and the user's follow-up questions suggest this was correct. It assumed that the BAD badge should be clickable to undo the ignore action, which is a reasonable UX pattern but not the only possible design (an alternative might have been a separate "Undo" button in the bad hosts panel). It assumed that the immediate UI refresh pattern (re-rendering offers and dashboard after the DELETE) was the right level of responsiveness.
One notable assumption is that the host_id (later machine_id) used in the bad_hosts table is the correct identifier for the unignore action. At this point in the conversation, the system was still using host_id (the Vast.ai operator account ID) rather than machine_id (the specific physical machine identifier). The data integrity fix to switch to machine_id would come in the next chunk (<msg id=1377+>), but at the time of this message, the assumption was that host_id was sufficient. This assumption would later prove incorrect when a single operator with diverse hardware was unfairly penalized.
Input and Output Knowledge
To understand this message, a reader needs knowledge of the vast-manager's database schema (the existence of bad_hosts and host_perf tables), the deployment workflow (instances are deployed, benchmarked, and either pass or get bad-listed), and the UI architecture (offers are fetched from the search API, dashboard from the dashboard API, and they render independently).
The message creates new knowledge: the BAD badge is now interactive, the data provenance model is explicitly documented, and the relationship between the two data sources is clarified. This knowledge is immediately actionable—users can now undo accidental ignores without needing to find a separate management interface.
The Thinking Process
The assistant's reasoning is visible in the sequence of messages leading to [msg 1376]. In [msg 1372], the assistant greps the codebase to understand the rendering logic. In [msg 1373], it traces the data flow and identifies the root cause: two independent data sources. It then makes a design decision: rather than changing the data model (e.g., always requiring a reason or perf number for bad hosts), it changes the UI to make the badge interactive and explains the architecture to the user.
This is a pragmatic choice. Changing the data model to require a performance number for every bad host would break the "Ignore" workflow (where a host is rejected without ever being benchmarked). Adding a synthetic "no data" display would add complexity. Instead, the assistant preserves the clean separation of concerns and makes the interaction model clearer.
Conclusion
Message [msg 1376] is a small but revealing moment in the vast-manager development. It demonstrates how a seemingly simple UI question—"why does this badge show without a number?"—can expose fundamental architectural decisions about data provenance, separation of concerns, and the relationship between human judgment and empirical measurement. The fix is elegant: make the badge clickable, explain the data model, and trust the user to understand. In doing so, the assistant transforms a point of confusion into an opportunity for education and empowerment.