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
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "yaml"
5
+
6
+ module Proctolog
7
+ # The desk reached from away from this Mac, by its owner alone: from their
8
+ # phone, or a computer elsewhere, through whichever of Tailscale or ngrok
9
+ # this Mac has. Tailscale lets in the devices of the owner's own tailnet
10
+ # and puts nothing on the internet; ngrok puts the desk there behind its
11
+ # sign-in with Google, for the addresses the owner allows.
12
+ #
13
+ # A share is of the desk on one port, kept as a file in the cache by that
14
+ # port. It stays open until it is closed, across the desk's restarts, and
15
+ # while its file says so the desk answers to the share's name as to its own.
16
+ # `who` is whom it lets in, as it was opened.
17
+ class Share < Data.define(:port, :way, :address, :who, :process)
18
+ Refused = Class.new(StandardError)
19
+
20
+ def initialize(port:, way:, address:, who: way.who, process: nil) = super
21
+
22
+ class << self
23
+ # The ways this Mac could share the desk, Tailscale first: it puts
24
+ # nothing on the internet.
25
+ def ways(path: ENV.fetch("PATH", "")) = [Tailscale.new(path:), Ngrok.new(path:)]
26
+
27
+ # The share of the desk on port, while its way still carries it. One
28
+ # whose way let it go (ngrok ended with the Mac's restart, say) is
29
+ # forgotten.
30
+ def at(port, ways: self.ways)
31
+ kept(port, ways:)&.then do |share|
32
+ next share if share.way.carrying?(share)
33
+
34
+ file(port).delete
35
+ nil
36
+ end
37
+ end
38
+
39
+ # As its file says, without asking its way: for every page of a burst,
40
+ # which says whether the desk is shared.
41
+ def kept(port, ways: self.ways)
42
+ record = recorded(port) or return
43
+ way = ways.find { it.to_param == record["way"] } or return
44
+
45
+ new(port:, way:, address: record["address"], who: record.fetch("who") { way.who }, process: record["process"])
46
+ end
47
+
48
+ # Shares the desk on port through the way named, which is remembered,
49
+ # or the one last named while it is ready, or the first one ready.
50
+ # `allow` names whom ngrok lets in from now on. A share already open is
51
+ # only answered. A trial desk, on marks of its own, is never shared: it
52
+ # tries a change on this Mac, and its settings are the owner's.
53
+ def open(port, via: nil, allow: nil, ways: self.ways)
54
+ raise Refused, "A trial desk is not shared: it is for trying a change on this Mac." if Desk.trial?
55
+
56
+ Settings.current.with(allow:).save if allow
57
+ at(port, ways:) || chosen(via, ways).open(port).tap do
58
+ it.keep
59
+ Settings.current.with(via:).save if via
60
+ end
61
+ end
62
+
63
+ # Whether a request came through the desk's share: by the name it was
64
+ # asked for. Read from the file alone, as every request from away asks.
65
+ def carries?(request, port: Desk.port) = recorded(port)&.then { URI(it["address"]).host.casecmp?(request.host) } || false
66
+
67
+ def folder = Desk.cache.join("shares")
68
+
69
+ def file(port) = folder.join("#{port}.yml")
70
+
71
+ private
72
+
73
+ def recorded(port) = file(port).then { YAML.safe_load_file(it) if it.file? }
74
+
75
+ def chosen(via, ways)
76
+ if via
77
+ way = ways.find { it.to_param == via } or raise Refused, "The desk is shared through #{ways.map(&:to_param).to_sentence(last_word_connector: " or ", two_words_connector: " or ")}, not #{via}."
78
+ way.ready? ? way : raise(Refused, "#{way.unready}.")
79
+ else
80
+ remembered = Settings.current.via
81
+ ways.find { it.to_param == remembered && it.ready? } || ways.find(&:ready?) || raise(Refused, unshareable(ways))
82
+ end
83
+ end
84
+
85
+ def unshareable(ways) = "Neither #{ways.map(&:name).join(" nor ")} can share the desk yet. #{ways.map { "#{it.unready}." }.join(" ")}"
86
+ end
87
+
88
+ def keep = file.tap { it.dirname.mkpath }.write({"way" => way.to_param, "address" => address, "who" => who, "process" => process}.compact.to_yaml)
89
+
90
+ def close
91
+ way.close(self)
92
+ file.delete if file.exist?
93
+ end
94
+
95
+ def host = URI(address).host
96
+
97
+ def file = self.class.file(port)
98
+
99
+ def to_s = "The desk is shared at #{address} through #{way.name}: #{who}."
100
+ end
101
+ end
@@ -19,24 +19,43 @@ module Proctolog
19
19
  # under it. `scenarios` are the names of this pull request's scenarios, and
