proctolog 0.1.0

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.
Files changed (228) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +178 -0
  4. data/app/controllers/proctolog/acceptances_controller.rb +29 -0
  5. data/app/controllers/proctolog/application_controller.rb +32 -0
  6. data/app/controllers/proctolog/bursts_controller.rb +34 -0
  7. data/app/controllers/proctolog/choices_controller.rb +30 -0
  8. data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
  9. data/app/controllers/proctolog/decisions_controller.rb +33 -0
  10. data/app/controllers/proctolog/entries_controller.rb +14 -0
  11. data/app/controllers/proctolog/evidence_controller.rb +23 -0
  12. data/app/controllers/proctolog/freshness_controller.rb +27 -0
  13. data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
  14. data/app/controllers/proctolog/memberships_controller.rb +20 -0
  15. data/app/controllers/proctolog/mentions_controller.rb +16 -0
  16. data/app/controllers/proctolog/moments_controller.rb +26 -0
  17. data/app/controllers/proctolog/note_params.rb +24 -0
  18. data/app/controllers/proctolog/notes_controller.rb +56 -0
  19. data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
  20. data/app/controllers/proctolog/pictures_controller.rb +27 -0
  21. data/app/controllers/proctolog/prompts_controller.rb +28 -0
  22. data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
  23. data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
  24. data/app/controllers/proctolog/put_away_controller.rb +19 -0
  25. data/app/controllers/proctolog/recordings_controller.rb +28 -0
  26. data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
  27. data/app/controllers/proctolog/review_data_controller.rb +12 -0
  28. data/app/controllers/proctolog/rules_controller.rb +20 -0
  29. data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
  30. data/app/controllers/proctolog/scenario_scoped.rb +21 -0
  31. data/app/controllers/proctolog/scenarios_controller.rb +67 -0
  32. data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
  33. data/app/controllers/proctolog/waiting_controller.rb +14 -0
  34. data/app/controllers/proctolog/with_decisions.rb +28 -0
  35. data/app/helpers/proctolog/bursts_helper.rb +52 -0
  36. data/app/helpers/proctolog/decisions_helper.rb +102 -0
  37. data/app/helpers/proctolog/desk_helper.rb +61 -0
  38. data/app/helpers/proctolog/mentions_helper.rb +27 -0
  39. data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
  40. data/app/helpers/proctolog/prompts_helper.rb +21 -0
  41. data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
  42. data/app/helpers/proctolog/recordings_helper.rb +30 -0
  43. data/app/helpers/proctolog/rounds_helper.rb +27 -0
  44. data/app/helpers/proctolog/scenarios_helper.rb +123 -0
  45. data/app/helpers/proctolog/standings_helper.rb +80 -0
  46. data/app/jobs/proctolog/application_job.rb +8 -0
  47. data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
  48. data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
  49. data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
  50. data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
  51. data/app/models/proctolog/acceptance.rb +47 -0
  52. data/app/models/proctolog/app.rb +92 -0
  53. data/app/models/proctolog/browser.rb +42 -0
  54. data/app/models/proctolog/burst.rb +268 -0
  55. data/app/models/proctolog/burst_notices.rb +143 -0
  56. data/app/models/proctolog/chapters.rb +42 -0
  57. data/app/models/proctolog/check.rb +122 -0
  58. data/app/models/proctolog/choices.rb +112 -0
  59. data/app/models/proctolog/code_review.rb +251 -0
  60. data/app/models/proctolog/decision_record.rb +12 -0
  61. data/app/models/proctolog/decisions.rb +199 -0
  62. data/app/models/proctolog/description.rb +165 -0
  63. data/app/models/proctolog/difference.rb +138 -0
  64. data/app/models/proctolog/doctor.rb +127 -0
  65. data/app/models/proctolog/evidence.rb +29 -0
  66. data/app/models/proctolog/failure.rb +7 -0
  67. data/app/models/proctolog/focus.rb +48 -0
  68. data/app/models/proctolog/fresh_sitting.rb +35 -0
  69. data/app/models/proctolog/github.rb +353 -0
  70. data/app/models/proctolog/handover/brief.rb +138 -0
  71. data/app/models/proctolog/handover/layers.rb +128 -0
  72. data/app/models/proctolog/handover/places.rb +98 -0
  73. data/app/models/proctolog/handover/taken.rb +41 -0
  74. data/app/models/proctolog/handover/worklist.rb +35 -0
  75. data/app/models/proctolog/handover.rb +123 -0
  76. data/app/models/proctolog/housekeeping.rb +23 -0
  77. data/app/models/proctolog/kit.rb +36 -0
  78. data/app/models/proctolog/marks.rb +130 -0
  79. data/app/models/proctolog/membership.rb +91 -0
  80. data/app/models/proctolog/mentions.rb +117 -0
  81. data/app/models/proctolog/note.rb +94 -0
  82. data/app/models/proctolog/outside.rb +45 -0
  83. data/app/models/proctolog/overlay/brief.rb +52 -0
  84. data/app/models/proctolog/overlay/written.rb +33 -0
  85. data/app/models/proctolog/overlay.rb +87 -0
  86. data/app/models/proctolog/owner.rb +16 -0
  87. data/app/models/proctolog/pick.rb +47 -0
  88. data/app/models/proctolog/picture.rb +138 -0
  89. data/app/models/proctolog/pieces_rendering.rb +40 -0
  90. data/app/models/proctolog/project.rb +118 -0
  91. data/app/models/proctolog/pull_request.rb +89 -0
  92. data/app/models/proctolog/recording.rb +194 -0
  93. data/app/models/proctolog/release.rb +59 -0
  94. data/app/models/proctolog/rendering.rb +303 -0
  95. data/app/models/proctolog/reports.rb +66 -0
  96. data/app/models/proctolog/response_files.rb +55 -0
  97. data/app/models/proctolog/rounds.rb +305 -0
  98. data/app/models/proctolog/route.rb +151 -0
  99. data/app/models/proctolog/rules.rb +121 -0
  100. data/app/models/proctolog/scenario.rb +126 -0
  101. data/app/models/proctolog/server.rb +273 -0
  102. data/app/models/proctolog/since.rb +108 -0
  103. data/app/models/proctolog/size.rb +30 -0
  104. data/app/models/proctolog/standing.rb +132 -0
  105. data/app/models/proctolog/theme.rb +40 -0
  106. data/app/models/proctolog/unopened/numbers.rb +48 -0
  107. data/app/models/proctolog/unopened/opening.rb +88 -0
  108. data/app/models/proctolog/unopened.rb +203 -0
  109. data/app/models/proctolog/visit.rb +26 -0
  110. data/app/views/layouts/proctolog/application.html.erb +58 -0
  111. data/app/views/proctolog/acceptances/new.html.erb +34 -0
  112. data/app/views/proctolog/bursts/_line.html.erb +28 -0
  113. data/app/views/proctolog/bursts/_merged.html.erb +23 -0
  114. data/app/views/proctolog/bursts/_moved.html.erb +16 -0
  115. data/app/views/proctolog/bursts/_notices.html.erb +39 -0
  116. data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
  117. data/app/views/proctolog/bursts/_rules.html.erb +27 -0
  118. data/app/views/proctolog/bursts/_side.html.erb +49 -0
  119. data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
  120. data/app/views/proctolog/bursts/show.html.erb +54 -0
  121. data/app/views/proctolog/code_reviews/show.html.erb +33 -0
  122. data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
  123. data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
  124. data/app/views/proctolog/decisions/_box.html.erb +27 -0
  125. data/app/views/proctolog/decisions/_choices.html.erb +9 -0
  126. data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
  127. data/app/views/proctolog/decisions/_part.html.erb +61 -0
  128. data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
  129. data/app/views/proctolog/decisions/_theme.html.erb +13 -0
  130. data/app/views/proctolog/decisions/index.html.erb +75 -0
  131. data/app/views/proctolog/notes/_card.html.erb +19 -0
  132. data/app/views/proctolog/notes/_template.html.erb +41 -0
  133. data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
  134. data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
  135. data/app/views/proctolog/prompts/show.html.erb +41 -0
  136. data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
  137. data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
  138. data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
  139. data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
  140. data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
  141. data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
  142. data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
  143. data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
  144. data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
  145. data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
  146. data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
  147. data/app/views/proctolog/pull_requests/show.html.erb +145 -0
  148. data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
  149. data/app/views/proctolog/recordings/_list.html.erb +8 -0
  150. data/app/views/proctolog/recordings/_note.html.erb +22 -0
  151. data/app/views/proctolog/recordings/index.html.erb +33 -0
  152. data/app/views/proctolog/recordings/show.html.erb +118 -0
  153. data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
  154. data/app/views/proctolog/scenarios/_note.html.erb +18 -0
  155. data/app/views/proctolog/scenarios/_open.html.erb +9 -0
  156. data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
  157. data/app/views/proctolog/scenarios/_server.html.erb +20 -0
  158. data/app/views/proctolog/scenarios/index.html.erb +62 -0
  159. data/bin/proctolog +330 -0
  160. data/completions/_proctolog +143 -0
  161. data/config/application.rb +94 -0
  162. data/config/database.yml +16 -0
  163. data/config/environment.rb +6 -0
  164. data/config/initializers/grouse.rb +113 -0
  165. data/config/initializers/grouse_notes.rb +9 -0
  166. data/config/keys.yml +6 -0
  167. data/config/routes.rb +71 -0
  168. data/config/storage.yml +4 -0
  169. data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
  170. data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
  171. data/docs/manual/app.md +157 -0
  172. data/docs/manual/bursts.md +129 -0
  173. data/docs/manual/deciding.md +89 -0
  174. data/docs/manual/desk.png +0 -0
  175. data/docs/manual/films.md +20 -0
  176. data/docs/manual/handover.md +93 -0
  177. data/docs/manual/pr-format.md +75 -0
  178. data/docs/manual/reading.md +233 -0
  179. data/docs/manual/unopened.md +80 -0
  180. data/docs/manual/updates.md +55 -0
  181. data/lib/proctolog/installation.rb +125 -0
  182. data/lib/proctolog/version.rb +7 -0
  183. data/public/asking.js +50 -0
  184. data/public/code-review.js +40 -0
  185. data/public/cursor.js +292 -0
  186. data/public/decision-screenshots.js +97 -0
  187. data/public/decisions.css +949 -0
  188. data/public/decisions.js +1289 -0
  189. data/public/desk.css +582 -0
  190. data/public/desk.js +126 -0
  191. data/public/favicon.png +0 -0
  192. data/public/keys.js +228 -0
  193. data/public/mentions.js +389 -0
  194. data/public/messages.js +103 -0
  195. data/public/notes.js +731 -0
  196. data/public/pick.css +99 -0
  197. data/public/plaque.png +0 -0
  198. data/public/pr-number.js +11 -0
  199. data/public/prompt.css +46 -0
  200. data/public/prompt.js +16 -0
  201. data/public/pull_request.css +805 -0
  202. data/public/recording.css +379 -0
  203. data/public/recording.js +394 -0
  204. data/public/release.js +25 -0
  205. data/public/respond.css +260 -0
  206. data/public/respond.js +95 -0
  207. data/public/review-drafts.js +949 -0
  208. data/public/review-rounds.js +1447 -0
  209. data/public/scenarios.css +253 -0
  210. data/public/scenarios.js +36 -0
  211. data/public/schemas/proctolog-response-v1.json +101 -0
  212. data/public/side.js +133 -0
  213. data/public/standing.css +478 -0
  214. data/public/terminal-apl.svg +6 -0
  215. data/public/terminal-cuneiform.svg +3 -0
  216. data/public/terminal-grain-dots.svg +6 -0
  217. data/public/terminal-grain-weave.svg +6 -0
  218. data/public/terminal-grain-wedges.svg +6 -0
  219. data/public/terminal-shades.svg +3 -0
  220. data/public/terminal.css +4426 -0
  221. data/public/toast.js +26 -0
  222. data/public/try.js +124 -0
  223. data/public/viewer.js +808 -0
  224. data/public/words.js +81 -0
  225. data/skills/proctolog-handover/SKILL.md +77 -0
  226. data/skills/proctolog-pr-writing/SKILL.md +228 -0
  227. data/skills/proctolog-pr-writing/references/example-description.md +33 -0
  228. metadata +366 -0
