proctolog 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -3
- data/README.md +15 -4
- data/app/controllers/proctolog/application_controller.rb +1 -1
- data/app/controllers/proctolog/bursts_controller.rb +1 -1
- data/app/controllers/proctolog/choices_controller.rb +3 -1
- data/app/controllers/proctolog/decisions_controller.rb +1 -1
- data/app/controllers/proctolog/notes_controller.rb +2 -2
- data/app/controllers/proctolog/prompts_controller.rb +1 -1
- data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
- data/app/controllers/proctolog/review_data_controller.rb +3 -1
- data/app/controllers/proctolog/shares_controller.rb +27 -0
- data/app/helpers/proctolog/decisions_helper.rb +6 -2
- data/app/helpers/proctolog/desk_helper.rb +48 -8
- data/app/helpers/proctolog/rounds_helper.rb +4 -2
- data/app/helpers/proctolog/shares_helper.rb +11 -0
- data/app/helpers/proctolog/standings_helper.rb +5 -4
- data/app/models/proctolog/burst.rb +17 -8
- data/app/models/proctolog/choices.rb +8 -0
- data/app/models/proctolog/doctor.rb +9 -1
- data/app/models/proctolog/github.rb +1 -1
- data/app/models/proctolog/marks.rb +1 -1
- data/app/models/proctolog/note.rb +4 -0
- data/app/models/proctolog/owner.rb +3 -0
- data/app/models/proctolog/pick.rb +12 -0
- data/app/models/proctolog/rounds.rb +37 -0
- data/app/models/proctolog/share/ngrok.rb +120 -0
- data/app/models/proctolog/share/settings.rb +23 -0
- data/app/models/proctolog/share/tailscale.rb +103 -0
- data/app/models/proctolog/share.rb +101 -0
- data/app/models/proctolog/standing.rb +24 -7
- data/app/views/proctolog/bursts/_line.html.erb +2 -2
- data/app/views/proctolog/bursts/_side.html.erb +6 -5
- data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
- data/app/views/proctolog/notes/_card.html.erb +7 -3
- data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
- data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
- data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
- data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
- data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
- data/app/views/proctolog/shares/_line.html.erb +5 -0
- data/app/views/proctolog/shares/show.html.erb +48 -0
- data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
- data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
- data/bin/proctolog +92 -6
- data/completions/_proctolog +28 -4
- data/config/application.rb +23 -4
- data/config/initializers/grouse.rb +1 -1
- data/config/routes.rb +3 -0
- data/docs/manual/bursts.md +7 -2
- data/docs/manual/deciding.md +4 -1
- data/docs/manual/handover.md +6 -0
- data/docs/manual/reading.md +54 -27
- data/docs/manual/sharing.md +77 -0
- data/lib/proctolog/changelog.rb +50 -0
- data/lib/proctolog/version.rb +1 -1
- data/public/asking.js +31 -4
- data/public/decisions.js +4 -4
- data/public/desk.js +10 -6
- data/public/keys.js +6 -0
- data/public/notes.js +164 -58
- data/public/pick.css +21 -0
- data/public/respond.css +6 -0
- data/public/respond.js +51 -7
- data/public/review-drafts.js +47 -9
- data/public/review-rounds.js +27 -3
- data/public/share.css +58 -0
- data/public/side.js +11 -20
- data/public/terminal.css +295 -34
- data/public/viewer.js +700 -173
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efc3995bd997f8129593d45bbe4de75341a7b27f43401cb329566ac552baffb5
|
|
4
|
+
data.tar.gz: 47456fcf6f060778f085e96a41d015e8d2b14c6d3e450178f8122b7bd6295bf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0d7b8c993fe32e7a08c4212de1a30f287c10e0b1ce33a4d58a85c557f172f55a82196098ec6a8b5d894b2c003733809599e1db279a85a67ade37e680de2dae3
|
|
7
|
+
data.tar.gz: 3b38800d1b7bd1ef1aeb3dc364a87510ab3bf55837c8f3827b5ccc509c870aef742b5a78bbefbe3d2a6acb92cf131e7fde0b6d317e39686dbcef19dc2422dcf2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
What changed in each version of the `proctolog` gem, newest first
|
|
4
|
-
|
|
5
|
-
|
|
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.2.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The desk is at <http://proctolog.localhost:4040>, a name of its own that a
|
|
14
|
+
browser's history finds from `procto`; `localhost:4040` still works.
|
|
15
|
+
- Share the desk with your phone or another computer, through Tailscale (on
|
|
16
|
+
your tailnet, which the sharing page names, as one account can be in
|
|
17
|
+
several) or ngrok (behind its sign-in with Google, for your address): from
|
|
18
|
+
the foot of the pane at the left, with `proctolog share`, or with
|
|
19
|
+
`proctolog serve --share`. `proctolog share status` and `doctor` say what
|
|
20
|
+
each one needs. Neither is installed for you.
|
|
21
|
+
- "Start here" heads the page of open pull requests until you have a burst of
|
|
22
|
+
your own, and whenever there is nothing to decide: how many are written for
|
|
23
|
+
PRoctolog and how to open one, how to have them rewritten when none is, or
|
|
24
|
+
how to have some written when nothing is open.
|
|
25
|
+
- **back to you** on a line of the Route, and `↩` on it in the pane, once the
|
|
26
|
+
agent's answers have come back for you to confirm or answer, or it
|
|
27
|
+
addressed a note of yours. The `↩` follows as you go, without the page
|
|
28
|
+
loading again.
|
|
29
|
+
- A panel beside the pictures of a change, in the viewer: the five ways to
|
|
30
|
+
look at them, each a button with its key, and the changed regions, counted
|
|
31
|
+
and numbered. `n` and `p`, or a press on a number, focus on a region and
|
|
32
|
+
keep your zoom; `z` goes near it and `0` back to the whole. The panel shows
|
|
33
|
+
the region as the before has it and as the after does, and Enter, or a
|
|
34
|
+
press on either, writes a note on it, in any view.
|
|
35
|
+
- Shift B sets blink going by itself, or stops it.
|
|
36
|
+
- Once the viewer shows only part of a picture, a map in its corner shows the
|
|
37
|
+
whole, with the part on the stage framed; a press on the map goes there.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Decisions are beads, on the Route and in the pane at the left: a full-size
|
|
42
|
+
bead for a blocking decision, a small one for a non-blocking, solid once
|
|
43
|
+
reviewed, hollow while it waits. They take the place of the counts and of
|
|
44
|
+
`x`, `~` and `-`, and in the pane follow your choices as you make them.
|
|
45
|
+
- `N` opens the next pull request that asks for you: one with a blocking
|
|
46
|
+
decision left, or something back from the agent.
|
|
47
|
+
- With no burst open, the pane lists your bursts with how many decisions of
|
|
48
|
+
each grade wait, rather than how far each one is.
|
|
49
|
+
- In the viewer, a view's key again goes back to side by side. Changed regions
|
|
50
|
+
are outlined dashed and a note's box framed solid, so the two are told apart
|
|
51
|
+
at a glance.
|
|
52
|
+
- A note on a picture is saved where you are: the page doesn't load again or
|
|
53
|
+
move, zoomed in or not, and closing the viewer draws the notes there where
|
|
54
|
+
the page stands, rather than coming back at the last one.
|
|
55
|
+
- `bin/release` refuses a patch that adds something, and says when lines are
|
|
56
|
+
still left under Unreleased.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- "Review alone" on a pull request in no stack saved nothing, and said the
|
|
61
|
+
page was older than the desk's last start.
|
|
62
|
+
- "Ask Claude now" on a pull request nobody has opened yet sent the session to
|
|
63
|
+
GitHub, which has nothing of it. It names the branch, its base and the file
|
|
64
|
+
its description is kept in now, and has the session read it with git.
|
|
65
|
+
- The note button on selected text sat away from the selection on a burst's
|
|
66
|
+
pages.
|
|
67
|
+
- A note of yours that an agent answered stayed open in the marks, for the
|
|
68
|
+
page's Notes and for an agent reading them. It follows its answer now:
|
|
69
|
+
addressed once the answer is in, confirmed once you confirm it, open again
|
|
70
|
+
once you answer back, and its card points you to Returned work.
|
|
71
|
+
- `bin/release` and the Release workflow read the changelog whatever the
|
|
72
|
+
locale: a `⌘` or `↩` in it stopped them where none was set.
|
|
73
|
+
|
|
74
|
+
**You do**: nothing, unless you want to share the desk: install Tailscale or
|
|
75
|
+
ngrok, as [docs/manual/sharing.md](docs/manual/sharing.md) says.
|
|
6
76
|
|
|
7
77
|
## 0.1.1
|
|
8
78
|
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
9
81
|
- The Try it rail follows the reading column and its keys rather than the far
|
|
10
82
|
edge of the window, and grows to 20rem on wide screens; its outline scrolls
|
|
11
83
|
on its own, leaving the starter room.
|
|
@@ -18,6 +90,8 @@ that asks something of you says what under **You do**.
|
|
|
18
90
|
|
|
19
91
|
The first one published as a gem.
|
|
20
92
|
|
|
93
|
+
### Added
|
|
94
|
+
|
|
21
95
|
- `rvx proctolog install`, or
|
|
22
96
|
`gem install proctolog` and `proctolog install`, in place of cloning the
|
|
23
97
|
repository. Under rv it runs on a Ruby 4 of its own, whatever Ruby the shell
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ whose PRs you review:
|
|
|
19
19
|
rvx proctolog install
|
|
20
20
|
cd path/to/your-repo
|
|
21
21
|
proctolog doctor # fix what it marks !!
|
|
22
|
-
proctolog serve # then open http://localhost:4040
|
|
22
|
+
proctolog serve # then open http://proctolog.localhost:4040
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Pick a PR, a stack, or a few ticked ones, and press `?` once you're in. When
|
|
@@ -79,7 +79,7 @@ cd path/to/your-repo
|
|
|
79
79
|
proctolog serve
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Open <http://localhost:4040>. It lists the repo's open PRs: GitHub's stacks
|
|
82
|
+
Open <http://proctolog.localhost:4040>. It lists the repo's open PRs: GitHub's stacks
|
|
83
83
|
whole, then the rest, yours first. Press "Review alone" on one, "Review the
|
|
84
84
|
stack" on a stack, or tick a few and review them together. That's all the setup
|
|
85
85
|
there is.
|
|
@@ -147,7 +147,7 @@ bundle config set --local with grouse && bundle install
|
|
|
147
147
|
|
|
148
148
|
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
149
|
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>.
|
|
150
|
+
at and, if you ask, a screenshot. Read them at <http://proctolog.localhost:4040/grouse>.
|
|
151
151
|
An agent working on the desk reads them through Grouse's export API;
|
|
152
152
|
[AGENTS.md](AGENTS.md) tells it how. They're kept under
|
|
153
153
|
`~/.local/share/proctolog/grouse_notes`, one store for every repo you review.
|
|
@@ -156,15 +156,26 @@ A voice note is written out by ElevenLabs, and every note written up as an
|
|
|
156
156
|
issue by Anthropic, with keys the desk reads from 1Password when it serves:
|
|
157
157
|
`config/keys.yml` holds the references, never the keys, and `op` (1Password's
|
|
158
158
|
CLI, signed in) reads them. Point them at your own items there if yours live
|
|
159
|
-
elsewhere.
|
|
159
|
+
elsewhere. A trial desk, on marks of its own (`PROCTOLOG_MARKS`), reads none,
|
|
160
|
+
so an agent trying a change never has 1Password ask you to unlock it.
|
|
160
161
|
|
|
161
162
|
Without a key a voice note arrives "audio only", with a recording that plays,
|
|
162
163
|
and a note's page says "No structurer set up". `bin/rails grouse:doctor`, in the
|
|
163
164
|
desk's checkout, says what Grouse is missing.
|
|
164
165
|
|
|
166
|
+
## From your phone
|
|
167
|
+
|
|
168
|
+
`proctolog share` puts the desk where your other devices reach it and nobody
|
|
169
|
+
else does: through Tailscale, at your Mac's name on your tailnet, or through
|
|
170
|
+
ngrok, behind its sign-in with Google for your own address. The pane at the
|
|
171
|
+
left says whether it's shared, and opens the page that shares it. Neither is
|
|
172
|
+
installed for you; `proctolog share status` says what each one needs. More in
|
|
173
|
+
[the address, and sharing it](docs/manual/sharing.md).
|
|
174
|
+
|
|
165
175
|
## More
|
|
166
176
|
|
|
167
177
|
- [Bursts, the route and where things live](docs/manual/bursts.md)
|
|
178
|
+
- [The address, and sharing it](docs/manual/sharing.md)
|
|
168
179
|
- [How the desk runs your app](docs/manual/app.md)
|
|
169
180
|
- [Reading a PR by keyboard](docs/manual/reading.md)
|
|
170
181
|
- [Deciding](docs/manual/deciding.md)
|
|
@@ -4,7 +4,7 @@ module Proctolog
|
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
5
|
rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}", status: :service_unavailable }
|
|
6
6
|
rescue_from(ActionController::InvalidAuthenticityToken) do
|
|
7
|
-
redirect_back_or_to root_path, alert: "
|
|
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.
|
|
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.
|
|
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
|
|
14
|
-
# answer
|
|
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|
|
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
#
|
|
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"}
|
|
41
|
-
safe_join([whose, (
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
|
@@ -16,7 +16,7 @@ module Proctolog
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
NEEDS = %i[ruby gh launcher checkout repository bursts].freeze
|
|
19
|
-
NICETIES = %i[release skills chrome diffle imagemagick ffmpeg app port].freeze
|
|
19
|
+
NICETIES = %i[release skills chrome diffle imagemagick ffmpeg app share port].freeze
|
|
20
20
|
|
|
21
21
|
# project is nil when the directory is no Git checkout.
|
|
22
22
|
def initialize(project, home: Dir.home, path: ENV.fetch("PATH", ""))
|
|
@@ -104,6 +104,14 @@ module Proctolog
|
|
|
104
104
|
fine("app.yml names #{app.parts.to_sentence}")
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
# Tailscale or ngrok, either of which shares the desk with the owner's phone.
|
|
108
|
+
def share
|
|
109
|
+
ready, unready = Share.ways(path: @path.join(File::PATH_SEPARATOR)).partition(&:ready?)
|
|
110
|
+
return fine("Sharing through #{ready.map(&:name).to_sentence}: proctolog share") if ready.any?
|
|
111
|
+
|
|
112
|
+
off("Nothing shares the desk beyond this Mac. #{unready.map { "#{it.unready}." }.join(" ")}")
|
|
113
|
+
end
|
|
114
|
+
|
|
107
115
|
def port = answers?(4040) ? off("Port 4040 answers already: a desk may be open there, or serve --port 4041") : fine("Port 4040 is free")
|
|
108
116
|
|
|
109
117
|
def fine(line) = Finding.new(:fine, line)
|
|
@@ -240,7 +240,7 @@ module Proctolog
|
|
|
240
240
|
# when the owner presses the button that shows this command first. An
|
|
241
241
|
# alternate marks store remains a trial, even when a data directory moves.
|
|
242
242
|
def rewrite(number, text)
|
|
243
|
-
(
|
|
243
|
+
(Desk.trial? || ENV.key?("PROCTOLOG_READ_ONLY")) and raise Unreachable, "A trial or read-only desk does not write to GitHub."
|
|
244
244
|
|
|
245
245
|
Tempfile.create(["description", ".md"]) do |file|
|
|
246
246
|
file.write(text)
|
|
@@ -20,7 +20,7 @@ module Proctolog
|
|
|
20
20
|
def self.root = Pathname(ENV.fetch("PROCTOLOG_MARKS") { Project.current.marks })
|
|
21
21
|
|
|
22
22
|
# A desk with marks of its own is a trial, whose marks are not the owner's.
|
|
23
|
-
def self.trial? =
|
|
23
|
+
def self.trial? = Desk.trial?
|
|
24
24
|
|
|
25
25
|
def self.directory_of(burst) = root.join(burst.slug)
|
|
26
26
|
|
|
@@ -82,6 +82,10 @@ module Proctolog
|
|
|
82
82
|
# Still needs someone: the agent while it is open, the owner once addressed.
|
|
83
83
|
def live? = open? || addressed?
|
|
84
84
|
|
|
85
|
+
# Answered by an agent in a handover: the owner judges the answer under
|
|
86
|
+
# Returned work, which moves the note (Rounds#move_notes).
|
|
87
|
+
def answered_in_a_handover? = reply&.key?("response")
|
|
88
|
+
|
|
85
89
|
# The decision of a description that this note answers otherwise: a change
|
|
86
90
|
# asked for on words of a box. A question or a remark leaves the box open.
|
|
87
91
|
def answers_in(description) = (description.decision_quoted_by(place) if change? && (place in Quote[section: Description::DECISIONS]))
|
|
@@ -11,6 +11,9 @@ module Proctolog
|
|
|
11
11
|
|
|
12
12
|
def self.git_name = Open3.capture2("git", "config", "--get", "user.name").first.strip
|
|
13
13
|
|
|
14
|
+
# The address ngrok lets in when the owner named none (Share::Settings).
|
|
15
|
+
def self.git_email = Open3.capture2("git", "config", "--get", "user.email").first.strip
|
|
16
|
+
|
|
14
17
|
def first_name = name.split.first
|
|
15
18
|
end
|
|
16
19
|
end
|
|
@@ -32,6 +32,18 @@ module Proctolog
|
|
|
32
32
|
|
|
33
33
|
def empty? = @pull_requests.empty? && unopened.empty?
|
|
34
34
|
|
|
35
|
+
# Those of every kind with decisions for the reviewer, open or not.
|
|
36
|
+
def written = [*@pull_requests, *unopened].select { written?(it) }
|
|
37
|
+
|
|
38
|
+
# What the page first tells someone who has not reviewed here yet, or
|
|
39
|
+
# who would otherwise find nothing to do: nil once neither is true.
|
|
40
|
+
def start(first_time:)
|
|
41
|
+
if empty? then :nothing_open
|
|
42
|
+
elsif written.none? then :nothing_written
|
|
43
|
+
elsif first_time then :first_time
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
# Whether a pull request nobody has opened yet sits on another pull
|
|
36
48
|
# request's branch, so that it tops a chain.
|
|
37
49
|
def chained?(pull_request) = [*@pull_requests, *unopened].any? { it.head == pull_request.base }
|