20
20
  # `named` the preconditions the burst file names for it. `progress` is how
21
21
  # far the owner's choices on its decisions are; without one, only the boxes
22
- # ticked on GitHub count as reviewed.
23
- def initialize(stop:, marks:, progress: nil, under: nil, above: [], scenarios: [], named: [])
24
- @stop, @marks, @under, @above, @scenarios, @named = stop, marks, under, above, scenarios, named
22
+ # ticked on GitHub count as reviewed. `returned` is what came back from an
23
+ # agent on it.
24
+ def initialize(stop:, marks:, progress: nil, returned: Rounds::Returned.new, under: nil, above: [], scenarios: [], named: [])
25
+ @stop, @marks, @returned, @under, @above, @scenarios, @named = stop, marks, returned, under, above, scenarios, named
25
26
  @progress = progress || Choices::Progress.of(decisions.index_with(&:ticked))
26
27
  end
27
28
 
28
29
  # Whose move it is: :waiting on something outside, the :agent's, the
29
- # :owner's, or nobody's because it is :settled.
30
+ # :owner's, or nobody's because it is :settled. What came back from the
31
+ # agent makes it the owner's, whatever else is still with the agent.
30
32
  def turn
31
33
  if marks.waiting_on then :waiting
34
+ elsif back_to_owner? then :owner
32
35
  elsif for_the_agent.any? then :agent
33
- elsif good_to_go? && !changed? && for_the_owner.empty? then :settled
36
+ elsif good_to_go? && !changed? then :settled
34
37
  else :owner
35
38
  end
36
39
  end
37
40
 
38
41
  def settled? = turn == :settled
39
42
 
43
+ # What came back from the agent for the owner, in a few words each: the
44
+ # notes it addressed, its answers to confirm and its questions.
45
+ def for_the_owner
46
+ addressed = notes(:addressed).size
47
+ [*("#{count(addressed, "note")} addressed, to confirm" if addressed.positive?),
48
+ *("#{@returned.to_confirm} returned, to confirm" if @returned.to_confirm.positive?),
49
+ *("#{count(@returned.questions, "question")} for you" if @returned.questions.positive?)]
50
+ end
51
+
52
+ def back_to_owner? = for_the_owner.any?
53
+
54
+ # Whether it waits for the owner: a blocking decision left, or something
55
+ # back from the agent. The non-blocking wait for one gesture, and hold
56
+ # nothing up.
57
+ def asks_for_owner? = back_to_owner? || progress.blocking_left.positive?
58
+
40
59
  def good_to_go? = marks.good_to_go.present?
41
60
 
42
61
  # What moved since "good to go" was given: the code, the description, both
@@ -91,8 +110,6 @@ module Proctolog
91
110
  *("#{count(stop.open_threads, "review thread")} unresolved" if stop.open_threads.positive?)]
92
111
  end
93
112
 
94
- def for_the_owner = notes(:addressed).any? ? ["#{count(notes(:addressed).size, "note")} addressed, to confirm"] : []
95
-
96
113
  def notes(state) = marks.notes.select { it.state == state.to_s }
97
114
 
98
115
  def count(number, noun) = "#{number} #{noun.pluralize(number)}"
@@ -1,6 +1,6 @@
1
1
  <%# locals: (burst:, stop:, standing:) %>
2
- <%# One pull request of the route, in a line: its number, its title, and at the right whose it is when it is not the owner's, and how far its decisions are. The line the cursor is on opens out: its title in full, what else there is to know of it, and its keys beside it. %>
3
- <%= tag.li id: "pr-#{stop.number}", data: {stop: "", blocking_left: standing.progress.blocking_left, turn: standing.turn} do %>
2
+ <%# One pull request of the route, in a line: its number, its title, and at the right whether something came back from the agent, or else whose it is when it is not the owner's, and its decisions as beads. The line the cursor is on opens out: its title in full, what else there is to know of it, and its keys beside it. %>
3
+ <%= tag.li id: "pr-#{stop.number}", data: {stop: "", asks: standing.asks_for_owner?, turn: standing.turn} do %>
4
4
  <p class="what">
