proctolog 0.1.1 → 0.3.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -3
  3. data/README.md +32 -18
  4. data/app/controllers/proctolog/application_controller.rb +2 -2
  5. data/app/controllers/proctolog/bursts_controller.rb +1 -1
  6. data/app/controllers/proctolog/choices_controller.rb +3 -1
  7. data/app/controllers/proctolog/decisions_controller.rb +1 -1
  8. data/app/controllers/proctolog/notes_controller.rb +2 -2
  9. data/app/controllers/proctolog/prompts_controller.rb +1 -1
  10. data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
  11. data/app/controllers/proctolog/review_data_controller.rb +3 -1
  12. data/app/controllers/proctolog/shares_controller.rb +27 -0
  13. data/app/helpers/proctolog/decisions_helper.rb +6 -2
  14. data/app/helpers/proctolog/desk_helper.rb +48 -8
  15. data/app/helpers/proctolog/rounds_helper.rb +4 -2
  16. data/app/helpers/proctolog/shares_helper.rb +11 -0
  17. data/app/helpers/proctolog/standings_helper.rb +5 -4
  18. data/app/models/proctolog/burst.rb +17 -8
  19. data/app/models/proctolog/choices.rb +8 -0
  20. data/app/models/proctolog/doctor.rb +23 -12
  21. data/app/models/proctolog/github.rb +1 -1
  22. data/app/models/proctolog/localhost.rb +17 -0
  23. data/app/models/proctolog/marks.rb +1 -1
  24. data/app/models/proctolog/note.rb +4 -0
  25. data/app/models/proctolog/owner.rb +3 -0
  26. data/app/models/proctolog/pick.rb +12 -0
  27. data/app/models/proctolog/rounds.rb +37 -0
  28. data/app/models/proctolog/server.rb +1 -7
  29. data/app/models/proctolog/setup.rb +190 -0
  30. data/app/models/proctolog/share/ngrok.rb +120 -0
  31. data/app/models/proctolog/share/settings.rb +23 -0
  32. data/app/models/proctolog/share/tailscale.rb +103 -0
  33. data/app/models/proctolog/share.rb +101 -0
  34. data/app/models/proctolog/standing.rb +24 -7
  35. data/app/views/proctolog/bursts/_line.html.erb +2 -2
  36. data/app/views/proctolog/bursts/_side.html.erb +6 -5
  37. data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
  38. data/app/views/proctolog/notes/_card.html.erb +7 -3
  39. data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
  40. data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
  41. data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
  42. data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
  43. data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
  44. data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
  45. data/app/views/proctolog/shares/_line.html.erb +5 -0
  46. data/app/views/proctolog/shares/show.html.erb +48 -0
  47. data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
  48. data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
  49. data/bin/proctolog +154 -27
  50. data/completions/_proctolog +31 -6
  51. data/config/application.rb +23 -4
  52. data/config/initializers/grouse.rb +1 -1
  53. data/config/routes.rb +3 -0
  54. data/docs/manual/bursts.md +7 -2
  55. data/docs/manual/deciding.md +4 -1
  56. data/docs/manual/handover.md +6 -0
  57. data/docs/manual/pr-format.md +1 -1
  58. data/docs/manual/reading.md +54 -27
  59. data/docs/manual/setup.md +59 -0
  60. data/docs/manual/sharing.md +77 -0
  61. data/docs/manual/updates.md +11 -4
  62. data/lib/proctolog/changelog/news.rb +67 -0
  63. data/lib/proctolog/changelog.rb +75 -0
  64. data/lib/proctolog/installation.rb +19 -0
  65. data/lib/proctolog/version.rb +1 -1
  66. data/public/asking.js +31 -4
  67. data/public/decisions.js +4 -4
  68. data/public/desk.js +10 -6
  69. data/public/keys.js +6 -0
  70. data/public/notes.js +164 -58
  71. data/public/pick.css +21 -0
  72. data/public/respond.css +6 -0
  73. data/public/respond.js +51 -7
  74. data/public/review-drafts.js +47 -9
  75. data/public/review-rounds.js +27 -3
  76. data/public/share.css +58 -0
  77. data/public/side.js +11 -20
  78. data/public/terminal.css +295 -34
  79. data/public/viewer.js +700 -173
  80. metadata +22 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6eb6543861d7372fd74ab555d42c786fd2880d7f236a2a606cb8d8a69dda443
4
- data.tar.gz: 9c3785dbab77433adad9a7d388bd451864a213ca206cb3e99a919b9ade5773ce
3
+ metadata.gz: 7e70671fa7a287f7273aa19d34e110411abdaf6eaa53af119c538e6f9f558b83
4
+ data.tar.gz: 715e723cad15056e1c5e878e21f89895e10a8a970550a6eb84183e9ccfc2c84a
5
5
  SHA512:
6
- metadata.gz: 83f284129d2ab34f5235510c39b196a181158a5b853dcbe41b31b855f0400df548c41e0d631902e5b398a8f1fcb893856b0509d3bd70d7232ddbbf128cf62090
7
- data.tar.gz: 80cf8f0953c7f7badbc7c5ed7791b4211b2f9a5d9047bb7bb7c8a2117b2f6ff133edc409c12b46c948d58e03862a536cf94f5e27adea2b9715d55a649d5d4900
6
+ metadata.gz: ab6e65d6dffbb0cf8cfa2b1dd228879e3aae55f32f5118a77873c5e69fab28e840dfce9240cf44d88efc30e59a7cb4b8e271d800e987126e6c3514d60a718109
7
+ data.tar.gz: 38aa55282ff812f1eb8ff1fe22b6dbff1493f0debfe592c6b57f6e21a0cd6a2b7343ad86d45363202cfa3318070f9116a71039bffec371866a60c2cc8d1680f4
data/CHANGELOG.md CHANGED
@@ -1,11 +1,117 @@
1
1
  # Changelog
2
2
 
3
- What changed in each version of the `proctolog` gem, newest first. Versions
4
- are semantic ([docs/manual/updates.md](docs/manual/updates.md)); a release
5
- that asks something of you says what under **You do**.
3
+ What changed in each version of the `proctolog` gem, newest first: what it
4
+ adds, what it changes and what it fixes. Versions are semantic
5
+ ([docs/manual/updates.md](docs/manual/updates.md)); a release that asks
6
+ something of you says what under **You do**. Unreleased is what main has that
7
+ no gem has yet.
8
+
9
+ ## 0.3.0
10
+
11
+ ### Added
12
+
13
+ - `proctolog setup` is the one command to remember: it takes you from
14
+ wherever you are to an open desk, a step at a time. It links PRoctolog's
15
+ command, completion and skills, then goes through doctor's checks in the
16
+ order you meet them, each saying why it matters, checked again when you
17
+ press Enter, and stamped done or left for later. Outside a checkout, it
18
+ asks for one. Its sharing step says what Tailscale and ngrok each give, and
19
+ where to get them. Of anyone else's, it installs nothing
20
+ ([docs/manual/setup.md](docs/manual/setup.md)). A bare `proctolog`, and
21
+ `serve` outside a checkout, point to it.
22
+ - `proctolog update` says what's new since the version you had, from this
23
+ changelog: each change by its first sentence, under Added, Changed and
24
+ Fixed, and in full what you must do. It starts with the update after this
25
+ one, as the version you leave is the one that runs it.
26
+
27
+ ### Changed
28
+
29
+ - `proctolog install` is another name for `setup`: it links as before, then
30
+ takes you through the rest. After an update it only links, and says what's
31
+ new.
32
+ - `doctor` says how to install Chrome when it is missing.
33
+
34
+ ### Fixed
35
+
36
+ - `proctolog update` said it pulled a checkout forward, and to restart the
37
+ desk, when origin had nothing newer. It says the checkout is the latest now,
38
+ and changes nothing.
39
+ - On macOS 27 the desk took every port on this Mac for answering: Try it
40
+ showed the app up and never started it, `doctor` said port 4040 was taken,
41
+ and `proctolog share start` shared a desk that was not there.
42
+
43
+ ## 0.2.0
44
+
45
+ ### Added
46
+
47
+ - The desk is at <http://proctolog.localhost:4040>, a name of its own that a
48
+ browser's history finds from `procto`; `localhost:4040` still works.
49
+ - Share the desk with your phone or another computer, through Tailscale (on
50
+ your tailnet, which the sharing page names, as one account can be in
51
+ several) or ngrok (behind its sign-in with Google, for your address): from
52
+ the foot of the pane at the left, with `proctolog share`, or with
53
+ `proctolog serve --share`. `proctolog share status` and `doctor` say what
54
+ each one needs. Neither is installed for you.
55
+ - "Start here" heads the page of open pull requests until you have a burst of
56
+ your own, and whenever there is nothing to decide: how many are written for
57
+ PRoctolog and how to open one, how to have them rewritten when none is, or
58
+ how to have some written when nothing is open.
59
+ - **back to you** on a line of the Route, and `↩` on it in the pane, once the
60
+ agent's answers have come back for you to confirm or answer, or it
61
+ addressed a note of yours. The `↩` follows as you go, without the page
62
+ loading again.
63
+ - A panel beside the pictures of a change, in the viewer: the five ways to
64
+ look at them, each a button with its key, and the changed regions, counted
65
+ and numbered. `n` and `p`, or a press on a number, focus on a region and
66
+ keep your zoom; `z` goes near it and `0` back to the whole. The panel shows
67
+ the region as the before has it and as the after does, and Enter, or a
68
+ press on either, writes a note on it, in any view.
69
+ - Shift B sets blink going by itself, or stops it.
70
+ - Once the viewer shows only part of a picture, a map in its corner shows the
71
+ whole, with the part on the stage framed; a press on the map goes there.
72
+
73
+ ### Changed
74
+
75
+ - Decisions are beads, on the Route and in the pane at the left: a full-size
76
+ bead for a blocking decision, a small one for a non-blocking, solid once
77
+ reviewed, hollow while it waits. They take the place of the counts and of
78
+ `x`, `~` and `-`, and in the pane follow your choices as you make them.
79
+ - `N` opens the next pull request that asks for you: one with a blocking
80
+ decision left, or something back from the agent.
81
+ - With no burst open, the pane lists your bursts with how many decisions of
82
+ each grade wait, rather than how far each one is.
83
+ - In the viewer, a view's key again goes back to side by side. Changed regions
84
+ are outlined dashed and a note's box framed solid, so the two are told apart
85
+ at a glance.
86
+ - A note on a picture is saved where you are: the page doesn't load again or
87
+ move, zoomed in or not, and closing the viewer draws the notes there where
88
+ the page stands, rather than coming back at the last one.
89
+ - `bin/release` refuses a patch that adds something, and says when lines are
90
+ still left under Unreleased.
91
+
92
+ ### Fixed
93
+
94
+ - "Review alone" on a pull request in no stack saved nothing, and said the
95
+ page was older than the desk's last start.
96
+ - "Ask Claude now" on a pull request nobody has opened yet sent the session to
97
+ GitHub, which has nothing of it. It names the branch, its base and the file
98
+ its description is kept in now, and has the session read it with git.
99
+ - The note button on selected text sat away from the selection on a burst's
100
+ pages.
101
+ - A note of yours that an agent answered stayed open in the marks, for the
102
+ page's Notes and for an agent reading them. It follows its answer now:
103
+ addressed once the answer is in, confirmed once you confirm it, open again
104
+ once you answer back, and its card points you to Returned work.
105
+ - `bin/release` and the Release workflow read the changelog whatever the
106
+ locale: a `⌘` or `↩` in it stopped them where none was set.
107
+
108
+ **You do**: nothing, unless you want to share the desk: install Tailscale or
109
+ ngrok, as [docs/manual/sharing.md](docs/manual/sharing.md) says.
6
110
 
