Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-06-06 - Interactive badges as spans
**Learning:** Found interactive badges (like `healthBadge` and `staleBadge`) built as `<span>` elements with `onclick` handlers, breaking keyboard navigation and screen reader semantics. There are also inputs and selects with no `aria-label` or `<label>` tag.
**Action:** Always check if an `onclick` is on a non-interactive element like a `div` or `span`. Convert them to `<button>` or add appropriate `role="button"` and `tabindex="0"`. Add missing `aria-label` attributes to form elements not directly tied to visible labels.
109 changes: 109 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions src/filigree/static/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
<button id="pillActive" onclick="toggleStatusPill('active')" class="px-2 py-1 rounded text-xs font-medium bg-accent text-primary">Active</button>
<div class="relative">
<button id="pillDone" onclick="toggleStatusPill('done')" class="px-2 py-1 rounded text-xs font-medium bg-overlay text-secondary border border-strong">Done</button>
<select id="doneTimeBound" onchange="onDoneTimeBoundChange()" class="hidden absolute top-full right-0 mt-1 bg-overlay text-primary text-xs rounded px-1 py-0.5 border border-strong z-10">
<select id="doneTimeBound" onchange="onDoneTimeBoundChange()" class="hidden absolute top-full right-0 mt-1 bg-overlay text-primary text-xs rounded px-1 py-0.5 border border-strong z-10" aria-label="Time bound for done issues">
<option value="1">24 hours</option>
<option value="3">3 days</option>
<option value="7">7 days</option>
Expand Down Expand Up @@ -217,7 +217,7 @@

<div class="flex items-center gap-3 text-xs text-secondary">
<span id="refreshIndicator" class="opacity-0 text-accent">Refreshing...</span>
<span id="healthBadge" onclick="showHealthBreakdown()" class="cursor-pointer px-2 py-0.5 rounded text-xs font-bold" title="System Health Score — click for breakdown">--</span>
<button id="healthBadge" onclick="showHealthBreakdown()" class="cursor-pointer px-2 py-0.5 rounded text-xs font-bold" title="System Health Score — click for breakdown" style="min-height: unset">--</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Give the health badge button a descriptive name

When this badge is reachable by keyboard, its accessible name comes from its text content, which is just -- initially and later only the numeric score from computeHealthScore(). Screen reader users tabbing to it will hear something like “82 button” without the “system health” context or that it opens a breakdown; the title is not a reliable accessible name when the button has content. Add an aria-label here and update it alongside the score.

Useful? React with 👍 / 👎.

<div class="relative">
<button onclick="toggleSettingsMenu(event)" id="settingsGear" class="text-xs px-2 py-1 rounded bg-overlay bg-overlay-hover" title="Settings" aria-label="Settings menu">&#9881;</button>
<div id="settingsDropdown" class="hidden absolute right-0 top-full mt-1 rounded-lg shadow-xl text-xs z-50" style="background:var(--surface-base);border:1px solid var(--border-strong);min-width:160px">
Expand Down Expand Up @@ -340,7 +340,7 @@
<button id="btnCluster" onclick="switchKanbanMode('cluster')" class="px-2 py-0.5 rounded" title="Group issues by parent epic with progress bars">Cluster</button>
<button id="btnList" onclick="switchKanbanMode('list')" class="px-2 py-0.5 rounded" title="Table list view for large projects">List</button>
<span class="text-muted">|</span>
<select id="filterType" onchange="applyTypeFilter()" class="bg-overlay text-primary text-xs rounded px-2 py-0.5 border border-strong" title="Filter to one issue type and show its workflow states as columns">
<select id="filterType" onchange="applyTypeFilter()" class="bg-overlay text-primary text-xs rounded px-2 py-0.5 border border-strong" title="Filter to one issue type and show its workflow states as columns" aria-label="Filter by issue type">
<option value="">All types</option>
</select>
<span id="typeFilterPill" class="hidden text-xs px-2 py-0.5 rounded border text-accent" style="background:var(--accent-subtle);border-color:var(--accent)">
Expand Down Expand Up @@ -373,7 +373,7 @@
<div class="shrink-0 px-6 pt-6 pb-2">
<div class="max-w-3xl mx-auto flex items-center gap-3">
<span class="text-base font-semibold text-primary">Insights</span>
<select id="insightsDays" onchange="loadMetrics()" class="bg-overlay text-xs rounded px-2 py-1 border border-strong">
<select id="insightsDays" onchange="loadMetrics()" class="bg-overlay text-xs rounded px-2 py-1 border border-strong" aria-label="Time range for insights">
<option value="7">7 days</option>
<option value="30" selected>30 days</option>
<option value="90">90 days</option>
Expand All @@ -394,7 +394,7 @@
<span class="text-base font-semibold text-primary">Files</span>
<div class="flex items-center gap-2">
<input id="filesSearch" type="text" placeholder="Filter by path..."
class="bg-overlay text-primary text-xs rounded px-3 py-1 border border-strong w-64 focus:outline-none focus-accent">
class="bg-overlay text-primary text-xs rounded px-3 py-1 border border-strong w-64 focus:outline-none focus-accent" aria-label="Filter files by path">
<label class="flex items-center gap-1 text-xs text-secondary">
<input type="checkbox" id="filesCriticalOnly" style="accent-color:var(--accent)"> Critical only
</label>
Expand Down Expand Up @@ -468,7 +468,7 @@ <h2 class="text-base font-semibold text-primary m-0">Releases</h2>
<span>Blocked: <b id="footBlocked" class="text-red-400">0</b></span>
<span>Deps: <b id="footDeps" class="text-primary">0</b></span>
<canvas id="sparkline" width="100" height="20" title="14-day throughput trend" class="opacity-70"></canvas>
<span id="staleBadge" class="hidden text-xs bg-red-900/50 text-red-400 px-2 py-0.5 rounded border border-red-800 cursor-pointer" onclick="showStaleIssues()" title="WIP issues with no updates for >2 hours. Click to view."></span>
<button id="staleBadge" class="hidden text-xs bg-red-900/50 text-red-400 px-2 py-0.5 rounded border border-red-800 cursor-pointer" onclick="showStaleIssues()" title="WIP issues with no updates for >2 hours. Click to view." style="min-height: unset"></button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve touch targets for converted badges

