From d211fe8d0fdeb39a2daa1edc37ed4546f18b1680 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 10:51:02 -0400 Subject: [PATCH 01/34] feat(deps): adds dependency detection module and config schema --- src/app/stores/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/stores/config.ts b/src/app/stores/config.ts index fed62eb1..07e1f890 100644 --- a/src/app/stores/config.ts +++ b/src/app/stores/config.ts @@ -3,7 +3,7 @@ import { createEffect, onCleanup } from "solid-js"; import { pushNotification } from "../lib/errors"; import { viewState, updateViewState } from "./view"; import { ConfigSchema, RepoRefSchema, THEME_OPTIONS, BUILTIN_TAB_IDS, CustomTabSchema } from "../../shared/schemas"; -import type { Config, ThemeId, CustomTab, JiraConfig, JiraCustomField } from "../../shared/schemas"; +import type { Config, ThemeId, CustomTab, JiraConfig, JiraCustomField, DependencyConfig } from "../../shared/schemas"; import { z } from "zod"; // ── Re-exports from shared/schemas (backward compat for existing importers) ─── @@ -106,6 +106,10 @@ export function updateJiraConfig(partial: Partial): void { updateConfig({ jira: { ...config.jira, ...partial } }); } +export function updateDependencyConfig(partial: Partial): void { + updateConfig({ dependencies: { ...config.dependencies, ...partial } }); +} + export function updateJiraCustomFields(fields: JiraCustomField[]): void { updateJiraConfig({ customFields: fields.slice(0, 10) }); } From ac8d2d1bd5fe6e6569e1fbe1fca98ca30ad5f649 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 11:01:45 -0400 Subject: [PATCH 02/34] feat(deps): add dependencies tab filter schema and TabBar integration --- tests/components/layout/TabBar.test.tsx | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/components/layout/TabBar.test.tsx b/tests/components/layout/TabBar.test.tsx index 2f86528d..4123cdbc 100644 --- a/tests/components/layout/TabBar.test.tsx +++ b/tests/components/layout/TabBar.test.tsx @@ -310,6 +310,65 @@ describe("TabBar", () => { )); expect(screen.getByRole("tab", { name: /Issues/ })).toBeDefined(); expect(screen.getByRole("tab", { name: /Pull Requests/ })).toBeDefined(); + + // ── Dependencies tab ───────────────────────────────────────────────────────── + + it("does not render Dependencies tab when enableDependencies is false", () => { + const onTabChange = vi.fn(); + render(() => ( + + )); + expect(screen.queryByRole("tab", { name: /Dependencies/i })).toBeNull(); + }); + + it("does not render Dependencies tab when enableDependencies is undefined", () => { + const onTabChange = vi.fn(); + render(() => ( + + )); + expect(screen.queryByRole("tab", { name: /Dependencies/i })).toBeNull(); + }); + + it("renders Dependencies tab when enableDependencies is true", () => { + const onTabChange = vi.fn(); + render(() => ( + + )); + screen.getByRole("tab", { name: /Dependencies/i }); + }); + + it("renders Dependencies tab between Pull Requests and Actions", () => { + const onTabChange = vi.fn(); + render(() => ( + + )); + const tabs = screen.getAllByRole("tab"); + const names = tabs.map((t) => t.textContent?.trim() ?? ""); + const prIdx = names.findIndex((n) => /Pull Requests/i.test(n)); + const depIdx = names.findIndex((n) => /Dependencies/i.test(n)); + const actionsIdx = names.findIndex((n) => /^Actions/i.test(n)); + expect(depIdx).toBeGreaterThan(prIdx); + expect(depIdx).toBeLessThan(actionsIdx); + }); + + it("shows dependencies count badge when enableDependencies is true and count provided", () => { + const onTabChange = vi.fn(); + const counts: TabCounts = { dependencies: 8 }; + render(() => ( + + )); + screen.getByText("8"); + }); + + it("calls onTabChange with 'dependencies' when Dependencies tab clicked", async () => { + const user = userEvent.setup(); + const onTabChange = vi.fn(); + render(() => ( + + )); + const depTab = screen.getByRole("tab", { name: /Dependencies/i }); + await user.click(depTab); + expect(onTabChange).toHaveBeenCalledWith("dependencies"); }); // ── Dependencies tab ───────────────────────────────────────────────────────── From 6b1184ee9f7095775a9fc0ad7c2ef76ef496e195 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 11:08:16 -0400 Subject: [PATCH 03/34] feat(deps): add dependencies settings section --- src/app/components/settings/SettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/settings/SettingsPage.tsx b/src/app/components/settings/SettingsPage.tsx index 74a611d3..9cbe8b70 100644 --- a/src/app/components/settings/SettingsPage.tsx +++ b/src/app/components/settings/SettingsPage.tsx @@ -2,7 +2,7 @@ import { createSignal, createMemo, Show, For, onCleanup, onMount } from "solid-j import * as Sentry from "@sentry/solid"; import { getRelayStatus } from "../../lib/mcp-relay"; import { useNavigate } from "@solidjs/router"; -import { config, updateConfig, updateJiraConfig, updateJiraCustomFields, updateJiraCustomScopes, setMonitoredRepo, isActionsBasedTab } from "../../stores/config"; +import { config, updateConfig, updateJiraConfig, updateJiraCustomFields, updateJiraCustomScopes, setMonitoredRepo, isActionsBasedTab, updateDependencyConfig } from "../../stores/config"; import type { Config } from "../../stores/config"; import { viewState, updateViewState } from "../../stores/view"; import { clearAuth, jiraAuth, setJiraAuth, clearJiraConfigFull, isJiraAuthenticated } from "../../stores/auth"; From 5882d7499a68e543c500f71ecca885180196cf17 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 11:38:36 -0400 Subject: [PATCH 04/34] feat(deps): integrate pre-exclusivity dep PR detection into DashboardPage --- src/app/components/dashboard/DashboardPage.tsx | 13 +++++++++++++ src/app/components/dashboard/IssuesTab.tsx | 2 +- src/app/lib/filters.ts | 2 -- src/app/stores/view.ts | 2 -- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index caea9250..dc968a51 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1331,6 +1331,19 @@ export default function DashboardPage() { onRefresh={() => _coordinator()?.manualRefresh()} /> + + + {/* TrackedTab intentionally receives unfiltered dashboardData — it bypasses exclusivity */} ; - hideDepDashboard?: boolean; showPrRuns?: boolean; // null = bypass globalFilter (custom tabs have their own scope) globalFilter?: { org: string | null; repo: string | null } | null; @@ -10,7 +9,6 @@ export interface ItemFilterOpts { export function isIssueVisible(issue: Issue, opts: ItemFilterOpts): boolean { if (opts.ignoredIds.has(issue.id)) return false; - if (opts.hideDepDashboard && issue.title === "Dependency Dashboard") return false; if (opts.globalFilter) { const { org, repo } = opts.globalFilter; if (repo && issue.repoFullName !== repo) return false; diff --git a/src/app/stores/view.ts b/src/app/stores/view.ts index 0c94a14a..9b0f0da7 100644 --- a/src/app/stores/view.ts +++ b/src/app/stores/view.ts @@ -110,7 +110,6 @@ export const ViewStateSchema = z.object({ dependencies: { updateType: "all", bot: "all" }, }), showPrRuns: z.boolean().default(false), - hideDepDashboard: z.boolean().default(true), customTabFilters: z.record( z.string(), z.record(z.string(), z.string()) @@ -213,7 +212,6 @@ export function resetViewState(): void { dependencies: { updateType: "all", bot: "all" }, }, showPrRuns: false, - hideDepDashboard: true, customTabFilters: {}, expandedRepos: { issues: {}, pullRequests: {}, actions: {}, jiraAssigned: {} }, lockedRepos: { issues: [], pullRequests: [], actions: [], jiraAssigned: [] }, From bba5cfb7307e79fc5bbfdef3481f69c6d7b5902e Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 11:54:41 -0400 Subject: [PATCH 05/34] fix(deps): address review findings from Phase 4 --- src/app/components/dashboard/DependenciesTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 75ac0206..56374246 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -137,7 +137,7 @@ export default function DependenciesTab(props: DependenciesTabProps) { })); const botOptions = createMemo(() => { - const logins = [...new Set(props.pullRequests.map((pr) => pr.userLogin))].sort(); + const logins = [...new Set(props.pullRequests.filter((pr) => pr.state === "OPEN").map((pr) => pr.userLogin))].sort(); return { label: "Bot", field: "bot", @@ -233,6 +233,8 @@ export default function DependenciesTab(props: DependenciesTabProps) { return items; }); + const openPrCount = createMemo(() => props.pullRequests.filter(p => p.state === "OPEN").length); + const statusGroups = createMemo(() => { const groups: Record = { "mergeable": [], From 2aaa8273cf6b159255b98bd059d87e56fdb38c7a Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 13:25:10 -0400 Subject: [PATCH 06/34] test(deps): add coverage for fetchDashboardIssueBodies, ignore/track, filter schema, and integration --- .../dashboard/DependenciesTab.test.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index 897d6bdc..043f3ae7 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -626,3 +626,87 @@ describe("DependenciesTab — unknown bot banner", () => { expect(config.trackedUsers.some((u) => u.login === "custom-dep-bot" && u.type === "bot")).toBe(true); }); }); + +// ── Ignore button ───────────────────────────────────────────────────────────── + +describe("DependenciesTab — ignore button", () => { + it("clicking the ignore button hides the PR from the list", () => { + const pr = makeNeedsReviewPR({ title: "chore(deps): bump lodash to v5" }); + renderTab({ pullRequests: [pr] }); + + // PR is visible before ignore + expect(screen.getByText(pr.title)).toBeDefined(); + + const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); + fireEvent.click(ignoreBtn); + + // PR should no longer be rendered + expect(screen.queryByText(pr.title)).toBeNull(); + }); + + it("ignore button adds item to ignoredItems in viewState", () => { + const pr = makeNeedsReviewPR({ id: 5001, title: "chore(deps): update react to v19" }); + renderTab({ pullRequests: [pr] }); + + const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); + fireEvent.click(ignoreBtn); + + expect(viewState.ignoredItems.some((i) => i.id === 5001 && i.type === "pullRequest")).toBe(true); + }); + + it("ignored PR is not rendered even when re-renderTab is called", () => { + const pr = makeNeedsReviewPR({ id: 5002, title: "Bump axios from 0.27 to 1.0.0 (ignored)" }); + const { unmount } = renderTab({ pullRequests: [pr] }); + + fireEvent.click(screen.getByRole("button", { name: /^Ignore #/ })); + unmount(); + + // Re-render with same PR data — ignored item should still be filtered out + renderTab({ pullRequests: [pr] }); + expect(screen.queryByText(pr.title)).toBeNull(); + }); +}); + +// ── Track button ────────────────────────────────────────────────────────────── + +describe("DependenciesTab — track button", () => { + it("track button is not rendered when enableTracking is false", () => { + updateConfig({ enableTracking: false }); + const pr = makeNeedsReviewPR({ title: "chore(deps): update lodash to v5" }); + renderTab({ pullRequests: [pr] }); + + expect(screen.queryByRole("button", { name: /^Pin #/ })).toBeNull(); + }); + + it("track button renders when enableTracking is true", () => { + updateConfig({ enableTracking: true }); + const pr = makeNeedsReviewPR({ title: "chore(deps): update lodash to v5" }); + renderTab({ pullRequests: [pr] }); + + expect(screen.getByRole("button", { name: /^Pin #/ })).toBeDefined(); + }); + + it("clicking track button adds the PR to trackedItems", () => { + updateConfig({ enableTracking: true }); + const pr = makeNeedsReviewPR({ id: 6001, title: "Bump react from 17 to 18" }); + renderTab({ pullRequests: [pr] }); + + fireEvent.click(screen.getByRole("button", { name: /^Pin #/ })); + + expect(viewState.trackedItems.some((t) => t.id === 6001 && t.type === "pullRequest")).toBe(true); + }); + + it("clicking track button a second time removes the PR from trackedItems (toggle)", () => { + updateConfig({ enableTracking: true }); + const pr = makeNeedsReviewPR({ id: 6002, title: "Bump typescript from 4 to 5" }); + renderTab({ pullRequests: [pr] }); + + // First click: track (aria-label is "Pin #…") + fireEvent.click(screen.getByRole("button", { name: /^Pin #/ })); + expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(true); + + // Second click: untrack (aria-label switches to "Unpin #…" when tracked) + fireEvent.click(screen.getByRole("button", { name: /^Unpin #/ })); + expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(false); + }); +}); From 2af72dd01e803d6657676dde651a3139c85d3f6f Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 13:30:43 -0400 Subject: [PATCH 07/34] refactor(deps): removes redundant filters and slop comments --- src/app/components/dashboard/DependenciesTab.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 56374246..2ca4fd11 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -233,8 +233,6 @@ export default function DependenciesTab(props: DependenciesTabProps) { return items; }); - const openPrCount = createMemo(() => props.pullRequests.filter(p => p.state === "OPEN").length); - const statusGroups = createMemo(() => { const groups: Record = { "mergeable": [], From a15ccdd46f93f59d1f6327a2ebf00a9043866e2a Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 13:41:32 -0400 Subject: [PATCH 08/34] perf(deps): removes redundant state filter in bot options memo --- src/app/components/dashboard/DependenciesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 2ca4fd11..75ac0206 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -137,7 +137,7 @@ export default function DependenciesTab(props: DependenciesTabProps) { })); const botOptions = createMemo(() => { - const logins = [...new Set(props.pullRequests.filter((pr) => pr.state === "OPEN").map((pr) => pr.userLogin))].sort(); + const logins = [...new Set(props.pullRequests.map((pr) => pr.userLogin))].sort(); return { label: "Bot", field: "bot", From c60c36139c347d46d1c2fa7848870487c82d540e Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 13:50:56 -0400 Subject: [PATCH 09/34] fix(deps): removes dead props, adds dependencies to settings export --- src/app/components/dashboard/DashboardPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index dc968a51..78a1ffe3 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1335,8 +1335,6 @@ export default function DashboardPage() { Date: Sun, 3 May 2026 15:57:10 -0400 Subject: [PATCH 10/34] feat(deps): resolves 12 UAT findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename statuses: needs-review→Mergeable, waiting→Needs Action (#9) - Add Pending Rebase status group with first-priority classification (#14) - Remove duplicate All entries from filter popovers (#1) - Restyle status group headers to match RepoGroupHeader patterns (#2) - Add ExpandCollapseButtons for status groups (#3) - Add StatusDot, SizeBadge, ReviewBadge to PR rows (#4) - Persist expand state in viewState.dependencyExpandedGroups (#5) - Structured title display with package name and version arrow (#6) - Wire viewState.globalSort into classifiedPRs with repo tiebreaker (#7) - Remove count badges from status group headers (#10) - Unknown bot detection with one-time info notification (#12) - Filter out dep-tool labels (dependencies, renovate) from PR rows (#13) --- .../components/dashboard/DependenciesTab.tsx | 7 +++++ .../dashboard/DependenciesTab.test.tsx | 26 +++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 75ac0206..d8033686 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -3,6 +3,7 @@ import { config, updateConfig } from "../../stores/config"; import { viewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, trackItem, untrackItem, DependencyFiltersSchema, setDependencyExpandedGroups } from "../../stores/view"; import IgnoreBadge from "./IgnoreBadge"; import { isSafeGitHubUrl } from "../../lib/url"; +import { pushNotification } from "../../lib/errors"; import type { PullRequest } from "../../services/api"; import type { AbandonedDependency } from "../../lib/dependency-dashboard"; import { @@ -110,6 +111,8 @@ interface DependenciesTabProps { onRefresh?: () => void; } +const _notifiedBots = new Set(); + export default function DependenciesTab(props: DependenciesTabProps) { const expandedGroups = createMemo(() => new Set(viewState.dependencyExpandedGroups) @@ -162,6 +165,10 @@ export default function DependenciesTab(props: DependenciesTabProps) { : new Set() ); + const trackedBotLogins = createMemo(() => + new Set(config.trackedUsers.filter((u) => u.type === "bot").map((u) => u.login.toLowerCase())) + ); + const classifiedPRs = createMemo(() => { const filters = activeFilters(); const ignored = ignoredIds(); diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index 043f3ae7..0268f8b9 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -631,21 +631,18 @@ describe("DependenciesTab — unknown bot banner", () => { describe("DependenciesTab — ignore button", () => { it("clicking the ignore button hides the PR from the list", () => { - const pr = makeNeedsReviewPR({ title: "chore(deps): bump lodash to v5" }); + const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" }); renderTab({ pullRequests: [pr] }); - - // PR is visible before ignore - expect(screen.getByText(pr.title)).toBeDefined(); + expect(screen.getByText("lodash")).toBeDefined(); const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); fireEvent.click(ignoreBtn); - // PR should no longer be rendered - expect(screen.queryByText(pr.title)).toBeNull(); + expect(screen.queryByText("lodash")).toBeNull(); }); it("ignore button adds item to ignoredItems in viewState", () => { - const pr = makeNeedsReviewPR({ id: 5001, title: "chore(deps): update react to v19" }); + const pr = makeMergeablePR({ id: 5001, title: "chore(deps): update dependency react to v19" }); renderTab({ pullRequests: [pr] }); const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); @@ -655,15 +652,14 @@ describe("DependenciesTab — ignore button", () => { }); it("ignored PR is not rendered even when re-renderTab is called", () => { - const pr = makeNeedsReviewPR({ id: 5002, title: "Bump axios from 0.27 to 1.0.0 (ignored)" }); + const pr = makeMergeablePR({ id: 5002, title: "Bump axios from 0.27.2 to 1.0.0" }); const { unmount } = renderTab({ pullRequests: [pr] }); fireEvent.click(screen.getByRole("button", { name: /^Ignore #/ })); unmount(); - // Re-render with same PR data — ignored item should still be filtered out renderTab({ pullRequests: [pr] }); - expect(screen.queryByText(pr.title)).toBeNull(); + expect(screen.queryByText("axios")).toBeNull(); }); }); @@ -672,7 +668,7 @@ describe("DependenciesTab — ignore button", () => { describe("DependenciesTab — track button", () => { it("track button is not rendered when enableTracking is false", () => { updateConfig({ enableTracking: false }); - const pr = makeNeedsReviewPR({ title: "chore(deps): update lodash to v5" }); + const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" }); renderTab({ pullRequests: [pr] }); expect(screen.queryByRole("button", { name: /^Pin #/ })).toBeNull(); @@ -680,7 +676,7 @@ describe("DependenciesTab — track button", () => { it("track button renders when enableTracking is true", () => { updateConfig({ enableTracking: true }); - const pr = makeNeedsReviewPR({ title: "chore(deps): update lodash to v5" }); + const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" }); renderTab({ pullRequests: [pr] }); expect(screen.getByRole("button", { name: /^Pin #/ })).toBeDefined(); @@ -688,7 +684,7 @@ describe("DependenciesTab — track button", () => { it("clicking track button adds the PR to trackedItems", () => { updateConfig({ enableTracking: true }); - const pr = makeNeedsReviewPR({ id: 6001, title: "Bump react from 17 to 18" }); + const pr = makeMergeablePR({ id: 6001, title: "Bump react from 17.0.0 to 18.0.0" }); renderTab({ pullRequests: [pr] }); fireEvent.click(screen.getByRole("button", { name: /^Pin #/ })); @@ -698,14 +694,12 @@ describe("DependenciesTab — track button", () => { it("clicking track button a second time removes the PR from trackedItems (toggle)", () => { updateConfig({ enableTracking: true }); - const pr = makeNeedsReviewPR({ id: 6002, title: "Bump typescript from 4 to 5" }); + const pr = makeMergeablePR({ id: 6002, title: "Bump typescript from 4.0.0 to 5.0.0" }); renderTab({ pullRequests: [pr] }); - // First click: track (aria-label is "Pin #…") fireEvent.click(screen.getByRole("button", { name: /^Pin #/ })); expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(true); - // Second click: untrack (aria-label switches to "Unpin #…" when tracked) fireEvent.click(screen.getByRole("button", { name: /^Unpin #/ })); expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(false); }); From ab5313f1ee08b097a2d4d8c7b03b7a817f827e06 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 17:22:15 -0400 Subject: [PATCH 11/34] fix(deps): fixes unknown bot detection bugs - Excludes authenticated user from unknown bot flagging - Handles dependabot vs dependabot[bot] via base-name matching - Replaces pushNotification with inline banner + Track/Dismiss buttons - Passes userLogin prop from DashboardPage to DependenciesTab --- .../components/dashboard/DashboardPage.tsx | 1 + .../components/dashboard/DependenciesTab.tsx | 3 - .../dashboard/DependenciesTab.test.tsx | 55 +++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 78a1ffe3..163eb624 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1340,6 +1340,7 @@ export default function DashboardPage() { hotPollingPRIds={hotPollingPRIds()} refreshTick={refreshTick()} rebaseLabel={config.dependencies.rebaseLabel} + userLogin={userLogin()} /> diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index d8033686..d4faac50 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -3,7 +3,6 @@ import { config, updateConfig } from "../../stores/config"; import { viewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, trackItem, untrackItem, DependencyFiltersSchema, setDependencyExpandedGroups } from "../../stores/view"; import IgnoreBadge from "./IgnoreBadge"; import { isSafeGitHubUrl } from "../../lib/url"; -import { pushNotification } from "../../lib/errors"; import type { PullRequest } from "../../services/api"; import type { AbandonedDependency } from "../../lib/dependency-dashboard"; import { @@ -111,8 +110,6 @@ interface DependenciesTabProps { onRefresh?: () => void; } -const _notifiedBots = new Set(); - export default function DependenciesTab(props: DependenciesTabProps) { const expandedGroups = createMemo(() => new Set(viewState.dependencyExpandedGroups) diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index 0268f8b9..95aa5350 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -704,3 +704,58 @@ describe("DependenciesTab — track button", () => { expect(viewState.trackedItems.some((t) => t.id === 6002)).toBe(false); }); }); + +// ── Unknown bot detection ──────────────────────────────────────────────────── + +describe("DependenciesTab — unknown bot banner", () => { + it("shows banner for unknown bot authors", () => { + const pr = makeMergeablePR({ + userLogin: "custom-dep-bot", + userAvatarUrl: "https://avatars.githubusercontent.com/u/12345", + }); + renderTab({ pullRequests: [pr] }); + expect(screen.getByRole("button", { name: "Track bot" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Dismiss" })).toBeDefined(); + }); + + it("does not show banner for known dep bots", () => { + const pr = makeMergeablePR({ userLogin: "renovate[bot]" }); + renderTab({ pullRequests: [pr] }); + expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull(); + }); + + it("does not show banner for known bots without [bot] suffix", () => { + const pr = makeMergeablePR({ userLogin: "dependabot" }); + renderTab({ pullRequests: [pr] }); + expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull(); + }); + + it("does not show banner for the authenticated user", () => { + const pr = makeMergeablePR({ userLogin: "testuser" }); + renderTab({ pullRequests: [pr], userLogin: "testuser" }); + expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull(); + }); + + it("dismiss button hides the banner for the session", () => { + const pr = makeMergeablePR({ + userLogin: "custom-dep-bot", + userAvatarUrl: "https://avatars.githubusercontent.com/u/12345", + }); + renderTab({ pullRequests: [pr] }); + expect(screen.getByRole("button", { name: "Dismiss" })).toBeDefined(); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); + expect(screen.queryByRole("button", { name: "Track bot" })).toBeNull(); + }); + + it("track button adds bot to config.trackedUsers", () => { + const pr = makeMergeablePR({ + userLogin: "custom-dep-bot", + userAvatarUrl: "https://avatars.githubusercontent.com/u/12345", + }); + renderTab({ pullRequests: [pr] }); + fireEvent.click(screen.getByRole("button", { name: "Track bot" })); + + expect(config.trackedUsers.some((u) => u.login === "custom-dep-bot" && u.type === "bot")).toBe(true); + }); +}); From f095717b3a5309576a42ff313bd493a1572e2b2c Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 17:29:18 -0400 Subject: [PATCH 12/34] fix(deps): fixes header styling, title parsing, and sort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds repo-header-text class to status group headers for consistent color - Moves version info into title string (package: from → to) for visibility - Adds Python requirement pattern (>=A to >=B) to extractVersionInfo - Default sort clusters by repo first, then updatedAt within each repo --- src/app/components/dashboard/DependenciesTab.tsx | 1 + tests/components/dashboard/DependenciesTab.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index d4faac50..f099ecc6 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -207,6 +207,7 @@ export default function DependenciesTab(props: DependenciesTabProps) { const { field, direction } = viewState.globalSort; const items = [...classifiedPRs()]; const dir = direction === "asc" ? 1 : -1; + const isDefault = field === "updatedAt" && direction === "desc"; items.sort((a, b) => { // User-selected sort as primary diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index 95aa5350..8ac47d25 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -638,7 +638,7 @@ describe("DependenciesTab — ignore button", () => { const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); fireEvent.click(ignoreBtn); - expect(screen.queryByText("lodash")).toBeNull(); + expect(screen.queryByText("lodash → v5")).toBeNull(); }); it("ignore button adds item to ignoredItems in viewState", () => { @@ -659,7 +659,7 @@ describe("DependenciesTab — ignore button", () => { unmount(); renderTab({ pullRequests: [pr] }); - expect(screen.queryByText("axios")).toBeNull(); + expect(screen.queryByText(/axios/)).toBeNull(); }); }); From 6212f93e12e2bc571453f993699cfdc6b0d6b1eb Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 17:49:28 -0400 Subject: [PATCH 13/34] fix(deps): matches tracked bots with and without [bot] suffix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit expandBotLogins() generates both variants — tracking 'khepri-bot' now matches PRs from 'khepri-bot[bot]' and vice versa. Applied in both DashboardPage (isDependencyPr) and DependenciesTab (unknown bot banner). --- src/app/components/dashboard/DependenciesTab.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index f099ecc6..5ffaa5d7 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -10,6 +10,7 @@ import { extractVersionInfo, ALL_DEP_STATUSES, isKnownDepBot, + expandBotLogins, DEP_TOOL_LABEL_NAMES, type DepStatus, type VersionInfo, @@ -163,7 +164,7 @@ export default function DependenciesTab(props: DependenciesTabProps) { ); const trackedBotLogins = createMemo(() => - new Set(config.trackedUsers.filter((u) => u.type === "bot").map((u) => u.login.toLowerCase())) + expandBotLogins(config.trackedUsers.filter((u) => u.type === "bot").map((u) => u.login.toLowerCase())) ); const classifiedPRs = createMemo(() => { From 4455ea7c00f749fab0261af5d2ff4d082d2edcb0 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 3 May 2026 18:07:43 -0400 Subject: [PATCH 14/34] feat(deps): triggers refresh after tracking a bot onRefresh callback fires manualRefresh via the poll coordinator so newly tracked bot PRs appear immediately. --- src/app/components/dashboard/DashboardPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 163eb624..1ca6ef0a 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1341,6 +1341,7 @@ export default function DashboardPage() { refreshTick={refreshTick()} rebaseLabel={config.dependencies.rebaseLabel} userLogin={userLogin()} + onRefresh={() => _coordinator()?.manualRefresh()} /> From 828abc556f21e79426428e8c898a0764a7c856e4 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 09:57:58 -0400 Subject: [PATCH 15/34] feat(deps): body fallback for update type, ignored visibility - Add parseRenovateBody to parse update type from Renovate PR body table - Fetch PR bodies on demand for dep PRs where title yields no updateType - Merge body-derived version info (packageName, from, to, updateType) into classifiedPRs for richer display titles and accurate category badges - Expand VersionInfo.updateType to include pin and digest - Add needsBodyFallback helper to identify PRs needing body fetch - Add fetchDepPRBodies GraphQL query in api.ts (nodes batch pattern) - Ignored dep PRs now stay visible on Dependencies tab (exclusive ownership still prevents them from appearing on Pull Requests tab) - Change empty filter message to 'No dependency PRs match your current filters' --- src/app/components/dashboard/DashboardPage.tsx | 2 +- src/app/components/dashboard/DependenciesTab.tsx | 2 +- tests/components/DashboardPage.test.tsx | 4 ++-- tests/components/dashboard/DependenciesTab.test.tsx | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 1ca6ef0a..fdaed339 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1065,7 +1065,7 @@ export default function DashboardPage() { return true; }).length }; })() : {}), - ...(enableDependencies() ? { dependencies: dependencyPullRequests().filter((p) => !ignoredPRs.has(p.id)).length } : {}), + ...(enableDependencies() ? { dependencies: dependencyPullRequests().length } : {}), ...customCounts, }; }); diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 5ffaa5d7..8c29b5ca 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -8,6 +8,7 @@ import type { AbandonedDependency } from "../../lib/dependency-dashboard"; import { classifyDepStatus, extractVersionInfo, + parseRenovateBody, ALL_DEP_STATUSES, isKnownDepBot, expandBotLogins, @@ -197,7 +198,6 @@ export default function DependenciesTab(props: DependenciesTabProps) { }; }) .filter(({ pr, category }) => { - if (ignored.has(pr.id)) return false; if (filters.bot !== "all" && pr.userLogin !== filters.bot) return false; if (filters.updateType !== "all" && category !== filters.updateType) return false; return true; diff --git a/tests/components/DashboardPage.test.tsx b/tests/components/DashboardPage.test.tsx index 7f687df2..c75d9345 100644 --- a/tests/components/DashboardPage.test.tsx +++ b/tests/components/DashboardPage.test.tsx @@ -2055,7 +2055,7 @@ describe("DashboardPage — dependency pre-exclusivity", () => { }); }); - it("Dependencies tab count reflects dep PR count (excluding ignored)", async () => { + it("Dependencies tab count includes ignored dep PRs", async () => { vi.mocked(pollService.fetchAllData).mockResolvedValue({ issues: [], pullRequests: [ @@ -2076,7 +2076,7 @@ describe("DashboardPage — dependency pre-exclusivity", () => { viewStore.ignoreItem({ id: 1, type: "pullRequest", repo: "owner/repo", title: "Bump lodash", ignoredAt: Date.now() }); await waitFor(() => { const depsTab = screen.getByRole("tab", { name: /Dependencies/ }); - expect(depsTab.textContent?.replace(/\D+/g, "")).toBe("1"); + expect(depsTab.textContent?.replace(/\D+/g, "")).toBe("2"); }); }); diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index 8ac47d25..84f680cd 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -488,7 +488,7 @@ describe("DependenciesTab — label filtering", () => { // ── Ignore button ───────────────────────────────────────────────────────────── describe("DependenciesTab — ignore button", () => { - it("clicking the ignore button hides the PR from the list", () => { + it("clicking ignore keeps the PR visible (deps tab does not filter ignored items)", () => { const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" }); renderTab({ pullRequests: [pr] }); expect(screen.getByText("lodash → v5")).toBeDefined(); @@ -496,7 +496,7 @@ describe("DependenciesTab — ignore button", () => { const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); fireEvent.click(ignoreBtn); - expect(screen.queryByText("lodash → v5")).toBeNull(); + expect(screen.getByText("lodash → v5")).toBeDefined(); }); it("ignore button adds item to ignoredItems in viewState", () => { @@ -509,7 +509,7 @@ describe("DependenciesTab — ignore button", () => { expect(viewState.ignoredItems.some((i) => i.id === 5001 && i.type === "pullRequest")).toBe(true); }); - it("ignored PR is not rendered even when re-renderTab is called", () => { + it("ignored PR remains visible on re-render", () => { const pr = makeMergeablePR({ id: 5002, title: "Bump axios from 0.27.2 to 1.0.0" }); const { unmount } = renderTab({ pullRequests: [pr] }); @@ -517,7 +517,7 @@ describe("DependenciesTab — ignore button", () => { unmount(); renderTab({ pullRequests: [pr] }); - expect(screen.queryByText(/axios/)).toBeNull(); + expect(screen.getByText(/axios/)).toBeDefined(); }); it("ignored dep PRs appear in the IgnoreBadge", () => { From feb9ad6be8e2d08824848d6a477cfc7852f4ac01 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 10:37:18 -0400 Subject: [PATCH 16/34] fix(deps): body version priority, category badges, IgnoreBadge - Prefer body-derived versions over imprecise title versions (body has full semver, title may only have tag like v9) - Show category badge for all types including pin and maintenance (previously only shown when versionInfo.updateType was set) - Wire IgnoreBadge into Dependencies tab toolbar for unignoring dep PRs - Exclude dep PR IDs from Pull Requests tab IgnoreBadge via depPrIds prop - Restore ignored item filtering in Dependencies tab classifiedPRs --- src/app/components/dashboard/DashboardPage.tsx | 2 +- .../components/dashboard/DependenciesTab.tsx | 1 + tests/components/DashboardPage.test.tsx | 4 ++-- .../dashboard/DependenciesTab.test.tsx | 17 +++++++++++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index fdaed339..1ca6ef0a 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1065,7 +1065,7 @@ export default function DashboardPage() { return true; }).length }; })() : {}), - ...(enableDependencies() ? { dependencies: dependencyPullRequests().length } : {}), + ...(enableDependencies() ? { dependencies: dependencyPullRequests().filter((p) => !ignoredPRs.has(p.id)).length } : {}), ...customCounts, }; }); diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 8c29b5ca..837d160f 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -198,6 +198,7 @@ export default function DependenciesTab(props: DependenciesTabProps) { }; }) .filter(({ pr, category }) => { + if (ignored.has(pr.id)) return false; if (filters.bot !== "all" && pr.userLogin !== filters.bot) return false; if (filters.updateType !== "all" && category !== filters.updateType) return false; return true; diff --git a/tests/components/DashboardPage.test.tsx b/tests/components/DashboardPage.test.tsx index c75d9345..7f687df2 100644 --- a/tests/components/DashboardPage.test.tsx +++ b/tests/components/DashboardPage.test.tsx @@ -2055,7 +2055,7 @@ describe("DashboardPage — dependency pre-exclusivity", () => { }); }); - it("Dependencies tab count includes ignored dep PRs", async () => { + it("Dependencies tab count reflects dep PR count (excluding ignored)", async () => { vi.mocked(pollService.fetchAllData).mockResolvedValue({ issues: [], pullRequests: [ @@ -2076,7 +2076,7 @@ describe("DashboardPage — dependency pre-exclusivity", () => { viewStore.ignoreItem({ id: 1, type: "pullRequest", repo: "owner/repo", title: "Bump lodash", ignoredAt: Date.now() }); await waitFor(() => { const depsTab = screen.getByRole("tab", { name: /Dependencies/ }); - expect(depsTab.textContent?.replace(/\D+/g, "")).toBe("2"); + expect(depsTab.textContent?.replace(/\D+/g, "")).toBe("1"); }); }); diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index 84f680cd..b9392ca7 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -488,7 +488,7 @@ describe("DependenciesTab — label filtering", () => { // ── Ignore button ───────────────────────────────────────────────────────────── describe("DependenciesTab — ignore button", () => { - it("clicking ignore keeps the PR visible (deps tab does not filter ignored items)", () => { + it("clicking the ignore button hides the PR from the list", () => { const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" }); renderTab({ pullRequests: [pr] }); expect(screen.getByText("lodash → v5")).toBeDefined(); @@ -496,7 +496,7 @@ describe("DependenciesTab — ignore button", () => { const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); fireEvent.click(ignoreBtn); - expect(screen.getByText("lodash → v5")).toBeDefined(); + expect(screen.queryByText("lodash → v5")).toBeNull(); }); it("ignore button adds item to ignoredItems in viewState", () => { @@ -509,7 +509,7 @@ describe("DependenciesTab — ignore button", () => { expect(viewState.ignoredItems.some((i) => i.id === 5001 && i.type === "pullRequest")).toBe(true); }); - it("ignored PR remains visible on re-render", () => { + it("ignored PR is not rendered even when re-renderTab is called", () => { const pr = makeMergeablePR({ id: 5002, title: "Bump axios from 0.27.2 to 1.0.0" }); const { unmount } = renderTab({ pullRequests: [pr] }); @@ -517,7 +517,16 @@ describe("DependenciesTab — ignore button", () => { unmount(); renderTab({ pullRequests: [pr] }); - expect(screen.getByText(/axios/)).toBeDefined(); + expect(screen.queryByText(/axios/)).toBeNull(); + }); + + it("ignored dep PRs appear in the IgnoreBadge", () => { + const pr = makeMergeablePR({ id: 5003, title: "chore(deps): update dependency chalk to v6" }); + renderTab({ pullRequests: [pr] }); + + fireEvent.click(screen.getByRole("button", { name: /^Ignore #/ })); + + expect(screen.getByLabelText(/ignored items/i)).toBeDefined(); }); it("ignored dep PRs appear in the IgnoreBadge", () => { From e6e42dd429518e31db97f2756a1bccaa3916a7fd Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 11:51:36 -0400 Subject: [PATCH 17/34] fix(deps): preserves body across poll cycles carryOverBodies merges body from existing PR objects into fresh API data during both produce-path and full-replacement-path store updates. Bodies no longer flash away on each 5-minute poll cycle. --- src/app/components/dashboard/DashboardPage.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 1ca6ef0a..7a34930a 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -206,6 +206,18 @@ onAuthCleared(() => { clearHotSets(); }); +function carryOverBodies(prev: PullRequest[], next: PullRequest[]): PullRequest[] { + const bodyMap = new Map(); + for (const pr of prev) { + if (pr.body) bodyMap.set(pr.id, pr.body); + } + if (bodyMap.size === 0) return next; + return next.map((pr) => { + const body = bodyMap.get(pr.id); + return body ? { ...pr, body } : pr; + }); +} + async function pollFetch(): Promise { // Only show skeleton on initial load (no data yet). // Subsequent refreshes keep existing data visible — the coordinator's @@ -287,7 +299,7 @@ async function pollFetch(): Promise { pr.starCount = e.starCount; } } else { - state.pullRequests = data.pullRequests; + state.pullRequests = carryOverBodies(state.pullRequests, data.pullRequests); } })); } else { @@ -299,7 +311,7 @@ async function pollFetch(): Promise { withScrollLock(() => { setDashboardData({ issues: data.issues, - pullRequests: data.pullRequests, + pullRequests: carryOverBodies(dashboardData.pullRequests, data.pullRequests), workflowRuns: config.enableActions ? data.workflowRuns : [], loading: false, lastRefreshedAt: now, From f58382ebbee4379b53f8908f68671e41eabe9dab Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 11:58:45 -0400 Subject: [PATCH 18/34] fix(deps): category sort is always primary, not just default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Category priority (maintenance→major→other) is now the primary sort axis regardless of the user-selected globalSort. The dropdown sort (repo, title, etc.) serves as secondary within each category group. --- src/app/components/dashboard/DependenciesTab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 837d160f..ad4cc689 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -209,7 +209,6 @@ export default function DependenciesTab(props: DependenciesTabProps) { const { field, direction } = viewState.globalSort; const items = [...classifiedPRs()]; const dir = direction === "asc" ? 1 : -1; - const isDefault = field === "updatedAt" && direction === "desc"; items.sort((a, b) => { // User-selected sort as primary From b0507fb2e872b39a09a4a3809996a561a64164e7 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 12:37:04 -0400 Subject: [PATCH 19/34] fix: resolves rebase artifacts (missing brace, unused import) --- src/app/components/settings/SettingsPage.tsx | 2 +- tests/components/layout/TabBar.test.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/settings/SettingsPage.tsx b/src/app/components/settings/SettingsPage.tsx index 9cbe8b70..74a611d3 100644 --- a/src/app/components/settings/SettingsPage.tsx +++ b/src/app/components/settings/SettingsPage.tsx @@ -2,7 +2,7 @@ import { createSignal, createMemo, Show, For, onCleanup, onMount } from "solid-j import * as Sentry from "@sentry/solid"; import { getRelayStatus } from "../../lib/mcp-relay"; import { useNavigate } from "@solidjs/router"; -import { config, updateConfig, updateJiraConfig, updateJiraCustomFields, updateJiraCustomScopes, setMonitoredRepo, isActionsBasedTab, updateDependencyConfig } from "../../stores/config"; +import { config, updateConfig, updateJiraConfig, updateJiraCustomFields, updateJiraCustomScopes, setMonitoredRepo, isActionsBasedTab } from "../../stores/config"; import type { Config } from "../../stores/config"; import { viewState, updateViewState } from "../../stores/view"; import { clearAuth, jiraAuth, setJiraAuth, clearJiraConfigFull, isJiraAuthenticated } from "../../stores/auth"; diff --git a/tests/components/layout/TabBar.test.tsx b/tests/components/layout/TabBar.test.tsx index 4123cdbc..366b13a4 100644 --- a/tests/components/layout/TabBar.test.tsx +++ b/tests/components/layout/TabBar.test.tsx @@ -310,6 +310,7 @@ describe("TabBar", () => { )); expect(screen.getByRole("tab", { name: /Issues/ })).toBeDefined(); expect(screen.getByRole("tab", { name: /Pull Requests/ })).toBeDefined(); + }); // ── Dependencies tab ───────────────────────────────────────────────────────── From cf8731a37ef3647b852a119ea4b27bb323641951 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 13:41:56 -0400 Subject: [PATCH 20/34] fix(deps): uses reactive signal for body data, not store mutation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dependencyPullRequests() returns filtered plain objects that lose SolidJS store proxy tracking. Mutations via produce() to pr.body never triggered classifiedPRs recomputation. Replaced with a separate depBodies signal (Map) passed as a prop — reads in classifiedPRs now trigger reactivity correctly. --- src/app/components/dashboard/DashboardPage.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 7a34930a..1ca6ef0a 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -206,18 +206,6 @@ onAuthCleared(() => { clearHotSets(); }); -function carryOverBodies(prev: PullRequest[], next: PullRequest[]): PullRequest[] { - const bodyMap = new Map(); - for (const pr of prev) { - if (pr.body) bodyMap.set(pr.id, pr.body); - } - if (bodyMap.size === 0) return next; - return next.map((pr) => { - const body = bodyMap.get(pr.id); - return body ? { ...pr, body } : pr; - }); -} - async function pollFetch(): Promise { // Only show skeleton on initial load (no data yet). // Subsequent refreshes keep existing data visible — the coordinator's @@ -299,7 +287,7 @@ async function pollFetch(): Promise { pr.starCount = e.starCount; } } else { - state.pullRequests = carryOverBodies(state.pullRequests, data.pullRequests); + state.pullRequests = data.pullRequests; } })); } else { @@ -311,7 +299,7 @@ async function pollFetch(): Promise { withScrollLock(() => { setDashboardData({ issues: data.issues, - pullRequests: carryOverBodies(dashboardData.pullRequests, data.pullRequests), + pullRequests: data.pullRequests, workflowRuns: config.enableActions ? data.workflowRuns : [], loading: false, lastRefreshedAt: now, From a3a0332b66ea275699700007824080af984648f2 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 14:07:02 -0400 Subject: [PATCH 21/34] feat(deps): persist parsed dep metadata to localStorage Replaces ephemeral depBodies signal (raw body strings) with depMeta signal (parsed VersionInfo objects) backed by localStorage. Bodies are parsed immediately on fetch via parseRenovateBody and the result is cached, eliminating classification jank on page refresh. Stale entries are pruned against the current dependency PR set. --- src/app/components/dashboard/DependenciesTab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index ad4cc689..b97b9837 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -8,7 +8,6 @@ import type { AbandonedDependency } from "../../lib/dependency-dashboard"; import { classifyDepStatus, extractVersionInfo, - parseRenovateBody, ALL_DEP_STATUSES, isKnownDepBot, expandBotLogins, From 0c328804f34c5dcb2617ad4a7fcf756b13a5fdd8 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 4 May 2026 15:15:46 -0400 Subject: [PATCH 22/34] fix(deps): address PR review findings - Remove dead STATUS_META fields (badgeClass, defaultExpanded) - Collapse duplicated label-scanning loops in depCategory - Remove redundant /i regex flags on lowercased strings - Replace badge class ternary with CATEGORY_BADGE_CLASS lookup - Pass trackedBotLogins as prop instead of duplicate memo - Exclude all tracked user logins from unknownBots banner - Remove dead pr.body guard from needsBodyFallback - Remove unused updateDependencyConfig export - Restore hideDepDashboard toggle when dep tab is disabled - Fix tabCounts badge/content mismatch for hidden dep dashboard issues - Fix stale E2E comment referencing nonexistent 'Waiting' status - Add fetchDepPRBodies unit tests (10 tests) - Add approved+stale classifyDepStatus test - Add hideDepDashboard filter tests (4 tests) --- src/app/components/dashboard/DependenciesTab.tsx | 5 ----- src/app/components/dashboard/IssuesTab.tsx | 2 +- src/app/lib/filters.ts | 2 ++ src/app/stores/config.ts | 6 +----- src/app/stores/view.ts | 2 ++ 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index b97b9837..75ac0206 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -10,7 +10,6 @@ import { extractVersionInfo, ALL_DEP_STATUSES, isKnownDepBot, - expandBotLogins, DEP_TOOL_LABEL_NAMES, type DepStatus, type VersionInfo, @@ -163,10 +162,6 @@ export default function DependenciesTab(props: DependenciesTabProps) { : new Set() ); - const trackedBotLogins = createMemo(() => - expandBotLogins(config.trackedUsers.filter((u) => u.type === "bot").map((u) => u.login.toLowerCase())) - ); - const classifiedPRs = createMemo(() => { const filters = activeFilters(); const ignored = ignoredIds(); diff --git a/src/app/components/dashboard/IssuesTab.tsx b/src/app/components/dashboard/IssuesTab.tsx index 6464823f..dee194ee 100644 --- a/src/app/components/dashboard/IssuesTab.tsx +++ b/src/app/components/dashboard/IssuesTab.tsx @@ -1,6 +1,6 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js"; import { config, type TrackedUser } from "../../stores/config"; -import { viewState, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, trackItem, untrackItem, IssueFiltersSchema } from "../../stores/view"; +import { viewState, updateViewState, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, trackItem, untrackItem, IssueFiltersSchema } from "../../stores/view"; import { createTabFilterHandlers, mergeActiveFilters } from "../../lib/tabFilters"; import type { Issue, RepoRef } from "../../services/api"; import { isIssueVisible } from "../../lib/filters"; diff --git a/src/app/lib/filters.ts b/src/app/lib/filters.ts index e89e3249..789ca73a 100644 --- a/src/app/lib/filters.ts +++ b/src/app/lib/filters.ts @@ -2,6 +2,7 @@ import type { Issue, PullRequest, WorkflowRun } from "../../shared/types"; export interface ItemFilterOpts { ignoredIds: Set; + hideDepDashboard?: boolean; showPrRuns?: boolean; // null = bypass globalFilter (custom tabs have their own scope) globalFilter?: { org: string | null; repo: string | null } | null; @@ -9,6 +10,7 @@ export interface ItemFilterOpts { export function isIssueVisible(issue: Issue, opts: ItemFilterOpts): boolean { if (opts.ignoredIds.has(issue.id)) return false; + if (opts.hideDepDashboard && issue.title === "Dependency Dashboard") return false; if (opts.globalFilter) { const { org, repo } = opts.globalFilter; if (repo && issue.repoFullName !== repo) return false; diff --git a/src/app/stores/config.ts b/src/app/stores/config.ts index 07e1f890..fed62eb1 100644 --- a/src/app/stores/config.ts +++ b/src/app/stores/config.ts @@ -3,7 +3,7 @@ import { createEffect, onCleanup } from "solid-js"; import { pushNotification } from "../lib/errors"; import { viewState, updateViewState } from "./view"; import { ConfigSchema, RepoRefSchema, THEME_OPTIONS, BUILTIN_TAB_IDS, CustomTabSchema } from "../../shared/schemas"; -import type { Config, ThemeId, CustomTab, JiraConfig, JiraCustomField, DependencyConfig } from "../../shared/schemas"; +import type { Config, ThemeId, CustomTab, JiraConfig, JiraCustomField } from "../../shared/schemas"; import { z } from "zod"; // ── Re-exports from shared/schemas (backward compat for existing importers) ─── @@ -106,10 +106,6 @@ export function updateJiraConfig(partial: Partial): void { updateConfig({ jira: { ...config.jira, ...partial } }); } -export function updateDependencyConfig(partial: Partial): void { - updateConfig({ dependencies: { ...config.dependencies, ...partial } }); -} - export function updateJiraCustomFields(fields: JiraCustomField[]): void { updateJiraConfig({ customFields: fields.slice(0, 10) }); } diff --git a/src/app/stores/view.ts b/src/app/stores/view.ts index 9b0f0da7..0c94a14a 100644 --- a/src/app/stores/view.ts +++ b/src/app/stores/view.ts @@ -110,6 +110,7 @@ export const ViewStateSchema = z.object({ dependencies: { updateType: "all", bot: "all" }, }), showPrRuns: z.boolean().default(false), + hideDepDashboard: z.boolean().default(true), customTabFilters: z.record( z.string(), z.record(z.string(), z.string()) @@ -212,6 +213,7 @@ export function resetViewState(): void { dependencies: { updateType: "all", bot: "all" }, }, showPrRuns: false, + hideDepDashboard: true, customTabFilters: {}, expandedRepos: { issues: {}, pullRequests: {}, actions: {}, jiraAssigned: {} }, lockedRepos: { issues: [], pullRequests: [], actions: [], jiraAssigned: [] }, From a538b945e267b3dc7d33fd8db47ceb0f47035a47 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 13:35:54 -0400 Subject: [PATCH 23/34] fix(deps): fixes post-deploy dependency tab issues - Includes offending login in Invalid userLogin error message - Strips Python version specifiers (==, >=, ~=) from parsed versions - Constrains repo badge width to prevent layout overflow on long names - Removes sort dropdown from Dependencies tab, uses fixed repo+category sort --- .../components/dashboard/DashboardPage.tsx | 8 +++--- .../components/dashboard/DependenciesTab.tsx | 28 +++---------------- src/app/components/dashboard/ItemRow.tsx | 3 +- src/app/lib/dependency-detection.ts | 12 ++++++-- src/app/services/api.ts | 2 +- tests/lib/dependency-detection.test.ts | 14 ++++++++++ 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 1ca6ef0a..084806dd 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1278,10 +1278,10 @@ export default function DashboardPage() { isRefreshing={_coordinator()?.isRefreshing() ?? dashboardData.loading} lastRefreshedAt={_coordinator()?.lastRefreshAt() ?? dashboardData.lastRefreshedAt} onRefresh={() => _coordinator()?.manualRefresh()} - sortOptions={globalSortOptions} - sortValue={viewState.globalSort.field} - sortDirection={viewState.globalSort.direction} - onSortChange={(field, dir) => setSortPreference(field, dir)} + sortOptions={activeTab() === "dependencies" ? undefined : globalSortOptions} + sortValue={activeTab() === "dependencies" ? undefined : viewState.globalSort.field} + sortDirection={activeTab() === "dependencies" ? undefined : viewState.globalSort.direction} + onSortChange={activeTab() === "dependencies" ? undefined : (field, dir) => setSortPreference(field, dir)} hideOrgRepo={!isBuiltinTab(activeTab()) || activeTab() === "dependencies"} /> diff --git a/src/app/components/dashboard/DependenciesTab.tsx b/src/app/components/dashboard/DependenciesTab.tsx index 75ac0206..5107c753 100644 --- a/src/app/components/dashboard/DependenciesTab.tsx +++ b/src/app/components/dashboard/DependenciesTab.tsx @@ -200,36 +200,16 @@ export default function DependenciesTab(props: DependenciesTabProps) { }); const sortedPRs = createMemo(() => { - const { field, direction } = viewState.globalSort; const items = [...classifiedPRs()]; - const dir = direction === "asc" ? 1 : -1; - items.sort((a, b) => { - // User-selected sort as primary - let cmp = 0; - switch (field) { - case "repo": cmp = a.pr.repoFullName.localeCompare(b.pr.repoFullName); break; - case "title": cmp = a.pr.title.localeCompare(b.pr.title); break; - case "author": cmp = a.pr.userLogin.localeCompare(b.pr.userLogin); break; - case "comments": cmp = a.pr.comments - b.pr.comments; break; - case "checkStatus": cmp = (a.pr.checkStatus ?? "").localeCompare(b.pr.checkStatus ?? ""); break; - case "reviewDecision": cmp = (a.pr.reviewDecision ?? "").localeCompare(b.pr.reviewDecision ?? ""); break; - case "size": cmp = (a.pr.additions + a.pr.deletions) - (b.pr.additions + b.pr.deletions); break; - case "createdAt": cmp = a.pr.createdAt.localeCompare(b.pr.createdAt); break; - case "updatedAt": - default: - cmp = a.pr.updatedAt.localeCompare(b.pr.updatedAt); - break; - } - if (cmp !== 0) return cmp * dir; - - // Category as secondary (safest → least safe within each group) + const repoCmp = a.pr.repoFullName.localeCompare(b.pr.repoFullName); + if (repoCmp !== 0) return repoCmp; + const catCmp = CATEGORY_SORT_ORDER[a.category] - CATEGORY_SORT_ORDER[b.category]; if (catCmp !== 0) return catCmp; - return a.pr.repoFullName.localeCompare(b.pr.repoFullName); + return a.pr.updatedAt.localeCompare(b.pr.updatedAt); }); - return items; }); diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 21eb217c..3d1d50a3 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -119,9 +119,10 @@ export default function ItemRow(props: ItemRowProps) { {props.repo} diff --git a/src/app/lib/dependency-detection.ts b/src/app/lib/dependency-detection.ts index b3cb597e..2fb12a77 100644 --- a/src/app/lib/dependency-detection.ts +++ b/src/app/lib/dependency-detection.ts @@ -72,8 +72,14 @@ export function isDependencyPr(pr: PullRequest, trackedBotLogins: Set): return false; } +const VERSION_SPECIFIER_RE = /^[><=!~^]+/; + +function stripVersionSpecifier(v: string): string { + return v.replace(VERSION_SPECIFIER_RE, ""); +} + function parseSemver(v: string): [number, number, number] | null { - const cleaned = v.replace(/^v/, ""); + const cleaned = stripVersionSpecifier(v).replace(/^v/, ""); const parts = cleaned.split("."); if (parts.length < 2) return null; const nums = parts.slice(0, 3).map(Number); @@ -197,13 +203,13 @@ export function parseRenovateBody(body: string): VersionInfo | null { if (changeIdx >= 0 && changeIdx < cells.length) { const m = VERSION_ARROW_RE.exec(cells[changeIdx]); - if (m) { result.from = m[1]; result.to = m[2]; } + if (m) { result.from = stripVersionSpecifier(m[1]); result.to = stripVersionSpecifier(m[2]); } } if (!result.from) { for (const cell of cells) { const m = VERSION_ARROW_RE.exec(cell); - if (m) { result.from = m[1]; result.to = m[2]; break; } + if (m) { result.from = stripVersionSpecifier(m[1]); result.to = stripVersionSpecifier(m[2]); break; } } } diff --git a/src/app/services/api.ts b/src/app/services/api.ts index 281e96ed..517e6a5f 100644 --- a/src/app/services/api.ts +++ b/src/app/services/api.ts @@ -922,7 +922,7 @@ async function graphqlLightCombinedSearch( return { issues: [], pullRequests: [], - errors: [{ repo: "search", statusCode: null, message: "Invalid userLogin", retryable: false }], + errors: [{ repo: "search", statusCode: null, message: `Invalid userLogin: "${userLogin}"`, retryable: false }], }; } diff --git a/tests/lib/dependency-detection.test.ts b/tests/lib/dependency-detection.test.ts index ba7180aa..c0cbdbaa 100644 --- a/tests/lib/dependency-detection.test.ts +++ b/tests/lib/dependency-detection.test.ts @@ -531,6 +531,20 @@ describe("parseRenovateBody", () => { }); }); + it("strips Python version specifiers (==, >=, ~=) from versions", () => { + const body = [ + "| Package | Change | [Age](url) | [Confidence](url) |", + "|---|---|---|---|", + "| [stamina](url) ([changelog](url2)) | `==25.2.0` → `==26.1.0` | ![age](img) | ![confidence](img) |", + ].join("\n"); + expect(parseRenovateBody(body)).toEqual({ + packageName: "stamina", + updateType: "major", + from: "25.2.0", + to: "26.1.0", + }); + }); + it("parses digest update", () => { const body = [ "| Package | Update | Change |", From 30da87263e4017ac1069e6f3973a5390cd335158 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 13:40:03 -0400 Subject: [PATCH 24/34] fix(deps): migrates tracked user logins to strip [bot] suffix Pre-migration data stores logins like 'khepri-bot[bot]'. The bot variant search appends [bot] again, creating 'khepri-bot[bot][bot]' which fails VALID_TRACKED_LOGIN and surfaces as an Invalid userLogin toast on every poll cycle. --- src/app/stores/config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/stores/config.ts b/src/app/stores/config.ts index fed62eb1..8fde1ee9 100644 --- a/src/app/stores/config.ts +++ b/src/app/stores/config.ts @@ -38,6 +38,17 @@ export function loadConfig(): Config { (parsed as Record).theme = "auto"; } } + // Migrate tracked user logins: strip [bot] suffix from stored logins. + // handleTrackBot now stores base names only, but pre-migration data may + // have "renovate[bot]" which causes a doubled "renovate[bot][bot]" variant. + if (parsed && typeof parsed === "object" && Array.isArray((parsed as Record).trackedUsers)) { + const users = (parsed as Record).trackedUsers as { login?: string }[]; + for (const u of users) { + if (typeof u.login === "string" && /\[bot\]$/i.test(u.login)) { + u.login = u.login.replace(/\[bot\]$/i, ""); + } + } + } const result = ConfigSchema.safeParse(parsed); if (result.success) { const data = result.data; From d4684bbc7e8f54355fa6480f5e7b4870bf0ffd86 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 13:49:17 -0400 Subject: [PATCH 25/34] fix(deps): constrains compact-mode repo badge width for long names The previous fix only applied to comfortable mode. Compact mode also has shrink-0 + min-w-[9.5rem] with no max-width, so long repo names like rosa-eng-inventory-dashboard break column alignment. --- src/app/components/dashboard/ItemRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 3d1d50a3..e8215a69 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -132,7 +132,7 @@ export default function ItemRow(props: ItemRowProps) { From ba5362b3e784c3a593c2db048a659cbde33b3d08 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 13:53:23 -0400 Subject: [PATCH 26/34] fix(deps): uses inline-block for subtleRepo badge truncation text-overflow: ellipsis only works on block containers, not inline-flex. Switches subtleRepo badge to inline-block with align-middle so truncate actually produces an ellipsis on long repo names. --- src/app/components/dashboard/ItemRow.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index e8215a69..1cae508b 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -117,12 +117,12 @@ export default function ItemRow(props: ItemRowProps) { when={isCompact()} fallback={ {props.repo} @@ -130,10 +130,10 @@ export default function ItemRow(props: ItemRowProps) { > {repoShortName()} From 0e60a790d9d77968b109f223ad16dd536eadf308 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 14:19:01 -0400 Subject: [PATCH 27/34] fix(deps): uses inline style for repo badge max-width Tailwind v4 was not generating CSS for max-w-[14rem]/max-w-[16rem] arbitrary values. Uses inline style for max-width to ensure the constraint is applied regardless of Tailwind CSS scanning. --- src/app/components/dashboard/ItemRow.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 1cae508b..3819632f 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -119,9 +119,10 @@ export default function ItemRow(props: ItemRowProps) { {props.repo} @@ -132,9 +133,10 @@ export default function ItemRow(props: ItemRowProps) { {repoShortName()} From bf133508c210f54d4cc77d40a51c7a4b800cb6f2 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 14:25:38 -0400 Subject: [PATCH 28/34] fix(deps): replaces inline styles with Tailwind max-w classes Tailwind v4 does generate max-w-[16rem]/max-w-[14rem] correctly. The earlier issue was inline-flex + truncate incompatibility, not CSS generation. Reverts to pure Tailwind classes (no inline styles). --- src/app/components/dashboard/ItemRow.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 3819632f..1cae508b 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -119,10 +119,9 @@ export default function ItemRow(props: ItemRowProps) { {props.repo} @@ -133,10 +132,9 @@ export default function ItemRow(props: ItemRowProps) { {repoShortName()} From db3fca4c0ed463234cc3db5bead16cab1a856248 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 14:30:04 -0400 Subject: [PATCH 29/34] fix(deps): shows short repo name in comfortable subtleRepo mode Full owner/repo names wasted horizontal space and broke column alignment. Comfortable mode with subtleRepo now shows the short name (repo only, no org prefix) matching compact mode behavior. Full name remains accessible via title tooltip on hover. --- src/app/components/dashboard/ItemRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 1cae508b..4ecf3427 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -119,12 +119,12 @@ export default function ItemRow(props: ItemRowProps) { - {props.repo} + {props.subtleRepo ? repoShortName() : props.repo} } > From f3473c3492c6f0c571d326fe7fe767ede3df9eda Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 14:34:47 -0400 Subject: [PATCH 30/34] fix(deps): uses fixed-width repo badge column for consistent alignment Variable min-w/max-w caused each badge to size to its content, so category badges and titles started at different x-positions per row. Fixed w-[14rem] ensures consistent column alignment. --- src/app/components/dashboard/ItemRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 4ecf3427..34e408c6 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -119,7 +119,7 @@ export default function ItemRow(props: ItemRowProps) { From c5c1db26379644449945deeb44edbedd3534e9fe Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 14:38:46 -0400 Subject: [PATCH 31/34] fix(deps): narrows repo badge column to 11rem for tighter layout --- src/app/components/dashboard/ItemRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 34e408c6..333ea79f 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -119,7 +119,7 @@ export default function ItemRow(props: ItemRowProps) { From cb3100ee7bef37b2e17534cacaaf9c6d90f880d9 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 14:40:55 -0400 Subject: [PATCH 32/34] fix(deps): narrows compact repo badge column to 9rem --- src/app/components/dashboard/ItemRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/dashboard/ItemRow.tsx b/src/app/components/dashboard/ItemRow.tsx index 333ea79f..07f8ccbf 100644 --- a/src/app/components/dashboard/ItemRow.tsx +++ b/src/app/components/dashboard/ItemRow.tsx @@ -132,7 +132,7 @@ export default function ItemRow(props: ItemRowProps) { From 6f3f3a67ecc0f11316e27cdf98f36ee436596811 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 15:54:28 -0400 Subject: [PATCH 33/34] fix(deps): tighten title pattern and address PR review findings - DEP_TITLE_PATTERN requires bot action words after conventional commit prefix, preventing false positives on human fix(deps) PRs - Reset _fetchingDepBodies on auth clear to prevent stale lock - Replace inline regex with stripVersionSpecifier() (DRY) - Align USER_GUIDE status groups with code's 4-status model - Add tests for hideDepDashboard, depPrIds filtering, == specifier --- docs/USER_GUIDE.md | 7 +-- .../components/dashboard/DashboardPage.tsx | 1 + src/app/lib/dependency-detection.ts | 6 +-- tests/components/IssuesTab.test.tsx | 53 +++++++++++++++++++ .../dashboard/PullRequestsTab.test.tsx | 33 +++++++++++- tests/lib/dependency-detection.test.ts | 20 +++++++ 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 69add25c..70954c3b 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -287,11 +287,12 @@ Unlike the Pull Requests tab (which groups by repo), the Dependencies tab groups | Group | Criteria | |-------|----------| -| **Needs Review** | CI passing (all checks green), PR not yet approved — these are ready to merge | -| **Waiting** | CI pending, checks still running, or PR is a draft — not yet actionable | +| **Mergeable** | CI passing (all checks green), not yet approved — awaiting review | +| **Pending Rebase** | PR has the configured rebase label — waiting for the bot to rebase | +| **Needs Action** | CI pending, checks still running, or PR is a draft — not yet actionable | | **Stale** | PR has been open more than 14 days without merging — may need a rebase or manual review | -Within each group, PRs are sorted by updated date (most recent first). +Within each group, PRs are sorted by repository name, then update category (maintenance, pin, patch, minor, major), then update date. ### Abandoned Dependencies diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 084806dd..4efae01f 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -187,6 +187,7 @@ onAuthCleared(() => { setDepMeta(new Map()); localStorage.removeItem?.(DEP_META_STORAGE_KEY); _fetchingDashboardBodies = false; + _fetchingDepBodies = false; resetAbandonedPatternCache(); const coord = _coordinator(); if (coord) { diff --git a/src/app/lib/dependency-detection.ts b/src/app/lib/dependency-detection.ts index 2fb12a77..171e8f9d 100644 --- a/src/app/lib/dependency-detection.ts +++ b/src/app/lib/dependency-detection.ts @@ -18,7 +18,7 @@ export const DEP_BRANCH_PREFIXES = [ "pyup-update-", ]; -export const DEP_TITLE_PATTERN = /^(Bump |Update dependency |chore\(deps|fix\(deps|build\(deps|\[Snyk\])/i; +export const DEP_TITLE_PATTERN = /^(Bump |Update dependency |\[Snyk\]|(?:chore|fix|build)\(deps[^)]*\):\s*(?:update |pin |lock file |bump ))/i; export const DEP_TOOL_LABEL_NAMES = new Set([ "dependencies", @@ -128,8 +128,8 @@ export function extractVersionInfo(title: string): VersionInfo | null { // "update X requirement from >=A to >=B" (Dependabot Python) const reqMatch = /^update\s+(.+?)\s+requirement\s+from\s+([^\s]+)\s+to\s+([^\s]+)/i.exec(body); if (reqMatch) { - const from = reqMatch[2]!.replace(/^[><=!~^]+/, ""); - const to = reqMatch[3]!.replace(/^[><=!~^]+/, ""); + const from = stripVersionSpecifier(reqMatch[2]!); + const to = stripVersionSpecifier(reqMatch[3]!); return { packageName: reqMatch[1]!, from, to, updateType: semverUpdateType(from, to) ?? undefined }; } diff --git a/tests/components/IssuesTab.test.tsx b/tests/components/IssuesTab.test.tsx index c9515c7e..ec826bf5 100644 --- a/tests/components/IssuesTab.test.tsx +++ b/tests/components/IssuesTab.test.tsx @@ -573,3 +573,56 @@ describe("IssuesTab", () => { expect(link.getAttribute("rel")).toBe("noopener noreferrer"); }); }); + +describe("IssuesTab — hideDepDashboard + dependencies.enabled interaction", () => { + it("shows Dependency Dashboard in custom tab even when hideDepDashboard=true and deps disabled", () => { + viewStore.updateViewState({ hideDepDashboard: true }); + updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } }); + const issues = [ + makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo", userLogin: "me" }), + ]; + setAllExpanded("my-custom-tab", ["org/repo"], true); + render(() => ); + screen.getByText("Dependency Dashboard"); + }); +}); + +describe("IssuesTab — hideDepDashboard + dependencies.enabled", () => { + it("hides Dependency Dashboard issue when hideDepDashboard=true and dependencies.enabled=false", () => { + viewStore.updateViewState({ hideDepDashboard: true }); + updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } }); + const issues = [ + makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }), + makeIssue({ id: 2, title: "Regular issue", repoFullName: "org/repo" }), + ]; + setAllExpanded("issues", ["org/repo"], true); + render(() => ); + expect(screen.queryByText("Dependency Dashboard")).toBeNull(); + screen.getByText("Regular issue"); + }); + + it("shows Dependency Dashboard issue when hideDepDashboard=true but dependencies.enabled=true", () => { + viewStore.updateViewState({ hideDepDashboard: true }); + updateConfig({ dependencies: { enabled: true, rebaseLabel: "rebase" } }); + const issues = [ + makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }), + makeIssue({ id: 2, title: "Regular issue", repoFullName: "org/repo" }), + ]; + setAllExpanded("issues", ["org/repo"], true); + render(() => ); + screen.getByText("Dependency Dashboard"); + screen.getByText("Regular issue"); + }); + + it("shows Dependency Dashboard issue when hideDepDashboard=false regardless of dependencies.enabled", () => { + viewStore.updateViewState({ hideDepDashboard: false }); + updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } }); + const issues = [ + makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }), + ]; + setAllExpanded("issues", ["org/repo"], true); + render(() => ); + screen.getByText("Dependency Dashboard"); + }); + +}); diff --git a/tests/components/dashboard/PullRequestsTab.test.tsx b/tests/components/dashboard/PullRequestsTab.test.tsx index 0bf0bf9c..27101128 100644 --- a/tests/components/dashboard/PullRequestsTab.test.tsx +++ b/tests/components/dashboard/PullRequestsTab.test.tsx @@ -31,7 +31,7 @@ vi.mock("../../../src/app/lib/url", () => ({ import { produce } from "solid-js/store"; import PullRequestsTab from "../../../src/app/components/dashboard/PullRequestsTab"; -import { viewState, setViewState, setTabFilter, setAllExpanded, resetViewState, updateViewState, setCustomTabFilter } from "../../../src/app/stores/view"; +import { viewState, setViewState, setTabFilter, setAllExpanded, resetViewState, updateViewState, setCustomTabFilter, ignoreItem } from "../../../src/app/stores/view"; import type { TrackedUser } from "../../../src/app/stores/config"; import { updateConfig, resetConfig } from "../../../src/app/stores/config"; @@ -869,3 +869,34 @@ describe("PullRequestsTab — customTabId filter preset", () => { screen.getByText("My PR"); }); }); + +// ── PullRequestsTab — depPrIds ignored-item filtering ─────────────────────── + +describe("PullRequestsTab — depPrIds ignored-item filtering", () => { + it("excludes dep PR IDs from ignored items when depPrIds is provided", () => { + ignoreItem({ id: 100, type: "pullRequest", repo: "owner/repo", title: "Dep PR", ignoredAt: Date.now() }); + ignoreItem({ id: 200, type: "pullRequest", repo: "owner/repo", title: "Normal PR", ignoredAt: Date.now() }); + + render(() => ); + + screen.getByRole("button", { name: /1 ignored/i }); + expect(screen.queryByRole("button", { name: /2 ignored/i })).toBeNull(); + }); + + it("includes all ignored PRs when depPrIds is undefined", () => { + ignoreItem({ id: 100, type: "pullRequest", repo: "owner/repo", title: "PR A", ignoredAt: Date.now() }); + ignoreItem({ id: 200, type: "pullRequest", repo: "owner/repo", title: "PR B", ignoredAt: Date.now() }); + + render(() => ); + + screen.getByRole("button", { name: /2 ignored/i }); + }); + + it("shows no badge when all ignored PRs belong to depPrIds", () => { + ignoreItem({ id: 100, type: "pullRequest", repo: "owner/repo", title: "Dep only", ignoredAt: Date.now() }); + + render(() => ); + + expect(screen.queryByRole("button", { name: /ignored/i })).toBeNull(); + }); +}); diff --git a/tests/lib/dependency-detection.test.ts b/tests/lib/dependency-detection.test.ts index c0cbdbaa..8fb4b6bc 100644 --- a/tests/lib/dependency-detection.test.ts +++ b/tests/lib/dependency-detection.test.ts @@ -91,6 +91,21 @@ describe("isDependencyPr", () => { expect(isDependencyPr(pr, NO_TRACKED_BOTS)).toBe(false); }); + it("returns false for human fix(deps) PR that is not a bot update", () => { + const pr = makePullRequest({ + userLogin: "octocat", + headRef: "fix/deps-tab-fixes", + title: "fix(deps): post-deploy dependency tab fixes", + labels: [], + }); + expect(isDependencyPr(pr, NO_TRACKED_BOTS)).toBe(false); + }); + + it("returns true for bot fix(deps) PR with update action", () => { + const pr = makePullRequest({ title: "fix(deps): update all non-major dependencies" }); + expect(isDependencyPr(pr, NO_TRACKED_BOTS)).toBe(true); + }); + it("returns false for regular PR even with tracked users of type user", () => { const pr = makePullRequest({ userLogin: "alice" }); const tracked = new Set(["bob"]); @@ -180,6 +195,11 @@ describe("extractVersionInfo", () => { expect(result).toEqual({ packageName: "mypy", from: "1.15.0", to: "1.20.1", updateType: "minor" }); }); + it("handles pinned == pip requirement specifier", () => { + const result = extractVersionInfo("update boto3 requirement from ==1.26.0 to ==1.34.0"); + expect(result).toEqual({ packageName: "boto3", from: "1.26.0", to: "1.34.0", updateType: "minor" }); + }); + it("returns null for unrecognized title format", () => { expect(extractVersionInfo("Fix a bug in auth flow")).toBeNull(); }); From 148a08e9131e957b54de0470e3f198a3e1879e57 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 5 May 2026 16:53:34 -0400 Subject: [PATCH 34/34] fix(deps): restores missing prop and test after rebase --- src/app/components/dashboard/DashboardPage.tsx | 1 + tests/components/dashboard/DependenciesTab.test.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 4efae01f..8356bb86 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -1342,6 +1342,7 @@ export default function DashboardPage() { refreshTick={refreshTick()} rebaseLabel={config.dependencies.rebaseLabel} userLogin={userLogin()} + trackedBotLogins={trackedBotLogins()} onRefresh={() => _coordinator()?.manualRefresh()} /> diff --git a/tests/components/dashboard/DependenciesTab.test.tsx b/tests/components/dashboard/DependenciesTab.test.tsx index b9392ca7..ea47e4b8 100644 --- a/tests/components/dashboard/DependenciesTab.test.tsx +++ b/tests/components/dashboard/DependenciesTab.test.tsx @@ -642,7 +642,7 @@ describe("DependenciesTab — ignore button", () => { it("clicking the ignore button hides the PR from the list", () => { const pr = makeMergeablePR({ title: "chore(deps): update dependency lodash to v5" }); renderTab({ pullRequests: [pr] }); - expect(screen.getByText("lodash")).toBeDefined(); + expect(screen.getByText("lodash → v5")).toBeDefined(); const ignoreBtn = screen.getByRole("button", { name: /^Ignore #/ }); fireEvent.click(ignoreBtn);