7
111
  ## 0.1.1
8
112
 
113
+ ### Changed
114
+
9
115
  - The Try it rail follows the reading column and its keys rather than the far
10
116
  edge of the window, and grows to 20rem on wide screens; its outline scrolls
11
117
  on its own, leaving the starter room.
@@ -18,6 +124,8 @@ that asks something of you says what under **You do**.
18
124
 
19
125
  The first one published as a gem.
20
126
 
127
+ ### Added
128
+
21
129
  - `rvx proctolog install`, or
22
130
  `gem install proctolog` and `proctolog install`, in place of cloning the
23
131
  repository. Under rv it runs on a Ruby 4 of its own, whatever Ruby the shell
data/README.md CHANGED
@@ -12,16 +12,16 @@ it works.
12
12
 
13
13
  ## The short version
14
14
 
15
- Install it with [rv](https://rv.dev), then run it from a checkout of the repo
16
- whose PRs you review:
15
+ With [rv](https://rv.dev), from a checkout of the repo whose PRs you review:
17
16
 
18
17
  ```sh
19
- rvx proctolog install
20
18
  cd path/to/your-repo
21
- proctolog doctor # fix what it marks !!
22
- proctolog serve # then open http://localhost:4040
19
+ rvx proctolog setup # a step at a time, to http://proctolog.localhost:4040
23
20
  ```
24
21
 
22
+ `setup` is the one command to remember: it links PRoctolog's own pieces, says
23
+ what to do about the rest, and opens the desk. `install` is another name for it.
24
+
25
25
  Pick a PR, a stack, or a few ticked ones, and press `?` once you're in. When
26
26
  your agent opens PRs, tell it to "write these PRs for PRoctolog", so they come
27
27
  with decisions to review. Everything below is the long version.
@@ -41,11 +41,10 @@ brew install diffle imagemagick ffmpeg
41
41
  With rv, whatever Ruby your shell or your repo runs:
42
42
 
43
43
  ```sh
44
- rvx proctolog install
45
- proctolog doctor
44
+ rvx proctolog setup
46
45
  ```
47
46
 
48
- rv gives PRoctolog a Ruby 4 and gems of its own, and `install` puts a
47
+ rv gives PRoctolog a Ruby 4 and gems of its own, and `setup` puts a
49
48
  `proctolog` in `~/.local/bin` that runs it through `rvx`. The apps you review
50
49
  keep their own Ruby: what the desk starts for them never sees PRoctolog's.
51
50
  rv installs from gem.coop, which mirrors RubyGems.
@@ -54,20 +53,24 @@ With Ruby 4.0 already in your shell:
54
53
 
55
54
  ```sh
56
55
  gem install proctolog
57
- proctolog install
58
- proctolog doctor
56
+ proctolog setup
59
57
  ```
60
58
 
61
- Either way `install` also links the zsh completion into `~/.zsh/completions`
62
- and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`. `doctor`
63
- says what's missing, a line each, with the fix: `!!` for what the desk can't do
59
+ Either way `setup` first links the zsh completion into `~/.zsh/completions`
60
+ and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`.
61
+ Then it takes the rest a step at a time: gh, the checkout, a first burst, what
62
+ turns features on, sharing the desk with your phone. Each step says why it
63
+ matters, checks again once you've acted, and is stamped done or left for
64
+ later; the last one opens the desk ([setting up](docs/manual/setup.md)). Run it
65
+ again whenever: what's done is stamped and passed. `doctor` asks the same
66
+ things all at once, a line each with the fix: `!!` for what the desk can't do
64
67
  without, `--` for what only turns a feature off.
65
68
 
66
69
  When a newer PRoctolog is out, the desk says so, and `proctolog update` takes
67
70
  it ([versions and updates](docs/manual/updates.md)).
68
71
 
69
72
  Working on PRoctolog itself? Run it from a checkout instead: clone it, then
70
- `bundle install && ruby bin/install`, which also puts a `proctolog` that runs
73
+ `bundle install && bin/proctolog setup`, which also puts a `proctolog` that runs
71
74
  the checkout in `~/.local/bin`. Keep the checkout where it is, since
72
75
  everything links back to it. The desk tells a checkout of a newer release
73
76
  too, and `proctolog update` pulls it forward.
@@ -79,7 +82,7 @@ cd path/to/your-repo
79
82
  proctolog serve
80
83
  ```
81
84
 
82
- Open <http://localhost:4040>. It lists the repo's open PRs: GitHub's stacks
85
+ Open <http://proctolog.localhost:4040>. It lists the repo's open PRs: GitHub's stacks
83
86
  whole, then the rest, yours first. Press "Review alone" on one, "Review the
84
87
  stack" on a stack, or tick a few and review them together. That's all the setup
85
88
  there is.
@@ -104,7 +107,7 @@ Your review is saved as you go, on your Mac under `~/.local/share/proctolog/`.
104
107
 
105
108
  PRoctolog finds decisions under a `## Decisions for the owner` heading, one
106
109
  checkbox line each. The `proctolog-pr-writing` skill teaches your agent the
107
- format, and `proctolog install` already put it where the agent looks. Tell it to
110
+ format, and `proctolog setup` already put it where the agent looks. Tell it to
108
111
  "write these PRs for PRoctolog", or "rewrite this PR's description for
109
112
  PRoctolog" for one that's open. Check a description before publishing it:
110
113
 
@@ -147,7 +150,7 @@ bundle config set --local with grouse && bundle install
147
150
 
148
151
  Then every page of the desk carries Grouse's overlay, at the bottom right: say or type what's wrong with the page you're
149
152
  on, and the note keeps the page, the templates that drew it, what you pointed
150
- at and, if you ask, a screenshot. Read them at <http://localhost:4040/grouse>.
153
+ at and, if you ask, a screenshot. Read them at <http://proctolog.localhost:4040/grouse>.
151
154
  An agent working on the desk reads them through Grouse's export API;
152
155
  [AGENTS.md](AGENTS.md) tells it how. They're kept under
153
156
  `~/.local/share/proctolog/grouse_notes`, one store for every repo you review.
@@ -156,15 +159,26 @@ A voice note is written out by ElevenLabs, and every note written up as an
156
159
  issue by Anthropic, with keys the desk reads from 1Password when it serves:
157
160
  `config/keys.yml` holds the references, never the keys, and `op` (1Password's
158
161
  CLI, signed in) reads them. Point them at your own items there if yours live
159
- elsewhere.
162
+ elsewhere. A trial desk, on marks of its own (`PROCTOLOG_MARKS`), reads none,
163
+ so an agent trying a change never has 1Password ask you to unlock it.
160
164
 
161
165
  Without a key a voice note arrives "audio only", with a recording that plays,
162
166
  and a note's page says "No structurer set up". `bin/rails grouse:doctor`, in the
163
167
  desk's checkout, says what Grouse is missing.
164
168
 
169
+ ## From your phone
170
+
171
+ `proctolog share` puts the desk where your other devices reach it and nobody
172
+ else does: through Tailscale, at your Mac's name on your tailnet, or through
173
+ ngrok, behind its sign-in with Google for your own address. The pane at the
174
+ left says whether it's shared, and opens the page that shares it. Neither is
175
+ installed for you; `proctolog share status` says what each one needs. More in
176
+ [the address, and sharing it](docs/manual/sharing.md).
177
+
165
178
  ## More
166
179
 
167
180
  - [Bursts, the route and where things live](docs/manual/bursts.md)
181
+ - [The address, and sharing it](docs/manual/sharing.md)
168
182
  - [How the desk runs your app](docs/manual/app.md)
169
183
  - [Reading a PR by keyboard](docs/manual/reading.md)
170
184
  - [Deciding](docs/manual/deciding.md)
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Proctolog
4
4
  class ApplicationController < ActionController::Base
5
- rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}", status: :service_unavailable }
5
+ rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}\n\nIf gh is not set up on this Mac yet, proctolog setup takes you through it.", status: :service_unavailable }
6
6
  rescue_from(ActionController::InvalidAuthenticityToken) do