The inline min-height: unset on the converted badge buttons overrides the coarse-pointer rule that makes buttons at least 44px tall, so on touch devices the stale badge remains a tiny text pill despite now being the primary tap target for opening stale issues. Remove the override for coarse pointers or replace it with styling that keeps the 44px hit area while preserving the visual pill size.

Useful? React with 👍 / 👎.

<span class="ml-auto text-muted" id="footVersion"></span>
</footer>

Expand Down
68 changes: 34 additions & 34 deletions src/filigree/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

// --- Module imports ---

import { computeHealthScore, computeImpactScores } from "./analytics.js";
import { fetchAllData, fetchDashboardConfig, fetchFileSchema, fetchProjects } from "./api.js";
import {
applyFilters,
Expand All @@ -21,15 +22,15 @@ import {
callbacks as filtersCallbacks,
loadPreset,
loadProjectFilterSettings,
onDoneTimeBoundChange,
populatePresets,
savePreset,
populateTypeFilter,
savePreset,
toggleBlocked,
toggleCardSelect,
toggleMultiSelect,
toggleReady,
toggleStatusPill,
onDoneTimeBoundChange,
trackChanges,
updateTypeFilterUI,
} from "./filters.js";
Expand All @@ -48,9 +49,9 @@ import {
batchSetPriority,
closeSettingsMenu,
copyIssueId,
endTour,
escHtml,
escJsSingleAttr,
endTour,
reloadServer,
showCreateForm,
showToast,
Expand Down Expand Up @@ -82,22 +83,40 @@ import {
showAddBlocker,
updateIssue,
} from "./views/detail.js";
import { computeHealthScore, computeImpactScores } from "./analytics.js";
import {
clearScanSourceFilter,
closeFileDetail,
closeFinding,
createIssueFromFinding,
filesPageNext,
filesPagePrev,
filterFindings,
filterTimeline,
loadFiles,
loadMoreFindings,
loadMoreTimeline,
openFileDetail,
selectFinding,
showLinkIssueModal,
sortFiles,
submitLinkIssue,
switchFileTab,
} from "./views/files.js";
import {
callbacks as graphCallbacks,
graphFit,
renderGraph,
refreshCriticalPathState,
renderGraph,
setCriticalPathStateFromPath,
showHealthBreakdown,
toggleCriticalPath,
} from "./views/graph.js";
import {
renderGraphSidebar,
graphSidebarSelectAll,
attachSidebarListeners,
graphSidebarClearAll,
graphSidebarSelectAll,
rebuildTreeIndex,
attachSidebarListeners,
renderGraphSidebar,
callbacks as sidebarCallbacks,
} from "./views/graphSidebar.js";
import {
Expand All @@ -112,31 +131,7 @@ import {
showStaleIssues,
updateStaleBadge,
} from "./views/metrics.js";
import {
loadPlanView,
loadWorkflow,
loadWorkflowInModal,
showWorkflowModal,
} from "./views/workflow.js";
import {
closeFinding,
clearScanSourceFilter,
closeFileDetail,
createIssueFromFinding,
filesPageNext,
filesPagePrev,
filterFindings,
filterTimeline,
loadFiles,
loadMoreFindings,
loadMoreTimeline,
openFileDetail,
selectFinding,
showLinkIssueModal,
sortFiles,
submitLinkIssue,
switchFileTab,
} from "./views/files.js";
import { loadReady } from "./views/ready.js";
import {
collapseAllReleaseTree,
loadReleases,
Expand All @@ -145,7 +140,12 @@ import {
toggleReleaseExpand,
toggleReleaseTreeNode,
} from "./views/releases.js";
import { loadReady } from "./views/ready.js";
import {
loadPlanView,
loadWorkflow,
loadWorkflowInModal,
showWorkflowModal,
} from "./views/workflow.js";

// ---------------------------------------------------------------------------
// Core data fetching (lives here because it touches every module)
Expand Down
9 changes: 4 additions & 5 deletions src/filigree/static/js/views/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { fetchCriticalPath } from "../api.js";
import { CATEGORY_COLORS, state, THEME_COLORS } from "../state.js";
import { resolveGraphScope, handleGhostClick } from "./graphSidebar.js";
import { handleGhostClick, resolveGraphScope } from "./graphSidebar.js";

// --- Callbacks for functions not yet available at import time ---

Expand Down Expand Up @@ -319,7 +319,7 @@ export function renderGraph() {
const filteredIds = new Set(filteredNodes.map((n) => n.id));
const search = document.getElementById("filterSearch")?.value?.toLowerCase().trim() || "";

let cyNodes = filteredNodes.map((n) => {
const cyNodes = filteredNodes.map((n) => {
const title = n.title || n.id;
const isGhost = ghostIds.has(n.id);
const matchesSearch =
Expand All @@ -340,7 +340,7 @@ export function renderGraph() {
};
});

let cyEdges = scopeEdges
const cyEdges = scopeEdges
.filter((e) => filteredIds.has(e.source) && filteredIds.has(e.target))
.map((e) => ({
data: {
Expand Down Expand Up @@ -407,8 +407,7 @@ export function renderGraph() {
state.cy.destroy();

const canReusePositions =
cyNodes.length > 0 &&
cyNodes.every((n) => Object.prototype.hasOwnProperty.call(previousPositions, n.data.id));
cyNodes.length > 0 && cyNodes.every((n) => Object.hasOwn(previousPositions, n.data.id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the graph check compatible with older browsers

This dashboard JS is served directly as browser modules without transpilation, and replacing Object.prototype.hasOwnProperty.call(...) with Object.hasOwn(...) makes graph re-renders throw in browsers that do not implement the newer ES2022 helper. In those environments, any graph topology change reaches this branch and prevents the graph from rebuilding; keep the previous helper or add a compatibility wrapper.

Useful? React with 👍 / 👎.

const graphMinZoom = computeGraphMinZoom(cyNodes.length);
state.cy = cytoscape({
container,
Expand Down
2 changes: 1 addition & 1 deletion src/filigree/static/js/views/health.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Code Health view — hotspots, severity donut, scan coverage, recent scans.
// ---------------------------------------------------------------------------

import { fetchFiles, fetchFileStats, fetchHotspots, fetchScanRuns } from "../api.js";
import { fetchFileStats, fetchFiles, fetchHotspots, fetchScanRuns } from "../api.js";
import { SEVERITY_COLORS, state } from "../state.js";
import { escHtml, escJsSingleAttr, relativeTime } from "../ui.js";

Expand Down
2 changes: 1 addition & 1 deletion src/filigree/static/js/views/ready.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// ---------------------------------------------------------------------------

import { fetchReady } from "../api.js";
import { escHtml, escJsSingleAttr } from "../ui.js";
import { callbacks } from "../router.js";
import { escHtml, escJsSingleAttr } from "../ui.js";

const PRIORITY_LABELS = ["P0", "P1", "P2", "P3", "P4"];
const PRIORITY_COLORS = [
Expand Down
10 changes: 5 additions & 5 deletions src/filigree/static/js/views/releases.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { escHtml, escJsSingleAttr } from "../ui.js";

// --- Module-level state ---

let expandedReleaseIds = new Set();
let releaseTreeCache = new Map();
let expandedNodeIds = new Set();
const expandedReleaseIds = new Set();
const releaseTreeCache = new Map();
const expandedNodeIds = new Set();
let showReleased = false;
let loadingReleaseIds = new Set();
let errorReleaseIds = new Set();
const loadingReleaseIds = new Set();
const errorReleaseIds = new Set();
let _pendingFocusTarget = null;

export function scrollToReleaseCard(cardId) {
Expand Down
2 changes: 1 addition & 1 deletion tests/api/test_graph_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_hover_traversal_uses_outgoers_not_full_edge_scan(self) -> None:
def test_topology_change_reuses_positions_only_when_all_nodes_have_positions(self) -> None:
graph_js = (STATIC_DIR / "js" / "views" / "graph.js").read_text()
assert "const canReusePositions =" in graph_js
assert "cyNodes.every((n) => Object.prototype.hasOwnProperty.call(previousPositions, n.data.id))" in graph_js
assert "cyNodes.every((n) => Object.hasOwn(previousPositions, n.data.id))" in graph_js
assert "positions: (node) => previousPositions[node.id()]," in graph_js
assert "previousPositions[node.id()] || { x: 0, y: 0 }" not in graph_js

Expand Down