detached 67508cc

#4 feat(issues): native issues, owner inbox and moderated public form

state
merged · 67508cc · fast-forward
branch
feat/issue-form → main
opened
bnema ·
updated

Implements ADR 0031: native issues, the owner Inbox, and the moderated public issue form. It contains the three stacked branches feat/issues → feat/inbox → feat/issue-form, and each phase was reviewed before the next.

What it does

  • Issues:
    • Issues share the per-repo number with merge requests, so #n is one or the other and links redirect both ways.
    • New scope issue:write. Agents can create, comment, close and reopen issues without moderation.
    • Per-repo setting issues: off|owner|public, default off. A repo with issues off answers the same 404 as an unknown repo.
    • CLI ermite issue create|list|show|comment|close|reopen and repo edit --issues.
    • Web: an Issues tab, live updates, and owner forms.
  • Inbox (/-/inbox, owner only):
    • Counters at the top, then the sections Awaiting moderation, Open merge requests and Open issues.
    • Repository filter, and an owner live stream across all repositories.
    • Button at the top left of the home page, shown only when signed in.
  • Public form (mode public):
    • JavaScript required. A same-origin worker solves a proof of work signed by the server (18 to 21 bits, adaptive).
    • Honeypot field and a minimum fill time of 3 s.
    • Single-use challenge and form token.
    • Limits of 3/hour and 10/day per network, and a cap of 200 pending issues, both reserved atomically.
    • Bans last 30 days and are keyed on an HMAC of the address. No IP is written to disk.
    • Until the proof of work is valid, every refusal is the same generic 400, so nothing reveals a ban, a limit or a full queue.
    • Moderation (approve, reject, reject and ban) happens in the Inbox and over the API.
    • Approved anonymous issues render untrusted Markdown. Pending issues render as plain text.
  • Storage: schema 3 → 4. The upgrade adds issue:write to stored tokens that already hold every scope, so they keep their owner rights.

Verification

  • Phase reviews (go-reviewer) and a JS review (ts-reviewer). All findings are fixed, including:
    • comment loss after retries;
    • issue events delivered after issues were turned off;
    • the bypass of the per-network limit and of the cap under concurrency;
    • the ban oracle;
    • the emoji/NFC counters;
    • restoring the page from bfcache.
  • Final wrap-up review: every point is fixed except the deferred one listed below.
  • End-to-end test in headless Chromium at production difficulty, measured as:
    • a proof-of-work solve of 0.25 to 4.4 s;
    • moderation, then approval as #1;
    • 403 without Origin, 400 on token reuse, 429 with Retry-After;
    • the no-JS message shown instead of the form.
  • go vet, go test -race ./... (full suite), hexcheck, templ drift, buf generate, the mocks and go mod tidy all pass. All 155 JS tests pass. All commits are signed.

Before leaving draft

  • Merge MR !1 (feat/mr-notes) first. Then rebase this branch on main and extend the v3 → v4 upgrade so that it grants mr:review and issue:write to tokens holding the 9 original scopes. Also restore mr:review in client.AllScopes() and drop the temporary Unimplemented MR note handlers in rpc. Do not deploy main between the two merges: existing all-scope tokens would lose their owner rights.
  • Merge the ermite-api MR (feat/issue-form) and tag it. Replace the local replace bnema.dev/ermite-api with go get bnema.dev/ermite-api@<tag>, then run go mod tidy.
  • Deferred (GO-004, scaling): the home badge and every Inbox event compute the full Overview. This is fine on a solo instance; a lighter count-only path can come later.
  • Behind a reverse proxy, set ERMITE_TRUSTED_PROXIES. Without it, every visitor shares the proxy's per-network limit.
CommitSubjectAuthorDateSignatureStatus
67508cctest(usecase): run the issue moderation subtests in parallelbriceGPG Verified
f6f76c8build(deps): ermite-api v0.9.0briceGPG Verified
7e0e489feat(kvstore): grant mr:review and issue:write to full tokens in the schema 4 upgradebriceGPG Verified
fa37646merge: main with merge request notes into feat/issue-formbriceGPG Verified
346e37fdocs: correct issue access, limiter and comment docsbriceGPG Verified
e53302brefactor(kvstore): keep the hexcheck waiver on issue approvalbriceGPG Verified
ddab9a2fix(rpc): answer not found to non-owners before parsing moderation requestsbriceGPG Verified
f8b1e92refactor: drop the unused pending issue state, ListOpen and InboxMergeRequestbriceGPG Verified
1486f6drefactor(domain): build approved issues in the domainbriceGPG Verified
0284609perf(kvstore): page issues without decoding the whole historybriceGPG Verified
17b5f65fix(kvstore): bound the numbering locksbriceGPG Verified
b0db7adfix(usecase): refresh the inbox after a rejectionbriceGPG Verified
03a1ab8Merge branch 'feat/inbox' into feat/issue-formbriceGPG Verified
0092b37Merge branch 'feat/issues' into feat/inboxbriceGPG Verified
30f7beafix(rpc): build against the api with merge request notes and reject scope mr:reviewbriceGPG Verified
b5344bffix(usecase): verify the proof of work before any admission check and reserve quota and queue atomicallybriceGPG Verified
bc78630fix(web): count issue form limits like the server and refresh the challenge after back navigationbriceGPG Verified
e687d9cdocs: public issue form and moderationbriceGPG Verified
cce8cfafeat(web): public issue form and moderation in the inboxbriceGPG Verified
093dc62feat(web): proof of work solverbriceGPG Verified
3f9aa10feat(web): untrusted markdown for external issuesbriceGPG Verified
99db356feat(usecase): allow public issuesbriceGPG Verified
a26798bfeat(usecase): anonymous issue submission with adaptive proof of workbriceGPG Verified
ee8d39cfeat(rpc): serve pending issue moderation and wire it in the forgebriceGPG Verified
51f0135feat(usecase): add issue moderation, the form subkeys and the owner-only issue.submitted eventbriceGPG Verified
2576c64fix(clientlimit): keep the submission limits free of the domain importbriceGPG Verified
8d2ab49feat(clientlimit): add the sliding window submission limiterbriceGPG Verified
6a3e65dfeat(kvstore): store pending issues, bans and the issue form keybriceGPG Verified
dca1fe7feat(ports): add the pending issue, key, ban, submission limiter and moderation portsbriceGPG Verified
784ea9ffeat(domain): add pending issues, the issue submitted event and the form limitsbriceGPG Verified
babd8c2build(deps): use the local ermite-api issue form branchbriceGPG Verified
d66b108test(events): owner topic access matrix, token end, bearer refusal, stale indexbriceGPG Verified
063865bfix(events): deliver repo.updated and repo.deleted to owner watchers and refresh the inboxbriceGPG Verified
bc6524cfeat(web): owner inbox page and home buttonbriceGPG Verified
ab38ed2feat(events): owner topic across repositoriesbriceGPG Verified
974935dfeat(usecase): owner inbox overview across repositoriesbriceGPG Verified
e17a0e5feat(kvstore): list open merge requests and issues across repositoriesbriceGPG Verified
68460c1fix(kvstore): serialize numbering per repository so concurrent creates never exhaust retriesbriceGPG Verified
b2212f2test(app): merge the declaration with the assignment in the issue e2e test (staticcheck S1021)briceGPG Verified
2794dd4fix(kvstore): guard the repository key when updating an issue so a repo delete leaves no open-issue index keysbriceGPG Verified
02692c4fix(events): stop streaming issue events and views once issues are off or the repo is not visiblebriceGPG Verified
4f0da00fix(app): keep token preset cli working on servers without issues; return issue show write errors; correct issue help limitsbriceGPG Verified
a4f8ebdfix(usecase): report a conflict when every issue comment save conflicts instead of publishing IssueCommentedbriceGPG Verified
e3b6724merge(web): issue pages into feat/issuesbriceGPG Verified
68db1e6feat(web): annotate the issue tab and transition for hexcheckbriceGPG Verified
fd07554docs: document issuesbriceGPG Verified
582cf61feat(cli): add ermite issue commands and repo edit --issuesbriceGPG Verified
931429bfeat(web): add the issue pages, tab, redirects and live noticesbriceGPG Verified
a5d6827feat(rpc): serve IssueService and the issues setting, wire issuesbriceGPG Verified
4c8ce48feat(usecase): add the issue use cases and the issues settingbriceGPG Verified
c52c40ffeat(storage): store issues on the merge request sequence, schema 4briceGPG Verified
b41adf0feat(domain): add issues, the issues mode, text parsers and the issue:write scopebriceGPG Verified
6815c6cbuild(deps): use the local ermite-api issues branchbriceGPG Verified