7
- redirect_back_or_to root_path, alert: "This page was older than the desk's last start, so nothing was saved. Do it once more."
7
+ redirect_back_or_to root_path, alert: "The desk did not know this page any more, so nothing was saved. Do it once more."
8
8
  end
9
9
  rescue_from(Burst::Missing) { render plain: it.message, status: :not_found }
10
10
  # A button's press comes back to the page it was pressed on, with the
@@ -12,7 +12,7 @@ module Proctolog
12
12
  @route = @burst.route(fresh: params.key?(:fresh))
13
13
  return redirect_to(burst_path(@burst)) if params.key?(:fresh)
14
14
 
15
- @taken_in = @burst.reports.take_in
15
+ @taken_in = @burst.take_in
16
16
  @standings = @burst.standings(@route)
17
17
  @notices = @burst.notices
18
18
  @rules = @burst.rules.select { it.within?(focus) }
@@ -12,11 +12,13 @@ module Proctolog
12
12
  def show = render(json: {document: choices.document})
13
13
 
14
14
  # A desk from before bursts had their name reads `review`, so both are
15
- # written: one rolled back can still read what this one saved.
15
+ # written: one rolled back can still read what this one saved. What the
16
+ # owner said of an agent's answers moves the notes they answer at once.
16
17
  def update
