CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 32s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 33s
Milestone 333 step 5, and rule 27 — the counter had a tuning point from step 4 and no operator-facing one until now. The task said to reuse the snippet badge's classes rather than mint a parallel set, citing the eight duplicated CSS families the ledger already carries (#3207). `.usage-tag` lived in SnippetListView's SCOPED block, so "reuse" was not available: copying it into the rule pane would have been the ninth family, and importing it is not a thing a scoped block permits. So it was promoted rather than copied. Three pieces, each of which existed once and now exists once: - `components.css` gains `.usage-tag` / `.usage-dead`, geometry and colour only, with the scoped original deleted rather than left behind. - `UsageBadge.vue` holds the logic the two lists would otherwise duplicate — the >=3 dead-weight threshold, the empty-string-renders-nothing rule, the tooltip. - `types/usage.ts` holds `RecordUsage`, one client type over two tables. `SnippetUsage` becomes an alias, so no existing consumer changes. THE ADVICE IS A PROP, and that is the substance rather than the plumbing. The counts read identically for every kind; the remedy does not. A snippet offered and never opened should probably be rewritten or deleted — one action. A rule in the same position has TWO possible causes and the operator has to pick: its trigger may fire on the wrong work, in which case `when_to_apply` wants rewording, or it may genuinely not be wanted. Baking "delete it" into the component would give the wrong nudge half the time on the surface where being wrong is most expensive, since a deleted rule stops binding behaviour. The route zero-fills every row through `usage_for_rules`, one aggregate per page — per-row would be N+1 by construction. That matters more here than for snippets: every rule on every existing install predates `rule_usage_events`, so the zero-filled shape IS the common case for a while, and a route that attached the key only where it found events would leave the badge reading undefined on almost every row. `usage_for_rules` had no test at all — step 1 covered the write path and the zero shape and left the aggregate uncovered, which only became load-bearing when a list started rendering it. It now has an integration test over real Postgres, including that a rule with no events comes back zero-filled rather than absent. Recorded as snippet #3460, per the design system's own instruction that the component layer lives as snippets rather than as prose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcCs1CcQ1ormdnzSshKqvN
63 lines
2.2 KiB
Vue
63 lines
2.2 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* "N/M used" on a list row — surfaced vs opened, for any record kind.
|
||
*
|
||
* Extracted from SnippetListView when the rule list needed the same chip
|
||
* (milestone 333 step 5). The counts read identically for both; what differs
|
||
* is the ADVICE, which is why that is a prop. A snippet surfaced repeatedly
|
||
* and never opened should probably go; a rule in the same position may simply
|
||
* have a `when_to_apply` that fires on the wrong thing, and telling an
|
||
* operator to delete it would be the wrong nudge half the time.
|
||
*/
|
||
import type { RecordUsage } from "@/types/usage";
|
||
|
||
const props = defineProps<{
|
||
usage?: RecordUsage | null;
|
||
/** What to suggest when this record looks like dead weight. Appended to the
|
||
* tooltip; kind-specific, because the remedies are. */
|
||
deadWeightAdvice: string;
|
||
/** What the record is called in the tooltip's own sentence. */
|
||
noun?: string;
|
||
}>();
|
||
|
||
/** Offered repeatedly and never opened. Three rather than one because one or
|
||
* two surfacings is noise — the record may simply not have come up in a
|
||
* relevant context yet. */
|
||
const isDeadWeight = () =>
|
||
!!props.usage && props.usage.pull_count === 0 && props.usage.surfaced_count >= 3;
|
||
|
||
/** "" renders nothing. A record nobody has surfaced yet gets no badge at all:
|
||
* "0/0" would read as a verdict when it is an absence of evidence — and on a
|
||
* freshly-migrated install that is every row. */
|
||
const label = () => {
|
||
const u = props.usage;
|
||
if (!u || u.surfaced_count === 0) return "";
|
||
return `${u.pull_count}/${u.surfaced_count} used`;
|
||
};
|
||
|
||
const title = () => {
|
||
const u = props.usage;
|
||
if (!u) return "";
|
||
const last = u.last_pulled_at
|
||
? `Last opened ${new Date(u.last_pulled_at).toLocaleDateString()}.`
|
||
: "Never opened.";
|
||
const verdict = isDeadWeight() ? ` ${props.deadWeightAdvice}` : "";
|
||
return (
|
||
`Surfaced to an agent ${u.surfaced_count}×, opened in full ` +
|
||
`${u.pull_count}×. ${last}${verdict}`
|
||
);
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<span
|
||
v-if="label()"
|
||
class="usage-tag"
|
||
:class="{ 'usage-dead': isDeadWeight() }"
|
||
:title="title()"
|
||
>{{ label() }}</span>
|
||
</template>
|
||
|
||
<!-- The look lives in components.css (canon). Nothing scoped here on purpose:
|
||
a view that needs different spacing keeps that as its own remainder. -->
|