Checks 8

176 files · +18416 -1025
7/8
M README.md +1 -0
M docs/cli.md +7 -2
M docs/configuration.md +1 -1
A docs/issues.md +55 -0
M docs/tokens.md +2 -1
A gen/ermite/storage/issue.pb.go +541 -0
M gen/ermite/storage/merge_request.pb.go +109 -38
M go.mod +2 -2
M go.sum +2 -2
M internal/adapters/client/client.go +5 -0
M internal/adapters/client/client_test.go +3 -3
M internal/adapters/client/features.go +4 -1
M internal/adapters/client/scopes_test.go +2 -0
M internal/adapters/kvstore/export_test.go +41 -1
A internal/adapters/kvstore/issues.go +437 -0
A internal/adapters/kvstore/issues_pending.go +408 -0
A internal/adapters/kvstore/issues_pending_test.go +271 -0
A internal/adapters/kvstore/issues_test.go +561 -0
M internal/adapters/kvstore/mergerequests.go +123 -26
M internal/adapters/kvstore/mergerequests_test.go +1 -1
M internal/adapters/kvstore/repos.go +3 -1
M internal/adapters/kvstore/store.go +75 -4
M internal/adapters/kvstore/store_test.go +4 -4
M internal/adapters/rpc/api.go +8 -3
M internal/adapters/rpc/convert.go +19 -1
M internal/adapters/rpc/convert_test.go +40 -0
A internal/adapters/rpc/issue.go +262 -0
M internal/adapters/rpc/mergerequest.go +3 -1
M internal/adapters/rpc/mergerequest_test.go +11 -11
M internal/adapters/rpc/repo.go +10 -2
M internal/adapters/web/ASSETS.md +2 -0
M internal/adapters/web/assets.go +3 -1
M internal/adapters/web/events.go +59 -3
M internal/adapters/web/events_test.go +38 -1
M internal/adapters/web/fragments.go +36 -6
M internal/adapters/web/fragments.templ +1 -1
M internal/adapters/web/fragments_templ.go +1 -1
M internal/adapters/web/home.go +3 -0
A internal/adapters/web/inbox.go +294 -0
A internal/adapters/web/inbox.templ +115 -0
A internal/adapters/web/inbox_moderation_test.go +148 -0
A internal/adapters/web/inbox_templ.go +759 -0
A internal/adapters/web/inbox_test.go +310 -0
A internal/adapters/web/issues.go +363 -0
A internal/adapters/web/issues.templ +187 -0
A internal/adapters/web/issues_form.go +276 -0
A internal/adapters/web/issues_form.templ +67 -0
A internal/adapters/web/issues_form_templ.go +421 -0
A internal/adapters/web/issues_form_test.go +345 -0
A internal/adapters/web/issues_post.go +86 -0
A internal/adapters/web/issues_templ.go +885 -0
A internal/adapters/web/issues_test.go +332 -0
A internal/adapters/web/jstest/issue-form.test.mjs +183 -0
M internal/adapters/web/keys.templ +1 -0
M internal/adapters/web/keys_templ.go +1 -1
M internal/adapters/web/markdown.go +73 -7
M internal/adapters/web/markdown_test.go +67 -0
M internal/adapters/web/mr.go +13 -8
M internal/adapters/web/mr.templ +10 -3
M internal/adapters/web/mr_post.go +8 -1
M internal/adapters/web/mr_templ.go +289 -265
M internal/adapters/web/overlays.templ +1 -1
M internal/adapters/web/overlays_templ.go +1 -1
M internal/adapters/web/pages.templ +8 -0
M internal/adapters/web/pages_templ.go +409 -363
M internal/adapters/web/repo.go +5 -0
M internal/adapters/web/session.go +16 -1
M internal/adapters/web/shell.templ +3 -0
M internal/adapters/web/shell_templ.go +180 -161
M internal/adapters/web/static/css/60-mr.css +25 -0
M internal/adapters/web/static/js/ermite.js +2 -2
A internal/adapters/web/static/js/issue-form.js +175 -0
A internal/adapters/web/static/js/pow-worker.js +72 -0
M internal/adapters/web/view.go +12 -0
M internal/adapters/web/web.go +46 -1
M internal/app/api_helpers_test.go +2 -0
M internal/app/api_test.go +1 -1
M internal/app/cli_e2e_test.go +2 -2
M internal/app/cli_fake_test.go +27 -0
A internal/app/cli_issue.go +455 -0
A internal/app/cli_issue_e2e_test.go +154 -0
A internal/app/cli_issue_test.go +264 -0
M internal/app/cli_repo.go +61 -6
M internal/app/cli_repo_test.go +6 -5
M internal/app/cli_token.go +22 -0
M internal/app/cli_token_test.go +2 -3
M internal/app/cli_version.go +1 -0
M internal/app/cli_version_test.go +5 -5
M internal/app/cli_watch_test.go +1 -1
M internal/app/forge.go +71 -22
A internal/app/issue_api_test.go +245 -0
A internal/app/issue_moderation_api_test.go +139 -0
M internal/app/root.go +1 -1
M internal/app/testdata/web/blob-binary.html +3 -1
M internal/app/testdata/web/blob-code.html +3 -1
M internal/app/testdata/web/blob-empty.html +3 -1
M internal/app/testdata/web/blob-image.html +3 -1
M internal/app/testdata/web/blob-markdown.html +3 -1
M internal/app/testdata/web/blob-svg.html +3 -1
M internal/app/testdata/web/blob-text.html +3 -1
M internal/app/testdata/web/blob-too-large.html +3 -1
@@ -227,8 +227,9 @@
227227 <kbd>c</kbd>
228228 <kbd>r</kbd>
229229 <kbd>m</kbd>
230+<kbd>i</kbd>
230231 </dt>
231-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
232+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
232233 </div>
233234 <div>
234235 <dt>
@@ -410,6 +411,7 @@
410411 k-c from:body -> ermite.tab('c');
411412 k-r from:body -> ermite.tab('r');
412413 k-m from:body -> ermite.tab('m');
414+ k-i from:body -> ermite.tab('i');
413415 k-next from:body -> ermite.nextNote();
414416 k-split from:body -> ermite.layout('split');
415417 k-unified from:body -> ermite.layout('unified');
@@ -227,8 +227,9 @@
227 <kbd>c</kbd>227 <kbd>c</kbd>
228 <kbd>r</kbd>228 <kbd>r</kbd>
229 <kbd>m</kbd>229 <kbd>m</kbd>
230+<kbd>i</kbd>
230 </dt>231 </dt>
231-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>232+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
232 </div>233 </div>
233 <div>234 <div>
234 <dt>235 <dt>
@@ -410,6 +411,7 @@
410 k-c from:body -> ermite.tab('c');411 k-c from:body -> ermite.tab('c');
411 k-r from:body -> ermite.tab('r');412 k-r from:body -> ermite.tab('r');
412 k-m from:body -> ermite.tab('m');413 k-m from:body -> ermite.tab('m');
414+ k-i from:body -> ermite.tab('i');
413 k-next from:body -> ermite.nextNote();415 k-next from:body -> ermite.nextNote();
414 k-split from:body -> ermite.layout('split');416 k-split from:body -> ermite.layout('split');
415 k-unified from:body -> ermite.layout('unified');417 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-trojan.html +3 -1
@@ -294,8 +294,9 @@
294294 <kbd>c</kbd>
295295 <kbd>r</kbd>
296296 <kbd>m</kbd>
297+<kbd>i</kbd>
297298 </dt>
298-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
299+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
299300 </div>
300301 <div>
301302 <dt>
@@ -477,6 +478,7 @@
477478 k-c from:body -> ermite.tab('c');
478479 k-r from:body -> ermite.tab('r');
479480 k-m from:body -> ermite.tab('m');
481+ k-i from:body -> ermite.tab('i');
480482 k-next from:body -> ermite.nextNote();
481483 k-split from:body -> ermite.layout('split');
482484 k-unified from:body -> ermite.layout('unified');
@@ -294,8 +294,9 @@
294 <kbd>c</kbd>294 <kbd>c</kbd>
295 <kbd>r</kbd>295 <kbd>r</kbd>
296 <kbd>m</kbd>296 <kbd>m</kbd>
297+<kbd>i</kbd>
297 </dt>298 </dt>
298-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>299+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
299 </div>300 </div>
300 <div>301 <div>
301 <dt>302 <dt>
@@ -477,6 +478,7 @@
477 k-c from:body -> ermite.tab('c');478 k-c from:body -> ermite.tab('c');
478 k-r from:body -> ermite.tab('r');479 k-r from:body -> ermite.tab('r');
479 k-m from:body -> ermite.tab('m');480 k-m from:body -> ermite.tab('m');
481+ k-i from:body -> ermite.tab('i');
480 k-next from:body -> ermite.nextNote();482 k-next from:body -> ermite.nextNote();
481 k-split from:body -> ermite.layout('split');483 k-split from:body -> ermite.layout('split');
482 k-unified from:body -> ermite.layout('unified');484 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-big.html +3 -1
@@ -269,8 +269,9 @@
269269 <kbd>c</kbd>
270270 <kbd>r</kbd>
271271 <kbd>m</kbd>
272+<kbd>i</kbd>
272273 </dt>
273-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
274+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
274275 </div>
275276 <div>
276277 <dt>
@@ -452,6 +453,7 @@
452453 k-c from:body -> ermite.tab('c');
453454 k-r from:body -> ermite.tab('r');
454455 k-m from:body -> ermite.tab('m');
456+ k-i from:body -> ermite.tab('i');
455457 k-next from:body -> ermite.nextNote();
456458 k-split from:body -> ermite.layout('split');
457459 k-unified from:body -> ermite.layout('unified');
@@ -269,8 +269,9 @@
269 <kbd>c</kbd>269 <kbd>c</kbd>
270 <kbd>r</kbd>270 <kbd>r</kbd>
271 <kbd>m</kbd>271 <kbd>m</kbd>
272+<kbd>i</kbd>
272 </dt>273 </dt>
273-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>274+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
274 </div>275 </div>
275 <div>276 <div>
276 <dt>277 <dt>
@@ -452,6 +453,7 @@
452 k-c from:body -> ermite.tab('c');453 k-c from:body -> ermite.tab('c');
453 k-r from:body -> ermite.tab('r');454 k-r from:body -> ermite.tab('r');
454 k-m from:body -> ermite.tab('m');455 k-m from:body -> ermite.tab('m');
456+ k-i from:body -> ermite.tab('i');
455 k-next from:body -> ermite.nextNote();457 k-next from:body -> ermite.nextNote();
456 k-split from:body -> ermite.layout('split');458 k-split from:body -> ermite.layout('split');
457 k-unified from:body -> ermite.layout('unified');459 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-binary.html +3 -1
@@ -261,8 +261,9 @@
261261 <kbd>c</kbd>
262262 <kbd>r</kbd>
263263 <kbd>m</kbd>
264+<kbd>i</kbd>
264265 </dt>
265-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
266+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
266267 </div>
267268 <div>
268269 <dt>
@@ -444,6 +445,7 @@
444445 k-c from:body -> ermite.tab('c');
445446 k-r from:body -> ermite.tab('r');
446447 k-m from:body -> ermite.tab('m');
448+ k-i from:body -> ermite.tab('i');
447449 k-next from:body -> ermite.nextNote();
448450 k-split from:body -> ermite.layout('split');
449451 k-unified from:body -> ermite.layout('unified');
@@ -261,8 +261,9 @@
261 <kbd>c</kbd>261 <kbd>c</kbd>
262 <kbd>r</kbd>262 <kbd>r</kbd>
263 <kbd>m</kbd>263 <kbd>m</kbd>
264+<kbd>i</kbd>
264 </dt>265 </dt>
265-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>266+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
266 </div>267 </div>
267 <div>268 <div>
268 <dt>269 <dt>
@@ -444,6 +445,7 @@
444 k-c from:body -> ermite.tab('c');445 k-c from:body -> ermite.tab('c');
445 k-r from:body -> ermite.tab('r');446 k-r from:body -> ermite.tab('r');
446 k-m from:body -> ermite.tab('m');447 k-m from:body -> ermite.tab('m');
448+ k-i from:body -> ermite.tab('i');
447 k-next from:body -> ermite.nextNote();449 k-next from:body -> ermite.nextNote();
448 k-split from:body -> ermite.layout('split');450 k-split from:body -> ermite.layout('split');
449 k-unified from:body -> ermite.layout('unified');451 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-merge.html +3 -1
@@ -303,8 +303,9 @@
303303 <kbd>c</kbd>
304304 <kbd>r</kbd>
305305 <kbd>m</kbd>
306+<kbd>i</kbd>
306307 </dt>
307-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
308+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
308309 </div>
309310 <div>
310311 <dt>
@@ -486,6 +487,7 @@
486487 k-c from:body -> ermite.tab('c');
487488 k-r from:body -> ermite.tab('r');
488489 k-m from:body -> ermite.tab('m');
490+ k-i from:body -> ermite.tab('i');
489491 k-next from:body -> ermite.nextNote();
490492 k-split from:body -> ermite.layout('split');
491493 k-unified from:body -> ermite.layout('unified');
@@ -303,8 +303,9 @@
303 <kbd>c</kbd>303 <kbd>c</kbd>
304 <kbd>r</kbd>304 <kbd>r</kbd>
305 <kbd>m</kbd>305 <kbd>m</kbd>
306+<kbd>i</kbd>
306 </dt>307 </dt>
307-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>308+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
308 </div>309 </div>
309 <div>310 <div>
310 <dt>311 <dt>
@@ -486,6 +487,7 @@
486 k-c from:body -> ermite.tab('c');487 k-c from:body -> ermite.tab('c');
487 k-r from:body -> ermite.tab('r');488 k-r from:body -> ermite.tab('r');
488 k-m from:body -> ermite.tab('m');489 k-m from:body -> ermite.tab('m');
490+ k-i from:body -> ermite.tab('i');
489 k-next from:body -> ermite.nextNote();491 k-next from:body -> ermite.nextNote();
490 k-split from:body -> ermite.layout('split');492 k-split from:body -> ermite.layout('split');
491 k-unified from:body -> ermite.layout('unified');493 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-modify-split.html +3 -1
@@ -426,8 +426,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
426426 <kbd>c</kbd>
427427 <kbd>r</kbd>
428428 <kbd>m</kbd>
429+<kbd>i</kbd>
429430 </dt>
430-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
431+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
431432 </div>
432433 <div>
433434 <dt>
@@ -609,6 +610,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
609610 k-c from:body -> ermite.tab('c');
610611 k-r from:body -> ermite.tab('r');
611612 k-m from:body -> ermite.tab('m');
613+ k-i from:body -> ermite.tab('i');
612614 k-next from:body -> ermite.nextNote();
613615 k-split from:body -> ermite.layout('split');
614616 k-unified from:body -> ermite.layout('unified');
@@ -426,8 +426,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
426 <kbd>c</kbd>426 <kbd>c</kbd>
427 <kbd>r</kbd>427 <kbd>r</kbd>
428 <kbd>m</kbd>428 <kbd>m</kbd>
429+<kbd>i</kbd>
429 </dt>430 </dt>
430-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>431+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
431 </div>432 </div>
432 <div>433 <div>
433 <dt>434 <dt>
@@ -609,6 +610,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
609 k-c from:body -> ermite.tab('c');610 k-c from:body -> ermite.tab('c');
610 k-r from:body -> ermite.tab('r');611 k-r from:body -> ermite.tab('r');
611 k-m from:body -> ermite.tab('m');612 k-m from:body -> ermite.tab('m');
613+ k-i from:body -> ermite.tab('i');
612 k-next from:body -> ermite.nextNote();614 k-next from:body -> ermite.nextNote();
613 k-split from:body -> ermite.layout('split');615 k-split from:body -> ermite.layout('split');
614 k-unified from:body -> ermite.layout('unified');616 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-modify.html +3 -1
@@ -426,8 +426,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
426426 <kbd>c</kbd>
427427 <kbd>r</kbd>
428428 <kbd>m</kbd>
429+<kbd>i</kbd>
429430 </dt>
430-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
431+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
431432 </div>
432433 <div>
433434 <dt>
@@ -609,6 +610,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
609610 k-c from:body -> ermite.tab('c');
610611 k-r from:body -> ermite.tab('r');
611612 k-m from:body -> ermite.tab('m');
613+ k-i from:body -> ermite.tab('i');
612614 k-next from:body -> ermite.nextNote();
613615 k-split from:body -> ermite.layout('split');
614616 k-unified from:body -> ermite.layout('unified');
@@ -426,8 +426,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
426 <kbd>c</kbd>426 <kbd>c</kbd>
427 <kbd>r</kbd>427 <kbd>r</kbd>
428 <kbd>m</kbd>428 <kbd>m</kbd>
429+<kbd>i</kbd>
429 </dt>430 </dt>
430-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>431+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
431 </div>432 </div>
432 <div>433 <div>
433 <dt>434 <dt>
@@ -609,6 +610,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
609 k-c from:body -> ermite.tab('c');610 k-c from:body -> ermite.tab('c');
610 k-r from:body -> ermite.tab('r');611 k-r from:body -> ermite.tab('r');
611 k-m from:body -> ermite.tab('m');612 k-m from:body -> ermite.tab('m');
613+ k-i from:body -> ermite.tab('i');
612 k-next from:body -> ermite.nextNote();614 k-next from:body -> ermite.nextNote();
613 k-split from:body -> ermite.layout('split');615 k-split from:body -> ermite.layout('split');
614 k-unified from:body -> ermite.layout('unified');616 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-rename.html +3 -1
@@ -330,8 +330,9 @@
330330 <kbd>c</kbd>
331331 <kbd>r</kbd>
332332 <kbd>m</kbd>
333+<kbd>i</kbd>
333334 </dt>
334-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
335+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
335336 </div>
336337 <div>
337338 <dt>
@@ -513,6 +514,7 @@
513514 k-c from:body -> ermite.tab('c');
514515 k-r from:body -> ermite.tab('r');
515516 k-m from:body -> ermite.tab('m');
517+ k-i from:body -> ermite.tab('i');
516518 k-next from:body -> ermite.nextNote();
517519 k-split from:body -> ermite.layout('split');
518520 k-unified from:body -> ermite.layout('unified');
@@ -330,8 +330,9 @@
330 <kbd>c</kbd>330 <kbd>c</kbd>
331 <kbd>r</kbd>331 <kbd>r</kbd>
332 <kbd>m</kbd>332 <kbd>m</kbd>
333+<kbd>i</kbd>
333 </dt>334 </dt>
334-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>335+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
335 </div>336 </div>
336 <div>337 <div>
337 <dt>338 <dt>
@@ -513,6 +514,7 @@
513 k-c from:body -> ermite.tab('c');514 k-c from:body -> ermite.tab('c');
514 k-r from:body -> ermite.tab('r');515 k-r from:body -> ermite.tab('r');
515 k-m from:body -> ermite.tab('m');516 k-m from:body -> ermite.tab('m');
517+ k-i from:body -> ermite.tab('i');
516 k-next from:body -> ermite.nextNote();518 k-next from:body -> ermite.nextNote();
517 k-split from:body -> ermite.layout('split');519 k-split from:body -> ermite.layout('split');
518 k-unified from:body -> ermite.layout('unified');520 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-root.html +3 -1
@@ -649,8 +649,9 @@
649649 <kbd>c</kbd>
650650 <kbd>r</kbd>
651651 <kbd>m</kbd>
652+<kbd>i</kbd>
652653 </dt>
653-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
654+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
654655 </div>
655656 <div>
656657 <dt>
@@ -832,6 +833,7 @@
832833 k-c from:body -> ermite.tab('c');
833834 k-r from:body -> ermite.tab('r');
834835 k-m from:body -> ermite.tab('m');
836+ k-i from:body -> ermite.tab('i');
835837 k-next from:body -> ermite.nextNote();
836838 k-split from:body -> ermite.layout('split');
837839 k-unified from:body -> ermite.layout('unified');
@@ -649,8 +649,9 @@
649 <kbd>c</kbd>649 <kbd>c</kbd>
650 <kbd>r</kbd>650 <kbd>r</kbd>
651 <kbd>m</kbd>651 <kbd>m</kbd>
652+<kbd>i</kbd>
652 </dt>653 </dt>
653-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>654+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
654 </div>655 </div>
655 <div>656 <div>
656 <dt>657 <dt>
@@ -832,6 +833,7 @@
832 k-c from:body -> ermite.tab('c');833 k-c from:body -> ermite.tab('c');
833 k-r from:body -> ermite.tab('r');834 k-r from:body -> ermite.tab('r');
834 k-m from:body -> ermite.tab('m');835 k-m from:body -> ermite.tab('m');
836+ k-i from:body -> ermite.tab('i');
835 k-next from:body -> ermite.nextNote();837 k-next from:body -> ermite.nextNote();
836 k-split from:body -> ermite.layout('split');838 k-split from:body -> ermite.layout('split');
837 k-unified from:body -> ermite.layout('unified');839 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-trojan.html +3 -1
@@ -478,8 +478,9 @@
478478 <kbd>c</kbd>
479479 <kbd>r</kbd>
480480 <kbd>m</kbd>
481+<kbd>i</kbd>
481482 </dt>
482-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
483+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
483484 </div>
484485 <div>
485486 <dt>
@@ -661,6 +662,7 @@
661662 k-c from:body -> ermite.tab('c');
662663 k-r from:body -> ermite.tab('r');
663664 k-m from:body -> ermite.tab('m');
665+ k-i from:body -> ermite.tab('i');
664666 k-next from:body -> ermite.nextNote();
665667 k-split from:body -> ermite.layout('split');
666668 k-unified from:body -> ermite.layout('unified');
@@ -478,8 +478,9 @@
478 <kbd>c</kbd>478 <kbd>c</kbd>
479 <kbd>r</kbd>479 <kbd>r</kbd>
480 <kbd>m</kbd>480 <kbd>m</kbd>
481+<kbd>i</kbd>
481 </dt>482 </dt>
482-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>483+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
483 </div>484 </div>
484 <div>485 <div>
485 <dt>486 <dt>
@@ -661,6 +662,7 @@
661 k-c from:body -> ermite.tab('c');662 k-c from:body -> ermite.tab('c');
662 k-r from:body -> ermite.tab('r');663 k-r from:body -> ermite.tab('r');
663 k-m from:body -> ermite.tab('m');664 k-m from:body -> ermite.tab('m');
665+ k-i from:body -> ermite.tab('i');
664 k-next from:body -> ermite.nextNote();666 k-next from:body -> ermite.nextNote();
665 k-split from:body -> ermite.layout('split');667 k-split from:body -> ermite.layout('split');
666 k-unified from:body -> ermite.layout('unified');668 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-page2.html +3 -1
@@ -254,8 +254,9 @@
254254 <kbd>c</kbd>
255255 <kbd>r</kbd>
256256 <kbd>m</kbd>
257+<kbd>i</kbd>
257258 </dt>
258-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
259+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
259260 </div>
260261 <div>
261262 <dt>
@@ -423,6 +424,7 @@
423424 k-c from:body -> ermite.tab('c');
424425 k-r from:body -> ermite.tab('r');
425426 k-m from:body -> ermite.tab('m');
427+ k-i from:body -> ermite.tab('i');
426428 k-next from:body -> ermite.nextNote();
427429 k-split from:body -> ermite.layout('split');
428430 k-unified from:body -> ermite.layout('unified');
@@ -254,8 +254,9 @@
254 <kbd>c</kbd>254 <kbd>c</kbd>
255 <kbd>r</kbd>255 <kbd>r</kbd>
256 <kbd>m</kbd>256 <kbd>m</kbd>
257+<kbd>i</kbd>
257 </dt>258 </dt>
258-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>259+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
259 </div>260 </div>
260 <div>261 <div>
261 <dt>262 <dt>
@@ -423,6 +424,7 @@
423 k-c from:body -> ermite.tab('c');424 k-c from:body -> ermite.tab('c');
424 k-r from:body -> ermite.tab('r');425 k-r from:body -> ermite.tab('r');
425 k-m from:body -> ermite.tab('m');426 k-m from:body -> ermite.tab('m');
427+ k-i from:body -> ermite.tab('i');
426 k-next from:body -> ermite.nextNote();428 k-next from:body -> ermite.nextNote();
427 k-split from:body -> ermite.layout('split');429 k-split from:body -> ermite.layout('split');
428 k-unified from:body -> ermite.layout('unified');430 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-path.html +3 -1
@@ -269,8 +269,9 @@
269269 <kbd>c</kbd>
270270 <kbd>r</kbd>
271271 <kbd>m</kbd>
272+<kbd>i</kbd>
272273 </dt>
273-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
274+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
274275 </div>
275276 <div>
276277 <dt>
@@ -438,6 +439,7 @@
438439 k-c from:body -> ermite.tab('c');
439440 k-r from:body -> ermite.tab('r');
440441 k-m from:body -> ermite.tab('m');
442+ k-i from:body -> ermite.tab('i');
441443 k-next from:body -> ermite.nextNote();
442444 k-split from:body -> ermite.layout('split');
443445 k-unified from:body -> ermite.layout('unified');
@@ -269,8 +269,9 @@
269 <kbd>c</kbd>269 <kbd>c</kbd>
270 <kbd>r</kbd>270 <kbd>r</kbd>
271 <kbd>m</kbd>271 <kbd>m</kbd>
272+<kbd>i</kbd>
272 </dt>273 </dt>
273-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>274+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
274 </div>275 </div>
275 <div>276 <div>
276 <dt>277 <dt>
@@ -438,6 +439,7 @@
438 k-c from:body -> ermite.tab('c');439 k-c from:body -> ermite.tab('c');
439 k-r from:body -> ermite.tab('r');440 k-r from:body -> ermite.tab('r');
440 k-m from:body -> ermite.tab('m');441 k-m from:body -> ermite.tab('m');
442+ k-i from:body -> ermite.tab('i');
441 k-next from:body -> ermite.nextNote();443 k-next from:body -> ermite.nextNote();
442 k-split from:body -> ermite.layout('split');444 k-split from:body -> ermite.layout('split');
443 k-unified from:body -> ermite.layout('unified');445 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-root.html +4 -2
@@ -972,7 +972,7 @@
972972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
973973 htmx:sse:error -> ermiteLive.error(event.detail.status);
974974 htmx:sse:before:connection -> ermiteLive.gate(event);
975- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
975+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
976976 gap -> ermiteLive.reload()">
977977 </span>
978978 </div>
@@ -1058,8 +1058,9 @@
10581058 <kbd>c</kbd>
10591059 <kbd>r</kbd>
10601060 <kbd>m</kbd>
1061+<kbd>i</kbd>
10611062 </dt>
1062-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
1063+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
10631064 </div>
10641065 <div>
10651066 <dt>
@@ -1227,6 +1228,7 @@
12271228 k-c from:body -> ermite.tab('c');
12281229 k-r from:body -> ermite.tab('r');
12291230 k-m from:body -> ermite.tab('m');
1231+ k-i from:body -> ermite.tab('i');
12301232 k-next from:body -> ermite.nextNote();
12311233 k-split from:body -> ermite.layout('split');
12321234 k-unified from:body -> ermite.layout('unified');
@@ -972,7 +972,7 @@
972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
973 htmx:sse:error -> ermiteLive.error(event.detail.status);973 htmx:sse:error -> ermiteLive.error(event.detail.status);
974 htmx:sse:before:connection -> ermiteLive.gate(event);974 htmx:sse:before:connection -> ermiteLive.gate(event);
975- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);975+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
976 gap -> ermiteLive.reload()">976 gap -> ermiteLive.reload()">
977 </span>977 </span>
978 </div>978 </div>
@@ -1058,8 +1058,9 @@
1058 <kbd>c</kbd>1058 <kbd>c</kbd>
1059 <kbd>r</kbd>1059 <kbd>r</kbd>
1060 <kbd>m</kbd>1060 <kbd>m</kbd>
1061+<kbd>i</kbd>
1061 </dt>1062 </dt>
1062-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>1063+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
1063 </div>1064 </div>
1064 <div>1065 <div>
1065 <dt>1066 <dt>
@@ -1227,6 +1228,7 @@
1227 k-c from:body -> ermite.tab('c');1228 k-c from:body -> ermite.tab('c');
1228 k-r from:body -> ermite.tab('r');1229 k-r from:body -> ermite.tab('r');
1229 k-m from:body -> ermite.tab('m');1230 k-m from:body -> ermite.tab('m');
1231+ k-i from:body -> ermite.tab('i');
1230 k-next from:body -> ermite.nextNote();1232 k-next from:body -> ermite.nextNote();
1231 k-split from:body -> ermite.layout('split');1233 k-split from:body -> ermite.layout('split');
1232 k-unified from:body -> ermite.layout('unified');1234 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-small.html +4 -2
@@ -317,7 +317,7 @@
317317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
318318 htmx:sse:error -> ermiteLive.error(event.detail.status);
319319 htmx:sse:before:connection -> ermiteLive.gate(event);
320- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
320+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
321321 gap -> ermiteLive.reload()">
322322 </span>
323323 </div>
@@ -403,8 +403,9 @@
403403 <kbd>c</kbd>
404404 <kbd>r</kbd>
405405 <kbd>m</kbd>
406+<kbd>i</kbd>
406407 </dt>
407-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
408+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
408409 </div>
409410 <div>
410411 <dt>
@@ -592,6 +593,7 @@
592593 k-c from:body -> ermite.tab('c');
593594 k-r from:body -> ermite.tab('r');
594595 k-m from:body -> ermite.tab('m');
596+ k-i from:body -> ermite.tab('i');
595597 k-next from:body -> ermite.nextNote();
596598 k-split from:body -> ermite.layout('split');
597599 k-unified from:body -> ermite.layout('unified');
@@ -317,7 +317,7 @@
317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
318 htmx:sse:error -> ermiteLive.error(event.detail.status);318 htmx:sse:error -> ermiteLive.error(event.detail.status);
319 htmx:sse:before:connection -> ermiteLive.gate(event);319 htmx:sse:before:connection -> ermiteLive.gate(event);
320- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);320+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
321 gap -> ermiteLive.reload()">321 gap -> ermiteLive.reload()">
322 </span>322 </span>
323 </div>323 </div>
@@ -403,8 +403,9 @@
403 <kbd>c</kbd>403 <kbd>c</kbd>
404 <kbd>r</kbd>404 <kbd>r</kbd>
405 <kbd>m</kbd>405 <kbd>m</kbd>
406+<kbd>i</kbd>
406 </dt>407 </dt>
407-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>408+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
408 </div>409 </div>
409 <div>410 <div>
410 <dt>411 <dt>
@@ -592,6 +593,7 @@
592 k-c from:body -> ermite.tab('c');593 k-c from:body -> ermite.tab('c');
593 k-r from:body -> ermite.tab('r');594 k-r from:body -> ermite.tab('r');
594 k-m from:body -> ermite.tab('m');595 k-m from:body -> ermite.tab('m');
596+ k-i from:body -> ermite.tab('i');
595 k-next from:body -> ermite.nextNote();597 k-next from:body -> ermite.nextNote();
596 k-split from:body -> ermite.layout('split');598 k-split from:body -> ermite.layout('split');
597 k-unified from:body -> ermite.layout('unified');599 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/empty.html +3 -1
@@ -176,8 +176,9 @@
176176 <kbd>c</kbd>
177177 <kbd>r</kbd>
178178 <kbd>m</kbd>
179+<kbd>i</kbd>
179180 </dt>
180-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
181+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
181182 </div>
182183 <div>
183184 <dt>
@@ -276,6 +277,7 @@
276277 k-c from:body -> ermite.tab('c');
277278 k-r from:body -> ermite.tab('r');
278279 k-m from:body -> ermite.tab('m');
280+ k-i from:body -> ermite.tab('i');
279281 k-next from:body -> ermite.nextNote();
280282 k-split from:body -> ermite.layout('split');
281283 k-unified from:body -> ermite.layout('unified');
@@ -176,8 +176,9 @@
176 <kbd>c</kbd>176 <kbd>c</kbd>
177 <kbd>r</kbd>177 <kbd>r</kbd>
178 <kbd>m</kbd>178 <kbd>m</kbd>
179+<kbd>i</kbd>
179 </dt>180 </dt>
180-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>181+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
181 </div>182 </div>
182 <div>183 <div>
183 <dt>184 <dt>
@@ -276,6 +277,7 @@
276 k-c from:body -> ermite.tab('c');277 k-c from:body -> ermite.tab('c');
277 k-r from:body -> ermite.tab('r');278 k-r from:body -> ermite.tab('r');
278 k-m from:body -> ermite.tab('m');279 k-m from:body -> ermite.tab('m');
280+ k-i from:body -> ermite.tab('i');
279 k-next from:body -> ermite.nextNote();281 k-next from:body -> ermite.nextNote();
280 k-split from:body -> ermite.layout('split');282 k-split from:body -> ermite.layout('split');
281 k-unified from:body -> ermite.layout('unified');283 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/home.html +4 -2
@@ -191,7 +191,7 @@
191191 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
192192 htmx:sse:error -> ermiteLive.error(event.detail.status);
193193 htmx:sse:before:connection -> ermiteLive.gate(event);
194- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
194+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
195195 gap -> ermiteLive.reload()">
196196 </span>
197197 </div>
@@ -277,8 +277,9 @@
277277 <kbd>c</kbd>
278278 <kbd>r</kbd>
279279 <kbd>m</kbd>
280+<kbd>i</kbd>
280281 </dt>
281-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
282+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
282283 </div>
283284 <div>
284285 <dt>
@@ -383,6 +384,7 @@
383384 k-c from:body -> ermite.tab('c');
384385 k-r from:body -> ermite.tab('r');
385386 k-m from:body -> ermite.tab('m');
387+ k-i from:body -> ermite.tab('i');
386388 k-next from:body -> ermite.nextNote();
387389 k-split from:body -> ermite.layout('split');
388390 k-unified from:body -> ermite.layout('unified');
@@ -191,7 +191,7 @@
191 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();191 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
192 htmx:sse:error -> ermiteLive.error(event.detail.status);192 htmx:sse:error -> ermiteLive.error(event.detail.status);
193 htmx:sse:before:connection -> ermiteLive.gate(event);193 htmx:sse:before:connection -> ermiteLive.gate(event);
194- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);194+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
195 gap -> ermiteLive.reload()">195 gap -> ermiteLive.reload()">
196 </span>196 </span>
197 </div>197 </div>
@@ -277,8 +277,9 @@
277 <kbd>c</kbd>277 <kbd>c</kbd>
278 <kbd>r</kbd>278 <kbd>r</kbd>
279 <kbd>m</kbd>279 <kbd>m</kbd>
280+<kbd>i</kbd>
280 </dt>281 </dt>
281-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>282+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
282 </div>283 </div>
283 <div>284 <div>
284 <dt>285 <dt>
@@ -383,6 +384,7 @@
383 k-c from:body -> ermite.tab('c');384 k-c from:body -> ermite.tab('c');
384 k-r from:body -> ermite.tab('r');385 k-r from:body -> ermite.tab('r');
385 k-m from:body -> ermite.tab('m');386 k-m from:body -> ermite.tab('m');
387+ k-i from:body -> ermite.tab('i');
386 k-next from:body -> ermite.nextNote();388 k-next from:body -> ermite.nextNote();
387 k-split from:body -> ermite.layout('split');389 k-split from:body -> ermite.layout('split');
388 k-unified from:body -> ermite.layout('unified');390 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/not-found-page.html +3 -1
@@ -165,8 +165,9 @@
165165 <kbd>c</kbd>
166166 <kbd>r</kbd>
167167 <kbd>m</kbd>
168+<kbd>i</kbd>
168169 </dt>
169-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
170+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
170171 </div>
171172 <div>
172173 <dt>
@@ -259,6 +260,7 @@
259260 k-c from:body -> ermite.tab('c');
260261 k-r from:body -> ermite.tab('r');
261262 k-m from:body -> ermite.tab('m');
263+ k-i from:body -> ermite.tab('i');
262264 k-next from:body -> ermite.nextNote();
263265 k-split from:body -> ermite.layout('split');
264266 k-unified from:body -> ermite.layout('unified');
@@ -165,8 +165,9 @@
165 <kbd>c</kbd>165 <kbd>c</kbd>
166 <kbd>r</kbd>166 <kbd>r</kbd>
167 <kbd>m</kbd>167 <kbd>m</kbd>
168+<kbd>i</kbd>
168 </dt>169 </dt>
169-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>170+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
170 </div>171 </div>
171 <div>172 <div>
172 <dt>173 <dt>
@@ -259,6 +260,7 @@
259 k-c from:body -> ermite.tab('c');260 k-c from:body -> ermite.tab('c');
260 k-r from:body -> ermite.tab('r');261 k-r from:body -> ermite.tab('r');
261 k-m from:body -> ermite.tab('m');262 k-m from:body -> ermite.tab('m');
263+ k-i from:body -> ermite.tab('i');
262 k-next from:body -> ermite.nextNote();264 k-next from:body -> ermite.nextNote();
263 k-split from:body -> ermite.layout('split');265 k-split from:body -> ermite.layout('split');
264 k-unified from:body -> ermite.layout('unified');266 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/not-found.html +3 -1
@@ -165,8 +165,9 @@
165165 <kbd>c</kbd>
166166 <kbd>r</kbd>
167167 <kbd>m</kbd>
168+<kbd>i</kbd>
168169 </dt>
169-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
170+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
170171 </div>
171172 <div>
172173 <dt>
@@ -259,6 +260,7 @@
259260 k-c from:body -> ermite.tab('c');
260261 k-r from:body -> ermite.tab('r');
261262 k-m from:body -> ermite.tab('m');
263+ k-i from:body -> ermite.tab('i');
262264 k-next from:body -> ermite.nextNote();
263265 k-split from:body -> ermite.layout('split');
264266 k-unified from:body -> ermite.layout('unified');
@@ -165,8 +165,9 @@
165 <kbd>c</kbd>165 <kbd>c</kbd>
166 <kbd>r</kbd>166 <kbd>r</kbd>
167 <kbd>m</kbd>167 <kbd>m</kbd>
168+<kbd>i</kbd>
168 </dt>169 </dt>
169-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>170+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
170 </div>171 </div>
171 <div>172 <div>
172 <dt>173 <dt>
@@ -259,6 +260,7 @@
259 k-c from:body -> ermite.tab('c');260 k-c from:body -> ermite.tab('c');
260 k-r from:body -> ermite.tab('r');261 k-r from:body -> ermite.tab('r');
261 k-m from:body -> ermite.tab('m');262 k-m from:body -> ermite.tab('m');
263+ k-i from:body -> ermite.tab('i');
262 k-next from:body -> ermite.nextNote();264 k-next from:body -> ermite.nextNote();
263 k-split from:body -> ermite.layout('split');265 k-split from:body -> ermite.layout('split');
264 k-unified from:body -> ermite.layout('unified');266 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/refs.html +4 -2
@@ -199,7 +199,7 @@
199199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
200200 htmx:sse:error -> ermiteLive.error(event.detail.status);
201201 htmx:sse:before:connection -> ermiteLive.gate(event);
202- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
202+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
203203 gap -> ermiteLive.reload()">
204204 </span>
205205 </div>
@@ -285,8 +285,9 @@
285285 <kbd>c</kbd>
286286 <kbd>r</kbd>
287287 <kbd>m</kbd>
288+<kbd>i</kbd>
288289 </dt>
289-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
290+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
290291 </div>
291292 <div>
292293 <dt>
@@ -484,6 +485,7 @@
484485 k-c from:body -> ermite.tab('c');
485486 k-r from:body -> ermite.tab('r');
486487 k-m from:body -> ermite.tab('m');
488+ k-i from:body -> ermite.tab('i');
487489 k-next from:body -> ermite.nextNote();
488490 k-split from:body -> ermite.layout('split');
489491 k-unified from:body -> ermite.layout('unified');
@@ -199,7 +199,7 @@
199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
200 htmx:sse:error -> ermiteLive.error(event.detail.status);200 htmx:sse:error -> ermiteLive.error(event.detail.status);
201 htmx:sse:before:connection -> ermiteLive.gate(event);201 htmx:sse:before:connection -> ermiteLive.gate(event);
202- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);202+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
203 gap -> ermiteLive.reload()">203 gap -> ermiteLive.reload()">
204 </span>204 </span>
205 </div>205 </div>
@@ -285,8 +285,9 @@
285 <kbd>c</kbd>285 <kbd>c</kbd>
286 <kbd>r</kbd>286 <kbd>r</kbd>
287 <kbd>m</kbd>287 <kbd>m</kbd>
288+<kbd>i</kbd>
288 </dt>289 </dt>
289-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>290+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
290 </div>291 </div>
291 <div>292 <div>
292 <dt>293 <dt>
@@ -484,6 +485,7 @@
484 k-c from:body -> ermite.tab('c');485 k-c from:body -> ermite.tab('c');
485 k-r from:body -> ermite.tab('r');486 k-r from:body -> ermite.tab('r');
486 k-m from:body -> ermite.tab('m');487 k-m from:body -> ermite.tab('m');
488+ k-i from:body -> ermite.tab('i');
487 k-next from:body -> ermite.nextNote();489 k-next from:body -> ermite.nextNote();
488 k-split from:body -> ermite.layout('split');490 k-split from:body -> ermite.layout('split');
489 k-unified from:body -> ermite.layout('unified');491 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/summary.html +4 -2
@@ -225,7 +225,7 @@
225225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
226226 htmx:sse:error -> ermiteLive.error(event.detail.status);
227227 htmx:sse:before:connection -> ermiteLive.gate(event);
228- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
228+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
229229 gap -> ermiteLive.reload()">
230230 </span>
231231 </div>
@@ -311,8 +311,9 @@
311311 <kbd>c</kbd>
312312 <kbd>r</kbd>
313313 <kbd>m</kbd>
314+<kbd>i</kbd>
314315 </dt>
315-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
316+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
316317 </div>
317318 <div>
318319 <dt>
@@ -510,6 +511,7 @@
510511 k-c from:body -> ermite.tab('c');
511512 k-r from:body -> ermite.tab('r');
512513 k-m from:body -> ermite.tab('m');
514+ k-i from:body -> ermite.tab('i');
513515 k-next from:body -> ermite.nextNote();
514516 k-split from:body -> ermite.layout('split');
515517 k-unified from:body -> ermite.layout('unified');
@@ -225,7 +225,7 @@
225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
226 htmx:sse:error -> ermiteLive.error(event.detail.status);226 htmx:sse:error -> ermiteLive.error(event.detail.status);
227 htmx:sse:before:connection -> ermiteLive.gate(event);227 htmx:sse:before:connection -> ermiteLive.gate(event);
228- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);228+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
229 gap -> ermiteLive.reload()">229 gap -> ermiteLive.reload()">
230 </span>230 </span>
231 </div>231 </div>
@@ -311,8 +311,9 @@
311 <kbd>c</kbd>311 <kbd>c</kbd>
312 <kbd>r</kbd>312 <kbd>r</kbd>
313 <kbd>m</kbd>313 <kbd>m</kbd>
314+<kbd>i</kbd>
314 </dt>315 </dt>
315-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>316+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
316 </div>317 </div>
317 <div>318 <div>
318 <dt>319 <dt>
@@ -510,6 +511,7 @@
510 k-c from:body -> ermite.tab('c');511 k-c from:body -> ermite.tab('c');
511 k-r from:body -> ermite.tab('r');512 k-r from:body -> ermite.tab('r');
512 k-m from:body -> ermite.tab('m');513 k-m from:body -> ermite.tab('m');
514+ k-i from:body -> ermite.tab('i');
513 k-next from:body -> ermite.nextNote();515 k-next from:body -> ermite.nextNote();
514 k-split from:body -> ermite.layout('split');516 k-split from:body -> ermite.layout('split');
515 k-unified from:body -> ermite.layout('unified');517 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/tree-nested.html +3 -1
@@ -242,8 +242,9 @@
242242 <kbd>c</kbd>
243243 <kbd>r</kbd>
244244 <kbd>m</kbd>
245+<kbd>i</kbd>
245246 </dt>
246-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
247+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
247248 </div>
248249 <div>
249250 <dt>
@@ -425,6 +426,7 @@
425426 k-c from:body -> ermite.tab('c');
426427 k-r from:body -> ermite.tab('r');
427428 k-m from:body -> ermite.tab('m');
429+ k-i from:body -> ermite.tab('i');
428430 k-next from:body -> ermite.nextNote();
429431 k-split from:body -> ermite.layout('split');
430432 k-unified from:body -> ermite.layout('unified');
@@ -242,8 +242,9 @@
242 <kbd>c</kbd>242 <kbd>c</kbd>
243 <kbd>r</kbd>243 <kbd>r</kbd>
244 <kbd>m</kbd>244 <kbd>m</kbd>
245+<kbd>i</kbd>
245 </dt>246 </dt>
246-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>247+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
247 </div>248 </div>
248 <div>249 <div>
249 <dt>250 <dt>
@@ -425,6 +426,7 @@
425 k-c from:body -> ermite.tab('c');426 k-c from:body -> ermite.tab('c');
426 k-r from:body -> ermite.tab('r');427 k-r from:body -> ermite.tab('r');
427 k-m from:body -> ermite.tab('m');428 k-m from:body -> ermite.tab('m');
429+ k-i from:body -> ermite.tab('i');
428 k-next from:body -> ermite.nextNote();430 k-next from:body -> ermite.nextNote();
429 k-split from:body -> ermite.layout('split');431 k-split from:body -> ermite.layout('split');
430 k-unified from:body -> ermite.layout('unified');432 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/tree-root.html +3 -1
@@ -463,8 +463,9 @@
463463 <kbd>c</kbd>
464464 <kbd>r</kbd>
465465 <kbd>m</kbd>
466+<kbd>i</kbd>
466467 </dt>
467-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
468+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
468469 </div>
469470 <div>
470471 <dt>
@@ -646,6 +647,7 @@
646647 k-c from:body -> ermite.tab('c');
647648 k-r from:body -> ermite.tab('r');
648649 k-m from:body -> ermite.tab('m');
650+ k-i from:body -> ermite.tab('i');
649651 k-next from:body -> ermite.nextNote();
650652 k-split from:body -> ermite.layout('split');
651653 k-unified from:body -> ermite.layout('unified');
@@ -463,8 +463,9 @@
463 <kbd>c</kbd>463 <kbd>c</kbd>
464 <kbd>r</kbd>464 <kbd>r</kbd>
465 <kbd>m</kbd>465 <kbd>m</kbd>
466+<kbd>i</kbd>
466 </dt>467 </dt>
467-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>468+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
468 </div>469 </div>
469 <div>470 <div>
470 <dt>471 <dt>
@@ -646,6 +647,7 @@
646 k-c from:body -> ermite.tab('c');647 k-c from:body -> ermite.tab('c');
647 k-r from:body -> ermite.tab('r');648 k-r from:body -> ermite.tab('r');
648 k-m from:body -> ermite.tab('m');649 k-m from:body -> ermite.tab('m');
650+ k-i from:body -> ermite.tab('i');
649 k-next from:body -> ermite.nextNote();651 k-next from:body -> ermite.nextNote();
650 k-split from:body -> ermite.layout('split');652 k-split from:body -> ermite.layout('split');
651 k-unified from:body -> ermite.layout('unified');653 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/tree-slash-ref.html +3 -1
@@ -239,8 +239,9 @@
239239 <kbd>c</kbd>
240240 <kbd>r</kbd>
241241 <kbd>m</kbd>
242+<kbd>i</kbd>
242243 </dt>
243-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
244+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
244245 </div>
245246 <div>
246247 <dt>
@@ -422,6 +423,7 @@
422423 k-c from:body -> ermite.tab('c');
423424 k-r from:body -> ermite.tab('r');
424425 k-m from:body -> ermite.tab('m');
426+ k-i from:body -> ermite.tab('i');
425427 k-next from:body -> ermite.nextNote();
426428 k-split from:body -> ermite.layout('split');
427429 k-unified from:body -> ermite.layout('unified');
@@ -239,8 +239,9 @@
239 <kbd>c</kbd>239 <kbd>c</kbd>
240 <kbd>r</kbd>240 <kbd>r</kbd>
241 <kbd>m</kbd>241 <kbd>m</kbd>
242+<kbd>i</kbd>
242 </dt>243 </dt>
243-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>244+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
244 </div>245 </div>
245 <div>246 <div>
246 <dt>247 <dt>
@@ -422,6 +423,7 @@
422 k-c from:body -> ermite.tab('c');423 k-c from:body -> ermite.tab('c');
423 k-r from:body -> ermite.tab('r');424 k-r from:body -> ermite.tab('r');
424 k-m from:body -> ermite.tab('m');425 k-m from:body -> ermite.tab('m');
426+ k-i from:body -> ermite.tab('i');
425 k-next from:body -> ermite.nextNote();427 k-next from:body -> ermite.nextNote();
426 k-split from:body -> ermite.layout('split');428 k-split from:body -> ermite.layout('split');
427 k-unified from:body -> ermite.layout('unified');429 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/tree-subdir.html +3 -1
@@ -255,8 +255,9 @@
255255 <kbd>c</kbd>
256256 <kbd>r</kbd>
257257 <kbd>m</kbd>
258+<kbd>i</kbd>
258259 </dt>
259-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>
260+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
260261 </div>
261262 <div>
262263 <dt>
@@ -438,6 +439,7 @@
438439 k-c from:body -> ermite.tab('c');
439440 k-r from:body -> ermite.tab('r');
440441 k-m from:body -> ermite.tab('m');
442+ k-i from:body -> ermite.tab('i');
441443 k-next from:body -> ermite.nextNote();
442444 k-split from:body -> ermite.layout('split');
443445 k-unified from:body -> ermite.layout('unified');
@@ -255,8 +255,9 @@
255 <kbd>c</kbd>255 <kbd>c</kbd>
256 <kbd>r</kbd>256 <kbd>r</kbd>
257 <kbd>m</kbd>257 <kbd>m</kbd>
258+<kbd>i</kbd>
258 </dt>259 </dt>
259-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests</dd>260+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
260 </div>261 </div>
261 <div>262 <div>
262 <dt>263 <dt>
@@ -438,6 +439,7 @@
438 k-c from:body -> ermite.tab('c');439 k-c from:body -> ermite.tab('c');
439 k-r from:body -> ermite.tab('r');440 k-r from:body -> ermite.tab('r');
440 k-m from:body -> ermite.tab('m');441 k-m from:body -> ermite.tab('m');
442+ k-i from:body -> ermite.tab('i');
441 k-next from:body -> ermite.nextNote();443 k-next from:body -> ermite.nextNote();
442 k-split from:body -> ermite.layout('split');444 k-split from:body -> ermite.layout('split');
443 k-unified from:body -> ermite.layout('unified');445 k-unified from:body -> ermite.layout('unified');
M internal/app/web.go +5 -0
@@ -45,6 +45,11 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han
4545 Repos: f.Repos,
4646 Browse: f.Browse,
4747 MergeRequests: f.MergeRequests,
48+ Issues: f.Issues,
49+ IssueNumbers: f.Issues,
50+ Inbox: f.Inbox,
51+ IssueForm: f.IssueSubmissions,
52+ Moderation: f.IssueModeration,
4853 Runs: f.CI,
4954 Artifacts: artifacts,
5055 Events: f.EventLog,
@@ -45,6 +45,11 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han
45 Repos: f.Repos,45 Repos: f.Repos,
46 Browse: f.Browse,46 Browse: f.Browse,
47 MergeRequests: f.MergeRequests,47 MergeRequests: f.MergeRequests,
48+ Issues: f.Issues,
49+ IssueNumbers: f.Issues,
50+ Inbox: f.Inbox,
51+ IssueForm: f.IssueSubmissions,
52+ Moderation: f.IssueModeration,
48 Runs: f.CI,53 Runs: f.CI,
49 Artifacts: artifacts,54 Artifacts: artifacts,
50 Events: f.EventLog,55 Events: f.EventLog,
A internal/app/web_inbox_test.go +152 -0
A internal/app/web_issue_form_test.go +231 -0
A internal/app/web_issue_test.go +139 -0
M internal/clientlimit/export_test.go +7 -0
A internal/clientlimit/submissions.go +178 -0
A internal/clientlimit/submissions_test.go +148 -0
M internal/domain/event.go +72 -0
A internal/domain/issue.go +106 -0
A internal/domain/issue_test.go +138 -0
M internal/domain/mergerequest.go +3 -1
A internal/domain/pending_issue.go +122 -0
A internal/domain/pending_issue_test.go +90 -0
A internal/domain/submission.go +42 -0
M internal/domain/text.go +102 -0
M internal/domain/token.go +5 -1
M internal/domain/token_test.go +6 -1
M internal/ports/dto.go +69 -0
A internal/ports/mocks/InboxService.go +121 -0
A internal/ports/mocks/IssueBanStore.go +183 -0
A internal/ports/mocks/IssueKeyStore.go +109 -0
A internal/ports/mocks/IssueModerationService.go +280 -0
A internal/ports/mocks/IssueNumberResolver.go +126 -0
A internal/ports/mocks/IssueService.go +523 -0
A internal/ports/mocks/IssueStore.go +515 -0
A internal/ports/mocks/IssueSubmissionService.go +190 -0
M internal/ports/mocks/MergeRequestStore.go +62 -0
A internal/ports/mocks/PendingIssueStore.go +452 -0
A internal/ports/mocks/SubmissionLimiter.go +188 -0
M internal/ports/ports.go +81 -0
M internal/ports/services.go +94 -0
M internal/usecase/events.go +97 -3
A internal/usecase/events_owner_test.go +200 -0
M internal/usecase/events_test.go +43 -0
M internal/usecase/export_test.go +34 -0
A internal/usecase/inbox.go +164 -0
A internal/usecase/inbox_test.go +110 -0
A internal/usecase/issue_keys.go +62 -0
A internal/usecase/issue_moderation.go +119 -0
A internal/usecase/issue_moderation_test.go +251 -0
A internal/usecase/issue_pow_vector_test.go +39 -0
A internal/usecase/issue_submissions.go +383 -0
A internal/usecase/issue_submissions_test.go +670 -0
A internal/usecase/issues.go +291 -0
A internal/usecase/issues_integration_test.go +87 -0
A internal/usecase/issues_test.go +276 -0
M internal/usecase/mergerequests_integration_test.go +1 -1
M internal/usecase/repos_write.go +13 -3
M internal/usecase/repos_write_test.go +42 -0
A proto/ermite/storage/issue.proto +71 -0
M proto/ermite/storage/merge_request.proto +10 -0
M skills/ermite/CLI.md +15 -0

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 2

Tags 41

↑↓ moveenter openesc close