17
18
  incoming = JSON.parse(request.raw_post)
18
19
  mine?(incoming) or raise KeyError
19
20
  choices.keep(incoming.merge("burst" => @burst.slug, "review" => @burst.slug))
21
+ @burst.take_in
20
22
  render json: {revision: choices.revision}
21
23
  end
22
24
 
@@ -13,7 +13,7 @@ module Proctolog
13
13
  return redirect_to(burst_decisions_path(@burst, show: params[:show]))
14
14
  end
15
15
 
16
- @burst.reports.take_in
16
+ @burst.take_in
17
17
  # Listed and counted within the focus; the stars and ticks it sends are
18
18
  # found among the whole burst's.
19
19
  @decisions = @burst.decisions(pages:, focus:)
@@ -10,8 +10,8 @@ module Proctolog
10
10
 
11
11
  before_action { @burst = Burst.find(params.expect(:burst_id)) }
12
12
 
13
- # A note written in the picture viewer is saved without leaving it: the
14
- # answer says where the page shows it, for once the viewer closes.
13
+ # A note on a picture, on the page or in the viewer, is saved without
14
+ # leaving either: the answer names the note and where the page shows it.
15
15
  def create
16
16
  note = marks.pin(kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip, place:)
17
17
  respond_to do |format|
@@ -8,7 +8,7 @@ module Proctolog
8
8
  before_action { @burst = Burst.find(params.expect(:burst_id)) }
9
9
 
10
10
  def show
11
- @taken_in = @burst.reports.take_in
11
+ @taken_in = @burst.take_in
12
12
  @handover = @burst.handover(@burst.route, focus:)
13
13
  end
14
14
 
@@ -11,13 +11,11 @@ module Proctolog
11
11
  @route = @burst.route(fresh: params.key?(:fresh))
12
12
  return redirect_to(burst_pull_request_path(@burst, params.expect(:number))) if params.key?(:fresh)
13
13
 
14
- @burst.reports.take_in
14
+ @burst.take_in
15
15
  @stop = @route.at(PullRequest.number(params.expect(:number))) or return render(plain: "##{params[:number]} is not part of #{@burst.name}.", status: :not_found)
16
16
  standings = @burst.standings(@route)
17
17
  @standing = standings.fetch(@stop)
