Almirah 0.4.3 → 0.4.5

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.
@@ -147,163 +147,6 @@ table.controlled td.item_text{
147
147
  max-width: 100%;
148
148
  max-height: 300px;
149
149
  }
150
- .workitem_gantt {
151
- --gantt-owner-width: 120px;
152
- --gantt-day-width: 26px;
153
- --gantt-month-h: 18px;
154
- overflow: auto;
155
- max-height: 420px;
156
- margin-bottom: 16px;
157
- border: 1px solid #e1e4e8;
158
- }
159
- .workitem_gantt .gantt_grid {
160
- display: grid;
161
- gap: 0;
162
- width: max-content;
163
- padding: 4px;
164
- }
165
- .workitem_gantt .gantt_corner,
166
- .workitem_gantt .gantt_owner,
167
- .workitem_gantt .gantt_owner_tracking,
168
- .workitem_gantt .gantt_band_corner,
169
- .workitem_gantt .gantt_buffer {
170
- position: sticky;
171
- left: 0;
172
- background: #fff;
173
- padding: 2px 8px;
174
- font-weight: 600;
175
- white-space: nowrap;
176
- border-right: 1px solid #e1e4e8;
177
- z-index: 2;
178
- }
179
- .workitem_gantt .gantt_corner {
180
- top: 0;
181
- border-bottom: 1px solid #bbb;
182
- z-index: 3;
183
- }
184
- .workitem_gantt .gantt_month_head {
185
- position: sticky;
186
- top: 0;
187
- background: #fff;
188
- text-align: center;
189
- font-size: 11px;
190
- font-weight: 600;
191
- color: #57606a;
192
- border-bottom: 1px solid #e1e4e8;
193
- border-left: 1px solid #d0d7de;
194
- white-space: nowrap;
195
- overflow: hidden;
196
- z-index: 1;
197
- }
198
- .workitem_gantt .gantt_day_head {
199
- position: sticky;
200
- top: var(--gantt-month-h);
201
- background: #fff;
202
- text-align: center;
203
- font-size: 11px;
204
- color: #57606a;
205
- border-bottom: 1px solid #bbb;
206
- z-index: 1;
207
- }
208
- /* Weekday-holiday highlight in the Chart.js palette red (ADR-205 amendment):
209
- the column carries the buffer bar's diagonal stripe texture, in red; the
210
- header stays a solid tint so the day number reads. */
211
- .workitem_gantt .gantt_day_head.gantt_nonworking { background: rgba(255, 99, 132, 0.22); color: #b23a52; }
212
- .workitem_gantt .gantt_day_head.gantt_friday { background: rgb(225, 241, 250); }
213
- .workitem_gantt .gantt_nonworking_col {
214
- background: repeating-linear-gradient(45deg,
215
- rgba(255, 99, 132, 0.20), rgba(255, 99, 132, 0.20) 5px,
216
- rgba(255, 99, 132, 0.07) 5px, rgba(255, 99, 132, 0.07) 10px);
217
- }
218
- .workitem_gantt .gantt_friday_col { background: rgb(225, 241, 250); }
219
- .workitem_gantt .gantt_bar,
220
- .workitem_gantt .gantt_track_bar {
221
- font-size: 11px;
222
- line-height: 1.5;
223
- margin: 1px;
224
- padding: 1px 5px;
225
- border-radius: 3px;
226
- overflow: hidden;
227
- white-space: nowrap;
228
- text-overflow: ellipsis;
229
- color: #fff;
230
- cursor: default;
231
- }
232
- .workitem_gantt .gantt_todo { background: #9aa5b1; }
233
- .workitem_gantt .gantt_inprogress { background: #ffffdd; color: #333; box-shadow: inset 0 0 0 1px #bbb; }
234
- .workitem_gantt .gantt_done { background: #cfc; color: #57606a; box-shadow: inset 0 0 0 1px #e1e4e8; }
235
- .workitem_gantt .gantt_blocked { box-shadow: inset 0 0 0 1px #bbb; }
236
- .workitem_gantt .gantt_critical { outline: 1px solid #9a6700; outline-offset: -1px; } /* buffer-bar amber (dark) */
237
- .workitem_gantt .gantt_release_band {
238
- text-align: center;
239
- font-size: 11px;
240
- font-weight: 600;
241
- color: #57606a;
242
- background: #eef1f4;
243
- border-bottom: 1px solid #e1e4e8;
244
- border-left: 1px solid #d0d7de;
245
- padding: 2px 4px;
246
- white-space: nowrap;
247
- overflow: hidden;
248
- text-overflow: ellipsis;
249
- }
250
- .workitem_gantt .gantt_buffer { color: #9a6700; }
251
- .critical_chain { margin-bottom: 16px; }
252
- .critical_chain h2 { font-size: 1.1em; margin-bottom: 6px; }
253
- .critical_chain .cc_group { margin: 8px 0; }
254
- .critical_chain .cc_group h3 { font-size: 0.95em; margin: 4px 0; }
255
- .critical_chain table.cc_chain { border-collapse: collapse; margin: 4px 0; }
256
- .critical_chain table.cc_chain th,
257
- .critical_chain table.cc_chain td { border: 1px solid #e1e4e8; padding: 2px 8px; text-align: left; font-size: 12px; }
258
- .critical_chain .cc_buffer,
259
- .critical_chain .cc_consumed,
260
- .critical_chain .cc_projected,
261
- .critical_chain .cc_finish { margin: 2px 0; font-size: 12px; }
262
- .critical_chain .cc_consumed { color: #57606a; }
263
- .critical_chain .cc_finish { font-weight: 600; }
264
- .critical_chain .cc_unestimated { color: #9a6700; font-style: italic; font-size: 12px; }
265
- .critical_chain .cc_group_body { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start; }
266
- .critical_chain .cc_plan { flex: 1 1 320px; }
267
- .critical_chain .cc_fever { flex: 1 1 360px; max-width: 480px; height: 320px; position: relative; }
268
- .critical_chain .cc_fever .fever_canvas { width: 100%; height: 100%; }
269
- .workitem_gantt .gantt_buffer_bar {
270
- font-size: 11px;
271
- line-height: 1.5;
272
- margin: 1px;
273
- padding: 1px 5px;
274
- border-radius: 3px;
275
- text-align: center;
276
- overflow: hidden;
277
- white-space: nowrap;
278
- text-overflow: ellipsis;
279
- color: #9a6700;
280
- background: repeating-linear-gradient(45deg, #ffe9b3, #ffe9b3 5px, #fff3d4 5px, #fff3d4 10px);
281
- box-shadow: inset 0 0 0 1px #e0c068;
282
- cursor: default;
283
- }
284
- .workitem_gantt .gantt_today { border-left: 2px solid #d33; z-index: 5; pointer-events: none; }
285
- /* Plan-vs-actual tracking lanes (ADR-213): hidden until the toolbar toggle adds
286
- show-actuals; a hidden lane's grid tracks collapse to zero height. */
287
- .workitem_gantt .gantt_tracking_lane { display: none; }
288
- .workitem_gantt.show-actuals .gantt_tracking_lane { display: block; }
289
- .workitem_gantt .gantt_owner_tracking { font-weight: 400; font-style: italic; color: #57606a; }
290
- .workitem_gantt .gantt_track_committed { background: #b0b6bd; }
291
- .workitem_gantt .gantt_track_logged { background: #36a2eb; }
292
- .gantt_container .gantt_toolbar {
293
- display: flex;
294
- justify-content: flex-end;
295
- gap: 6px;
296
- padding: 4px 0;
297
- }
298
- .gantt_container .gantt_actuals_toggle {
299
- font-size: 12px;
300
- padding: 3px 10px;
301
- border: 1px solid #d0d7de;
302
- border-radius: 4px;
303
- background: #f6f8fa;
304
- cursor: pointer;
305
- }
306
- .gantt_container .gantt_actuals_toggle:hover { background: #eef1f4; }
307
150
  table.controlled:not(.odd-even) tbody tr:nth-child(odd) { background-color:#f6f7f8; }
308
151
  table.controlled:not(.odd-even) tbody tr:nth-child(even) { background-color: #fff; }
309
152
  table.controlled:not(.odd-even) tbody tr:nth-child(odd):hover,
@@ -505,4 +348,16 @@ img{
505
348
  padding: 5px;
506
349
  text-align:center;
507
350
  background:#fff;
508
- }
351
+ }/* Risk register RPN threshold bands (ADR-217): acceptable / ALARP caution / unacceptable */
352
+ td.item_rpn {
353
+ text-align: center;
354
+ }
355
+ td.rpn_acceptable {
356
+ background-color: #d5f5e3;
357
+ }
358
+ td.rpn_caution {
359
+ background-color: #fdebd0;
360
+ }
361
+ td.rpn_unacceptable {
362
+ background-color: #fadbd8;
363
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Almirah
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleksandr Ivanov
@@ -77,19 +77,20 @@ files:
77
77
  - lib/almirah/doc_items/source_code_paragraph.rb
78
78
  - lib/almirah/doc_items/text_line.rb
79
79
  - lib/almirah/doc_items/todo_block.rb
80
- - lib/almirah/doc_items/work_item.rb
81
80
  - lib/almirah/doc_parser.rb
82
81
  - lib/almirah/doc_types/base_document.rb
83
82
  - lib/almirah/doc_types/coverage.rb
84
- - lib/almirah/doc_types/critical_chain_page.rb
85
83
  - lib/almirah/doc_types/decision.rb
86
- - lib/almirah/doc_types/decision_grouping.rb
87
84
  - lib/almirah/doc_types/decisions_overview.rb
88
85
  - lib/almirah/doc_types/implementation.rb
89
86
  - lib/almirah/doc_types/index.rb
90
87
  - lib/almirah/doc_types/persistent_document.rb
91
88
  - lib/almirah/doc_types/planning_dates.rb
92
89
  - lib/almirah/doc_types/protocol.rb
90
+ - lib/almirah/doc_types/risk_record.rb
91
+ - lib/almirah/doc_types/risk_registry_page.rb
92
+ - lib/almirah/doc_types/risks_overview.rb
93
+ - lib/almirah/doc_types/rpn_rendering.rb
93
94
  - lib/almirah/doc_types/source_file.rb
94
95
  - lib/almirah/doc_types/specification.rb
95
96
  - lib/almirah/doc_types/traceability.rb
@@ -99,12 +100,8 @@ files:
99
100
  - lib/almirah/link_registry.rb
100
101
  - lib/almirah/navigation_pane.rb
101
102
  - lib/almirah/project.rb
102
- - lib/almirah/project/critical_chain.rb
103
103
  - lib/almirah/project/doc_linker.rb
104
- - lib/almirah/project/fever_chart.rb
105
104
  - lib/almirah/project/project_data.rb
106
- - lib/almirah/project/work_item_scheduler.rb
107
- - lib/almirah/project/working_calendar.rb
108
105
  - lib/almirah/project_configuration.rb
109
106
  - lib/almirah/project_template.rb
110
107
  - lib/almirah/project_utility.rb
@@ -134,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
131
  - !ruby/object:Gem::Version
135
132
  version: '0'
136
133
  requirements: []
137
- rubygems_version: 4.0.6
134
+ rubygems_version: 4.0.1
138
135
  specification_version: 4
139
136
  summary: Almirah
140
137
  test_files: []
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A single Scope-table row of a Decision Record, modelled as a node in the
4
- # per-row dependency network (ADR-194). Each work item carries its canonical
5
- # identity (`<record>.<step>.<activity>`), the bounded per-row Status it reads
6
- # its readiness from (ADR-193), and predecessor / successor edges spanning both
7
- # intra-record (step order) and cross-record (Depends On) links.
8
- #
9
- # `predecessors` / `successors` follow the house list-of-single-key-hashes shape
10
- # (ADR-197): each entry maps a readable `record.step.activity` label to the
11
- # resolved WorkItem. Every cross-record edge additionally records whether it
12
- # stays within a decision_groups folder (in-group) or crosses to another
13
- # (cross-group); that tag is inert here and consumed by the critical-chain step
14
- # (ADR-195). Readiness ignores it — a cross-group predecessor blocks exactly
15
- # like an in-group one.
16
- class WorkItem
17
- # Canonical phase order used only as the fallback for activity-type-aligned
18
- # resolution when the prerequisite has no row of the dependent's exact Item.
19
- ACTIVITY_ORDER = %w[Analysis Requirements Code Tests].freeze
20
-
21
- attr_reader :record_id, :step, :activity, :owner, :status, :depends_on_refs,
22
- :focused_estimate, :safe_estimate, :record_sequence, :start_date, :target_date
23
- attr_accessor :predecessors, :successors, :cross_group_predecessor_labels, :resolved_dependencies
24
-
25
- def initialize(record_id:, step:, activity:, owner:, status:, depends_on_refs:, # rubocop:disable Metrics/ParameterLists
26
- focused_estimate: 0.0, safe_estimate: 0.0, record_sequence: 0,
27
- start_date: nil, target_date: nil)
28
- @record_id = record_id
29
- @step = step
30
- @activity = activity.to_s.strip
31
- @owner = owner.to_s.strip
32
- @status = status.to_s.strip
33
- @depends_on_refs = depends_on_refs # array of record ref strings, e.g. ["ADR-193"]
34
- # The row's authored committed window (ADR-213): the Scope Start Date and
35
- # Target Date as parsed Dates (nil when absent/unparseable). Distinct from the
36
- # computed schedule -- these are the author's intent, drawn on the tracking lane.
37
- @start_date = start_date
38
- @target_date = target_date
39
- # Per-row scheduling estimates in working days (ADR-195); the focused estimate
40
- # is the scheduling duration, safe - focused the safety aggregated into the
41
- # project buffer. record_sequence is the owning record's number, used only as
42
- # a deterministic priority tiebreak in the critical-chain scheduler.
43
- @focused_estimate = focused_estimate
44
- @safe_estimate = safe_estimate
45
- @record_sequence = record_sequence
46
- @predecessors = []
47
- @successors = []
48
- @cross_group_predecessor_labels = []
49
- # One entry per resolved Depends On reference, keyed by the authored ref:
50
- # { ref => { doc:, anchor:, label: } }. `anchor` is the target's resolved
51
- # work-item row anchor (nil when the target has no `#` step column, so the
52
- # link opens the record page); `label` is the resolved work item's id, for
53
- # the link tooltip. Used to render the Depends On cell as deep links.
54
- @resolved_dependencies = {}
55
- end
56
-
57
- def id
58
- "#{@record_id}.#{@step}.#{@activity}"
59
- end
60
-
61
- # The anchor of this work item's row in its rendered Scope table — the deep-link
62
- # target a dependent record points at. Namespaced with `.scope.` so it never
63
- # collides with the Affected Documents controlled table, whose rows anchor on
64
- # the same `<record>.<step>` scheme on the same page.
65
- def row_anchor
66
- "#{@record_id}.scope.#{@step}"
67
- end
68
-
69
- def add_resolved_dependency(ref, doc, anchor, label)
70
- @resolved_dependencies[ref] = { doc: doc, anchor: anchor, label: label }
71
- end
72
-
73
- def activity_rank
74
- ACTIVITY_ORDER.index(@activity) || ACTIVITY_ORDER.length
75
- end
76
-
77
- # A row is "started" once it leaves To Do; both In-Progress and Done count, so
78
- # a Done row whose predecessor never finished is still flagged as a violation.
79
- def started?
80
- @status == 'In-Progress' || @status == 'Done'
81
- end
82
-
83
- def done?
84
- @status == 'Done'
85
- end
86
-
87
- def add_predecessor(work_item, cross_group:)
88
- @predecessors << { work_item.id => work_item }
89
- @cross_group_predecessor_labels << work_item.id if cross_group
90
- end
91
-
92
- def add_successor(work_item)
93
- @successors << { work_item.id => work_item }
94
- end
95
-
96
- def predecessor_items
97
- @predecessors.map { |edge| edge.values.first }
98
- end
99
-
100
- def successor_items
101
- @successors.map { |edge| edge.values.first }
102
- end
103
-
104
- # Same-record, lower-numbered steps — the intra-record (phase-order) edges.
105
- def intra_record_predecessors
106
- predecessor_items.select { |p| p.record_id == @record_id }
107
- end
108
-
109
- # Resolved Depends On edges into other records (in-group or cross-group alike).
110
- def cross_record_predecessors
111
- predecessor_items.reject { |p| p.record_id == @record_id }
112
- end
113
-
114
- # Kitted when every predecessor — intra- and cross-record, regardless of the
115
- # in-group / cross-group tag — is Done; trivially kitted when it has none.
116
- def fully_kitted?
117
- predecessor_items.all?(&:done?)
118
- end
119
-
120
- # A started row with an unfinished lower-numbered step in the same record.
121
- def phase_order_violation?
122
- started? && intra_record_predecessors.any? { |p| !p.done? }
123
- end
124
-
125
- # A started row whose resolved cross-record predecessor is not yet Done.
126
- def cross_record_violation?
127
- started? && cross_record_predecessors.any? { |p| !p.done? }
128
- end
129
- end
@@ -1,218 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'date'
4
- require 'json'
5
- require_relative 'base_document'
6
- require_relative 'decision_grouping'
7
- require_relative 'planning_dates'
8
- require_relative '../html_safe'
9
- require_relative '../project/critical_chain'
10
- require_relative '../project/fever_chart'
11
- require_relative '../project/working_calendar'
12
-
13
- # The dedicated Critical Chain & Project Buffer page (ENH-202): one block per
14
- # decision group, each showing the ordered critical chain rows, the project
15
- # buffer, and the projected duration; a group with no estimates is marked
16
- # unestimated. The chain and buffer are ADR-195's CriticalChain, reused
17
- # unchanged -- this page only relocates the rendering off the overview.
18
- class CriticalChainPage < BaseDocument
19
- include HtmlSafe
20
- include DecisionGrouping
21
- include PlanningDates
22
-
23
- # Recent Fridays sampled for the fever-chart trail (ADR-196), matching the
24
- # velocity chart's window.
25
- FEVER_TRAIL_WEEKS = 6
26
-
27
- attr_accessor :project
28
-
29
- def initialize(project)
30
- super()
31
- @project = project
32
- @title = 'Critical Chain & Project Buffer'
33
- @id = 'critical-chain'
34
- end
35
-
36
- def to_console
37
- puts "\e[36mCritical Chain: #{@id}\e[0m"
38
- end
39
-
40
- def needs_chartjs?
41
- true
42
- end
43
-
44
- def to_html(output_file_path)
45
- html_rows = ['', "<h1>#{@title}</h1>\n", render_critical_chain]
46
- save_html_to_file(html_rows, nil, output_file_path)
47
- end
48
-
49
- private
50
-
51
- def render_critical_chain
52
- ratio = @project.configuration.get_buffer_ratio
53
- hours_per_day = @project.configuration.get_hours_per_day
54
- lookup = record_lookup
55
- blocks = grouped_work_items.each_with_index.map do |(name, items), index|
56
- critical_chain_block(name, items, ratio, lookup, hours_per_day, index)
57
- end
58
- blocks << %(\t<p class="cc_unestimated">No decision records to plan.</p>\n) if blocks.empty?
59
- %(<div class="critical_chain">\n#{blocks.join}</div>\n)
60
- end
61
-
62
- # The calendar a group's projected completion date is measured on (ADR-211):
63
- # one anchored at the group's declared planning.groups start date, or the
64
- # shared project-anchored calendar when the group declares none. Memoised per
65
- # group name so each group reuses one calendar.
66
- def group_calendar(name)
67
- @group_calendars ||= {}
68
- @group_calendars[name] ||= begin
69
- start = @project.configuration.get_group_start_dates[name]
70
- start ? WorkingCalendar.new(anchor: start, holidays: @project.configuration.get_holidays) : working_calendar
71
- end
72
- end
73
-
74
- # An upcased-record-id => Decision map, so a chain row can reach its owning
75
- # record's effort log (ADR-196).
76
- def record_lookup
77
- @project.project_data.decisions.to_h { |doc| [doc.id.to_s.upcase, doc] }
78
- end
79
-
80
- # The shared working calendar (ADR-205) projecting the working-day projected
81
- # duration onto a real completion date.
82
- def working_calendar
83
- @working_calendar ||= WorkingCalendar.new(anchor: @project.configuration.get_start_date,
84
- holidays: @project.configuration.get_holidays)
85
- end
86
-
87
- def critical_chain_block(name, items, ratio, lookup, hours_per_day, index) # rubocop:disable Metrics/ParameterLists
88
- plan = CriticalChain.new(items, buffer_ratio: ratio)
89
- header = %(\t<div class="cc_group">\n\t\t<h3>#{escape_text(name)}</h3>\n)
90
- body = if plan.estimated?
91
- cc_group_body(plan, lookup, hours_per_day, index, group_calendar(name))
92
- else
93
- %(\t\t<p class="cc_unestimated">No estimates — plan not sized.</p>\n)
94
- end
95
- "#{header}#{body}\t</div>\n"
96
- end
97
-
98
- # The estimated group's plan (chain table + buffer + projected duration) on the
99
- # left and its buffer-consumption fever chart on the right, side by side
100
- # (ADR-196). The chart is omitted when no chain row carries a positive estimate.
101
- def cc_group_body(plan, lookup, hours_per_day, index, calendar)
102
- fever = FeverChart.new(plan, lookup, hours_per_day: hours_per_day)
103
- left = %(\t\t<div class="cc_plan">\n#{cc_chain_html(plan, fever, calendar)}\t\t</div>\n)
104
- right = fever.plottable? ? %(\t\t<div class="cc_fever">\n#{fever_chart_html(fever, index)}\t\t</div>\n) : ''
105
- %(\t\t<div class="cc_group_body">\n#{left}#{right}\t\t</div>\n)
106
- end
107
-
108
- def cc_chain_html(plan, fever, calendar)
109
- lines = [%(\t\t<table class="cc_chain">\n),
110
- "\t\t\t<thead><th>Record</th><th>Item</th><th>Owner</th><th>Duration</th></thead>\n"]
111
- plan.chain.each { |wi| lines << cc_chain_row(wi) }
112
- lines << "\t\t</table>\n"
113
- projected = format_days(plan.projected_duration)
114
- finish = calendar.date_for(plan.projected_duration)
115
- lines << %(\t\t<p class="cc_buffer">Project buffer: #{plan.buffer} working days</p>\n)
116
- lines << cc_consumed_html(plan, fever)
117
- lines << %(\t\t<p class="cc_projected">Projected duration: #{projected} working days</p>\n)
118
- lines << %(\t\t<p class="cc_finish">Projected completion: #{finish.strftime('%d-%m-%Y')}</p>\n)
119
- lines.join
120
- end
121
-
122
- # The live buffer-health line shown under the buffer figure: how much of the
123
- # project buffer the chain's overruns have eaten so far, as a percentage and as
124
- # days of the baseline buffer. The percentage and day count both come from the
125
- # fever chart, so the figure and the chart's live point always agree, and the
126
- # "of N baseline days" makes explicit that the denominator is the baseline
127
- # buffer -- not the (remaining-work) "Project buffer" figure above it. Empty
128
- # when the plan carries no buffer to consume (no positive-estimate chain row, or
129
- # zero baseline buffer).
130
- def cc_consumed_html(plan, fever)
131
- return '' unless fever.plottable? && plan.baseline_buffer.positive?
132
-
133
- today = Date.today
134
- consumed_pct = fever.live_point(today).last.round
135
- consumed_days = format_days(fever.consumed_days(today).round(1))
136
- baseline = plan.baseline_buffer
137
- unit = baseline == 1 ? 'day' : 'days'
138
- detail = "#{consumed_days} of #{baseline} baseline #{unit}"
139
- %(\t\t<p class="cc_consumed">Buffer consumed: #{consumed_pct}% (#{detail})</p>\n)
140
- end
141
-
142
- def cc_chain_row(work_item)
143
- cells = [work_item.record_id.upcase, work_item.activity, work_item.owner, format_days(work_item.focused_estimate)]
144
- "\t\t\t<tr>#{cells.map { |c| "<td>#{escape_text(c.to_s)}</td>" }.join}</tr>\n"
145
- end
146
-
147
- # A working-day count without a trailing ".0" when it is whole.
148
- def format_days(value)
149
- value == value.to_i ? value.to_i.to_s : value.to_s
150
- end
151
-
152
- # The fever chart: the effort-only historical trail over recent Fridays plus the
153
- # live point (which credits Done rows), drawn over the green/yellow/red zones.
154
- def fever_chart_html(fever, index)
155
- today = Date.today
156
- fridays = recent_fridays(today, FEVER_TRAIL_WEEKS)
157
- dates = fridays + [today]
158
- points = fever.trail(fridays) + [fever.live_point(today)]
159
- coords = points.zip(dates).map do |(completion, consumption), date|
160
- { x: completion.round(2), y: consumption.round(2), d: date.strftime('%b %-d') }
161
- end
162
- radii = Array.new(coords.length - 1, 3) + [6]
163
- colors = points.map { |completion, consumption| zone_color(completion, consumption) }
164
- fever_canvas_script("fever_chart_#{index}", coords, radii, colors)
165
- end
166
-
167
- def fever_canvas_script(canvas_id, coords, radii, colors)
168
- <<~HTML
169
- \t\t\t<canvas id="#{canvas_id}" class="fever_canvas"></canvas>
170
- \t\t\t<script>
171
- \t\t\t#{fever_zone_plugin_js}
172
- \t\t\tnew Chart(document.getElementById('#{canvas_id}'), {
173
- \t\t\t\ttype: 'scatter',
174
- \t\t\t\tdata: { datasets: [{ label: 'Buffer health', data: #{coords.to_json}, showLine: true,
175
- \t\t\t\t\tborderColor: 'rgba(80,80,80,0.7)', pointRadius: #{radii.to_json},
176
- \t\t\t\t\tpointBackgroundColor: #{colors.to_json}, pointBorderColor: '#333' }] },
177
- \t\t\t\toptions: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false },
178
- \t\t\t\t\ttooltip: { callbacks: { label: (ctx) => ctx.raw.d + ': (' + ctx.parsed.x + ', ' + ctx.parsed.y + ')' } } },
179
- \t\t\t\t\tscales: { x: { title: { display: true, text: 'Chain completion %' }, min: 0, max: 100 },
180
- \t\t\t\t\t\ty: { title: { display: true, text: 'Buffer consumption %' }, min: 0, suggestedMax: 100 } } },
181
- \t\t\t\tplugins: [window.feverZonesPlugin]
182
- \t\t\t});
183
- \t\t\t</script>
184
- HTML
185
- end
186
-
187
- # The conventional CCPM zone for a point: green below the lower one-third
188
- # diagonal, red above the upper, yellow between (ADR-196).
189
- def zone_color(completion, consumption)
190
- lower = (2.0 / 3.0) * completion
191
- upper = (100.0 / 3.0) + (2.0 / 3.0) * completion
192
- return '#2e9e2e' if consumption <= lower
193
- return '#e0a200' if consumption <= upper
194
-
195
- '#d33'
196
- end
197
-
198
- # A page-global Chart.js plugin (declared once, idempotently) that paints the
199
- # three fever zones behind every fever chart on the page.
200
- def fever_zone_plugin_js
201
- <<~JS.strip
202
- window.feverZonesPlugin = window.feverZonesPlugin || { id: 'feverZones', beforeDraw(chart) {
203
- const a = chart.chartArea; if (!a) return; const x = chart.scales.x, y = chart.scales.y;
204
- const px = v => x.getPixelForValue(v), py = v => y.getPixelForValue(v);
205
- const lower = c => (2/3)*c, upper = c => 100/3 + (2/3)*c, ctx = chart.ctx;
206
- ctx.save();
207
- ctx.fillStyle = 'rgba(224,162,0,0.10)'; ctx.fillRect(a.left, a.top, a.width, a.height);
208
- ctx.fillStyle = 'rgba(46,158,46,0.12)'; ctx.beginPath();
209
- ctx.moveTo(px(0), py(0)); ctx.lineTo(px(100), py(lower(100)));
210
- ctx.lineTo(px(100), py(y.min)); ctx.lineTo(px(0), py(y.min)); ctx.closePath(); ctx.fill();
211
- ctx.fillStyle = 'rgba(221,51,51,0.12)'; ctx.beginPath();
212
- ctx.moveTo(px(0), py(upper(0))); ctx.lineTo(px(100), py(100));
213
- ctx.lineTo(px(100), py(y.max)); ctx.lineTo(px(0), py(y.max)); ctx.closePath(); ctx.fill();
214
- ctx.restore();
215
- } };
216
- JS
217
- end
218
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Shared helper for the planning views: the non-empty decision groups (ADR-197)
4
- # paired with their Scope-row work items. Used by the overview's group-segmented
5
- # Gantt (ADR-201) and the dedicated Critical Chain page (ADR-195 / ENH-202).
6
- module DecisionGrouping
7
- # [[group-name, [WorkItem, ...]], ...] in decision_groups (folder-encounter)
8
- # order, only groups that have at least one work item.
9
- def grouped_work_items
10
- by_record = @project.project_data.work_items.values.group_by(&:record_id)
11
- @project.project_data.decision_groups.filter_map do |group|
12
- name = group.keys.first
13
- items = group.values.first.flat_map { |doc| by_record[doc.id] || [] }
14
- [name, items] unless items.empty?
15
- end
16
- end
17
- end