Skip to content

Commit fa95c19

Browse files
committed
Add team activity stats: thread breakdown, responsiveness, community reach
The team activity dashboard only showed raw message/patch counts and required manually summing started+other threads to get an active-thread total, with no signal on how fast the team responds or whether it's engaging external contributors. Adds deduped active-thread and unique- contributor counts, a new/joined/continuing thread breakdown, first- response and backlog metrics, and community-reach counts via a new TeamActivityStats service, plus a persisted per-team week-start-day setting instead of a transient request param. Signed-off-by: Kai Wagner <kai.wagner@percona.com>
1 parent 39c0314 commit fa95c19

19 files changed

Lines changed: 637 additions & 19 deletions

app/assets/stylesheets/components/profile.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,61 @@
323323
justify-content: center;
324324
}
325325

326+
.team-stats {
327+
display: flex;
328+
flex-direction: column;
329+
gap: var(--spacing-4);
330+
margin-bottom: var(--spacing-4);
331+
}
332+
333+
.team-stats-group-title {
334+
font-size: var(--font-size-sm);
335+
font-weight: var(--font-weight-semibold);
336+
color: var(--color-text-primary);
337+
margin-bottom: var(--spacing-2);
338+
}
339+
340+
.team-stats-group-note {
341+
color: var(--color-text-muted);
342+
font-weight: var(--font-weight-normal);
343+
margin-left: var(--spacing-1);
344+
}
345+
346+
.team-stats-grid {
347+
display: grid;
348+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
349+
gap: var(--spacing-3);
350+
}
351+
352+
.team-stat-tile {
353+
padding: var(--spacing-3);
354+
background: var(--color-bg-card);
355+
border: var(--border-width) solid var(--color-border);
356+
border-radius: var(--border-radius-lg);
357+
}
358+
359+
.team-stat-value {
360+
font-size: var(--font-size-xl);
361+
font-weight: var(--font-weight-semibold);
362+
color: var(--color-text-primary);
363+
}
364+
365+
.team-stat-label {
366+
color: var(--color-text-muted);
367+
font-size: var(--font-size-sm);
368+
}
369+
370+
.team-stat-sub {
371+
color: var(--color-text-muted);
372+
font-size: var(--font-size-xs);
373+
}
374+
375+
@media (max-width: 720px) {
376+
.team-stats-grid {
377+
grid-template-columns: repeat(2, 1fr);
378+
}
379+
}
380+
326381
.activity-types {
327382
display: flex;
328383
flex-wrap: wrap;

app/assets/stylesheets/components/settings.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
.settings-page input[type="text"],
6969
.settings-page input[type="email"],
7070
.settings-page input[type="password"],
71-
.settings-page input[type="number"] {
71+
.settings-page input[type="number"],
72+
.settings-page select {
7273
width: 100%;
7374
padding: var(--spacing-4) var(--spacing-6);
7475
border-radius: 999px;

app/controllers/concerns/profile_activity.rb

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ def person_ids_for_query
2020
ids.is_a?(Set) ? ids.to_a : ids
2121
end
2222

23-
def load_activity_data(scope: nil, year: nil)
23+
def load_activity_data(scope: nil, year: nil, window_start: nil, window_end: nil)
2424
@week_start_day = parse_week_start_day
2525
@activity_filters = parse_activity_filters
2626
effective_scope = scope || default_recent_scope
27+
@activity_window_start = window_start || default_window_start
28+
@activity_window_end = window_end || Time.current
2729
@activity_entries = build_activity_entries(scope: effective_scope, filters: @activity_filters)
2830
@activity_summary = build_activity_summary(scope: effective_scope, filters: @activity_filters)
2931
@activity_period ||= { type: :recent } if scope.nil?
@@ -33,10 +35,13 @@ def load_activity_data(scope: nil, year: nil)
3335
@weekday_labels = WeekCalculation.weekday_labels(@week_start_day)
3436
end
3537

38+
def default_window_start
39+
1.month.ago.beginning_of_day
40+
end
41+
3642
def default_recent_scope
3743
ids = person_ids_for_query
38-
start_date = 1.month.ago.beginning_of_day
39-
Message.where(sender_person_id: ids, created_at: start_date..)
44+
Message.where(sender_person_id: ids, created_at: default_window_start..)
4045
end
4146

4247
def build_activity_entries(scope: nil, filters: nil)
@@ -99,17 +104,11 @@ def build_activity_summary(scope: nil, filters: nil)
99104

100105
filter_symbols = filters&.map(&:to_sym)&.to_set
101106

102-
summary = {
103-
total: 0,
104-
started_thread: 0,
105-
replied_own_thread: 0,
106-
replied_other_thread: 0,
107-
replied_other_topics: 0,
108-
sent_first_patch: 0,
109-
sent_followup_patch: 0
110-
}
107+
summary = empty_activity_summary
111108

112109
replied_other_topic_ids = Set.new
110+
active_thread_ids = Set.new
111+
contributor_ids = Set.new
113112

114113
messages.each do |message|
115114
topic = message.topic
@@ -129,10 +128,18 @@ def build_activity_summary(scope: nil, filters: nil)
129128
if activity_types.include?(:replied_other_thread)
130129
replied_other_topic_ids << topic.id
131130
end
131+
if activity_types.include?(:started_thread)
132+
key = message.is_patch_submission? ? :new_patch_series_count : :discussion_started_count
133+
summary[key] += 1
134+
end
135+
active_thread_ids << topic.id
136+
contributor_ids << message.sender_person_id
132137
end
133138
end
134139

135140
summary[:replied_other_topics] = replied_other_topic_ids.size
141+
summary[:active_thread_count] = active_thread_ids.size
142+
summary[:unique_contributor_count] = contributor_ids.size
136143
summary
137144
end
138145

@@ -143,6 +150,10 @@ def empty_activity_summary
143150
replied_own_thread: 0,
144151
replied_other_thread: 0,
145152
replied_other_topics: 0,
153+
active_thread_count: 0,
154+
unique_contributor_count: 0,
155+
discussion_started_count: 0,
156+
new_patch_series_count: 0,
146157
sent_first_patch: 0,
147158
sent_followup_patch: 0
148159
}

app/controllers/settings/teams_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def team_params
5757
end
5858

5959
def team_update_params
60-
params.require(:team).permit(:visibility)
60+
params.require(:team).permit(:visibility, :week_start_day)
6161
end
6262

6363
def require_team_admin!

app/controllers/teams_profile_controller.rb

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@ class TeamsProfileController < ApplicationController
77
def show
88
@members = @team.team_members.includes(user: { person: :default_alias }).order(:role, :created_at)
99
load_activity_data
10+
load_team_stats
1011
end
1112

1213
def contributions
1314
load_activity_data
15+
load_team_stats
1416
render :activity
1517
end
1618

1719
def daily_activity
1820
date = parse_activity_date
1921
@activity_period = { type: :day, date: date }
20-
load_activity_data(scope: messages_scope_for_date(date), year: date.year)
22+
load_activity_data(
23+
scope: messages_scope_for_date(date), year: date.year,
24+
window_start: date.beginning_of_day, window_end: date.end_of_day
25+
)
26+
load_team_stats
2127
render :activity
2228
end
2329

@@ -27,18 +33,26 @@ def monthly_activity
2733
start_date = Date.new(year, month, 1)
2834
end_date = start_date.end_of_month
2935
@activity_period = { type: :month, year: year, month: month }
30-
load_activity_data(scope: messages_scope_for_range(start_date, end_date), year: year)
36+
load_activity_data(
37+
scope: messages_scope_for_range(start_date, end_date), year: year,
38+
window_start: start_date.beginning_of_day, window_end: end_date.end_of_day
39+
)
40+
load_team_stats
3141
render :activity
3242
end
3343

3444
def weekly_activity
3545
year = params[:year].to_i
3646
week = params[:week].to_i
37-
wday_start = WeekCalculation.parse_week_start(params[:week_start])
47+
wday_start = parse_week_start_day
3848
start_date = WeekCalculation.week_start_date(year, week, wday_start)
3949
end_date = start_date + 6
4050
@activity_period = { type: :week, year: year, week: week, start_date: start_date, end_date: end_date }
41-
load_activity_data(scope: messages_scope_for_range(start_date, end_date), year: year)
51+
load_activity_data(
52+
scope: messages_scope_for_range(start_date, end_date), year: year,
53+
window_start: start_date.beginning_of_day, window_end: end_date.end_of_day
54+
)
55+
load_team_stats
4256
render :activity
4357
end
4458

@@ -48,6 +62,19 @@ def activity_person_ids
4862
@member_person_ids
4963
end
5064

65+
def parse_week_start_day
66+
return WeekCalculation.parse_week_start(params[:week_start]) if params[:week_start].present?
67+
@team.week_start_day
68+
end
69+
70+
def load_team_stats
71+
@team_stats = TeamActivityStats.new(
72+
team_person_ids: @member_person_ids,
73+
window_start: @activity_window_start,
74+
window_end: @activity_window_end
75+
).call
76+
end
77+
5178
def load_team
5279
@team = Team.find_by!(name: params[:name])
5380
@member_person_ids = @team.users.joins(:person).pluck("people.id").to_set

app/helpers/team_stats_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module TeamStatsHelper
2+
def format_duration_hours(hours)
3+
return "–" if hours.nil?
4+
hours < 24 ? "#{hours.round(1)}h" : "#{(hours / 24.0).round(1)}d"
5+
end
6+
end

app/models/team.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Team < ApplicationRecord
1313

1414
validates :name, presence: true
1515
validates :name, format: { with: /\A[a-zA-Z0-9_\-\.]+\z/ }
16+
validates :week_start_day, inclusion: { in: 0..6 }
1617
validate :name_available_in_reservations
1718

1819
after_create :reserve_name

0 commit comments

Comments
 (0)