5
5
  <%= link_to "##{stop.number}", burst_pull_request_path(burst, stop.number), class: "number" %>
6
6
  <%= link_to stop.title, burst_pull_request_path(burst, stop.number), class: "title" %>
@@ -1,7 +1,7 @@
1
1
  <%# locals: (burst:, at: nil) %>
2
- <%# The pane at the left of every page of a burst: the plaque the desk is named for, where the desk's pages are, and the route with how far each pull request's decisions are, so the owner always sees where they stand. `at` is the pull request the page is on. Focused on a part, it lists that part, with the key back to the whole burst, and counts what is left in the part against the burst; a pull request opened from outside the part stands apart under it. A page that keeps choices rewrites the counts as they change. data-desk keeps it out of a page's reading order. %>
2
+ <%# The pane at the left of every page of a burst: the plaque the desk is named for, where the desk's pages are, and the route with how far each pull request's decisions are and what came back from the agent, so the owner always sees where they stand. `at` is the pull request the page is on. Focused on a part, it lists that part, with the key back to the whole burst, and counts what is left in the part against the burst; a pull request opened from outside the part stands apart under it. A page that keeps choices rewrites the counts as they change. data-desk keeps it out of a page's reading order. %>
3
3
  <% route = burst.route %>
4
- <% progress = burst.progress(route) %>
4
+ <% standings = burst.standings(route) %>
5
5
  <aside class="side" aria-label="The burst" data-desk>
6
6
  <div class="head">
7
7
  <%= link_to image_tag("/plaque.png", alt: "", class: "plaque"), burst_path(burst), tabindex: -1, aria: {hidden: true} %>
@@ -19,7 +19,7 @@
19
19
  <p class="focus">Only <%= focus.part.in_words %> · <%= link_to url_for(part: nil), data: {key: "P"} do %><kbd>P</kbd> the whole burst<% end %></p>
20
20
  <% end %>
21
21
  <%= tag.p class: "overall", data: {side_overall: "", part_prs: (focus.map(&:number).join(" ") unless focus.whole?)} do %>
22
- <%= side_gauge(progress.each_value.sum(Proctolog::Choices::NONE), part: (focus.sum(Proctolog::Choices::NONE) { progress.fetch(it.number) } unless focus.whole?)) %>
22
+ <%= side_gauge(standings.each_value.sum(Proctolog::Choices::NONE, &:progress), part: (focus.sum(Proctolog::Choices::NONE) { standings.fetch(it).progress } unless focus.whole?)) %>
23
23
  <% end %>
24
24
  <%# Only how many notices there are: they are read at the head of the route. %>
25
25
  <% if (notices = burst.notices).any? %>
@@ -30,14 +30,14 @@
30
30
  <h2><%= part_heading(part, route) %></h2>
31
31
  <ol class="stops">
32
32
  <% route.stops_of(part).each do |stop| %>
33
- <%= render "proctolog/bursts/side_stop", burst:, stop:, progress: progress.fetch(stop.number), at: %>
33
+ <%= render "proctolog/bursts/side_stop", burst:, stop:, standing: standings.fetch(stop), at: %>
34
34
  <% end %>
35
35
  </ol>
36
36
  <% end %>
37
37
  <% if (outside = route.at(at)) && !focus.include?(outside) %>
38
38
  <h2>Outside this part</h2>
39
39
  <ol class="stops">
40
- <%= render "proctolog/bursts/side_stop", burst:, stop: outside, progress: progress.fetch(outside.number), at: %>
40
+ <%= render "proctolog/bursts/side_stop", burst:, stop: outside, standing: standings.fetch(outside), at: %>
41
41
  </ol>
42
42
  <% end %>
43
43
 
@@ -45,5 +45,6 @@
45
45
  <%= tag.div class: "read", data: {freshness: burst_freshness_path(burst), read_at: burst.read_at.iso8601} do %>
