Skip to content

team-view: align TEAM_BULLET_* aggregates with the IR roster #69

Description

@il10241024

🔗 Mirrored from cyberfabric/cyber-insight-front#40
Originally opened by dzarlax on 2026-04-29T10:46:02Z


Context

#117 unified the Team Members table with the sidebar menu by sourcing the roster from the Identity Resolution subtree (per-person `TEAM_MEMBER` queries against `person_id eq`). The bullet sections below the table — `TEAM_BULLET_DELIVERY`, `TEAM_BULLET_QUALITY`, `TEAM_BULLET_COLLAB`, `TEAM_BULLET_AI` — were left on the legacy `org_unit_id` filter as a known residual.

This re-introduces the original mismatch one level down on the same screen: members and bullets now disagree on "who counts as the team".

Repro symptoms

Scenario Members table Bullet sections
Executive clicks a subordinate in the sidebar (`teamId = email`) Subordinate's IR subtree Empty — `org_unit_id eq ''` returns no rows on real data
Team Lead viewing own department IR subtree of viewer (depth 1+ subordinates) All members of `org_unit_id`, incl. contractors, transferred employees, anyone with a different supervisor in same unit
Direct reports only toggle ON Filtered to `is_direct` Unchanged — still aggregating the entire `org_unit`

The medians, ranges, and member-scale denominators in the bullet bars therefore reflect a different population than the table the user is looking at.

Code pointers

  • `src/screensets/insight/actions/teamViewActions.ts:171-174` — the four `api.queryMetric` calls still pass `bulletFilter = org_unit_id eq '...' and ...`.
  • `src/screensets/insight/actions/teamViewActions.ts:213-215` — `teamSize` denominator for member-scale AI bullets is computed from the IR roster (correct), but the numerator (`active_ai_members` etc.) comes from the org_unit aggregate. So "5 / 12" can pair an org_unit numerator with an IR-roster denominator.

Options

A. FE-side aggregation from member rows

For metrics whose underlying values are already on each `TeamMember` (e.g., focus_time_pct, ai_loc_share_pct, build_success_pct, prs_merged), compute median / min / max client-side over the visible roster. Mirrors what `deriveTeamKpis` already does for the hero strip.

Pros: no backend dependency, immediate consistency, deterministic with what the table shows.
Cons: doesn't cover bullets sourced from per-person fields the FE doesn't load — e.g. meeting hours, Slack engagement, M365 emails, Teams messages, m365_files_shared. Those would have to be either (a) added to `TEAM_MEMBER` per-person rows, or (b) fetched separately per-person via `IC_BULLET_*` and aggregated, which is what `TeamMetricsModal` already does on demand for the "View team stats" modal.

B. Backend filter `manager_email`

Add an OData filter to analytics-api that resolves a manager email to its full IR subtree (using the existing identity service `get_by_email`) and rewrites it into `person_id IN (...)` on the underlying CH query. Then FE switches all four bullet calls to `manager_email eq ''` and the legacy `org_unit_id` path is reserved for the executive-on-org-unit case.

Pros: single filter, parametrized, scales, IDOR-safe (BE can validate that the viewer can access the requested subtree).
Cons: requires backend PR (insight repo). Today's analytics-api OData parser only handles `eq` on `metric_date / person_id / drill_id / org_unit_id` — it will need either a new field handler or a more general `person_id IN (...)` parser. Identity service is already deployed and called elsewhere.

C. Hybrid

For metrics decomposable from `TeamMember` rows, aggregate on FE (option A). For metrics that aren't (`active_ai_members`, meeting hours, etc.), keep the org_unit aggregate and surface a dashed line / tooltip in the bullet UI explaining "based on department, may differ from filtered roster".

Recommendation

Start with A for the metrics it covers — that's a self-contained FE change and removes the executive-drilldown empty-bullets bug today. File a separate, properly-scoped backend issue (option B) for the metrics A can't handle, or accept C as the long-term shape.

Acceptance criteria

  • Bullet sections respect the same roster the Members table shows in both managerial scenarios (own-dept and email drilldown).
  • `Direct reports only` toggle reflects in bullet medians/ranges.
  • Member-scale denominators (e.g. `active_ai_members / N`) match the visible roster size.
  • Executive drilldown into a subordinate produces non-empty bullets where the underlying data exists.
  • No regression for the executive-on-org_unit fallback path (`teamId = department string`, no IR pivot resolved) — that case keeps the legacy `org_unit_id` flow.

Out of scope

  • Backend OData parser improvements (track in a separate issue on `cyberfabric/insight` if option B is chosen).
  • The unrelated `Dev Time` semantics issue (focus hours/day vs time-per-task) — separate ticket.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions