Repository navigation
feat(roadmap-planner): Dashboard tab + reshaped Member profile + prs_opened metric - #185
Conversation
…opened metric Adds a fourth tab to Team Analytics — a Dashboard that surfaces 180-day team activity through pre-baked aggregations the operator can drive with three filters (window 4/12/26 weeks, pillar, metric). Click any of the five KPI tiles (PRs merged / PRs opened / Reviews / Jira done / Story pts) to switch the highlighted line on the throughput trend, swap the metric on the pillar mix area, donut, top-movers bars, and per-member ranking. The Member profile tab is reshaped to match: drops the inline sidebar form, replaces it with a compact header carrying a [Switch member ▼] chip and a [✎ Edit identity] button — both open modal overlays with the same data the inline form had. Below the header sits the same five-tile KPI strip, the multi-line weekly contribution chart, and a 2×2 grid of Components touched, Rank vs team, Week-over-week pulse, and This sprint. Backend: new prs_opened weekly metric (migration 0004), aggregator pass mirroring prs_merged but bucketing by created_at, plumbed through MemberWeekRow / MemberSummary / Bucket so the dashboard's Inflow vs Outflow panel works without out-of-band sqlite reads. Theming: every visual var in the new code uses the existing semantic tokens (--bg-elevated, --accent, --ocean, --amber, --forest, --crimson, --shadow-plate, etc.), so light / dark / Atlas all swap without per-mode branching. Only hardcoded color is the modal scrim — intentional dark-with-alpha that works in both modes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🤖 AI Code Review
SummaryThis PR adds a new Dashboard tab to Team Analytics with KPI tiles, throughput trends, and per-member rankings; reshapes the Member profile tab with a new header, modals, and chart panels; and introduces the Review Statistics
Critical Issues(none — no blocking issues) Warnings
Suggestions
Positive Feedback
Reviewed by alaudabot — focused on high-impact areas given the PR size (+2083/-237). ℹ️ About this reviewThis review was automatically generated using the
|
…e-keyed `pillarKeys = [...orderedPillarNames, "Unassigned"]` is built from the configured/derived pillar list, but team rollups can carry a member whose primary pillar isn't in that list (e.g. when basic data hasn't loaded yet, or the configured `team_analytics.pillars` order is empty and a member's pillar comes from the per-member attribution chain). `stack[p][i] += …` in pillarStack and `totals[p] += …` in donutSlices both crashed with `Cannot read properties of undefined (reading '0')` on those rows. Lazily seed both maps so an unmapped pillar still shows up rather than throwing. Also normalize `week_start` comparison: API returns the full ISO timestamp (`"2025-11-03T00:00:00Z"`), the dashboard's weeks array uses 10-char dates (`"2025-11-03"`). Pre-fix, no buckets ever matched → charts rendered as flat zeros even when data was present. Includes a regression smoke test that mounts DashboardView with `orderedPillarNames=[]` and members carrying real pillars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Header: switch from flex to a 4-column CSS grid so the [Switch member ▼] chip and [✎ Edit identity] button sit on row 1 with the name, while the long meta line wraps below spanning the full width. Pre-fix the meta dragged the right-side buttons out of vertical alignment with the title. - Drop double rules: cards inside the member-profile panel body lose their own border / box-shadow / background. The outer panel already provides the boundary; the doubled lines read as overkill. - Normalize spacing: the body becomes a flex column with a single 18px gap, matching the dash-grid's horizontal gap. KPI strip, weekly chart, and the 2×2 sub-grid all share the same vertical rhythm. Pre-fix the gap between the weekly chart and the cards below was zero while the gaps inside the sub-grid were 18px. - Tighten card-title spacing (4px to chart) and shrink switcher chip + edit button to a shared 28px height for visual parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/test-all |
|
/retest |
|
/test artifact-scanner-image |
|
/test roadmap-planner-image |
|
🔄 Retesting failed pipelines Triggered retests for: |
|
/test-all |
1 similar comment
|
/test-all |
Drop the card-inside-a-panel structure that produced doubled borders
and uneven padding. The member profile now mirrors the Dashboard tab
exactly:
- Standalone bordered header strip (avatar · name · switcher chip ·
edit-identity button · meta line wrapping below).
- Edge-to-edge KPI strip (its own border + radius from .ta-kpis,
sits between header and grid).
- .ta-dash-grid below: weekly chart spans both columns, then the 2×2
sub-grid with Components · Rank · Pulse · Sprint. Each section is
its own .ta-panel with the standard head + body, so charts have
consistent padding and the borders read as one tier rather than
two.
- "Back to team overview" backlink removed; the tab nav above already
carries that affordance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… scan error Three small fixes for the member profile: 1. **Member profile sprint + components were always empty.** Backend logs showed `sprint pick: sql: Scan error on column index 1, name "MAX(r.captured_at)": unsupported Scan, storing driver.Value type string into type *time.Time`. SQLite's aggregate functions strip column type affinity and return TEXT, which the Go driver refuses to scan into *time.Time. Switch the (unused) lastSeen scan to sql.NullString. Also degrade the sprint pass to best-effort so components-touched is still returned when the sprint scan fails. 2. **prs_opened missing from MemberDetail response.** The handler builds resp manually and didn't pick up the field I added to MemberSummary in the previous commit, so the profile's KPI tile fell back to summing week_totals. 3. **Member profile: KPI→panel-grid gap matched header→KPI gap (18px)** so both arrows in the screenshot read the same vertically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fix matching
DEVOPS Jira issues rarely carry components, so the member-profile
"Components touched" panel was empty for most members. Reuse the
existing pillar-fallback chain (issue's own components → fixVersion
prefix matching) to derive a component name when the column is empty:
for each fixVersion, pick the longest component name from the
matched pillar's configured components[] that is itself a prefix of
the version (same -/v boundary rules as PillarsForVersions). E.g.
fixVersion "tektoncd-operator-v4.11.0" against pillar components
[tekton, tekton-cli, tektoncd-operator] → "tektoncd-operator".
Extracted two reusable helpers on PillarMap:
- PillarsFor(directComps, versions) — components-first chain shared
by attributionByMember + PillarThroughput.
- ComponentsFor(directComps, versions) — same chain, terminates on
derived component names instead of pillars.
Adds a forward pillarToComponents index so the longest-prefix match
is O(comps_per_pillar) per version. Direct components win and pass
through verbatim (case preserved); step-2 outputs are lowercased.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/test-all |
| const sums = weeks.map((_, i) => categories.reduce((a, c) => a + ((stackByCategory[c] || [])[i] || 0), 0)); | ||
| const max = Math.max(1, ...sums); | ||
| const xStep = (W - pad.l - pad.r) / Math.max(1, weeks.length - 1); | ||
| const offsets = weeks.map(() => H - pad.b); |
There was a problem hiding this comment.
Bug (bug/mutation-during-render): offsets is mutated inside .map() at line 181 via offsets[i] -= h. Using a mutable array with shared state across render passes can cause visual glitches on subsequent renders. Use a fold/reduce pattern instead:
| const offsets = weeks.map(() => H - pad.b); | |
| const result = series.reduce((acc, v, i) => { | |
| const h = (v / max) * (H - pad.t - pad.b); | |
| return { top: [...acc.top, `${pad.l + i * xStep},${acc.offset - h}`], offset: acc.offset - h }; | |
| }, { top: [], offset: H - pad.b }); | |
| const top = result.top; | |
| const bot = series.map((v, i) => `${pad.l + i * xStep},${result.offset + (v / max) * (H - pad.t - pad.b)}`).reverse(); |
| points: detail?.jira_points_done ?? totals.points ?? sumOver(weeks, 'points'), | ||
| }; | ||
|
|
||
| const filteredRoster = sortedRoster.filter((r) => { |
There was a problem hiding this comment.
Warning (performance/recompute): filteredRoster is computed inline without useMemo, causing a full filter + sort on every render. With a large roster this is noticeable. Wrap it:
| const filteredRoster = sortedRoster.filter((r) => { | |
| const filteredRoster = useMemo(() => { | |
| if (!switchFilter) return sortedRoster; | |
| const f = switchFilter.toLowerCase(); | |
| return sortedRoster.filter((r) => | |
| r.name.toLowerCase().includes(f) || (r.id || '').toLowerCase().includes(f) || (r.pillar || '').toLowerCase().includes(f) | |
| ); | |
| }, [switchFilter, sortedRoster]); |
|
|
||
| // GroupedBarChart — pairs of bars per week. | ||
| function GroupedBarChart({ weeks, seriesA, seriesB, labelA, labelB, colorA, colorB, height = 220 }) { | ||
| const W = 600, H = height, pad = { l: 44, r: 14, t: 14, b: 36 }; |
There was a problem hiding this comment.
Warning (style/inconsistent-width): GroupedBarChart uses hardcoded W = 600 while LineChart (line 85) uses W = 880 and StackedAreaChart (line 157) uses W = 600. Mixed chart widths can cause visual misalignment in the grid. Consider using a shared constant.
| /* ---------------- date helpers ---------------- */ | ||
| function isoDate(d) { return new Date(d).toISOString().slice(0, 10); } | ||
| function mondayOf(d) { | ||
| const x = new Date(d); |
There was a problem hiding this comment.
Suggestion (refactor/duplication): LineChart, StackedAreaChart, and GroupedBarChart share nearly identical SVG boilerplate: grid lines, Y-axis ticks, x-axis labels. Extract to a shared ChartGrid helper to reduce duplication and ensure consistent axis rendering.
| x.setUTCHours(0, 0, 0, 0); | ||
| return x; | ||
| } | ||
| function lastNWeeks(n) { |
There was a problem hiding this comment.
Suggestion (test/testability): lastNWeeks(n) calls new Date() internally, making it impossible to test with a fixed "today". Consider passing an optional now parameter for deterministic testing.
Reader-facing reference for every chart on the Dashboard, Member profile, Team overview, and Metrics tabs. Each entry lists the chart type, what it shows, the data source, and how to read it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| matchedPillars[pl] = true | ||
| } | ||
| } | ||
| for pillar := range matchedPillars { |
There was a problem hiding this comment.
Warning (bug/null-pointer): p.pillarToComponents[pillar] may be nil if the pillar matched only via versionPrefixes but has no configured components[]. Calling range over nil causes a runtime panic.
| for pillar := range matchedPillars { | |
| for pillar := range matchedPillars { | |
| if p.pillarToComponents[pillar] == nil { | |
| continue | |
| } | |
| best := "" | |
| for _, c := range p.pillarToComponents[pillar] { |
| // dropping components from the response too. | ||
| return out, nil | ||
| } | ||
| if sprintID.Valid && sprintID.String != "" { |
There was a problem hiding this comment.
Warning (style/logging): Sprint-query errors are now silently swallowed and the function returns partial data (components-touched only). The old behavior failed the entire call. The best-effort approach is pragmatic, but operators have no visibility into why the sprint card is missing (e.g. misconfigured Jira board). Consider logging the error so it surfaces in traces without blocking the response.
Summary
[Switch member ▼]chip ·[✎ Edit identity]button · meta strip. Below: the same five KPI tiles, a 5-line Weekly contribution chart with highlight, then a 2×2 grid of Components touched · Rank vs team · Week-over-week pulse · This sprint.prs_openedweekly metric (migration 0004) — aggregator mirror ofprs_mergedbut buckets bypull_requests.created_at. Surfaces throughMemberWeekRow/MemberSummary/Bucketso the Dashboard's Inflow vs Outflow panel reads it directly.--bg-elevated,--accent,--ocean,--amber,--forest,--crimson,--shadow-plate, etc.), so light/dark × platform/atlas all swap without per-mode branching.Files
backend/internal/storage/migrations/0004_prs_opened.sql(new)backend/internal/contributions/aggregator.go— opened-PR passbackend/internal/contributions/service.go—PRsOpenedfield onMemberSummary+Bucketbackend/internal/storage/{store.go,generic.go}—MemberWeekRow.PRsOpenedfrontend/src/components/TeamAnalyticsDashboard.jsx(new)frontend/src/components/TeamAnalytics.jsx— 4-tab nav + reshaped MemberView with modalsfrontend/src/components/TeamAnalytics.css— dashboard, KPI strip, modals, headerTest plan
go test ./...(passing locally — opened-PR pass + plumbing tests pick up cleanly).npm run build+npm test(passing locally — vite build clean, vitest 4/4).devops-road-dev.alaudatech.net, hit the new Dashboard tab, click each KPI tile to verify the panels redraw, drill into a member, open the Switch-member modal, open the Edit-identity modal, save → confirm aggregator rebuild fires.🤖 Generated with Claude Code