Skip to content

feat(roadmap-planner): Dashboard tab + reshaped Member profile + prs_opened metric - #185

Merged
danielfbm merged 7 commits into
mainfrom
feat/roadmap-planner-team-analytics-dashboard
May 10, 2026
Merged

danielfbm merged 7 commits into
mainfrom
feat/roadmap-planner-team-analytics-dashboard

Conversation

@danielfbm

Copy link
Copy Markdown
Contributor

Summary

  • New Dashboard tab as the first tab on Team Analytics — a single screen of pre-baked aggregations driven by three filters (Window 4/12/26 weeks · Pillar · Metric). The metric chooser sits on five clickable KPI tiles (PRs merged / PRs opened / Reviews / Jira done / Story pts); clicking a tile highlights the chosen line on the throughput trend and swaps the metric on the pillar mix, donut, top-movers bars, and per-member ranking. Every drill-in opens the Member profile tab.
  • Member profile tab reshape: drops the inline sidebar form. New compact header has avatar · name · [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.
  • New prs_opened weekly metric (migration 0004) — aggregator mirror of prs_merged but buckets by pull_requests.created_at. Surfaces through MemberWeekRow / MemberSummary / Bucket so the Dashboard's Inflow vs Outflow panel reads it directly.
  • Theming: every new visual var uses existing semantic tokens (--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 pass
  • backend/internal/contributions/service.go — PRsOpened field on MemberSummary + Bucket
  • backend/internal/storage/{store.go,generic.go} — MemberWeekRow.PRsOpened
  • frontend/src/components/TeamAnalyticsDashboard.jsx (new)
  • frontend/src/components/TeamAnalytics.jsx — 4-tab nav + reshaped MemberView with modals
  • frontend/src/components/TeamAnalytics.css — dashboard, KPI strip, modals, header

Test plan

  • Backend: go test ./... (passing locally — opened-PR pass + plumbing tests pick up cleanly).
  • Frontend: npm run build + npm test (passing locally — vite build clean, vitest 4/4).
  • Dev rollout: bump image on 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.
  • Theme swap: toggle Atlas ↔ Platform and Light ↔ Dark — every panel should re-render in the active token set with no hard-coded colors leaking through (modal scrim is the only hex/rgba and is intentional).

🤖 Generated with Claude Code

…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>
@alaudabot

alaudabot commented May 10, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AI Code Review

Property Value
Model opencode/minimax-m2.5-free
Style strict
Issues Found 0
Config Source centralized
Profile ❌ Not Found
Personalized Prompt ❌ No
Prompt Path .github/review/profiles/alaudadevops/toolbox/pr-review.md
Alauda Skills ✅ base-acp-operator-list, base-acp-operator-release, base-authoring, base-m365, base-ocp-operator-list, base-skill-setup, builders-alauda-pipeline, builders-component-knowledge, builders-confluence, builders-jira, builders-notify-wecom, builders-prd-to-testcase, builders-publish-errata, builders-roadmap-studio, builders-story-split, cross-repo-add-mirror, cross-repo-publish, devops-autodns, devops-candidate-version-supervisor, devops-connectors-acceptance-test, devops-connectors-explore, devops-connectors-poc-case, devops-connectors-review, devops-connectors-unit-test, devops-connectors-upgrade-test, devops-connectors-write-user-docs, devops-creating-tekton-pipelines, devops-fix-go-vulns, devops-fork-alauda-binary-release, devops-gen-advanced-form-descriptors, devops-jira-rfd-acceptance, devops-knowledge-adoption, devops-refresh-containerfile-digests, devops-refresh-containerfile-tags, devops-replace-strings, devops-scan-docker-keywords, devops-sync-alauda-github-releases, devops-tekton-dynamic-form-optimizer, devops-tekton-operator-task-e2e, devops-tekton-pipeline-delivery, devops-tekton-task-delivery, devops-tekton-task-overview-template, devops-tekton-task-version-upgrade, devops-tekton-upgrade-notes, devops-tool-report-troubleshoot, devops-ui-e2e-code-audit, devops-ui-e2e-fix-base-on-report, devops-ui-e2e-regression-and-fix, devops-ui-generate-e2e-from-feature, devops-ui-pre-setup, devops-upgrade-go, devops-upstream-backport-cve, devops-upstream-upgrade
Reviewed at 2026-05-10 16:16:39 UTC

Summary

This 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 prs_opened weekly metric via a new DB migration. Backend changes include PillarsFor/ComponentsFor APIs and a new aggregator pass. Overall the implementation is solid with good test coverage and regression tests, but there are a few items to address.

Review Statistics

Category Count
Critical Issues 0
Warnings 3
Suggestions 4
Files Reviewed 15

Critical Issues

(none — no blocking issues)

Warnings

  • roadmap-planner/backend/internal/contributions/service_extras.go:484 — MemberExtras now silently swallows sprint-query errors and returns partial data. The old behavior failed the entire call on sprint lookup errors. The new best-effort approach is pragmatically safer but the error is now invisible to operators who may want to know a sprint query failed (e.g. misconfigured Jira board). Consider at least logging the error so it surfaces in traces.

  • roadmap-planner/backend/internal/contributions/pillar_mapping.go:155 — In ComponentsFor, the loop for _, c := range p.pillarToComponents[pillar] iterates over p.pillarToComponents[pillar] which may be nil (pillar has no configured components but matched via versionPrefixes). This would cause a runtime panic. Add a nil check: if p.pillarToComponents[pillar] == nil { continue }.

  • roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx:50 — colorForPillar mutates a hash accumulator internally via the for-loop h = (h * 31 + ...) pattern; this is correct but the hash function has no documented collision strategy. Since PILLAR_PALETTE[h % PILLAR_PALETTE.length] is used for display colors, collisions could cause two different pillars to share the same color, which is confusing. Consider a deterministic fallback to a distinct color for unknown pillars rather than relying on hash collisions.

Suggestions

  • roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx:67 — fmt uses Number.isInteger(n) after +v || 0 which coerces the value to a number but loses fractional story-points data (e.g. points: 2.5 would render as 2 rather than 2.5). Verify this is intentional; if fractional points are possible, use toFixed(1) instead.

  • roadmap-planner/frontend/src/components/TeamAnalytics.jsx:1137 — VALID_TABS now includes 'dashboard' but the URL-write logic in writeURLState skips writing 'team' (the old default). Since dashboard is the new default, it will be the only tab not written to the URL, making deep-linking to the Dashboard tab impossible (the URL will be bare while on the Dashboard tab). Either write dashboard explicitly to the URL or clarify the intended behavior in a comment.

  • roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx — GroupedBarChart uses hardcoded pad.l + 110 and pad.l + 124 for legend positioning. If label lengths vary (e.g. "PRs opened" vs "PRs merged"), the labels may overlap or clip. Consider using getBBox() or a flexible layout approach for the legend.

  • roadmap-planner/docs/team-analytics/CHARTS.md — Excellent addition. Consider adding a section on the prs_opened metric definition and how it differs from prs_merged (created_at vs merged_at), since the PR description mentions it but the docs don't explicitly define it.

Positive Feedback

  • The 0004_prs_opened.sql migration is clean and self-documenting; the DEFAULT 0 avoids needing a backfill.
  • The regression test in TeamAnalyticsDashboard.smoke.test.jsx:77 explicitly documents and guards against the 2026-05-10 crash, which is excellent defensive testing.
  • The backend PillarsFor/ComponentsFor refactor in pillar_mapping.go consolidates the duplicate fallback chain from two locations into a single shared implementation — a solid DRY improvement.
  • service_extras.go:441-451 includes a thoughtful fallback comment explaining why direct component tallying is preserved as a fallback when no fixVersion match is found; this helps future maintainers understand the intent.
  • The new smoke tests in TeamAnalytics.smoke.test.jsx cover realistic 20-member × 27-week data, including the empty-rows case and mixed pillar scenarios.
  • The CSS theming uses only semantic tokens (--accent, --ocean, --amber, etc.) throughout the new dashboard styles — light/dark/Atlas compatibility is well-handled.
  • The DonutChart handles the zero-data case gracefully with a "no data" placeholder rather than rendering a blank SVG.

Reviewed by alaudabot — focused on high-impact areas given the PR size (+2083/-237).


ℹ️ About this review

This review was automatically generated using the run-actions workflow.

  • Shared prompt: .github/prompts/code-review.md
  • Config source: centralized
  • Profile path: Not Found
  • Profile ref: 4a9568cdd56fea5ff37c463d60c67e04c765f13f
  • No repository-specific prompt configured
  • Alauda skills: base-acp-operator-list, base-acp-operator-release, base-authoring, base-m365, base-ocp-operator-list, base-skill-setup, builders-alauda-pipeline, builders-component-knowledge, builders-confluence, builders-jira, builders-notify-wecom, builders-prd-to-testcase, builders-publish-errata, builders-roadmap-studio, builders-story-split, cross-repo-add-mirror, cross-repo-publish, devops-autodns, devops-candidate-version-supervisor, devops-connectors-acceptance-test, devops-connectors-explore, devops-connectors-poc-case, devops-connectors-review, devops-connectors-unit-test, devops-connectors-upgrade-test, devops-connectors-write-user-docs, devops-creating-tekton-pipelines, devops-fix-go-vulns, devops-fork-alauda-binary-release, devops-gen-advanced-form-descriptors, devops-jira-rfd-acceptance, devops-knowledge-adoption, devops-refresh-containerfile-digests, devops-refresh-containerfile-tags, devops-replace-strings, devops-scan-docker-keywords, devops-sync-alauda-github-releases, devops-tekton-dynamic-form-optimizer, devops-tekton-operator-task-e2e, devops-tekton-pipeline-delivery, devops-tekton-task-delivery, devops-tekton-task-overview-template, devops-tekton-task-version-upgrade, devops-tekton-upgrade-notes, devops-tool-report-troubleshoot, devops-ui-e2e-code-audit, devops-ui-e2e-fix-base-on-report, devops-ui-e2e-regression-and-fix, devops-ui-generate-e2e-from-feature, devops-ui-pre-setup, devops-upgrade-go, devops-upstream-backport-cve, devops-upstream-upgrade

danielfbm and others added 2 commits May 10, 2026 05:24
…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>
@danielfbm

Copy link
Copy Markdown
Contributor Author

/test-all

@danielfbm

Copy link
Copy Markdown
Contributor Author

/retest

@alaudabot

Copy link
Copy Markdown
Contributor

/test artifact-scanner-image

@alaudabot

Copy link
Copy Markdown
Contributor

/test roadmap-planner-image

@alaudabot

Copy link
Copy Markdown
Contributor

🔄 Retesting failed pipelines

Triggered retests for:
• artifact-scanner-image
• roadmap-planner-image

@danielfbm

Copy link
Copy Markdown
Contributor Author

/test-all

1 similar comment
@danielfbm

Copy link
Copy Markdown
Contributor Author

/test-all

danielfbm and others added 3 commits May 10, 2026 08:38
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>
@danielfbm

Copy link
Copy Markdown
Contributor Author

/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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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 != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@danielfbm
danielfbm merged commit ab2485f into main May 10, 2026
15 checks passed
@danielfbm
danielfbm deleted the feat/roadmap-planner-team-analytics-dashboard branch May 10, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants