From f38dbb14ba5cccc7ce1b55e6ea533dd14cbe3509 Mon Sep 17 00:00:00 2001 From: Kumaresan <36756050+kumaresan-subramani@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:49:52 +0530 Subject: [PATCH] feat: add date range filter to dashboard and anti-patterns pages - Add date range preset buttons (Last 7/28/90/180 days, All time) - Add buildRangeFilter() helper to dynamically apply date filters - Update all RPC calls to use filtered date range - Wire date filter event listeners for instant re-render - Pass filtered context to rule editor testing Applies to: - Dashboard page: filters daily activity and all metrics - Anti-Patterns page: filters health scores and pattern findings - Rule editor: tests rules against selected date range --- src/webview/page-antipatterns.ts | 38 ++++++++++++++++++++++++--- src/webview/page-dashboard.ts | 44 ++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/webview/page-antipatterns.ts b/src/webview/page-antipatterns.ts index 09b3ba6..587f57f 100644 --- a/src/webview/page-antipatterns.ts +++ b/src/webview/page-antipatterns.ts @@ -215,12 +215,26 @@ function thresholdRow(key: string, value: number, ruleId: string): ComponentChil `; } +/* ── Module-level view state ── */ +let activeRangeDays = 0; + +function buildRangeFilter(f: DateFilter): Record { + const filter: Record = { ...f }; + if (activeRangeDays > 0) { + const d = new Date(); + d.setDate(d.getDate() - activeRangeDays); + filter.fromDate = d.toISOString().slice(0, 10); + } + return filter; +} + /* ── Main render ── */ export async function renderAntiPatterns(container: HTMLElement, currentFilter: DateFilter): Promise { + const filter = buildRangeFilter(currentFilter); const [apData, ruleData] = await Promise.all([ - rpc('getAntiPatterns', currentFilter as Record), - rpc('getRuleEditor', currentFilter as Record), + rpc('getAntiPatterns', filter), + rpc('getRuleEditor', filter), ]); const patterns = apData.patterns || []; @@ -246,6 +260,14 @@ export async function renderAntiPatterns(container: HTMLElement, currentFilter:

Review health scores across practice groups, drill into individual findings, and manage the rules that detect them. Switch to the Rules tab to browse, create, or edit detection rules using the built-in DSL.

+
+ + + + + +
+
@@ -539,8 +561,17 @@ export async function renderAntiPatterns(container: HTMLElement, currentFilter: // Wire tab switching wireTabBar(container); + // Wire date range filter + container.querySelector('#antiPatternsRange')?.addEventListener('click', (e) => { + const btn = (e.target as HTMLElement).closest('[data-range]'); + const range = btn?.dataset.range; + if (!btn || !range) return; + activeRangeDays = Number(range); + void renderAntiPatterns(container, currentFilter); + }); + // Wire up rules section - wireRulesSection(container, rules, previewMap, currentFilter); + wireRulesSection(container, rules, previewMap, filter); } /* ── Tab switching ── */ @@ -1231,4 +1262,3 @@ function renderLayerStatus(layers: RuleLayerInfo[], layerName: string): Componen if (!info.exists) return html`Directory not found`; return html`${info.ruleCount} rule${info.ruleCount !== 1 ? 's' : ''} loaded`; } - diff --git a/src/webview/page-dashboard.ts b/src/webview/page-dashboard.ts index 067577e..519cd32 100644 --- a/src/webview/page-dashboard.ts +++ b/src/webview/page-dashboard.ts @@ -14,6 +14,7 @@ import { llmAvailable } from './capabilities'; // Module-level view state — survives filter/harness changes. let activeMetric = 'requests'; +let activeRangeDays = 0; const DASHBOARD_LANGUAGE_ALIASES: Record = { py: 'python', @@ -96,6 +97,7 @@ function renderDashboardMarkup( totalSessions: number, totalLoc: number, skillCache: ReturnType, + currentFilter: DateFilter, ): void { const harnesses = harnessBreakdown.labels || []; const overallScore = scores.length > 0 ? Math.round(scores.reduce((s, g) => s + g.score, 0) / scores.length) : 0; @@ -125,8 +127,13 @@ function renderDashboardMarkup( ${harnesses.length > 0 && html`
${harnesses.map((h, i) => html`${h}`)}
`}
- ${!FF_TOKEN_REPORTING_ENABLED && html`
\u2139
Token Usage & Burndown temporarily hidden