18
- # Next to review is the next with blocking decisions left: the non-blocking
19
- # wait for one gesture, and do not hold up deciding.
20
- @next_to_review = focus.round_from(@stop).find { standings.fetch(it).progress.blocking_left.positive? }
18
+ @next_to_review = focus.round_from(@stop).find { standings.fetch(it).asks_for_owner? }
21
19
  @since = @burst.since(@stop) if @standing.changed?
22
20
  @scenarios = @burst.scenarios.select { it.pr == @stop.number }
23
21
  @rendering = Rendering.new(@burst.rendering(@stop.number), notes: @standing.marks.notes, pages: pages, linked: @stop.description.linked)
@@ -2,10 +2,12 @@
2
2
 
3
3
  module Proctolog
4
4
  # What the pages' script needs of the burst's review rounds, built from the
5
- # whole burst; with `pr`, that pull request's descriptions too.
5
+ # whole burst; with `pr`, that pull request's descriptions too. Answers an
6
+ # agent wrote since are taken in first, as a page's load takes them in.
6
7
  class ReviewDataController < ApplicationController
7
8
  def show
8
9
  @burst = Burst.find(params.expect(:burst_id))
10
+ @burst.take_in
9
11
  render json: helpers.round_data(@burst, pr: params[:pr]&.then { PullRequest.number(it) })
10
12
  end
11
13
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # The page that shares the desk with the owner's other devices, through
5
+ # whichever of Tailscale and ngrok this Mac has, and stops it (Share). A
6
+ # press comes back to it, with the sentence; `back` is the page it was
7
+ # opened from.
8
+ class SharesController < ApplicationController
9
+ before_action { @back = params[:back].to_s.then { (it.start_with?("/") && !it.start_with?("//")) ? it : root_path } }
10
+ rescue_from(Share::Refused) { redirect_to share_path(back: @back), alert: it.message }
11
+
12
+ def show
13
+ @share = Share.at(Desk.port)
14
+ @ways = Share.ways
15
+ end
16
+
17
+ def create
18
+ share = Share.open(Desk.port, via: params[:via].presence, allow: params[:allow]&.split(/[\s,]+/))
19
+ redirect_to share_path(back: @back), notice: share.to_s
20
+ end
21
+
22
+ def destroy
23
+ Share.at(Desk.port)&.close
24
+ redirect_to share_path(back: @back), notice: "The desk is only on this Mac again."
25
+ end
26
+ end
27
+ end
@@ -9,7 +9,11 @@ module Proctolog
9
9
  # focus is never taken for one whose decision is gone.
10
10
  # What the inventory holds changed with the desk: a page built from an
11
11
  # older one would read its decisions wrong.
12
- INVENTORY = 3
12
+ INVENTORY = 4
13
+
14
+ # Where the description of a pull request nobody has opened yet is kept,
15
+ # which an agent reads in place of GitHub's.
16
+ def unopened_file(stop) = (Unopened.find(stop.number)&.file&.to_s if stop.pull_request.unopened?)
13
17
 
14
18
  # The pictures' files and pixel signatures are looked up at each page,
15
19
  # not kept with the rest: a picture fetched since the reading is known.
@@ -20,7 +24,7 @@ module Proctolog
20
24
 
21
25
  def draft_decision(box)
22
26
  {id: box.to_param, pr: box.stop.number, title: box.stop.title, url: box.stop.url, branch: box.stop.pull_request.head,
23
- head: box.stop.head_oid, description: box.stop.description.digest, position: box.position, lead: box.lead,
27
+ base: box.stop.pull_request.base, file: unopened_file(box.stop), head: box.stop.head_oid, description: box.stop.description.digest, position: box.position, lead: box.lead,
24
28
  text: box.decision.text, key: box.decision.key, ticked: box.decision.ticked, protected: @burst.refusal(box.stop.number).present?,
25
29
  grade: box.decision.grade, theme: box.decision.theme_name, pictures: box.decision.pictures.map(&:to_h)}
26
30
  end
@@ -13,17 +13,48 @@ module Proctolog
13
13
  time.strftime((time.to_date == Date.current) ? "at %H:%M" : "on %-d %b at %H:%M")
14
14
  end
15
15
 