@@ -0,0 +1,145 @@
1
+ <% content_for :title, "##{@stop.number} #{@stop.title}" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/respond.css">
4
+ <link rel="stylesheet" href="/pull_request.css">
5
+ <link rel="stylesheet" href="/scenarios.css">
6
+ <link rel="stylesheet" href="/standing.css">
7
+ <link rel="stylesheet" href="/decisions.css">
8
+ <script type="module" src="/notes.js"></script>
9
+ <script type="module" src="/decisions.js"></script>
10
+ <script type="module" src="/cursor.js"></script>
11
+ <script type="module" src="/code-review.js"></script>
12
+ <script type="module" src="/try.js"></script>
13
+ <% end %>
14
+ <% content_for :mode, mode_line("Reading", "↑ ↓" => "move", "[ ]" => "sections") %>
15
+ <% content_for :keys do %>
16
+ <%# The docked list is short on purpose: a decision's choices are named in its own menu, and writing, a picture looked at closely and returned work each say their keys where they are. %>
17
+ <%# ↑ ↓ and j k are one key: the next block down, or up, of all the page shows, returned work included. %>
18
+ <%= keys_group "Read", "↑ ↓ j k" => "move, block by block", "[ ]" => "section", "← →" => "close, open", "Space" => "look closely" %>
19
+ <%= keys_group "Mark", "n" => "note on a block", "w" => "note on the whole", "f" => "decide, one at a time", "A" => "leave the non-blocking as is", "g" => "good to go" %>
20
+ <%= keys_group "Deciding", "← →" => "previous, next decision", "↑ ↓ j k" => "scroll", "R" => "make the answer a rule", "Esc" => "back to reading" %>
21
+ <%= keys_group "Go", "J K" => "layer above, below", "N" => "next to review", "h" => "the route", "v" => "returned work, Esc back" %>
22
+ <%= keys_group "Elsewhere", "d" => "code in Diffle", "e" => "handover", "r" => "read GitHub again", "G" => "on GitHub", "u" => "undo a choice", **({"t" => "try it, beside the page"} if @scenarios.any?) %>
23
+ <% end %>
24
+
25
+ <header class="pull-request">
26
+ <h1><span class="number">#<%= @stop.number %></span> <%= @stop.title %></h1>
27
+ <%= render "status", burst: @burst, stop: @stop, standing: @standing %>
28
+ <div class="review-actions" data-desk>
29
+ <button type="button" data-enter-decisions <%= "disabled" if @boxes.empty? %>><kbd>f</kbd> review decisions <span class="count" data-count-of="unreviewed"></span></button>
30
+ <button type="button" data-enter-responses hidden><kbd>v</kbd> returned work</button>
31
+ <nav class="along" aria-label="In the route">
32
+ <%= link_to burst_path(@burst, anchor: "pr-#{@stop.number}"), data: {key: "h"} do %><kbd>h</kbd> route<% end %>
33
+ <% if (earlier = focus.before(@stop)) %>
34
+ <%= link_to burst_pull_request_path(@burst, earlier.number), rel: "prev", data: {key: "K"}, title: earlier.title do %><kbd>K</kbd> previous #<%= earlier.number %><% end %>
35
+ <% end %>
36
+ <% if (later = focus.after(@stop)) %>
37
+ <%= link_to burst_pull_request_path(@burst, later.number), rel: "next", data: {key: "J"}, title: later.title do %><kbd>J</kbd> next #<%= later.number %><% end %>
38
+ <% end %>
39
+ <% if @next_to_review %>
40
+ <%= link_to burst_pull_request_path(@burst, @next_to_review.number), data: {key: "N"}, title: @next_to_review.title do %><kbd>N</kbd> next to review #<%= @next_to_review.number %><% end %>
41
+ <% end %>
42
+ </nav>
43
+ </div>
44
+ <%# What can be done with the whole pull request stands beside its title while the reading rests there, each with its key. %>
45
+ <div class="whole" data-desk>
46
+ <button type="button" data-note-whole><kbd>w</kbd> note on the whole</button>
47
+ <%= button_to burst_pull_request_code_review_path(@burst, @stop.number), form: {target: "_blank", data: {code: ""}} do %><kbd>d</kbd> code in Diffle<% end %>
48
+ <%= link_to burst_pull_request_path(@burst, @stop.number, fresh: 1) do %><kbd>r</kbd> read GitHub again<% end %>
49
+ <% if @stop.url %>
50
+ <%= link_to @stop.url, target: "_blank", rel: "noopener", data: {key: "G"} do %><kbd>G</kbd> on GitHub<% end %>
51
+ <% end %>
52
+ <%= link_to "code comments", burst_pull_request_code_review_path(@burst, @stop.number), class: "keyless" %>
53
+ </div>
54
+ <div class="slot" id="whole-slot" data-desk></div>
55
+ <%= render "proctolog/recordings/list", burst: @burst, stop: @stop %>
56
+ </header>
57
+
58
+ <% if @since %>
59
+ <%= render "since", since: @since, stop: @stop %>
60
+ <% end %>
61
+
62
+ <% if @rendering.lost.any? %>
63
+ <section class="lost" aria-labelledby="lost-heading">
64
+ <h2 id="lost-heading">Notes that lost their place</h2>
65
+ <p>The words or the picture they were written on are no longer in the description. Their old words are kept here.</p>
66
+ <ol class="cards">
67
+ <% @rendering.lost.each do |note| %>
68
+ <%= render "note", note:, rendering: @rendering, burst: @burst, stop: @stop %>
69
+ <% end %>
70
+ </ol>
71
+ </section>
72
+ <% end %>
73
+
74
+ <div data-inline-decisions></div>
75
+ <script type="application/json" id="decision-inventory"><%= raw json_escape(decision_inventory(@burst)) %></script>
76
+ <%= render "proctolog/decisions/round_data", burst: @burst, stop: @stop %>
77
+
78
+ <section data-round-results data-pr="<%= @stop.number %>" hidden></section>
79
+ <div class="page">
80
+ <main class="description">
81
+ <% @rendering.sections.each do |section| %>
82
+ <%= tag.details id: ("decisions" if section.heading == Proctolog::Description::DECISIONS), class: "section", open: unfolded?(section), data: {heading: section.heading.to_s} do %>
83
+ <summary>
84
+ <span class="heading"><%= section.heading || "Opening" %></span>
85
+ <span class="count"><%= count_of(section) %></span>
86
+ </summary>
87
+ <% if section.heading == Proctolog::Description::DECISIONS %>
88
+ <%# One cell for each decision, in order, which the script fills: where the owner is among them, and which are done. %>
89
+ <div class="stepper" data-stepper data-desk aria-hidden="true"></div>
90
+ <div class="actions views">
91
+ <button type="button" class="quiet" data-pending-view>To review <span class="count" data-count-of="unreviewed"></span></button>
92
+ <button type="button" class="quiet" data-all-view>All decisions <span class="count" data-count-of="all"></span></button>
93
+ </div>
94
+ <% end %>
95
+ <%= raw section_html(section, burst: @burst, boxes: @boxes) %>
96
+ <% if section.heading == Proctolog::Description::DECISIONS %>
97
+ <%= render "finish", burst: @burst, stop: @stop, boxes: @boxes, standing: @standing, next_to_review: @next_to_review, later: %>
98
+ <% end %>
99
+ <% end %>
100
+ <% end %>
101
+ </main>
102
+
103
+ <%# After the description, where its review stands: what must be met, the scenarios, how it connects, the notes and the handover. Each is a section like the description's own, so the same keys and the outline reach them. %>
104
+ <%= tag.details class: "section", open: true, data: {heading: "Standing"} do %>
105
+ <summary>
106
+ <span class="heading">Standing</span>
107
+ <%= tag.span conditions_met(@standing), class: "count", data: {waits: @standing.conditions.any? { it.met == false }} %>
108
+ </summary>
109
+ <%= render "standing", burst: @burst, stop: @stop, standing: @standing %>
110
+ <% end %>
111
+
112
+ <% if @scenarios.any? %>
113
+ <%= render "scenarios", burst: @burst, stop: @stop, scenarios: @scenarios, marks: @standing.marks %>
114
+ <% end %>
115
+
116
+ <%= tag.details class: "section", data: {heading: "Connections"} do %>
117
+ <summary><span class="heading">Connections</span></summary>
118
+ <%= render "connections", burst: @burst, route: @route, stop: @stop %>
119
+ <% end %>
120
+
121
+ <% open_notes = @rendering.about.count(&:open?) + @rendering.margin.count(&:open?) %>
122
+ <%= tag.details class: "section", open: (@rendering.about.any? || @rendering.margin.any?), data: {heading: "Notes"} do %>
123
+ <summary>
124
+ <span class="heading">Notes</span>
125
+ <%= tag.span (open_notes.zero? ? "none open" : "#{open_notes} open"), class: "count", data: {waits: open_notes.positive?} %>
126
+ </summary>
127
+ <%= render "margin", rendering: @rendering, burst: @burst, stop: @stop %>
128
+ <% end %>
129
+
130
+ <%= tag.details class: "section", data: {heading: "Handover"} do %>
131
+ <summary><span class="heading">Handover</span></summary>
132
+ <%= render "proctolog/decisions/draft_tools", burst: @burst, stop: @stop %>
133
+ <% end %>
134
+ </div>
135
+
136
+ <%# The rail at the right: the outline, and under it Try it, which sets up the scenarios. %>
137
+ <div class="rail">
138
+ <%= render "outline", rendering: @rendering, standing: @standing, scenarios: @scenarios, open_notes:, burst: @burst, code: @code %>
139
+ <% if @scenarios.any? %>
140
+ <%= render "try", burst: @burst, stop: @stop, scenarios: @scenarios %>
141
+ <% end %>
142
+ </div>
143
+
144
+ <%= render "proctolog/notes/template", action: burst_pull_request_notes_path(@burst, @stop.number) %>
145
+ <button type="button" id="note-selection" class="selection" hidden>Note</button>
@@ -0,0 +1,26 @@
1
+ <%# locals: (burst:, recording:) %>
2
+ <%# The script says which moment the note is on, and whether a rectangle was drawn. %>
3
+ <dialog id="note-dialog" aria-labelledby="note-heading">
4
+ <%= form_with url: burst_moments_path(burst), id: "note-form" do |form| %>
5
+ <h2 id="note-heading">A note on this moment</h2>
6
+ <blockquote class="words" data-about></blockquote>
7
+
8
+ <fieldset class="kinds">
9
+ <legend class="visually-hidden">What kind of note</legend>
10
+ <% Proctolog::Note::KINDS.each_with_index do |kind, index| %>
11
+ <label><%= form.radio_button :kind, kind, checked: index.zero? %> <%= kind.capitalize %></label>
12
+ <% end %>
13
+ </fieldset>
14
+
15
+ <%= form.text_area :text, rows: 5, required: true, autofocus: true, "aria-label": "The note", placeholder: "What should change, or what do you ask?" %>
16
+
17
+ <%= hidden_field_tag :recording, recording.to_param, id: nil %>
18
+ <%= hidden_field_tag :time, nil, id: nil %>
19
+ <%= hidden_field_tag :box, nil, id: nil %>
20
+
21
+ <div class="buttons">
22
+ <%= form.button "Save note", class: "primary" %>
23
+ <button type="submit" formmethod="dialog" formnovalidate class="quiet">Cancel</button>
24
+ </div>
25
+ <% end %>
26
+ </dialog>
@@ -0,0 +1,8 @@
1
+ <%# locals: (burst:, stop:) %>
2
+ <%# The recordings in a pull request's folder, as links. %>
3
+ <% recordings = Proctolog::Recording.in(burst, stop.number) %>
4
+ <% if recordings.any? %>
5
+ <p class="elsewhere recordings">Recordings:
6
+ <%= safe_join(recordings.map { player_link(burst, it) }, ", ") %>
7
+ </p>
8
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <%# locals: (note:, burst:) %>
2
+ <%# A note on a moment: it goes to its moment when pressed, and moves along like any other. %>
3
+ <%= tag.li id: "note-#{note.id}", class: "card", data: {card: note.id, kind: note.kind, state: note.state, time: note.place.time, box: note.place.box&.join(",")} do %>
4
+ <p class="meta"><span class="kind"><%= note.kind %></span> <span class="state"><%= note.state %></span></p>
5
+ <p class="text"><%= with_mentions(note.text, burst:) %></p>
6
+ <p class="where">
7
+ <button type="button" class="quiet" data-go><%= timecode(note.place.time) %></button>
8
+ <%= "· #{note.place.step}" if note.place.step %>
9
+ </p>
10
+ <% if note.reply %>
11
+ <p class="reply">
12
+ Addressed<%= " in " if note.reply["commit"] %><code><%= note.reply["commit"] %></code>.
13
+ <%= with_mentions(note.reply["text"], burst:) %>
14
+ </p>
15
+ <% end %>
16
+ <div class="moves">
17
+ <% path = burst_moment_path(burst, note.id) %>
18
+ <%= button_to "Confirm", path, method: :patch, params: {state: "confirmed"}, class: "quiet" if note.addressed? %>
19
+ <%= button_to "Reopen", path, method: :patch, params: {state: "open"}, class: "quiet" unless note.open? %>
20
+ <%= button_to "Withdraw", path, method: :patch, params: {state: "withdrawn"}, class: "quiet" unless note.withdrawn? %>
21
+ </div>
22
+ <% end %>
@@ -0,0 +1,33 @@
1
+ <% content_for :title, "Recordings · #{@burst.name}" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/recording.css">
4
+ <% end %>
5
+
6
+ <header class="films-head">
7
+ <h1>Recordings</h1>
8
+ <p class="tally"><%= @folders.sum { |_, recordings| recordings.size }.then { pluralize(it, "recording") } %> of <%= @burst.name %>. Each plays here, with its steps and the notes pinned to it.</p>
9
+ </header>
10
+
11
+ <% if @folders.empty? %>
12
+ <p class="empty">No recordings yet. A capture script that films a flow leaves its <code>.mp4</code> and <code>.vtt</code> in <code>tools/proctolog/films/<%= @burst.slug %>/sitting/</code>, or in a folder named for a pull request.</p>
13
+ <% end %>
14
+
15
+ <% @folders.each do |folder, recordings| %>
16
+ <section class="folder">
17
+ <h2><%= folder.to_i.positive? ? link_to(recording_folder(folder), burst_pull_request_path(@burst, folder)) : recording_folder(folder) %></h2>
18
+ <ul class="films">
19
+ <% recordings.each do |recording| %>
20
+ <li>
21
+ <p class="title"><%= player_link @burst, recording %></p>
22
+ <p class="facts">
23
+ <%= timecode(recording.length) %> ·
24
+ <%= pluralize(recording.chapters.count, "step") %> ·
25
+ <%= recording.width %>×<%= recording.height %> ·
26
+ <%= number_with_precision(recording.megabytes, precision: 1) %> MB ·
27
+ <%= pluralize(recording.noted_in(@burst.marks(recording.folder)).count(&:live?), "open note") %>
28
+ </p>
29
+ </li>
30
+ <% end %>
31
+ </ul>
32
+ </section>
33
+ <% end %>
@@ -0,0 +1,118 @@
1
+ <% content_for :title, "#{recording_title(@recording)} · Recordings" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/respond.css">
4
+ <link rel="stylesheet" href="/pull_request.css">
5
+ <link rel="stylesheet" href="/recording.css">
6
+ <script type="module" src="/recording.js"></script>
7
+ <% end %>
8
+ <% live, put_away = @notes.partition(&:live?) %>
9
+
10
+ <header class="film-head">
11
+ <p class="crumbs">
12
+ <%= link_to "Recordings", burst_recordings_path(@burst) %> /
13
+ <%= @recording.number ? link_to(recording_folder(@recording.folder), burst_pull_request_path(@burst, @recording.number)) : recording_folder(@recording.folder) %>
14
+ </p>
15
+ <div class="titling">
16
+ <h1><%= recording_title(@recording) %></h1>
17
+ <p class="facts">
18
+ <%= timecode(@recording.length) %> ·
19
+ <%= pluralize(@recording.chapters.count, "step") %> ·
20
+ <%= @recording.width %>×<%= @recording.height %> ·
21
+ <%= number_with_precision(@recording.megabytes, precision: 1) %> MB
22
+ </p>
23
+ <button type="button" class="quiet" data-fill aria-pressed="false">Fill the window</button>
24
+ </div>
25
+ </header>
26
+
27
+ <div class="film" data-film data-length="<%= @recording.length %>" style="--ratio: <%= (@recording.width.to_f / @recording.height).round(4) %>">
28
+ <section class="stage" aria-label="The recording">
29
+ <div class="screen" data-screen>
30
+ <video src="<%= @recording.url %>" preload="auto" playsinline muted>
31
+ <track kind="chapters" src="<%= @recording.captions_url %>" srclang="en" label="Steps" default>
32
+ </video>
33
+ <div class="regions" data-regions></div>
34
+ </div>
35
+
36
+ <div class="controls">
37
+ <button type="button" class="primary" data-play>Play</button>
38
+ <button type="button" data-frame-back title="One frame back">‹ Frame <kbd>,</kbd></button>
39
+ <button type="button" data-frame-forward title="One frame forward">Frame › <kbd>.</kbd></button>
40
+ <button type="button" data-step-back title="The previous step">‹ Step <kbd>[</kbd></button>
41
+ <button type="button" data-step-forward title="The next step">Step › <kbd>]</kbd></button>
42
+ <output class="clock" data-clock>0:00 / <%= timecode(@recording.length) %></output>
43
+ <button type="button" class="note-here" data-note-here>Note here <kbd>c</kbd></button>
44
+ </div>
45
+
46
+ <div class="timeline" data-timeline aria-label="Timeline: press to go to a moment">
47
+ <ol class="segments">
48
+ <% @recording.chapters.each_with_index do |chapter, index| %>
49
+ <li style="left: <%= film_position(chapter.from, @recording) %>; width: <%= film_position(chapter.to - chapter.from, @recording) %>" title="<%= chapter.name %>">
50
+ <span><%= index + 1 %> <%= chapter.name %></span>
51
+ </li>
52
+ <% end %>
53
+ </ol>
54
+ <% live.each do |note| %>
55
+ <%= tag.button type: "button", class: "pin", data: {pin: note.id, time: note.place.time, kind: note.kind, state: note.state},
56
+ style: "left: #{film_position(note.place.time, @recording)}", "aria-label": "Note at #{timecode(note.place.time)}: #{note.text.truncate(60)}" %>
57
+ <% end %>
58
+ <span class="playhead" data-playhead></span>
59
+ </div>
60
+ <p class="where" data-where aria-live="polite"></p>
61
+ </section>
62
+
63
+ <aside class="side">
64
+ <%# The strip: each step by the picture it opens on, under its name; a press goes there. A still comes once it is made, and without one the name stands alone. %>
65
+ <% if @recording.chapters.any? %>
66
+ <h2>Steps</h2>
67
+ <%# Against the round before: which steps changed, which are new, or why nothing could be said. %>
68
+ <p class="round" data-round><%= @changes.said %></p>
69
+ <ol class="steps">
70
+ <% @recording.chapters.each_with_index do |chapter, index| %>
71
+ <% change = @changes.of(chapter.name) %>
72
+ <%= tag.li data: {step: "", from: chapter.from, to: chapter.to, name: chapter.name, change: (change if change.in?(%i[changed new]))} do %>
73
+ <button type="button">
74
+ <span class="n"><%= index + 1 %></span>
75
+ <span class="name"><%= chapter.name %><% if change.in?(%i[changed new]) %> <span class="change"><%= (change == :new) ? "New step" : "Changed" %></span><% end %></span>
76
+ <time><%= timecode(chapter.from) %></time>
77
+ <%= tag.img src: burst_recording_still_path(@burst, index, @recording.to_param, film: @recording.digest[0, 12]), alt: "", loading: "lazy", class: "still", data: {still: ""} %>
78
+ </button>
79
+ <% end %>
80
+ <% end %>
81
+ </ol>
82
+ <% end %>
83
+
84
+ <h2>Notes</h2>
85
+ <% if live.any? %>
86
+ <ol class="cards">
87
+ <% live.sort_by { it.place.time }.each do |note| %>
88
+ <%= render "note", note:, burst: @burst %>
89
+ <% end %>
90
+ </ol>
91
+ <% else %>
92
+ <p class="empty">No notes yet. Press <kbd>c</kbd> on a moment, or drag across the frame to mark a place.</p>
93
+ <% end %>
94
+ <% if put_away.any? %>
95
+ <details class="set-aside">
96
+ <summary>Put away <span class="count"><%= put_away.size %></span></summary>
97
+ <ol class="cards">
98
+ <% put_away.sort_by { it.place.time }.each do |note| %>
99
+ <%= render "note", note:, burst: @burst %>
100
+ <% end %>
101
+ </ol>
102
+ </details>
103
+ <% end %>
104
+ </aside>
105
+ </div>
106
+
107
+ <%# The time each frame is first shown, so that one frame forward is one frame that changes. %>
108
+ <%= tag.script raw(json_escape(@recording.frames.to_json)), type: "application/json", id: "frames" %>
109
+
110
+ <%= render "dialog", burst: @burst, recording: @recording %>
111
+
112
+ <footer class="keys">
113
+ <span><kbd>Space</kbd> plays and pauses</span>
114
+ <span><kbd>,</kbd> <kbd>.</kbd> one frame back and forward</span>
115
+ <span><kbd>[</kbd> <kbd>]</kbd> the previous and next step</span>
116
+ <span><kbd>c</kbd> a note on this moment</span>
117
+ <span>Drag across the frame to mark a place first</span>
118
+ </footer>
@@ -0,0 +1,22 @@
1
+ <%# locals: (burst:) %>
2
+ <%# One dialog for every scenario: the script says which one the note is on, and where it goes. The token is the page's own, as the form's address is not known until a scenario is chosen. %>
3
+ <dialog id="note-dialog" aria-labelledby="note-heading">
4
+ <%= form_with url: burst_scenarios_path(burst), authenticity_token: form_authenticity_token, id: "note-form" do |form| %>
5
+ <h2 id="note-heading">A note on the scenario</h2>
6
+ <blockquote class="words" data-about></blockquote>
7
+
8
+ <fieldset class="kinds">
9
+ <legend class="visually-hidden">What kind of note</legend>
10
+ <% Proctolog::Note::KINDS.each_with_index do |kind, index| %>
11
+ <label><%= form.radio_button :kind, kind, checked: index.zero? %> <%= kind.capitalize %></label>
12
+ <% end %>
13
+ </fieldset>
14
+
15
+ <%= form.text_area :text, rows: 5, required: true, autofocus: true, "aria-label": "The note", placeholder: "What should change, or what do you ask?" %>
16
+
17
+ <div class="buttons">
18
+ <%= form.button "Save note", class: "primary" %>
19
+ <button type="submit" formmethod="dialog" formnovalidate class="quiet">Cancel</button>
20
+ </div>
21
+ <% end %>
22
+ </dialog>
@@ -0,0 +1,18 @@
1
+ <%# locals: (note:, scenario:, burst:) %>
2
+ <%# A note on a scenario, moved along from the page it is shown on. %>
3
+ <%= tag.li class: "card", data: {kind: note.kind, state: note.state} do %>
4
+ <p class="meta"><span class="kind"><%= note.kind %></span> <span class="state"><%= note.state %></span></p>
5
+ <p class="text"><%= with_mentions(note.text, burst:) %></p>
6
+ <% if note.reply %>
7
+ <p class="reply">
8
+ Addressed<%= " in " if note.reply["commit"] %><code><%= note.reply["commit"] %></code>.
9
+ <%= with_mentions(note.reply["text"], burst:) %>
10
+ </p>
11
+ <% end %>
12
+ <div class="moves">
13
+ <% path = burst_scenario_note_path(burst, scenario, note.id) %>
14
+ <%= button_to "Confirm", path, method: :patch, params: {state: "confirmed"}, class: "quiet" if note.addressed? %>
15
+ <%= button_to "Reopen", path, method: :patch, params: {state: "open"}, class: "quiet" unless note.open? %>
16
+ <%= button_to "Withdraw", path, method: :patch, params: {state: "withdrawn"}, class: "quiet" unless note.withdrawn? %>
17
+ </div>
18
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%# locals: (scenario:, state:, burst:) %>
2
+ <div class="act">
3
+ <% if state == :up %>
4
+ <%= button_to "Open", open_burst_scenario_path(burst, scenario), class: "open", form: {target: ("_blank" if scenario.browser.mine?)} %>
5
+ <% else %>
6
+ <button class="open" disabled>The server is not up</button>
7
+ <% end %>
8
+ <small><%= where_it_opens(scenario) %></small>
9
+ </div>
@@ -0,0 +1,48 @@
1
+ <%# locals: (scenario:, burst:, steps:, head:, seen:, notes:, overlays: [], act: nil) %>
2
+ <%# A scenario as a row: what to do, what was seen and the notes on it. `act` is what the page puts between, such as the button that opens it. `overlays` are the Grouse overlays its presses opened, whose notes a pull brings here. %>
3
+ <%= tag.li id: scenario.to_param, class: "scenario" do %>
4
+ <div class="text">
5
+ <h4><%= tag.span(scenario.sitting, class: "n") if scenario.sitting %><%= scenario.name %></h4>
6
+ <% if scenario.expect %>
7
+ <p class="expect"><%= scenario.expect %></p>
8
+ <% end %>
9
+ <% if scenario.steps && steps.any? %>
10
+ <details class="steps" open>
11
+ <summary><%= pluralize(steps.size, "step") %>, from the description</summary>
12
+ <ol>
13
+ <% steps.each do |step| %>
14
+ <%= tag.li step_words(step.text), value: step.number %>
15
+ <% end %>
16
+ </ol>
17
+ </details>
18
+ <% elsif scenario.steps %>
19
+ <p class="lost">The description has no steps under “<%= scenario.steps %>” in How to test locally now.</p>
20
+ <% end %>
21
+ </div>
22
+
23
+ <%= act %>
24
+
25
+ <div class="seen">
26
+ <p class="line" data-result="<%= seen&.fetch("result") %>"><%= seen_line(seen, head) %></p>
27
+ <div class="results">
28
+ <% Proctolog::Scenario::RESULTS.each do |label, result| %>
29
+ <%= button_to label, saw_burst_scenario_path(burst, scenario), params: {result:}, class: "result", aria: {pressed: (seen&.fetch("result") == result)} %>
30
+ <% end %>
31
+ <%= tag.button "Note", type: "button", class: "result", data: {note_scenario: burst_scenario_notes_path(burst, scenario), name: scenario.name} %>
32
+ </div>
33
+ <% if overlays.any? %>
34
+ <p class="line overlays"><%= overlays_line(overlays) %></p>
35
+ <div class="results">
36
+ <%= button_to "Pull notes", pull_burst_scenario_path(burst, scenario), class: "result" %>
37
+ </div>
38
+ <% end %>
39
+ </div>
40
+
41
+ <% if notes.any? %>
42
+ <ol class="cards">
43
+ <% notes.each do |note| %>
44
+ <%= render "proctolog/scenarios/note", note:, scenario:, burst: %>
45
+ <% end %>
46
+ </ol>
47
+ <% end %>
48
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <section class="server" data-state="<%= state %>" data-status-url="<%= burst_sitting_server_path(burst) %>">
2
+ <p class="status"><span class="light" aria-hidden="true"></span><span><%= server_status(server) %></span></p>
3
+ <p class="where">
4
+ Serves <%= server.branch ? tag.code(server.branch) : "as it is" %> from <code><%= server.checkout.to_s.sub(Dir.home, "~") %></code>
5
+ on database <code><%= server.slug %></code>, port <%= server.port %>.
6
+ </p>
7
+ <% if state == :down %>
8
+ <%= button_to "Start", burst_sitting_server_path(burst), class: "action" %>
9
+ <% elsif server.started_here? %>
10
+ <%= button_to "Stop", burst_sitting_server_path(burst), method: :delete, class: "action" %>
11
+ <% else %>
12
+ <p class="where">The desk did not start it, so it cannot stop it: stop it where it runs.</p>
13
+ <% end %>
14
+ <% if server.crashed? %>
15
+ <details open>
16
+ <summary>The end of its log</summary>
17
+ <pre><%= server.last_lines %></pre>
18
+ </details>
19
+ <% end %>
20
+ </section>
@@ -0,0 +1,62 @@
1
+ <% content_for :title, "Scenarios" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/respond.css">
4
+ <link rel="stylesheet" href="/pull_request.css">
5
+ <link rel="stylesheet" href="/scenarios.css">
6
+ <script src="/scenarios.js" defer></script>
7
+ <% end %>
8
+
9
+ <header class="burst">
10
+ <h1>Scenarios</h1>
11
+ <% unless @server.app.described? %>
12
+ <p class="lede">This checkout has no <code>.proctolog/app.yml</code> saying how to run its app, so these buttons cannot start it or open a scenario in it. The steps still read, and what you saw can still be marked.</p>
13
+ <% end %>
14
+ <%= render "server", burst: @burst, server: @server, state: @state %>
15
+ </header>
16
+
17
+ <main class="scenarios">
18
+ <% if @burst.scenarios.empty? %>
19
+ <p class="lede">This burst has no scenarios yet. They are written in <code>.proctolog/scenarios/<%= @burst.slug %>.yml</code>, a step or a pull request's “How to test locally” each.</p>
20
+ <% end %>
21
+
22
+ <% if @burst.scenarios.any?(&:sitting) %>
23
+ <section>
24
+ <h2>The sitting</h2>
25
+ <p class="lede">Steps of the whole burst, belonging to no one pull request. Each button opens the app where its step starts.</p>
26
+ <% sitting = @burst.marks("sitting") %>
27
+ <% @burst.scenarios.select(&:sitting).group_by(&:group).each do |group, scenarios| %>
28
+ <h3><%= group %></h3>
29
+ <ol class="steps-of">
30
+ <% scenarios.each do |scenario| %>
31
+ <%= render "scenario", scenario:, burst: @burst, steps: [], head: @server.head, seen: sitting.scenarios[scenario.name], notes: scenario.noted_in(sitting), overlays: sitting.overlays_of(scenario.name),
32
+ act: render("open", scenario:, state: @state, burst: @burst) %>
33
+ <% end %>
34
+ </ol>
35
+ <% end %>
36
+ </section>
37
+ <% end %>
38
+
39
+ <% if @burst.scenarios.any?(&:pr) %>
40
+ <section>
41
+ <h2>By pull request</h2>
42
+ <p class="lede">Each group of steps is read from the pull request's description as it stands now.</p>
43
+ <% scenarios_by_pull_request(@burst.scenarios.select { focus.whole? || focus.at(it.pr) }, focus).each do |number, scenarios| %>
44
+ <% stop = @route.at(number) %>
45
+ <% marks = @burst.marks(number) %>
46
+ <h3>
47
+ <%= link_to "##{number}", burst_pull_request_path(@burst, number), class: "number" %>
48
+ <%= stop ? stop.title : "no longer open" %>
49
+ </h3>
50
+ <ol class="steps-of">
51
+ <% scenarios.each do |scenario| %>
52
+ <%= render "scenario", scenario:, burst: @burst, steps: stop ? scenario.steps_in(stop.description) : [],
53
+ head: stop&.head_oid, seen: marks.scenarios[scenario.name], notes: scenario.noted_in(marks), overlays: marks.overlays_of(scenario.name),
54
+ act: render("open", scenario:, state: @state, burst: @burst) %>
55
+ <% end %>
56
+ </ol>
57
+ <% end %>
58
+ </section>
59
+ <% end %>
60
+ </main>
61
+
62
+ <%= render "dialog", burst: @burst %>