🔗 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.
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
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
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
Out of scope