46
46
  <p>GitHub read <%= tag.time read_at_in_words(burst.read_at), datetime: burst.read_at.iso8601 %> · <%= link_to "read again", burst_freshness_path(burst, again: 1, back: request.fullpath), class: "again", data: {key: "r"} %></p>
47
47
  <p class="moved" data-moved hidden></p>
48
+ <%= render "proctolog/shares/line" %>
48
49
  <% end %>
49
50
  </aside>
@@ -1,10 +1,10 @@
1
- <%# locals: (burst:, stop:, progress:, at:) %>
2
- <%# One pull request of the pane: its mark, its number, its title and how far its decisions are, marked when the page is about it. Held under the pointer, side.js says the same in words, with its title in full and the way to it on GitHub. %>
1
+ <%# locals: (burst:, stop:, standing:, at:) %>
2
+ <%# One pull request of the pane: ↩ when the agent's answers came back on it, its number, its title and its decisions as beads, marked when the page is about it. Held under the pointer, side.js says the same in words, with its title in full and the way to it on GitHub. %>
3
3
  <%= tag.li data: {side_pr: stop.number, github: stop.url}, aria: {current: ("page" if stop.number == at)} do %>
4
4
  <%= link_to burst_pull_request_path(burst, stop.number) do %>
5
- <span class="mark" data-side-mark><%= side_mark(progress) %></span>
5
+ <%= side_mark(standing.for_the_owner) %>
6
6
  <span class="number"><%= stop.number %></span>
7
7
  <span class="title"><%= stop.title %></span>
8
- <span class="fraction" data-side-fraction><%= side_fraction(progress) %></span>
8
+ <span class="decisions" data-side-decisions><%= decision_beads(standing.progress) %></span>
9
9
  <% end %>
10
10
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <%# locals: (note:, burst:, stop:, where: nil, back: nil, attributes: {}) %>
2
- <%# One note as a card: what it is, what was said, where it is pinned (`where`), the agent's reply, and the ways to move it along. A press of those comes back to `back` when the page that showed the card gave one. %>
2
+ <%# One note as a card: what it is, what was said, where it is pinned (`where`), the agent's reply, and the ways to move it along. A press of those comes back to `back` when the page that showed the card gave one. A note answered in a handover is confirmed or answered back under Returned work, which moves it (Rounds#move_notes), so it can only be withdrawn here. %>
3
3
  <%= tag.li(**{class: "card", data: {kind: note.kind, state: note.state}}.deep_merge(attributes)) do %>
4
4
  <p class="meta"><span class="kind"><%= note.kind %></span> <span class="state"><%= note.state %></span></p>
5
5
  <p class="text"><%= with_mentions(note.text, burst:) %></p>
@@ -12,8 +12,12 @@
12
12
  <% end %>
13
13
  <div class="moves">
14
14
  <% path = burst_pull_request_note_path(burst, stop.number, note.id) %>
15
- <%= button_to "Confirm", path, method: :patch, params: {state: "confirmed", return: back}.compact, class: "quiet" if note.addressed? %>
16
- <%= button_to "Reopen", path, method: :patch, params: {state: "open", return: back}.compact, class: "quiet" unless note.open? %>
15
+ <% if note.answered_in_a_handover? %>
16
+ <% if note.addressed? %><p class="judged">Confirm it, or answer back, under Returned work.</p><% end %>
17
+ <% else %>
18
+ <%= button_to "Confirm", path, method: :patch, params: {state: "confirmed", return: back}.compact, class: "quiet" if note.addressed? %>
19
+ <%= button_to "Reopen", path, method: :patch, params: {state: "open", return: back}.compact, class: "quiet" unless note.open? %>
20
+ <% end %>
17
21
  <%= button_to "Withdraw", path, method: :patch, params: {state: "withdrawn", return: back}.compact, class: "quiet" unless note.withdrawn? %>
18
22
  </div>
19
23
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <%# locals: (bursts:, read_at:) %>
2
- <%# The pane at the left where no burst is open: the plaque, where the desk's pages are, and the bursts to open, the newest first, each with how far its decisions are. Those put away stay off it, as on the page. %>
2
+ <%# The pane at the left where no burst is open: the plaque, where the desk's pages are, and the bursts to open, the newest first, each with how many of its decisions wait. Those put away stay off it, as on the page. %>
3
3
  <aside class="side" aria-label="The desk" data-desk>