16
- # How far one pull request is, as the side pane marks its row: every
17
- # decision reviewed, some, none yet (which takes no mark), or none to
18
- # review at all. The page's script writes the same marks as choices are made.
19
- def side_mark(progress)
20
- if !progress.any? then "-"
21
- elsif progress.done? then "x"
22
- elsif progress.reviewed.positive? then "~"
16
+ # A pull request's decisions as beads, the way deciding draws them: a
17
+ # bead to each blocking decision and a smaller one to each non-blocking,
18
+ # solid once reviewed and hollow while it waits. Each grade is labelled
19
+ # in words, which the pane's card shows. The page's script draws the same
20
+ # as choices are made (beadsOf in review-drafts.js).
21
+ def decision_beads(progress)
22
+ return unless progress.any?
23
+
24
+ tag.span(class: "beads") do
25
+ safe_join(progress.grades.select { it.total.positive? }.map do |grade|
26
+ tag.span(class: grade.name, role: "img", aria: {label: reviewed_in_words(grade)}) do
27
+ safe_join(Array.new(grade.total) { tag.span(class: ["bead", ("reviewed" if it < grade.reviewed)]) })
28
+ end
29
+ end)
30
+ end
31
+ end
32
+
33
+ # A whole burst has too many decisions for a bead each: a hollow bead of
34
+ # each grade that waits, and how many of it.
35
+ def decisions_left(progress)
36
+ waiting = progress.grades.select { it.left.positive? }
37
+ return if waiting.empty?
38
+
39
+ tag.span(class: "beads") do
40
+ safe_join(waiting.map do |grade|
41
+ tag.span(class: [grade.name, "count"], role: "img", aria: {label: "#{pluralize(grade.left, "#{grade.name} decision")} to review"}) do
42
+ tag.span(class: "bead") + grade.left.to_s
43
+ end
44
+ end)
23
45
  end
24
46
  end
25
47
 
26
- def side_fraction(progress) = ("#{progress.reviewed}/#{progress.total}" if progress.any?)
48
+ # The pane's mark for a pull request the agent's answers came back on,
49
+ # labelled with what came back (Standing#for_the_owner). The page's
50
+ # script marks the pane again from the same words as answers are judged.
51
+ def side_mark(came_back)
52
+ return tag.span(class: "mark", data: {side_mark: ""}) if came_back.empty?
53
+
54
+ tag.span("↩", class: "mark", data: {side_mark: ""}, role: "img", aria: {label: back_in_words(came_back)})
55
+ end
56
+
57
+ def back_in_words(came_back) = "back to you: #{came_back.to_sentence}"
27
58
 
28
59
  # The whole burst in one line: "6 of 439 decisions reviewed" over a bar.
29
60
  # Focused on a part, what is left in it against the burst: "120 / 380
@@ -57,5 +88,14 @@ module Proctolog
57
88
  private
58
89
 
59
90
  def keycaps(pressed) = safe_join(pressed.split.map { tag.kbd(it) })
91
+
92
+ # "1 of 4 blocking decisions reviewed".
93
+ def reviewed_in_words(grade)
94
+ decisions = "#{grade.name} decision".pluralize(grade.total)
95
+ if grade.left.zero? then (grade.total == 1) ? "its one #{decisions} reviewed" : "all #{grade.total} #{decisions} reviewed"
96
+ elsif grade.reviewed.zero? then "none of #{grade.total} #{decisions} reviewed yet"
97
+ else "#{grade.reviewed} of #{grade.total} #{decisions} reviewed"
98
+ end
99
+ end
60
100
  end
61
101
  end
@@ -7,13 +7,15 @@ module Proctolog
7
7
  # which a page takes in as its own so that it knows the answer to a pass
8
8
  # an agent took itself. On a pull request's page, its description now
9
9
  # and as each handover the desk kept found it, by batch, so the page can
10
- # say what changed since.
10
+ # say what changed since. `back` names the pull requests something came
11
+ # back on, in the words of the pane's mark.
11
12
  def round_data(burst, pr: nil)
12
13
  route = burst.route
13
14
  rounds = Rounds.new(burst, route:)
14
15
  responses = ResponseFiles.new(burst)
16
+ back = burst.standings(route).filter_map { |stop, standing| [stop.number, back_in_words(standing.for_the_owner)] if standing.back_to_owner? }.to_h
15
17
  {requests: rounds.requests, batches: Handover.kept(burst).map(&:batch), reports: responses.all, errors: responses.errors,
16
- responseFolder: responses.folder.to_s, heads: rounds.heads,
18
+ responseFolder: responses.folder.to_s, heads: rounds.heads, back:,
17
19
  **(route.at(pr)&.then { |stop| {descriptions: descriptions_of(burst, stop)} } || {})}
18
20
  end
19
21
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ module SharesHelper
5
+ # A way's sentence, with the addresses in it to follow: where to download
6
+ # it, or where to turn something on.
7
+ def linked_sentence(sentence)
8
+ safe_join(sentence.split(%r{(https?://[^\s,]*[^\s,.])}).map { it.start_with?("http") ? link_to(it, it, target: "_blank", rel: "noopener") : it })
9
+ end
10
+ end
11
+ end
@@ -34,11 +34,12 @@ module Proctolog
34
34
  safe_join([safe_join(parts, ", "), "."])
35
35
  end
36
36
 
37
- # What a line of the route says at its right: whose the pull request is when
38
- # it is not the owner's, then how far its decisions are.
37
+ # What a line of the route says at its right: whose the pull request is,
38
+ # "back to you" when the owner's turn is what came back from the agent,
39
+ # then its decisions as beads.
39
40
  def beside_a_line(standing)
40
- whose = {agent: "with the agent", settled: "settled", waiting: "waiting"}[standing.turn]
41
- safe_join([whose, (tag.span(side_fraction(standing.progress), class: "fraction") if standing.progress.any?)].compact, " · ")
41
+ whose = {owner: (tag.strong("back to you") if standing.back_to_owner?), agent: "with the agent", settled: "settled", waiting: "waiting"}.fetch(standing.turn)
42
+ safe_join([whose, decision_beads(standing.progress)].compact, " ")
42
43
  end
43
44
 
44
45
  def verdict_of(condition) = VERDICTS.fetch(condition.met)
@@ -124,14 +124,9 @@ module Proctolog
124
124
  def decisions(pages: {}, focus: route) = Decisions.new(route: focus, marks: method(:marks), renderings: method(:rendering), named: @waiting, pages:)
125
125
 
126
126
  # Where every stop of the route stands, in its order: each layer knows the
127
- # standing of the one it sits on.
128
- def standings(route)
129
- progress, scenarios = progress(route), scenarios_on(route)
130
- route.each_with_object({}) do |stop, standings|
131
- standings[stop] = Standing.new(stop:, marks: marks(stop.number), progress: progress.fetch(stop.number), under: route.under(stop)&.then { standings[it] },
132
- above: route.above(stop), scenarios: scenarios.select { it.pr == stop.number }.map(&:name), named: @before_it_goes.fetch(stop.number, []))
133
- end
134
- end
127
+ # standing of the one it sits on. Worked out once for a reading of a
128
+ # page, which asks for it from the page and from its pane.
129
+ def standings(route) = (@standings ||= {}.compare_by_identity)[route] ||= standings_on(route)
135
130
 
136
131
  # What moved in a pull request since the owner's "good to go", to show
137
132
  # when its standing says something did.
@@ -143,6 +138,11 @@ module Proctolog
143
138
 
144
139
  def reports = Reports.new(self)
145
140
 
141
+ # What came back from agents, taken into the marks: a report moves the
142
+ # note it names, and a handover's answers move theirs as the owner judges
143
+ # them (Rounds#move_notes). Answers the notes the reports moved.
144
+ def take_in = reports.take_in.tap { Rounds.new(self, route:).move_notes }
145
+
146
146
  # A description's picture as a file on this Mac, once the desk has shown it.
147
147
  def picture_file(asset) = (Project.current.pictures.glob("#{asset}.*").first if Picture.asset?(asset.to_s))
148
148
 
@@ -246,6 +246,15 @@ module Proctolog
246
246
 
247
247
  private
248
248
 
249
+ def standings_on(route)
250
+ progress, scenarios, returned = progress(route), scenarios_on(route), Rounds.new(self, route:).returned
251
+ route.each_with_object({}) do |stop, standings|
252
+ standings[stop] = Standing.new(stop:, marks: marks(stop.number), progress: progress.fetch(stop.number),
253
+ returned: returned.fetch(stop.number, Rounds::Returned.new), under: route.under(stop)&.then { standings[it] },
254
+ above: route.above(stop), scenarios: scenarios.select { it.pr == stop.number }.map(&:name), named: @before_it_goes.fetch(stop.number, []))
255
+ end
256
+ end
257
+
249
258
  # The pictures its descriptions show, each by its id and its kind, as GitHub's HTML addresses them.
250
259
  def pictures_of(route) = route.flat_map { Rendering.pictures(rendering(it.number)) }
251
260
 
@@ -9,6 +9,12 @@ module Proctolog
9
9
  # A writing older than the one kept: another page moved on since.
10
10
  Stale = Class.new(StandardError)
11
11
 
12
+ # One grade's share of a pull request's decisions: "blocking" or
13
+ # "non-blocking", how many it has, and how many of them wait.
14
+ Grade = Data.define(:name, :total, :left) do
15
+ def reviewed = total - left
16
+ end
17
+
12
18
  # How far one pull request's decisions are: a decision is reviewed once
13
19
  # the owner chose on its present words, or it is ticked on GitHub. Its
14
20
  # blocking decisions are counted apart: those left of them are what
@@ -31,6 +37,8 @@ module Proctolog
31
37
  def non_blocking = total - blocking
32
38
  def non_blocking_left = left - blocking_left
33
39
 
40
+ def grades = [Grade.new(name: "blocking", total: blocking, left: blocking_left), Grade.new(name: "non-blocking", total: non_blocking, left: non_blocking_left)]
41
+
34
42
  def +(other) = with(**members.to_h { [it, public_send(it) + other.public_send(it)] })
35
43
  end
36
44