These features are disabled until we can verify that reported numbers align with GitHub's billing data. They will be re-enabled once validated.

`} - ${scores.length > 0 && html`

Anti-Patterns Summary

View All Anti-Patterns \u2192
${scores.map(g => html`<${PracticeCard} g=${g} />`)}
`} + ${!FF_TOKEN_REPORTING_ENABLED && html`
\u2139
Token Usage & Burndown temporarily hidden

These features are disabled until we can verify that reported numbers align with GitHub's billing data. They will be re-enabled once validated.

`}
+ + + + + +
${scores.length > 0 && html`

Anti-Patterns Summary

View All Anti-Patterns \u2192
${scores.map(g => html`<${PracticeCard} g=${g} />`)}
`} ${llmAvailable() && html`

Scans your prompt history for repeated patterns that waste time re-explaining the same tasks.

${!skillCache && html`

Analyze your prompt history to discover skill opportunities.

`}
`}

Daily Activity

<${CanvasEl} id="dailyChart" height=${160} />
<${CanvasEl} id="wsChart" height=${140} title="Top Workspaces by Requests" /><${CanvasEl} id="harnessChart" height=${140} title="Requests by Harness" />
@@ -184,16 +191,27 @@ function renderDashboardSkillFinder(skillCache: ReturnType }); } +function buildRangeFilter(f: DateFilter): Record { + const filter: Record = { ...f }; + if (activeRangeDays > 0) { + const d = new Date(); + d.setDate(d.getDate() - activeRangeDays); + filter.fromDate = d.toISOString().slice(0, 10); + } + return filter; +} + export async function renderDashboard(container: HTMLElement, currentFilter: DateFilter): Promise { const emptyDaily: DailyActivity = { labels: [], values: [], sessions: [], loc: [], workspaces: [], byHarness: [] }; const emptyCodeProd: CodeProductionData = { summary: { totalAiLoc: 0, totalUserLoc: 0, totalLoc: 0, aiBlocks: 0, userBlocks: 0, aiRatio: 0, locCost2010: 0, costPerLoc: 0 }, byLanguage: { labels: [], aiLoc: [], userLoc: [] }, dailyTimeline: { labels: [], aiLoc: [], userLoc: [] }, byWorkspace: { labels: [], aiLoc: [], userLoc: [] }, dailyByWorkspace: {}, dailyByModel: {}, dailyByHarness: {} }; + const filter = buildRangeFilter(currentFilter); const [stats, daily, wsBreakdown, harnessBreakdown, antiPatterns, codeProd] = await rpcAllSettled([ - rpc<{ totalSessions: number; totalWorkspaces: number; totalRequests: number }>('getStats', currentFilter as Record), - rpc('getDailyActivity', currentFilter as Record), - rpc<{ labels: string[]; values: number[] }>('getWorkspaceBreakdown', currentFilter as Record), - rpc<{ labels: string[]; requests: number[] }>('getHarnessBreakdown', currentFilter as Record), - rpc('getAntiPatterns', currentFilter as Record), - rpc('getCodeProduction', currentFilter as Record), + rpc<{ totalSessions: number; totalWorkspaces: number; totalRequests: number }>('getStats', filter), + rpc('getDailyActivity', filter), + rpc<{ labels: string[]; values: number[] }>('getWorkspaceBreakdown', filter), + rpc<{ labels: string[]; requests: number[] }>('getHarnessBreakdown', filter), + rpc('getAntiPatterns', filter), + rpc('getCodeProduction', filter), ] as const, [ { totalSessions: 0, totalWorkspaces: 0, totalRequests: 0 }, emptyDaily, @@ -236,6 +254,7 @@ export async function renderDashboard(container: HTMLElement, currentFilter: Dat totalSessions, totalLoc, skillCache, + currentFilter, ); // ── Activity chart with switchable metrics ── @@ -295,6 +314,15 @@ export async function renderDashboard(container: HTMLElement, currentFilter: Dat renderActivityChart(); + // Range filter switching + document.getElementById('dashboardRange')?.addEventListener('click', (e) => { + const btn = (e.target as HTMLElement).closest('[data-range]'); + const range = btn?.dataset.range; + if (!btn || !range) return; + activeRangeDays = Number(range); + void renderDashboard(container, currentFilter); + }); + // Tab switching document.getElementById('activityTabs')?.addEventListener('click', (e) => { const btn = (e.target as HTMLElement).closest('[data-metric]');