4
4
  <div class="head">
5
5
  <%= link_to image_tag("/plaque.png", alt: "", class: "plaque"), open_pull_requests_path, tabindex: -1, aria: {hidden: true} %>
@@ -15,10 +15,10 @@
15
15
  <% progress = burst.progress(burst.route).each_value.sum(Proctolog::Choices::NONE) %>
16
16
  <li>
17
17
  <%= link_to burst_path(burst) do %>
18
- <span class="mark"><%= side_mark(progress) %></span>
18
+ <span class="mark"></span>
19
19
  <span class="number"></span>
20
20
  <span class="title"><%= burst.name %></span>
21
- <span class="fraction"><%= side_fraction(progress) %></span>
21
+ <span class="decisions"><%= decisions_left(progress) %></span>
22
22
  <% end %>
23
23
  </li>
24
24
  <% end %>
@@ -26,5 +26,6 @@
26
26
  <% end %>
27
27
  <div class="read">
28
28
  <p>GitHub read <%= tag.time read_at_in_words(read_at), datetime: read_at.iso8601 %> · <%= link_to "read again", open_pull_requests_path(fresh: 1), class: "again" %></p>
29
+ <%= render "proctolog/shares/line" %>
29
30
  </div>
30
31
  </aside>
@@ -0,0 +1,8 @@
1
+ <%# locals: (pick:, repository:, first_time:) %>
2
+ <%# At the head of the page, what to do here: for someone who has not reviewed here yet, or when nothing below has decisions to ask. %>
3
+ <% if (start = pick.start(first_time:)) %>
4
+ <section class="start" aria-labelledby="start">
5
+ <h2 id="start">Start here</h2>
6
+ <%= render "proctolog/open_pull_requests/start/#{start}", pick:, repository: %>
7
+ </section>
8
+ <% end %>
@@ -15,6 +15,8 @@
15
15
  </header>
16
16
 
17
17
  <main class="route pick">
18
+ <%= render "start", pick: @pick, repository: @repository, first_time: [*@bursts, *@put_away].none?(&:own?) %>
19
+
18
20
  <% if @bursts.any? || @put_away.any? %>
19
21
  <h2>Your bursts</h2>
20
22
  <ol class="lines">
@@ -36,10 +38,6 @@
36
38
  <% end %>
37
39
  <% end %>
38
40
 
39
- <% if @pick.empty? %>
40
- <p class="lede">Nothing is open in <%= @repository %>.</p>
41
- <% end %>
42
-
43
41
  <% @pick.stacks.each do |stack| %>
44
42
  <h2>Stack #<%= stack.number %>, <%= stack.pull_requests.size %> from the bottom up</h2>
45
43
  <%= button_to "Review the stack", review_stack_path(stack.number), class: "quiet" %>
@@ -59,7 +57,8 @@
59
57
 
60
58
  <% if (loose = @pick.loose).any? %>
61
59
  <h2><%= @pick.stacks.any? ? "In no stack" : "Pull requests" %></h2>
62
- <%= form_with url: bursts_path, method: :post do |form| %>
60
+ <%# Its "Review alone" buttons post elsewhere, so the form carries the session's token, not one good for bursts_path alone. %>
61
+ <%= form_with url: bursts_path, method: :post, authenticity_token: form_authenticity_token do |form| %>
63
62
  <ol class="lines">
64
63
  <% loose.each do |pull_request| %>
65
64
  <li>
