proctolog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/README.md +178 -0
- data/app/controllers/proctolog/acceptances_controller.rb +29 -0
- data/app/controllers/proctolog/application_controller.rb +32 -0
- data/app/controllers/proctolog/bursts_controller.rb +34 -0
- data/app/controllers/proctolog/choices_controller.rb +30 -0
- data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
- data/app/controllers/proctolog/decisions_controller.rb +33 -0
- data/app/controllers/proctolog/entries_controller.rb +14 -0
- data/app/controllers/proctolog/evidence_controller.rb +23 -0
- data/app/controllers/proctolog/freshness_controller.rb +27 -0
- data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
- data/app/controllers/proctolog/memberships_controller.rb +20 -0
- data/app/controllers/proctolog/mentions_controller.rb +16 -0
- data/app/controllers/proctolog/moments_controller.rb +26 -0
- data/app/controllers/proctolog/note_params.rb +24 -0
- data/app/controllers/proctolog/notes_controller.rb +56 -0
- data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
- data/app/controllers/proctolog/pictures_controller.rb +27 -0
- data/app/controllers/proctolog/prompts_controller.rb +28 -0
- data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
- data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
- data/app/controllers/proctolog/put_away_controller.rb +19 -0
- data/app/controllers/proctolog/recordings_controller.rb +28 -0
- data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
- data/app/controllers/proctolog/review_data_controller.rb +12 -0
- data/app/controllers/proctolog/rules_controller.rb +20 -0
- data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
- data/app/controllers/proctolog/scenario_scoped.rb +21 -0
- data/app/controllers/proctolog/scenarios_controller.rb +67 -0
- data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
- data/app/controllers/proctolog/waiting_controller.rb +14 -0
- data/app/controllers/proctolog/with_decisions.rb +28 -0
- data/app/helpers/proctolog/bursts_helper.rb +52 -0
- data/app/helpers/proctolog/decisions_helper.rb +102 -0
- data/app/helpers/proctolog/desk_helper.rb +61 -0
- data/app/helpers/proctolog/mentions_helper.rb +27 -0
- data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
- data/app/helpers/proctolog/prompts_helper.rb +21 -0
- data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
- data/app/helpers/proctolog/recordings_helper.rb +30 -0
- data/app/helpers/proctolog/rounds_helper.rb +27 -0
- data/app/helpers/proctolog/scenarios_helper.rb +123 -0
- data/app/helpers/proctolog/standings_helper.rb +80 -0
- data/app/jobs/proctolog/application_job.rb +8 -0
- data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
- data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
- data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
- data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
- data/app/models/proctolog/acceptance.rb +47 -0
- data/app/models/proctolog/app.rb +92 -0
- data/app/models/proctolog/browser.rb +42 -0
- data/app/models/proctolog/burst.rb +268 -0
- data/app/models/proctolog/burst_notices.rb +143 -0
- data/app/models/proctolog/chapters.rb +42 -0
- data/app/models/proctolog/check.rb +122 -0
- data/app/models/proctolog/choices.rb +112 -0
- data/app/models/proctolog/code_review.rb +251 -0
- data/app/models/proctolog/decision_record.rb +12 -0
- data/app/models/proctolog/decisions.rb +199 -0
- data/app/models/proctolog/description.rb +165 -0
- data/app/models/proctolog/difference.rb +138 -0
- data/app/models/proctolog/doctor.rb +127 -0
- data/app/models/proctolog/evidence.rb +29 -0
- data/app/models/proctolog/failure.rb +7 -0
- data/app/models/proctolog/focus.rb +48 -0
- data/app/models/proctolog/fresh_sitting.rb +35 -0
- data/app/models/proctolog/github.rb +353 -0
- data/app/models/proctolog/handover/brief.rb +138 -0
- data/app/models/proctolog/handover/layers.rb +128 -0
- data/app/models/proctolog/handover/places.rb +98 -0
- data/app/models/proctolog/handover/taken.rb +41 -0
- data/app/models/proctolog/handover/worklist.rb +35 -0
- data/app/models/proctolog/handover.rb +123 -0
- data/app/models/proctolog/housekeeping.rb +23 -0
- data/app/models/proctolog/kit.rb +36 -0
- data/app/models/proctolog/marks.rb +130 -0
- data/app/models/proctolog/membership.rb +91 -0
- data/app/models/proctolog/mentions.rb +117 -0
- data/app/models/proctolog/note.rb +94 -0
- data/app/models/proctolog/outside.rb +45 -0
- data/app/models/proctolog/overlay/brief.rb +52 -0
- data/app/models/proctolog/overlay/written.rb +33 -0
- data/app/models/proctolog/overlay.rb +87 -0
- data/app/models/proctolog/owner.rb +16 -0
- data/app/models/proctolog/pick.rb +47 -0
- data/app/models/proctolog/picture.rb +138 -0
- data/app/models/proctolog/pieces_rendering.rb +40 -0
- data/app/models/proctolog/project.rb +118 -0
- data/app/models/proctolog/pull_request.rb +89 -0
- data/app/models/proctolog/recording.rb +194 -0
- data/app/models/proctolog/release.rb +59 -0
- data/app/models/proctolog/rendering.rb +303 -0
- data/app/models/proctolog/reports.rb +66 -0
- data/app/models/proctolog/response_files.rb +55 -0
- data/app/models/proctolog/rounds.rb +305 -0
- data/app/models/proctolog/route.rb +151 -0
- data/app/models/proctolog/rules.rb +121 -0
- data/app/models/proctolog/scenario.rb +126 -0
- data/app/models/proctolog/server.rb +273 -0
- data/app/models/proctolog/since.rb +108 -0
- data/app/models/proctolog/size.rb +30 -0
- data/app/models/proctolog/standing.rb +132 -0
- data/app/models/proctolog/theme.rb +40 -0
- data/app/models/proctolog/unopened/numbers.rb +48 -0
- data/app/models/proctolog/unopened/opening.rb +88 -0
- data/app/models/proctolog/unopened.rb +203 -0
- data/app/models/proctolog/visit.rb +26 -0
- data/app/views/layouts/proctolog/application.html.erb +58 -0
- data/app/views/proctolog/acceptances/new.html.erb +34 -0
- data/app/views/proctolog/bursts/_line.html.erb +28 -0
- data/app/views/proctolog/bursts/_merged.html.erb +23 -0
- data/app/views/proctolog/bursts/_moved.html.erb +16 -0
- data/app/views/proctolog/bursts/_notices.html.erb +39 -0
- data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
- data/app/views/proctolog/bursts/_rules.html.erb +27 -0
- data/app/views/proctolog/bursts/_side.html.erb +49 -0
- data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
- data/app/views/proctolog/bursts/show.html.erb +54 -0
- data/app/views/proctolog/code_reviews/show.html.erb +33 -0
- data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
- data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
- data/app/views/proctolog/decisions/_box.html.erb +27 -0
- data/app/views/proctolog/decisions/_choices.html.erb +9 -0
- data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
- data/app/views/proctolog/decisions/_part.html.erb +61 -0
- data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
- data/app/views/proctolog/decisions/_theme.html.erb +13 -0
- data/app/views/proctolog/decisions/index.html.erb +75 -0
- data/app/views/proctolog/notes/_card.html.erb +19 -0
- data/app/views/proctolog/notes/_template.html.erb +41 -0
- data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
- data/app/views/proctolog/prompts/show.html.erb +41 -0
- data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
- data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
- data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
- data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
- data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
- data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
- data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
- data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
- data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
- data/app/views/proctolog/pull_requests/show.html.erb +145 -0
- data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
- data/app/views/proctolog/recordings/_list.html.erb +8 -0
- data/app/views/proctolog/recordings/_note.html.erb +22 -0
- data/app/views/proctolog/recordings/index.html.erb +33 -0
- data/app/views/proctolog/recordings/show.html.erb +118 -0
- data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
- data/app/views/proctolog/scenarios/_note.html.erb +18 -0
- data/app/views/proctolog/scenarios/_open.html.erb +9 -0
- data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
- data/app/views/proctolog/scenarios/_server.html.erb +20 -0
- data/app/views/proctolog/scenarios/index.html.erb +62 -0
- data/bin/proctolog +330 -0
- data/completions/_proctolog +143 -0
- data/config/application.rb +94 -0
- data/config/database.yml +16 -0
- data/config/environment.rb +6 -0
- data/config/initializers/grouse.rb +113 -0
- data/config/initializers/grouse_notes.rb +9 -0
- data/config/keys.yml +6 -0
- data/config/routes.rb +71 -0
- data/config/storage.yml +4 -0
- data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
- data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
- data/docs/manual/app.md +157 -0
- data/docs/manual/bursts.md +129 -0
- data/docs/manual/deciding.md +89 -0
- data/docs/manual/desk.png +0 -0
- data/docs/manual/films.md +20 -0
- data/docs/manual/handover.md +93 -0
- data/docs/manual/pr-format.md +75 -0
- data/docs/manual/reading.md +233 -0
- data/docs/manual/unopened.md +80 -0
- data/docs/manual/updates.md +55 -0
- data/lib/proctolog/installation.rb +125 -0
- data/lib/proctolog/version.rb +7 -0
- data/public/asking.js +50 -0
- data/public/code-review.js +40 -0
- data/public/cursor.js +292 -0
- data/public/decision-screenshots.js +97 -0
- data/public/decisions.css +949 -0
- data/public/decisions.js +1289 -0
- data/public/desk.css +582 -0
- data/public/desk.js +126 -0
- data/public/favicon.png +0 -0
- data/public/keys.js +228 -0
- data/public/mentions.js +389 -0
- data/public/messages.js +103 -0
- data/public/notes.js +731 -0
- data/public/pick.css +99 -0
- data/public/plaque.png +0 -0
- data/public/pr-number.js +11 -0
- data/public/prompt.css +46 -0
- data/public/prompt.js +16 -0
- data/public/pull_request.css +805 -0
- data/public/recording.css +379 -0
- data/public/recording.js +394 -0
- data/public/release.js +25 -0
- data/public/respond.css +260 -0
- data/public/respond.js +95 -0
- data/public/review-drafts.js +949 -0
- data/public/review-rounds.js +1447 -0
- data/public/scenarios.css +253 -0
- data/public/scenarios.js +36 -0
- data/public/schemas/proctolog-response-v1.json +101 -0
- data/public/side.js +133 -0
- data/public/standing.css +478 -0
- data/public/terminal-apl.svg +6 -0
- data/public/terminal-cuneiform.svg +3 -0
- data/public/terminal-grain-dots.svg +6 -0
- data/public/terminal-grain-weave.svg +6 -0
- data/public/terminal-grain-wedges.svg +6 -0
- data/public/terminal-shades.svg +3 -0
- data/public/terminal.css +4426 -0
- data/public/toast.js +26 -0
- data/public/try.js +124 -0
- data/public/viewer.js +808 -0
- data/public/words.js +81 -0
- data/skills/proctolog-handover/SKILL.md +77 -0
- data/skills/proctolog-pr-writing/SKILL.md +228 -0
- data/skills/proctolog-pr-writing/references/example-description.md +33 -0
- metadata +366 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 784717d224b472eba9ce15227d5ffb578dabce10741c802d6cd62e642f8aee96
|
|
4
|
+
data.tar.gz: 879591d6f7db0c4448563096e0e82ed566f68364c2c55e5b0ac03a739855a207
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0e7c19af833d9fe3099f47e221f7663cd9c43066dcdf08b7a96b48e04374af2ac4f4017158dc308f244a07abc827d78ba0563ae85e9d2f9a3c9bdcfb5bc175cc
|
|
7
|
+
data.tar.gz: 5da7be3fd3fcd54977e57f350b6f496ddda3d70b5c10982976116d8e1d814fc052a503526039195065f73ce1587415fb938fa251b93e8156313c4bb0b335f1dc
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
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**.
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
The first one published as a gem.
|
|
10
|
+
|
|
11
|
+
- `rvx proctolog install`, or
|
|
12
|
+
`gem install proctolog` and `proctolog install`, in place of cloning the
|
|
13
|
+
repository. Under rv it runs on a Ruby 4 of its own, whatever Ruby the shell
|
|
14
|
+
has, and the apps it starts never inherit it.
|
|
15
|
+
- `proctolog version`, and `proctolog update`, which takes a newer version
|
|
16
|
+
however yours was installed: rv, RubyGems, or a checkout it pulls forward.
|
|
17
|
+
- The desk asks RubyGems once a day for the latest version, and the next page
|
|
18
|
+
says when it's newer than yours.
|
|
19
|
+
|
|
20
|
+
**You do**, coming from a checkout: nothing, if you keep it: `proctolog
|
|
21
|
+
update` pulls it from now on. To move to the gem, remove
|
|
22
|
+
`~/.local/bin/proctolog`, the checkout's launcher, then install it one of the
|
|
23
|
+
ways above.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# PRoctolog
|
|
2
|
+
|
|
3
|
+
A review desk for stacks of pull requests an agent wrote. It runs on your Mac,
|
|
4
|
+
reads the PRs from GitHub, and walks you through every decision their
|
|
5
|
+
descriptions ask of you. What you answered becomes a handover: a short brief and
|
|
6
|
+
a file for each PR, for the agent to work through.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
It's rough. It has also been through a stack of 21 PRs and 439 decisions, so
|
|
11
|
+
it works.
|
|
12
|
+
|
|
13
|
+
## The short version
|
|
14
|
+
|
|
15
|
+
Install it with [rv](https://rv.dev), then run it from a checkout of the repo
|
|
16
|
+
whose PRs you review:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
rvx proctolog install
|
|
20
|
+
cd path/to/your-repo
|
|
21
|
+
proctolog doctor # fix what it marks !!
|
|
22
|
+
proctolog serve # then open http://localhost:4040
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Pick a PR, a stack, or a few ticked ones, and press `?` once you're in. When
|
|
26
|
+
your agent opens PRs, tell it to "write these PRs for PRoctolog", so they come
|
|
27
|
+
with decisions to review. Everything below is the long version.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
You need `gh` signed in with access to the repository you review, and either
|
|
32
|
+
[rv](https://rv.dev) or Ruby 4.0. Chrome, [Diffle](https://github.com/moritzwilksch/diffle),
|
|
33
|
+
ImageMagick and ffmpeg each switch something on, and nothing here installs
|
|
34
|
+
them; `doctor` says what each adds, and Homebrew has them all:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
brew install --cask google-chrome
|
|
38
|
+
brew install diffle imagemagick ffmpeg
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With rv, whatever Ruby your shell or your repo runs:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
rvx proctolog install
|
|
45
|
+
proctolog doctor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
rv gives PRoctolog a Ruby 4 and gems of its own, and `install` puts a
|
|
49
|
+
`proctolog` in `~/.local/bin` that runs it through `rvx`. The apps you review
|
|
50
|
+
keep their own Ruby: what the desk starts for them never sees PRoctolog's.
|
|
51
|
+
rv installs from gem.coop, which mirrors RubyGems.
|
|
52
|
+
|
|
53
|
+
With Ruby 4.0 already in your shell:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
gem install proctolog
|
|
57
|
+
proctolog install
|
|
58
|
+
proctolog doctor
|
|
59
|
+
```
|
|
60
|
+
|
|
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
|
|
64
|
+
without, `--` for what only turns a feature off.
|
|
65
|
+
|
|
66
|
+
When a newer PRoctolog is out, the desk says so, and `proctolog update` takes
|
|
67
|
+
it ([versions and updates](docs/manual/updates.md)).
|
|
68
|
+
|
|
69
|
+
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
|
|
71
|
+
the checkout in `~/.local/bin`. Keep the checkout where it is, since
|
|
72
|
+
everything links back to it. The desk tells a checkout of a newer release
|
|
73
|
+
too, and `proctolog update` pulls it forward.
|
|
74
|
+
|
|
75
|
+
## Your first review
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
cd path/to/your-repo
|
|
79
|
+
proctolog serve
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Open <http://localhost:4040>. It lists the repo's open PRs: GitHub's stacks
|
|
83
|
+
whole, then the rest, yours first. Press "Review alone" on one, "Review the
|
|
84
|
+
stack" on a stack, or tick a few and review them together. That's all the setup
|
|
85
|
+
there is.
|
|
86
|
+
|
|
87
|
+
On a PR, press `?` for the keys, `f` for the first decision waiting on you, and
|
|
88
|
+
`e` for the handover. Give your agent its brief, or open the new session the
|
|
89
|
+
page offers, which already has. Or skip the page: tell your agent to "check my
|
|
90
|
+
PRoctolog comments", halfway through if you like, and it runs
|
|
91
|
+
`proctolog handover` and takes what you decided so far. Either way it answers
|
|
92
|
+
with a JSON file PRoctolog reads back, so you can confirm each change or push
|
|
93
|
+
back.
|
|
94
|
+
|
|
95
|
+
What you review together is a burst. The page writes one for each pick, kept
|
|
96
|
+
under `~/.local/share/proctolog/` with your marks, never in the repo; "open
|
|
97
|
+
PRs" at the top of any page goes back to the list. From the terminal it's
|
|
98
|
+
`proctolog burst new login-fixes --top 1120 --pr 1131`, where `--top` brings
|
|
99
|
+
the chain of PRs under it, `--pr` one alone and `--stack` a stack of GitHub's.
|
|
100
|
+
|
|
101
|
+
Your review is saved as you go, on your Mac under `~/.local/share/proctolog/`.
|
|
102
|
+
|
|
103
|
+
## PRs PRoctolog reads well
|
|
104
|
+
|
|
105
|
+
PRoctolog finds decisions under a `## Decisions for the owner` heading, one
|
|
106
|
+
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
|
|
108
|
+
"write these PRs for PRoctolog", or "rewrite this PR's description for
|
|
109
|
+
PRoctolog" for one that's open. Check a description before publishing it:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
proctolog check --file body.md
|
|
113
|
+
proctolog check 1131
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Not ready to open PRs? Tell your agent to write them for PRoctolog without
|
|
117
|
+
opening them. The desk reviews each from a folder on your Mac as `#01`, `#02`,
|
|
118
|
+
and when the branches are opened, the review moves to their real numbers
|
|
119
|
+
([how](docs/manual/unopened.md)).
|
|
120
|
+
|
|
121
|
+
## Scenarios
|
|
122
|
+
|
|
123
|
+
A scenario button starts the app, signs the right member in and opens Chrome
|
|
124
|
+
on the right page. The desk doesn't know your app; it runs the few commands
|
|
125
|
+
the repo's `.proctolog/app.yml` names ([how](docs/manual/app.md)), on your
|
|
126
|
+
own checkout and a scratch database the first Start makes. An app with
|
|
127
|
+
Grouse in it can open Grouse's overlay there too, labelled with the scenario,
|
|
128
|
+
and what you say or type in it comes back as notes on that scenario
|
|
129
|
+
([how](docs/manual/app.md#grouses-overlay)). Each bold heading
|
|
130
|
+
with numbered steps in a PR's "How to test locally" is a scenario already. A
|
|
131
|
+
`scenarios/<burst>.yml` beside the burst only adds a starting state and page
|
|
132
|
+
([how](docs/manual/bursts.md)). A repo without an `app.yml` still lists each
|
|
133
|
+
scenario's steps, and you mark what you saw.
|
|
134
|
+
|
|
135
|
+
Films and before/after pairs need a camera the desk doesn't have yet; it's
|
|
136
|
+
next, on the same `app.yml`.
|
|
137
|
+
|
|
138
|
+
## Notes on the desk itself
|
|
139
|
+
|
|
140
|
+
Grouse is private, so the desk runs without it, and the gem never carries it.
|
|
141
|
+
In a checkout of PRoctolog, once you can read
|
|
142
|
+
[its repo](https://github.com/LemuelCushing/grouse-rails), turn it on:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
bundle config set --local with grouse && bundle install
|
|
146
|
+
```
|
|
147
|
+
|
|
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
|
+
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>.
|
|
151
|
+
An agent working on the desk reads them through Grouse's export API;
|
|
152
|
+
[AGENTS.md](AGENTS.md) tells it how. They're kept under
|
|
153
|
+
`~/.local/share/proctolog/grouse_notes`, one store for every repo you review.
|
|
154
|
+
|
|
155
|
+
A voice note is written out by ElevenLabs, and every note written up as an
|
|
156
|
+
issue by Anthropic, with keys the desk reads from 1Password when it serves:
|
|
157
|
+
`config/keys.yml` holds the references, never the keys, and `op` (1Password's
|
|
158
|
+
CLI, signed in) reads them. Point them at your own items there if yours live
|
|
159
|
+
elsewhere.
|
|
160
|
+
|
|
161
|
+
Without a key a voice note arrives "audio only", with a recording that plays,
|
|
162
|
+
and a note's page says "No structurer set up". `bin/rails grouse:doctor`, in the
|
|
163
|
+
desk's checkout, says what Grouse is missing.
|
|
164
|
+
|
|
165
|
+
## More
|
|
166
|
+
|
|
167
|
+
- [Bursts, the route and where things live](docs/manual/bursts.md)
|
|
168
|
+
- [How the desk runs your app](docs/manual/app.md)
|
|
169
|
+
- [Reading a PR by keyboard](docs/manual/reading.md)
|
|
170
|
+
- [Deciding](docs/manual/deciding.md)
|
|
171
|
+
- [Handover and the next pass](docs/manual/handover.md)
|
|
172
|
+
- [Writing a PR the desk reads](docs/manual/pr-format.md)
|
|
173
|
+
- [Before anything is opened](docs/manual/unopened.md)
|
|
174
|
+
- [Films](docs/manual/films.md)
|
|
175
|
+
- [Versions and updates](docs/manual/updates.md)
|
|
176
|
+
- [CONCEPTS.md](CONCEPTS.md) for the words, [ROADMAP.md](ROADMAP.md) for what's next
|
|
177
|
+
|
|
178
|
+
Working on the desk itself? Start at [AGENTS.md](AGENTS.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# Ticking boxes of one description, on GitHub or in the file of one nobody
|
|
5
|
+
# has opened yet. The first page shows which boxes and the command;
|
|
6
|
+
# nothing is ticked until the second press.
|
|
7
|
+
class AcceptancesController < ApplicationController
|
|
8
|
+
include WithDecisions
|
|
9
|
+
|
|
10
|
+
rescue_from(Acceptance::Refused, Github::Unreachable) { redirect_to decisions_path_for(number), alert: it.message }
|
|
11
|
+
|
|
12
|
+
def new
|
|
13
|
+
@boxes = decisions.to_accept(number, positions)
|
|
14
|
+
@stop = @boxes.first.stop
|
|
15
|
+
@acceptance = @burst.acceptance(number, @boxes.map(&:decision))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
boxes = decisions.to_accept(number, positions)
|
|
20
|
+
leads = @burst.acceptance(number, boxes.map(&:decision)).carry_out(seen: params.expect(:digest))
|
|
21
|
+
@burst.marks(number).accept(leads)
|
|
22
|
+
redirect_to decisions_path_for(number), notice: "Ticked #{leads.size} #{"box".pluralize(leads.size)} of ##{number} #{PullRequest.unopened?(number) ? "in its file" : "on GitHub"}."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def positions = params.expect(positions: []).map(&:to_i)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}", status: :service_unavailable }
|
|
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."
|
|
8
|
+
end
|
|
9
|
+
rescue_from(Burst::Missing) { render plain: it.message, status: :not_found }
|
|
10
|
+
# A button's press comes back to the page it was pressed on, with the
|
|
11
|
+
# sentence. A page that cannot be shown says it in place: going back to
|
|
12
|
+
# it would only ask again.
|
|
13
|
+
rescue_from(Failure) do
|
|
14
|
+
request.get? ? render(plain: it.message, status: :internal_server_error) : redirect_back_or_to(root_path, alert: it.message.truncate(1200))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
helper_method :focus
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# The part of the burst the owner is taking on its own, from `part`. One
|
|
22
|
+
# the burst does not have leaves the whole burst in sight, and says so.
|
|
23
|
+
def focus
|
|
24
|
+
@focus ||= Focus.on(@burst.route, params[:part]).tap do |focus|
|
|
25
|
+
flash.now[:alert] = "This burst has no part #{params[:part]}, so the whole burst is shown." if params[:part].present? && focus.whole?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Every address a page of a focused burst makes keeps the focus.
|
|
30
|
+
def default_url_options = (@burst && params[:part].present?) ? {part: focus.to_param}.compact : {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class BurstsController < ApplicationController
|
|
5
|
+
# The burst written last, or the open pull requests to pick one from.
|
|
6
|
+
def index = redirect_to(Burst.latest&.then { burst_path(it) } || open_pull_requests_path)
|
|
7
|
+
|
|
8
|
+
# The route of a burst. `fresh` reads GitHub again, then comes back to an
|
|
9
|
+
# address that a reload can repeat without asking GitHub each time.
|
|
10
|
+
def show
|
|
11
|
+
@burst = Burst.find(params.expect(:id))
|
|
12
|
+
@route = @burst.route(fresh: params.key?(:fresh))
|
|
13
|
+
return redirect_to(burst_path(@burst)) if params.key?(:fresh)
|
|
14
|
+
|
|
15
|
+
@taken_in = @burst.reports.take_in
|
|
16
|
+
@standings = @burst.standings(@route)
|
|
17
|
+
@notices = @burst.notices
|
|
18
|
+
@rules = @burst.rules.select { it.within?(focus) }
|
|
19
|
+
@merged = @burst.merged
|
|
20
|
+
@merged_shown = params.key?(:merged) || @burst.finished?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A burst of the pull requests ticked on the open pull requests, each the
|
|
24
|
+
# top of whatever chain is under it, named as the owner typed or else
|
|
25
|
+
# after the first one's branch.
|
|
26
|
+
def create
|
|
27
|
+
tops = params.fetch(:tops, []).map(&:to_i)
|
|
28
|
+
tops.any? or raise Failure, "Tick the pull requests to review together first."
|
|
29
|
+
picked = Github.new(Project.current.github_repository).open_pull_requests.select { tops.include?(it.number) }
|
|
30
|
+
slug = params[:slug].presence || Pick.slug_for(picked)
|
|
31
|
+
redirect_to burst_path(Burst.from(Burst.start(slug, name: params[:name].presence, tops:)).slug)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The choices of a burst as the pages' script keeps them: read whole, and
|
|
5
|
+
# written whole when the writing is newer than the one kept.
|
|
6
|
+
class ChoicesController < ApplicationController
|
|
7
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
8
|
+
|
|
9
|
+
rescue_from(Choices::Stale) { render json: {error: it.message, document: choices.document}, status: :conflict }
|
|
10
|
+
rescue_from(JSON::ParserError, KeyError) { render json: {error: "These are not the choices of #{@burst.name}."}, status: :unprocessable_content }
|
|
11
|
+
|
|
12
|
+
def show = render(json: {document: choices.document})
|
|
13
|
+
|
|
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.
|
|
16
|
+
def update
|
|
17
|
+
incoming = JSON.parse(request.raw_post)
|
|
18
|
+
mine?(incoming) or raise KeyError
|
|
19
|
+
choices.keep(incoming.merge("burst" => @burst.slug, "review" => @burst.slug))
|
|
20
|
+
render json: {revision: choices.revision}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def choices = @choices ||= Choices.of(@burst)
|
|
26
|
+
|
|
27
|
+
# A page from before bursts had their name says whose they are as `review`.
|
|
28
|
+
def mine?(incoming) = incoming.is_a?(Hash) && incoming["repository"] == @burst.repository && incoming.fetch("burst") { incoming["review"] } == @burst.slug
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class CodeReviewsController < ApplicationController
|
|
5
|
+
include PullRequestScoped
|
|
6
|
+
|
|
7
|
+
# The code comments of a pull request; as JSON, the tally its page's
|
|
8
|
+
# outline asks for again whenever the owner comes back to that tab.
|
|
9
|
+
def show
|
|
10
|
+
@code = CodeReview.new(@burst, @stop.number)
|
|
11
|
+
respond_to do |format|
|
|
12
|
+
format.html { @snapshot, @threads = @code.snapshot, @code.threads }
|
|
13
|
+
format.json { render json: @code.tally.to_h }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
redirect_to CodeReview.new(@burst, @stop.number).start(@stop, head: params[:head], base: params[:base]), allow_other_host: true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
CodeReview.new(@burst, @stop.number).stop_server
|
|
23
|
+
redirect_to burst_pull_request_code_review_path(@burst, @stop.number)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# Every decision of the burst on one page, and the star that marks one as
|
|
5
|
+
# needing thought, which goes back to the page it was pressed on.
|
|
6
|
+
class DecisionsController < ApplicationController
|
|
7
|
+
include WithDecisions
|
|
8
|
+
|
|
9
|
+
# `fresh` reads GitHub again, then comes back to an address a reload can repeat.
|
|
10
|
+
def index
|
|
11
|
+
if params.key?(:fresh)
|
|
12
|
+
@burst.route(fresh: true)
|
|
13
|
+
return redirect_to(burst_decisions_path(@burst, show: params[:show]))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@burst.reports.take_in
|
|
17
|
+
# Listed and counted within the focus; the stars and ticks it sends are
|
|
18
|
+
# found among the whole burst's.
|
|
19
|
+
@decisions = @burst.decisions(pages:, focus:)
|
|
20
|
+
@filter = @decisions.filter_for(params[:show] || "open")
|
|
21
|
+
# By pull request, or by theme: one kind of question across the burst.
|
|
22
|
+
@by_theme = params[:by] == "theme"
|
|
23
|
+
@kept = params[:kept]
|
|
24
|
+
@records = DecisionRecord.all
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update
|
|
28
|
+
box = decisions.box(number, params.expect(:position).to_i, params.expect(:lead))
|
|
29
|
+
@burst.marks(number).star(box.lead, on: params.expect(:starred) == "1")
|
|
30
|
+
redirect_to url_from(params[:return].to_s) || burst_decisions_path(@burst, anchor: "box-#{box.to_param}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The address a scenario's button hands a browser, good once: it sets the
|
|
5
|
+
# cookies that sign the browser in on the app and sends it there.
|
|
6
|
+
class EntriesController < ApplicationController
|
|
7
|
+
def show
|
|
8
|
+
visit = Visit.take(params.expect(:token)) or return render(plain: "This address was good once and has been used. Press the scenario's button again.", status: :gone)
|
|
9
|
+
|
|
10
|
+
response.headers["Set-Cookie"] = visit.cookies if visit.cookies.any?
|
|
11
|
+
redirect_to visit.to, allow_other_host: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The pictures an agent's answer rests on, shown where the answer is read.
|
|
5
|
+
class EvidenceController < ApplicationController
|
|
6
|
+
before_action { @evidence = Evidence.new(Burst.find(params.expect(:burst_id))) }
|
|
7
|
+
|
|
8
|
+
# Retaken under the same name as often as the agent works again, so never kept by the browser.
|
|
9
|
+
def show
|
|
10
|
+
file = @evidence.picture(params.expect(:path)) or return head(:not_found)
|
|
11
|
+
|
|
12
|
+
send_file file, type: Rack::Mime.mime_type(file.extname.downcase), disposition: :inline
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Where a before and its after differ, for the viewer to outline and go through.
|
|
16
|
+
def compare
|
|
17
|
+
files = params.expect(:before, :after).map { @evidence.picture(it) }
|
|
18
|
+
return head(:not_found) unless files.all?
|
|
19
|
+
|
|
20
|
+
render json: Difference.between(*files, cache: Project.current.cache).to_h.slice(:compared, :same, :sizes, :boxes)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# How fresh the desk's reading of GitHub is. A page asks whether GitHub has
|
|
5
|
+
# moved on once it is shown, so that opening a page never waits on GitHub.
|
|
6
|
+
# With `again` the desk reads GitHub anew and goes back to the page that
|
|
7
|
+
# asked, as the pages' own "Read again" links do.
|
|
8
|
+
class FreshnessController < ApplicationController
|
|
9
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
return again if params.key?(:again)
|
|
13
|
+
|
|
14
|
+
render json: {read_at: @burst.read_at.iso8601, **@burst.moved}
|
|
15
|
+
rescue Github::Unreachable
|
|
16
|
+
# A GitHub that does not answer says nothing has moved: the question is a courtesy.
|
|
17
|
+
render json: {changed: [], closed: []}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def again
|
|
23
|
+
@burst.route(fresh: true)
|
|
24
|
+
redirect_to url_from(params[:back].to_s) || burst_path(@burst)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The owner's "good to go" for the commit and the description a pull
|
|
5
|
+
# request has now, or the taking of it back.
|
|
6
|
+
class GoodToGoController < ApplicationController
|
|
7
|
+
include PullRequestScoped
|
|
8
|
+
|
|
9
|
+
# The mark is for what the owner was looking at: the page sends the commit
|
|
10
|
+
# and the description it showed, and GitHub is read again at this moment.
|
|
11
|
+
# A push or an edit in between means the mark is not given.
|
|
12
|
+
def create
|
|
13
|
+
now = @burst.route(fresh: true).at(@stop.number)
|
|
14
|
+
|
|
15
|
+
if now && [now.head_oid, now.description.digest] == [params[:head], params[:description]]
|
|
16
|
+
marks.settle(head: now.head_oid, described_at: now.described_at, description: now.description.digest)
|
|
17
|
+
back_to_the_page(anchor: "standing")
|
|
18
|
+
else
|
|
19
|
+
redirect_to burst_pull_request_path(@burst, @stop.number),
|
|
20
|
+
alert: "##{@stop.number} moved on GitHub since this page was read, so it was not marked. Look at it again as it is now."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def destroy
|
|
25
|
+
marks.unsettle
|
|
26
|
+
back_to_the_page(anchor: "standing")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The owner moving a burst's kept membership on: by dismissing a notice
|
|
5
|
+
# that a pull request joined or left, or by handing the burst over.
|
|
6
|
+
class MembershipsController < ApplicationController
|
|
7
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
8
|
+
|
|
9
|
+
def update
|
|
10
|
+
@burst.membership.acknowledge(PullRequest.number(params.expect(:number)), @burst.route)
|
|
11
|
+
redirect_to burst_path(@burst, anchor: "notices")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The pages' script says so once a handover is downloaded.
|
|
15
|
+
def handed_over
|
|
16
|
+
@burst.membership.hand_over(@burst.route)
|
|
17
|
+
head :no_content
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The pull requests a # may mean, asked as the owner types one into any box
|
|
5
|
+
# of the desk: mentions.js offers them under it. A checkout with no GitHub
|
|
6
|
+
# repository, or digits that are not digits, mean none.
|
|
7
|
+
class MentionsController < ApplicationController
|
|
8
|
+
def index
|
|
9
|
+
mentions = Mentions.here
|
|
10
|
+
digits = params[:number].to_s[/\A\d{1,9}\z/]
|
|
11
|
+
return render(json: []) unless mentions && digits
|
|
12
|
+
|
|
13
|
+
render json: mentions.matching(digits).map(&:to_h)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# A note pinned to a moment of a recording, and moving it along. Both go
|
|
5
|
+
# back to the player, which is where the owner was.
|
|
6
|
+
class MomentsController < ApplicationController
|
|
7
|
+
include NoteParams
|
|
8
|
+
|
|
9
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
recording = Recording.find(@burst, params.expect(:recording)) or return head(:not_found)
|
|
13
|
+
time = Float(params.expect(:time), exception: false) or raise ActionController::BadRequest, "A moment is a time in seconds"
|
|
14
|
+
moment = recording.moment(time, box: params[:box].presence&.then { box_of(it) }, keep_in: Marks.directory_of(@burst).join("frames"))
|
|
15
|
+
note = @burst.marks(recording.folder).pin(kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip, place: moment)
|
|
16
|
+
redirect_to burst_recording_path(@burst, recording.to_param, anchor: "note-#{note.id}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A note's id begins with the folder of its recording: "sitting-3", "1085-2".
|
|
20
|
+
def update
|
|
21
|
+
id = params.expect(:id)
|
|
22
|
+
@burst.marks(id[/\A(sitting|\d+)-\d+\z/, 1].to_s).move(id, to: one_of(Note::STATES, params.expect(:state))) or return head(:not_found)
|
|
23
|
+
redirect_back_or_to burst_recordings_path(@burst)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# What a controller that writes notes asks of its parameters.
|
|
5
|
+
module NoteParams
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def one_of(choices, value) = choices.include?(value) ? value : raise(ActionController::BadRequest, "#{value.inspect} is none of #{choices.to_sentence}")
|
|
11
|
+
|
|
12
|
+
# A picture's id as the pictures route takes it, and nothing else, since
|
|
13
|
+
# it is kept in the note as it was sent.
|
|
14
|
+
def asset_of(id)
|
|
15
|
+
Picture.asset?(id) ? id : raise(Failure, "That is not the id of a picture, so no note was saved.")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# "x,y,width,height", each a fraction of the picture.
|
|
19
|
+
def box_of(text)
|
|
20
|
+
box = text.split(",").map { Float(it, exception: false) }
|
|
21
|
+
(box.size == 4 && box.all? { it&.between?(0, 1) }) ? box : raise(ActionController::BadRequest, "A box is four fractions of the picture, not #{text.inspect}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# Writing a note, and moving it along: both come back to the page that sent
|
|
5
|
+
# them when it said where (a note under a box is written on the decisions page
|
|
6
|
+
# or on the pull request's, and stays there), else to the pull request's page
|
|
7
|
+
# at the note.
|
|
8
|
+
class NotesController < ApplicationController
|
|
9
|
+
include NoteParams
|
|
10
|
+
|
|
11
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
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.
|
|
15
|
+
def create
|
|
16
|
+
note = marks.pin(kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip, place:)
|
|
17
|
+
respond_to do |format|
|
|
18
|
+
format.html { back_to note }
|
|
19
|
+
format.json { render json: {at: at(note), id: note.id}, status: :created }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A state sent moves the note along; words sent without one are the note
|
|
24
|
+
# written again, as the picture viewer opens a note drawn on a picture.
|
|
25
|
+
def update
|
|
26
|
+
note = (params.key?(:state) ? moved : revised) or return head(:not_found)
|
|
27
|
+
respond_to do |format|
|
|
28
|
+
format.html { back_to note }
|
|
29
|
+
format.json { render json: {at: at(note), id: note.id} }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def marks = @marks ||= @burst.marks(PullRequest.number(params.expect(:pull_request_number)))
|
|
36
|
+
|
|
37
|
+
def moved = marks.move(params.expect(:id), to: one_of(Note::STATES, params.expect(:state)))
|
|
38
|
+
|
|
39
|
+
def revised = marks.revise(params.expect(:id), kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip)
|
|
40
|
+
|
|
41
|
+
def back_to(note) = redirect_to(url_from(params[:return].to_s) || at(note))
|
|
42
|
+
|
|
43
|
+
def at(note) = burst_pull_request_path(@burst, marks.number, anchor: "note-#{note.id}")
|
|
44
|
+
|
|
45
|
+
# Words of the description, a region of a picture, or neither: the whole pull request.
|
|
46
|
+
def place
|
|
47
|
+
if params.key?(:quote)
|
|
48
|
+
quote = params.expect(quote: %i[exact before after section])
|
|
49
|
+
Note::Quote.new(exact: quote.require(:exact), before: quote[:before].presence, after: quote[:after].presence, section: quote[:section].presence)
|
|
50
|
+
elsif params.key?(:picture)
|
|
51
|
+
picture = params.expect(picture: %i[picture asset box])
|
|
52
|
+
Note::Region.new(picture: picture.require(:picture), asset: asset_of(picture.require(:asset)), box: box_of(picture.require(:box)))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|