jirametrics 2.12pre9 → 2.12pre12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jirametrics/aging_work_table.rb +4 -0
- data/lib/jirametrics/change_item.rb +13 -3
- data/lib/jirametrics/cycletime_config.rb +1 -1
- data/lib/jirametrics/daily_view.rb +190 -0
- data/lib/jirametrics/examples/standard_project.rb +2 -0
- data/lib/jirametrics/file_system.rb +0 -2
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/index.css +43 -0
- data/lib/jirametrics/html_report_config.rb +1 -0
- data/lib/jirametrics/issue.rb +20 -15
- data/lib/jirametrics/jira_gateway.rb +4 -1
- data/lib/jirametrics/settings.json +2 -1
- data/lib/jirametrics.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c113cc459acf750ccafa11aa97aaeaa799f3a23b4867df0e21503af9fdb18038
|
4
|
+
data.tar.gz: 4a204e05b0e5c61d011c8005d5a82011ce280afa45e76cef5cea9844bde71915
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a1d16a963187788e906a6010642a4361e968ab127b12421a31753cf68f6399d99c418152e9ead9d4bd2887b40a6333f2a049fcd4c164734319d275f78d8e5e2
|
7
|
+
data.tar.gz: a376ba351aac4f70383a6401df5b48c8637a6b68cfe26b35a3eca2016abb7b7c3cea813c2b246b59574efc2b44a7da97bc05e7402e9250fe47027bcfd263bf52
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ChangeItem
|
4
|
-
attr_reader :field, :value_id, :old_value_id, :raw, :
|
4
|
+
attr_reader :field, :value_id, :old_value_id, :raw, :time
|
5
5
|
attr_accessor :value, :old_value
|
6
6
|
|
7
|
-
def initialize raw:,
|
7
|
+
def initialize raw:, author_raw:, time:, artificial: false
|
8
8
|
@raw = raw
|
9
|
+
@author_raw = author_raw
|
9
10
|
@time = time
|
10
11
|
raise 'ChangeItem.new() time cannot be nil' if time.nil?
|
11
12
|
raise "Time must be an object of type Time in the correct timezone: #{@time.inspect}" unless @time.is_a? Time
|
@@ -16,7 +17,14 @@ class ChangeItem
|
|
16
17
|
@old_value = @raw['fromString']
|
17
18
|
@old_value_id = @raw['from']&.to_i
|
18
19
|
@artificial = artificial
|
19
|
-
|
20
|
+
end
|
21
|
+
|
22
|
+
def author
|
23
|
+
@author_raw&.[]('displayName') || @author_raw&.[]('name') || 'Unknown author'
|
24
|
+
end
|
25
|
+
|
26
|
+
def author_icon_url
|
27
|
+
@author_raw&.[]('avatarUrls')&.[]('16x16')
|
20
28
|
end
|
21
29
|
|
22
30
|
def status? = (field == 'status')
|
@@ -35,6 +43,8 @@ class ChangeItem
|
|
35
43
|
|
36
44
|
def labels? = (field == 'labels')
|
37
45
|
|
46
|
+
def comment? = (field == 'comment')
|
47
|
+
|
38
48
|
# An alias for time so that logic accepting a Time, Date, or ChangeItem can all respond to :to_time
|
39
49
|
def to_time = @time
|
40
50
|
|
@@ -59,7 +59,7 @@ class CycleTimeConfig
|
|
59
59
|
'from' => '0',
|
60
60
|
'fromString' => ''
|
61
61
|
}
|
62
|
-
ChangeItem.new raw: raw, time: time,
|
62
|
+
ChangeItem.new raw: raw, time: time, artificial: true, author_raw: nil
|
63
63
|
end
|
64
64
|
|
65
65
|
def started_stopped_changes issue
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DailyView < ChartBase
|
4
|
+
attr_accessor :possible_statuses
|
5
|
+
|
6
|
+
def initialize _block
|
7
|
+
super()
|
8
|
+
|
9
|
+
header_text 'Daily View'
|
10
|
+
description_text <<-HTML
|
11
|
+
<div class="p">
|
12
|
+
This view shows all the items you'll want to discuss during your daily coordination meeting
|
13
|
+
(aka daily scrum, standup), in the order that you should be discussing them. The most important
|
14
|
+
items are at the top, and the least at the bottom.
|
15
|
+
</div>
|
16
|
+
<div class="p">
|
17
|
+
By default, we sort by priority first and then by age within each of those priorities.
|
18
|
+
Hover over the issue to make it stand out more.
|
19
|
+
</div>
|
20
|
+
HTML
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
aging_issues = select_aging_issues
|
25
|
+
|
26
|
+
return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
|
27
|
+
|
28
|
+
result = +''
|
29
|
+
result << render_top_text(binding)
|
30
|
+
aging_issues.each do |issue|
|
31
|
+
result << render_issue(issue)
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def select_aging_issues
|
37
|
+
aging_issues = issues.select do |issue|
|
38
|
+
started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
|
39
|
+
started_at && !stopped_at
|
40
|
+
end
|
41
|
+
|
42
|
+
today = date_range.end
|
43
|
+
aging_issues.collect do |issue|
|
44
|
+
[issue, issue.priority_name, issue.board.cycletime.age(issue, today: today)]
|
45
|
+
end.sort(&issue_sorter).collect(&:first)
|
46
|
+
end
|
47
|
+
|
48
|
+
def issue_sorter
|
49
|
+
priority_names = settings['priority_order']
|
50
|
+
lambda do |a, b|
|
51
|
+
a_issue, a_priority, a_age = *a
|
52
|
+
b_issue, b_priority, b_age = *b
|
53
|
+
|
54
|
+
a_priority_index = priority_names.index(a_priority)
|
55
|
+
b_priority_index = priority_names.index(b_priority)
|
56
|
+
|
57
|
+
if a_priority_index.nil? && b_priority_index.nil?
|
58
|
+
result = a_priority <=> b_priority
|
59
|
+
elsif a_priority_index.nil?
|
60
|
+
result = 1
|
61
|
+
elsif b_priority_index.nil?
|
62
|
+
result = -1
|
63
|
+
else
|
64
|
+
result = b_priority_index <=> a_priority_index
|
65
|
+
end
|
66
|
+
|
67
|
+
result = b_age <=> a_age if result.zero?
|
68
|
+
result = a_issue <=> b_issue if result.zero?
|
69
|
+
result
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def make_blocked_stalled_lines issue
|
74
|
+
today = date_range.end
|
75
|
+
|
76
|
+
blocked_stalled = issue.blocked_stalled_by_date(
|
77
|
+
date_range: today..today, chart_end_time: time_range.end, settings: settings
|
78
|
+
)[today]
|
79
|
+
|
80
|
+
lines = []
|
81
|
+
if blocked_stalled.blocked?
|
82
|
+
marker = color_block '--blocked-color'
|
83
|
+
lines << ["#{marker} Blocked by flag"] if blocked_stalled.flag
|
84
|
+
lines << ["#{marker} Blocked by status: #{blocked_stalled.status}"] if blocked_stalled.blocked_by_status?
|
85
|
+
blocked_stalled.blocking_issue_keys&.each do |key|
|
86
|
+
lines << ["#{marker} Blocked by issue: #{key}"]
|
87
|
+
blocking_issue = issues.find { |i| i.key == key }
|
88
|
+
lines << blocking_issue if blocking_issue
|
89
|
+
end
|
90
|
+
elsif blocked_stalled.stalled_by_status?
|
91
|
+
lines << ["#{color_block '--stalled-color'} Stalled by status: #{blocked_stalled.status}"]
|
92
|
+
else
|
93
|
+
lines << ["#{color_block '--stalled-color'} Stalled by inactivity: #{blocked_stalled.stalled_days} days"]
|
94
|
+
end
|
95
|
+
lines
|
96
|
+
end
|
97
|
+
|
98
|
+
def make_stats_lines issue
|
99
|
+
lines = []
|
100
|
+
|
101
|
+
title_line = +''
|
102
|
+
title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
|
103
|
+
title_line << "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
104
|
+
title_line << "<b><a href='#{issue.url}'>#{issue.key}</a></b> <i>#{issue.summary}</i>"
|
105
|
+
lines << [title_line]
|
106
|
+
|
107
|
+
chunks = []
|
108
|
+
|
109
|
+
chunks << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
|
110
|
+
|
111
|
+
age = issue.board.cycletime.age(issue, today: date_range.end)
|
112
|
+
chunks << "Age: <b>#{label_days age}</b>" if age
|
113
|
+
|
114
|
+
chunks << "Status: <b>#{issue.status.name}</b>"
|
115
|
+
|
116
|
+
column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
|
117
|
+
chunks << "Column: <b>#{column&.name || '(not visible on board)'}</b>"
|
118
|
+
|
119
|
+
if issue.assigned_to
|
120
|
+
chunks << "Who: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
|
121
|
+
end
|
122
|
+
|
123
|
+
chunks << "Due: <b>#{issue.due_date}</b>" if issue.due_date
|
124
|
+
lines << chunks
|
125
|
+
|
126
|
+
lines
|
127
|
+
end
|
128
|
+
|
129
|
+
def make_child_lines issue
|
130
|
+
lines = []
|
131
|
+
subtasks = issue.subtasks.reject { |i| i.done? }
|
132
|
+
|
133
|
+
unless subtasks.empty?
|
134
|
+
icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
|
135
|
+
lines << (icon_urls << 'Incomplete child issues')
|
136
|
+
lines += subtasks
|
137
|
+
end
|
138
|
+
lines
|
139
|
+
end
|
140
|
+
|
141
|
+
def jira_rich_text_to_html text
|
142
|
+
text.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>')
|
143
|
+
end
|
144
|
+
|
145
|
+
def make_comment_lines issue
|
146
|
+
comments = issue.changes.select { |i| i.comment? }.reverse
|
147
|
+
lines = []
|
148
|
+
return lines if comments.empty?
|
149
|
+
|
150
|
+
lines << ['Comments']
|
151
|
+
comments.each do |c|
|
152
|
+
text = jira_rich_text_to_html c.value
|
153
|
+
time = c.time.strftime '%b %d, %I:%M%P'
|
154
|
+
|
155
|
+
lines << [
|
156
|
+
"<div class='comment'>" \
|
157
|
+
"<img src='#{c.author_icon_url}' class='icon' title='#{c.author}' /> " \
|
158
|
+
"<span class='header' title='Timestamp: #{c.time}'>#{time}</span> " \
|
159
|
+
" → #{text}</div>"
|
160
|
+
]
|
161
|
+
end
|
162
|
+
lines
|
163
|
+
end
|
164
|
+
|
165
|
+
def assemble_issue_lines issue
|
166
|
+
lines = []
|
167
|
+
lines += make_stats_lines(issue)
|
168
|
+
lines += make_blocked_stalled_lines(issue)
|
169
|
+
lines += make_child_lines(issue)
|
170
|
+
lines += make_comment_lines(issue)
|
171
|
+
lines
|
172
|
+
end
|
173
|
+
|
174
|
+
def render_issue issue, css_class: 'daily_issue'
|
175
|
+
result = +''
|
176
|
+
result << "<div class='#{css_class}'>"
|
177
|
+
assemble_issue_lines(issue).each do |row|
|
178
|
+
if row.is_a? Issue
|
179
|
+
result << render_issue(row, css_class: 'child_issue')
|
180
|
+
else
|
181
|
+
result << '<div class="heading">'
|
182
|
+
row.each do |chunk|
|
183
|
+
result << "<div>#{chunk}</div>"
|
184
|
+
end
|
185
|
+
result << '</div>'
|
186
|
+
end
|
187
|
+
end
|
188
|
+
result << '</div>'
|
189
|
+
end
|
190
|
+
end
|
@@ -52,8 +52,6 @@ class FileSystem
|
|
52
52
|
# In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
|
53
53
|
# cases where this simple compression will drop the filesize by half.
|
54
54
|
def compress node
|
55
|
-
return node
|
56
|
-
|
57
55
|
if node.is_a? Hash
|
58
56
|
node.reject! { |_key, value| value.nil? || (value.is_a?(Array) && value.empty?) }
|
59
57
|
node.each_value { |value| compress value }
|
@@ -4,6 +4,7 @@
|
|
4
4
|
<th title="Age in days">Age</th>
|
5
5
|
<th title="Expedited">E</th>
|
6
6
|
<th title="Blocked / Stalled">B/S</th>
|
7
|
+
<th title="Priority">P</th>
|
7
8
|
<th>Issue</th>
|
8
9
|
<th>Status</th>
|
9
10
|
<th>Forecast</th>
|
@@ -29,6 +30,7 @@
|
|
29
30
|
<td style="text-align: right;"><%= issue_age || 'Not started' %></td>
|
30
31
|
<td><%= expedited_text(issue) %></td>
|
31
32
|
<td><%= blocked_text(issue) %></td>
|
33
|
+
<td><%= priority_text(issue) %></td>
|
32
34
|
<td>
|
33
35
|
<% parent_hierarchy(issue).each_with_index do |parent, index| %>
|
34
36
|
<% color = parent != issue ? "var(--hierarchy-table-inactive-item-text-color)" : 'var(--default-text-color)' %>
|
@@ -67,6 +67,9 @@
|
|
67
67
|
--sprint-burndown-sprint-color-4: red;
|
68
68
|
--sprint-burndown-sprint-color-5: brown;
|
69
69
|
|
70
|
+
--daily-view-selected-issue-background: lightgray;
|
71
|
+
--daily-view-issue-border: green;
|
72
|
+
--daily-view-selected-issue-border: red;
|
70
73
|
|
71
74
|
}
|
72
75
|
|
@@ -142,6 +145,44 @@ ul.quality_report {
|
|
142
145
|
border-top: 1px solid gray;
|
143
146
|
}
|
144
147
|
|
148
|
+
div.daily_issue:hover {
|
149
|
+
background: var(--daily-view-selected-issue-background);
|
150
|
+
border-color: var(--daily-view-selected-issue-border);
|
151
|
+
}
|
152
|
+
|
153
|
+
div.daily_issue {
|
154
|
+
border: 1px solid var(--daily-view-issue-border);
|
155
|
+
padding: 0.5em;
|
156
|
+
.heading {
|
157
|
+
vertical-align: middle;
|
158
|
+
display: flex;
|
159
|
+
gap: 0.5em;
|
160
|
+
align-items: center;
|
161
|
+
}
|
162
|
+
.comment {
|
163
|
+
margin-left: 1em;
|
164
|
+
padding-left: 2em;
|
165
|
+
text-indent: -2em;
|
166
|
+
.time {
|
167
|
+
font-size: 0.8em;
|
168
|
+
}
|
169
|
+
}
|
170
|
+
.icon {
|
171
|
+
width: 1em;
|
172
|
+
height: 1em;
|
173
|
+
}
|
174
|
+
margin-bottom: 0.5em;
|
175
|
+
}
|
176
|
+
div.child_issue:hover {
|
177
|
+
background: var(--body-background);
|
178
|
+
}
|
179
|
+
div.child_issue {
|
180
|
+
border: 1px dashed green;
|
181
|
+
margin: 0.2em;
|
182
|
+
margin-left: 1.5em;
|
183
|
+
padding: 0.5em;
|
184
|
+
}
|
185
|
+
|
145
186
|
@media screen and (prefers-color-scheme: dark) {
|
146
187
|
:root {
|
147
188
|
--warning-banner: #9F2B00;
|
@@ -174,6 +215,8 @@ ul.quality_report {
|
|
174
215
|
--wip-chart-duration-two-weeks-or-less-color: #cf9400;
|
175
216
|
--wip-chart-duration-four-weeks-or-less-color: #c25e00;
|
176
217
|
--wip-chart-duration-more-than-four-weeks-color: #8e0000;
|
218
|
+
|
219
|
+
--daily-view-selected-issue-background: #474747;
|
177
220
|
}
|
178
221
|
|
179
222
|
h1 {
|
@@ -33,6 +33,7 @@ class HtmlReportConfig
|
|
33
33
|
define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
|
34
34
|
define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
|
35
35
|
define_chart name: 'flow_efficiency_scatterplot', classname: 'FlowEfficiencyScatterplot'
|
36
|
+
define_chart name: 'daily_view', classname: 'DailyView'
|
36
37
|
|
37
38
|
define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
|
38
39
|
deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -44,9 +44,11 @@ class Issue
|
|
44
44
|
def key = @raw['key']
|
45
45
|
|
46
46
|
def type = @raw['fields']['issuetype']['name']
|
47
|
-
|
48
47
|
def type_icon_url = @raw['fields']['issuetype']['iconUrl']
|
49
48
|
|
49
|
+
def priority_name = @raw['fields']['priority']['name']
|
50
|
+
def priority_url = @raw['fields']['priority']['iconUrl']
|
51
|
+
|
50
52
|
def summary = @raw['fields']['summary']
|
51
53
|
|
52
54
|
def labels = @raw['fields']['labels'] || []
|
@@ -205,6 +207,10 @@ class Issue
|
|
205
207
|
nil
|
206
208
|
end
|
207
209
|
|
210
|
+
def first_time_visible_on_board
|
211
|
+
first_time_in_status(*board.visible_columns.collect(&:status_ids).flatten)
|
212
|
+
end
|
213
|
+
|
208
214
|
def parse_time text
|
209
215
|
Time.parse(text).getlocal(@timezone_offset)
|
210
216
|
end
|
@@ -230,6 +236,10 @@ class Issue
|
|
230
236
|
@raw['fields']&.[]('assignee')&.[]('displayName')
|
231
237
|
end
|
232
238
|
|
239
|
+
def assigned_to_icon_url
|
240
|
+
@raw['fields']&.[]('assignee')&.[]('avatarUrls')&.[]('16x16')
|
241
|
+
end
|
242
|
+
|
233
243
|
# Many test failures are simply unreadable because the default inspect on this class goes
|
234
244
|
# on for pages. Shorten it up.
|
235
245
|
def inspect
|
@@ -315,7 +325,7 @@ class Issue
|
|
315
325
|
|
316
326
|
# This mock change is to force the writing of one last entry at the end of the time range.
|
317
327
|
# By doing this, we're able to eliminate a lot of duplicated code in charts.
|
318
|
-
mock_change = ChangeItem.new time: end_time,
|
328
|
+
mock_change = ChangeItem.new time: end_time, artificial: true, raw: { 'field' => '' }, author_raw: nil
|
319
329
|
|
320
330
|
(changes + [mock_change]).each do |change|
|
321
331
|
previous_was_active = false if check_for_stalled(
|
@@ -580,7 +590,7 @@ class Issue
|
|
580
590
|
/(?<project_code1>[^-]+)-(?<id1>.+)/ =~ key
|
581
591
|
/(?<project_code2>[^-]+)-(?<id2>.+)/ =~ other.key
|
582
592
|
comparison = project_code1 <=> project_code2
|
583
|
-
comparison = id1 <=> id2 if comparison.zero?
|
593
|
+
comparison = id1.to_i <=> id2.to_i if comparison.zero?
|
584
594
|
comparison
|
585
595
|
end
|
586
596
|
|
@@ -687,32 +697,25 @@ class Issue
|
|
687
697
|
|
688
698
|
private
|
689
699
|
|
690
|
-
def assemble_author raw
|
691
|
-
raw['author']&.[]('displayName') || raw['author']&.[]('name') || 'Unknown author'
|
692
|
-
end
|
693
|
-
|
694
700
|
def load_history_into_changes
|
695
701
|
@raw['changelog']['histories']&.each do |history|
|
696
702
|
created = parse_time(history['created'])
|
697
703
|
|
698
|
-
# It should be impossible to not have an author but we've seen it in production
|
699
|
-
author = assemble_author history
|
700
704
|
history['items']&.each do |item|
|
701
|
-
@changes << ChangeItem.new(raw: item, time: created,
|
705
|
+
@changes << ChangeItem.new(raw: item, time: created, author_raw: history['author'])
|
702
706
|
end
|
703
707
|
end
|
704
708
|
end
|
705
709
|
|
706
710
|
def load_comments_into_changes
|
707
711
|
@raw['fields']['comment']['comments']&.each do |comment|
|
708
|
-
raw = {
|
712
|
+
raw = comment.merge({
|
709
713
|
'field' => 'comment',
|
710
714
|
'to' => comment['id'],
|
711
715
|
'toString' => comment['body']
|
712
|
-
}
|
713
|
-
author = assemble_author comment
|
716
|
+
})
|
714
717
|
created = parse_time(comment['created'])
|
715
|
-
@changes << ChangeItem.new(raw: raw, time: created,
|
718
|
+
@changes << ChangeItem.new(raw: raw, time: created, artificial: true, author_raw: comment['author'])
|
716
719
|
end
|
717
720
|
end
|
718
721
|
|
@@ -754,7 +757,9 @@ class Issue
|
|
754
757
|
first_status = first_change.old_value
|
755
758
|
first_status_id = first_change.old_value_id
|
756
759
|
end
|
757
|
-
|
760
|
+
|
761
|
+
creator = raw['fields']['creator']
|
762
|
+
ChangeItem.new time: created_time, artificial: true, author_raw: creator, raw: {
|
758
763
|
'field' => field_name,
|
759
764
|
'to' => first_status_id,
|
760
765
|
'toString' => first_status
|
@@ -26,7 +26,10 @@ class JiraGateway
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call_command command
|
29
|
-
|
29
|
+
log_entry = " #{command.gsub(/\s+/, ' ')}"
|
30
|
+
log_entry = log_entry.gsub(@jira_api_token, '[API_TOKEN]') if @jira_api_token
|
31
|
+
@file_system.log log_entry
|
32
|
+
|
30
33
|
result = `#{command}`
|
31
34
|
@file_system.log result unless $CHILD_STATUS.success?
|
32
35
|
return result if $CHILD_STATUS.success?
|
data/lib/jirametrics.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jirametrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.12pre12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bowler
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-06-26 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: random-word
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- lib/jirametrics/cycletime_config.rb
|
78
78
|
- lib/jirametrics/cycletime_histogram.rb
|
79
79
|
- lib/jirametrics/cycletime_scatterplot.rb
|
80
|
+
- lib/jirametrics/daily_view.rb
|
80
81
|
- lib/jirametrics/daily_wip_by_age_chart.rb
|
81
82
|
- lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
|
82
83
|
- lib/jirametrics/daily_wip_by_parent_chart.rb
|