@@ -0,0 +1,3 @@
1
+ <%# locals: (pick:, repository:) %>
2
+ <p><%= pluralize(pick.written.size, "pull request") %> below <%= (pick.written.size == 1) ? "is" : "are" %> written for PRoctolog: their facts count the decisions they ask you. Press <b>Review alone</b> on one, <b>Review the stack</b> for a whole stack, or tick a few to review them together.</p>
3
+ <p>Inside, <kbd>?</kbd> shows the keys.</p>
@@ -0,0 +1,3 @@
1
+ <%# locals: (pick:, repository:) %>
2
+ <p>Nothing is open in <%= repository %>, and no branch here waits to be opened.</p>
3
+ <p>The desk reviews pull requests whose descriptions list decisions for you. Ask your agent to “write these PRs for PRoctolog”: it opens them on GitHub, or leaves them on branches here for you to review first. Then <%= link_to "read GitHub again", open_pull_requests_path(fresh: 1) %>.</p>
@@ -0,0 +1,3 @@
1
+ <%# locals: (pick:, repository:) %>
2
+ <p>None of these is written for PRoctolog yet, so the desk has no decisions to ask you. Any of them still opens, to read with its pictures and code.</p>
3
+ <p>For decisions, ask your agent to “rewrite these PRs for PRoctolog”. <code>proctolog check <%= pick.loose.first&.number || pick.stacks.first&.top&.number %></code> says what a description still lacks.</p>
@@ -0,0 +1,5 @@
1
+ <%# At the foot of the pane at the left: whether the desk is shared, as its file says, and the page to share it or stop. %>
2
+ <% share = Proctolog::Share.kept(Proctolog::Desk.port) %>
3
+ <p class="share">
4
+ <%= share ? "Shared through #{share.way.name}" : "Only on this Mac" %> · <%= link_to (share ? "see it" : "share it"), share_path(back: request.fullpath), class: "again" %>
5
+ </p>
@@ -0,0 +1,48 @@
1
+ <% content_for :title, "Sharing the desk" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/share.css">
4
+ <% end %>
5
+
6
+ <header class="burst">
7
+ <h1>Sharing the desk</h1>
8
+ <p class="lede">
9
+ Review from your phone, or a computer elsewhere, while this Mac keeps the desk. Only you get in.
10
+ <%= link_to "Back", @back %>
11
+ </p>
12
+ </header>
13
+
14
+ <main class="route share">
15
+ <% if @share %>
16
+ <p class="now">Shared at <%= link_to @share.address, @share.address %> through <%= @share.way.name %>: <%= @share.who %>.</p>
17
+ <p class="hint">It stays shared until you stop it, the desk running or not.<% if Proctolog::Share.carries?(request) %> This page came through it: stopping it closes this page too.<% end %></p>
18
+ <%= button_to "Stop sharing", share_path, method: :delete, params: {back: @back}, class: "quiet" %>
19
+ <% else %>
20
+ <p class="now">Only on this Mac, at <%= link_to Proctolog::Desk.address, Proctolog::Desk.address %>.</p>
21
+
22
+ <h2>Share it through</h2>
23
+ <div class="ways">
24
+ <% @ways.each do |way| %>
25
+ <section class="way" aria-label="<%= way.name %>">
26
+ <h3><%= way.name %></h3>
27
+ <% if way.ready? %>
28
+ <%= form_with url: share_path, method: :post do |form| %>
29
+ <%= form.hidden_field :via, value: way.to_param %>
30
+ <%= form.hidden_field :back, value: @back %>
31
+ <%= render "proctolog/shares/ways/#{way.to_param}", way:, form: %>
32
+ <%= form.button "Share through #{way.name}", class: "quiet" %>
33
+ <% end %>
34
+ <% else %>
35
+ <p class="facts"><%= linked_sentence(way.unready) %>.</p>
36
+ <% end %>
37
+ </section>
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+
42
+ <h2>From a terminal</h2>
43
+ <p class="hint">
44
+ <code>proctolog share</code> shares it the way you named last, or the first one ready, and prints the address;
45
+ <code>proctolog share stop</code> stops it, and <code>proctolog share status</code> says what each way needs.
46
+ An agent can run them too, on a Mac you left working.
47
+ </p>
48
+ </main>
@@ -0,0 +1,6 @@
1
+ <%# locals: (way:, form:) %>
2
+ <p class="facts">At your ngrok account's address, on the internet, behind ngrok's sign-in with Google and a warning page of its own. Its free plan carries 1 GB a month, and every picture and film you open passes through it.</p>
3
+ <p class="allow">
4
+ <%= form.label :allow, "Who may sign in" %>
5
+ <%= form.text_field :allow, value: way.allowed.join(", "), spellcheck: false, autocomplete: "off" %>
6
+ </p>
@@ -0,0 +1,2 @@
1
+ <%# locals: (way:, form:) %>
2
+ <p class="facts">At this Mac's name on your tailnet, and nowhere on the internet. Your phone needs the Tailscale app, signed in to <%= way.tailnet ? "the tailnet #{way.tailnet}" : "the same tailnet" %>: one account can be in more than one.</p>