Skip to content

Commit b48c006

Browse files
fix(ops): P2 batch — tooltip rollout, Stages display, WCAG hit target, Telemetry filter (#367)
Four P2 items from the 2026-05-14 QA punch list, shipped as one batch since they're all small UX/data corrections to existing pages. ## P2-1 — Roll the fast CSS tooltip system out to remaining pages The Specs page had fast 100ms tooltips via [data-tooltip] + ::after pseudo-element; other pages used native title= with browser-controlled delays (Safari ~1.5s — feels broken). Convert four sites that were still on title=: - base.html running-badge (data-tooltip + data-tooltip-position=bottom) - base.html env-value project-root chip (ditto) - workflows.html scope-na n/a label - runner.js Recent-strip chip scope hover Added a new [data-tooltip-position="bottom"] CSS variant so topbar elements render their tooltip below the element instead of above (default position would clip off-screen). Each conversion preserves aria-label for screen readers since data-tooltip is visual-only. Native <option> title attributes left as-is — CSS tooltips can't escape the native dropdown. ## P2-4 — "Stages 0" → em-dash with tooltip Meta-orchestration workflows don't expose a stages array; rendering them as "0" implies zero stages rather than "unknown/not-applicable". Template now shows "—" with a tooltip explaining the difference. ## P2-6 — WCAG 2.5.5 AA click target on Specs pills .status-pill-editable::before pseudo-element overlays a 24x24px click area around the visual pill. Pseudo-element sits behind the visible content (z-index: -1) so it doesn't intercept hover for the tooltip. Visual size unchanged — only the hit area grew. ## P2-8 — Filter test/stub workflows from Telemetry top-list read_telemetry_summary now cross-references the canonical list_workflows() registry; entries not in the registry (historical test fixtures, removed workflows) drop from the top-20 cost sort. Defensive fallback: if registry introspection fails, show everything rather than hide all data. Real-world impact: clears entries like "test-tier-fallback" and "new-sample-workflow1" (visible in the rules sync logs) from the Telemetry page's "top spenders" rollup. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a490889 commit b48c006

5 files changed

Lines changed: 83 additions & 10 deletions

File tree

src/attune/ops/data.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,33 @@ def read_telemetry_summary(config: Config, *, recent_days: int = 7) -> Telemetry
414414
except OSError:
415415
return TelemetrySummary(0, 0.0, 0.0, [], [], None)
416416

417-
by_workflow = sorted(
418-
((k, by_workflow_count[k], round(by_workflow_cost[k], 4)) for k in by_workflow_count),
419-
key=lambda row: row[2],
420-
reverse=True,
421-
)[:20]
417+
# Filter test fixtures, demo stubs, and removed workflows from the
418+
# top-list. The telemetry file accumulates entries from every
419+
# workflow that ever ran, so old test fixtures (e.g. those listed
420+
# in .claude/rules/attune/bug-patterns.md sync logs:
421+
# "test-tier-fallback", "new-sample-workflow1") pollute the top
422+
# spenders rollup and distort the "where did my money go?" signal.
423+
# The current registry is the source of truth — anything not in it
424+
# is either a removed workflow or a stub from local development.
425+
# (P2-8 in the 2026-05-14 QA punch list.)
426+
try:
427+
canonical_names = {w.name for w in list_workflows()}
428+
except Exception: # noqa: BLE001
429+
# INTENTIONAL: if the registry introspection fails, fall back
430+
# to showing everything rather than hiding all data.
431+
canonical_names = None
432+
433+
def _is_canonical(name: str) -> bool:
434+
if canonical_names is None:
435+
return True
436+
return name in canonical_names
437+
438+
filtered = [
439+
(k, by_workflow_count[k], round(by_workflow_cost[k], 4))
440+
for k in by_workflow_count
441+
if _is_canonical(k)
442+
]
443+
by_workflow = sorted(filtered, key=lambda row: row[2], reverse=True)[:20]
422444

423445
today = date.today()
424446
cutoff = today.toordinal() - recent_days

src/attune/ops/static/css/main.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,23 @@ a.kpi:hover {
733733
border-color: var(--accent);
734734
outline: none;
735735
}
736+
/* WCAG 2.5.5 AA target size — the visible pill is intentionally
737+
compact (~18-22px tall) but the click target needs ≥24×24px. The
738+
pseudo-element expands the hit area around the pill without
739+
changing its visual size. ``z-index: -1`` keeps it behind the
740+
visible content so it doesn't intercept hover for the ::after
741+
tooltip; ``inset: -3px`` adds 3px of click area on each side
742+
(≈ 24px tall total at default font, padding-bounded above 24px
743+
wide for codes like "drf"). */
744+
.status-pill-editable::before {
745+
content: "";
746+
position: absolute;
747+
inset: -3px;
748+
min-width: 24px;
749+
min-height: 24px;
750+
border-radius: 99px;
751+
z-index: -1;
752+
}
736753
.status-pill.flash-ok {
737754
outline: 2px solid var(--ok);
738755
outline-offset: 1px;
@@ -799,6 +816,13 @@ a.kpi:hover {
799816
opacity: 1;
800817
visibility: visible;
801818
}
819+
/* Flip tooltip below the element. Use for items in the sticky
820+
topbar where the default above-the-element position would
821+
render outside the viewport. */
822+
[data-tooltip][data-tooltip-position="bottom"]::after {
823+
bottom: auto;
824+
top: calc(100% + 6px);
825+
}
802826
/* Suppress the tooltip while the pill is in edit mode (the inline
803827
<select> takes over — a competing tooltip would be confusing). */
804828
.status-pill[data-editing="1"]::after {

src/attune/ops/static/js/runner.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,10 @@
519519
idTxt.textContent = String(run.id).slice(0, 8);
520520
a.appendChild(idTxt);
521521
if (run.path) {
522-
a.title = "scope: " + run.path;
522+
// Fast CSS tooltip (matches the system used on Specs and
523+
// the topbar — P2-1 rollout).
524+
a.setAttribute("data-tooltip", "scope: " + run.path);
525+
a.setAttribute("aria-label", "scope: " + run.path);
523526
}
524527
a.appendChild(document.createTextNode(" "));
525528
var statusTxt = document.createElement("span");

src/attune/ops/templates/base.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,24 @@
1919
{% endfor %}
2020
</nav>
2121
{% if current_run %}
22-
<a class="running-badge" href="/runs/{{ current_run.id }}/view" title="A workflow is running — click to view its output">
22+
{# data-tooltip + data-tooltip-position=bottom: fast CSS tooltip
23+
positioned below the topbar so it doesn't render above the
24+
viewport. aria-label keeps the cue accessible to screen readers. #}
25+
<a class="running-badge" href="/runs/{{ current_run.id }}/view"
26+
data-tooltip="A workflow is running — click to view its output"
27+
data-tooltip-position="bottom"
28+
aria-label="A workflow is running — click to view its output">
2329
<span class="running-dot" aria-hidden="true"></span>
2430
<span class="running-label">running</span>
2531
<code class="running-workflow">{{ current_run.workflow }}</code>
2632
</a>
2733
{% endif %}
2834
<div class="env">
2935
<span class="env-label">project</span>
30-
<code class="env-value" title="{{ project_root }}">{{ project_root.split('/')[-1] }}</code>
36+
<code class="env-value"
37+
data-tooltip="{{ project_root }}"
38+
data-tooltip-position="bottom"
39+
aria-label="Project root: {{ project_root }}">{{ project_root.split('/')[-1] }}</code>
3140
</div>
3241
</header>
3342
<main class="main">{% block content %}{% endblock %}</main>

src/attune/ops/templates/workflows.html

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,20 @@ <h1>Workflows</h1>
3737
<tr data-workflow="{{ w.name }}"
3838
data-scope-default="{{ default_scopes[w.name] if allow_run and supports_path[w.name] else '' }}">
3939
<td><code>{{ w.name }}</code></td>
40-
<td class="num">{{ w.stages }}</td>
40+
{# Stages=0 means the workflow doesn't expose a stages array
41+
(typical for meta-orchestration workflows that compose
42+
other workflows rather than declaring an explicit pipeline).
43+
Render '—' with a tooltip so users don't read it as
44+
"this workflow has zero stages" (P2-4 in the QA punch list). #}
45+
<td class="num">
46+
{% if w.stages > 0 %}
47+
{{ w.stages }}
48+
{% else %}
49+
<span class="muted"
50+
data-tooltip="Workflow doesn't expose a stages array. Meta-orchestration workflows compose other workflows rather than declaring an explicit pipeline."
51+
aria-label="Stages not declared"></span>
52+
{% endif %}
53+
</td>
4154
<td>
4255
{% for stage, tier in w.tier_map.items() %}
4356
<span class="chip chip-{{ tier|lower }}">{{ stage }}: {{ tier }}</span>
@@ -68,7 +81,9 @@ <h1>Workflows</h1>
6881
placeholder="e.g. src/attune/security/"
6982
aria-label="Custom scope path for {{ w.name }}">
7083
{% else %}
71-
<span class="scope-na" title="This workflow doesn't accept a path argument">n/a</span>
84+
<span class="scope-na"
85+
data-tooltip="This workflow doesn't accept a path argument"
86+
aria-label="This workflow doesn't accept a path argument">n/a</span>
7287
{% endif %}
7388
</td>
7489
<td class="num">

0 commit comments

Comments
 (0)