detached cd99c65

refactor(web): one helper shapes the log lines of the page, raw feed and live feed

appendLogLine turns the runner's markers into structure and joins the pieces of
a line it cut, for the job page and the live feed (the SSE feed now sends merged
lines; a piece that opens a batch is marked and joined by the page). The dead
placeholders of actions.go are gone. The live log script moves out of ermite.js
into log.js with a testable follow(), covered by jstest/log.test.mjs (append,
join, scroll at the end only, close on ci-end).
commit
cd99c65322d694fa509569a3eaab1de8f1d5a9d4
author
brice · · 2026-10-12 19:20 UTC
parent
7513520
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
43 files · +502 -262
M internal/adapters/web/ASSETS.md +1 -0
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
99 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
1010 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
1111 | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | |
12+| `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | |
1213 | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | |
1314 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
1415 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
11 | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | |11 | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | |
12+| `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | |
12 | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | |13 | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | |
13 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |14 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
14 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |15 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
M internal/adapters/web/actions.go +41 -54
@@ -198,6 +198,9 @@ type LogLineView struct {
198198 // Group marks "##[group]" and "##[endgroup]" lines and Error the
199199 // "##[error]" ones; the text is shown without the marker.
200200 Group, End, Error bool
201+ // Continued marks a piece of a line whose beginning is in an earlier batch
202+ // of the live feed: the page appends its text to the line before it.
203+ Continued bool
201204 }
202205
203206 // JobPage is one job with its steps and its log.
@@ -452,6 +455,30 @@ func (s *Server) job(ctx context.Context, r *http.Request) (templ.Component, int
452455 return Job(l, p), http.StatusOK, nil
453456 }
454457
458+// appendLogLine adds the line n of a job log to the lines shown. The markers
459+// of the runner ("##[group]", "##[endgroup]", "##[error]") become structure and
460+// are not shown as text. A piece that continues a line too long to send whole
461+// is joined to the line before it; one that opens the list (its line is in an
462+// earlier batch) keeps Continued, and the page appends it to that line.
463+func appendLogLine(out []LogLineView, n int64, l ports.JobLogLine) []LogLineView {
464+ text := l.Text
465+ v := LogLineView{N: n, Step: l.Step, Time: l.Time.UTC().Format("15:04:05"), Continued: l.Continued}
466+ switch {
467+ case strings.HasPrefix(text, "##[group]"):
468+ v.Group, text = true, strings.TrimPrefix(text, "##[group]")
469+ case strings.HasPrefix(text, "##[endgroup]"):
470+ v.End, text = true, strings.TrimPrefix(text, "##[endgroup]")
471+ case strings.HasPrefix(text, "##[error]"):
472+ v.Error, text = true, strings.TrimPrefix(text, "##[error]")
473+ }
474+ v.Text = visibleText(text)
475+ if l.Continued && len(out) > 0 {
476+ out[len(out)-1].Text += v.Text
477+ return out
478+ }
479+ return append(out, v)
480+}
481+
455482 // logClient is the key of the visitor for the limit of log streams per client.
456483 func (s *Server) logClient(r *http.Request) string {
457484 return clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies))
@@ -465,40 +492,14 @@ func (s *Server) readLog(ctx context.Context, client string, repo domain.RepoNam
465492 }
466493 var out []LogLineView
467494 next := from
468- ranges := j.StepRanges()
469- stepOf := func(n int64) int32 {
470- for i, r := range ranges {
471- if n >= r.Start && n < r.End {
472- return j.Steps[i].Number
473- }
474- }
475- return 0
476- }
477495 stop := errors.New("enough")
478496 err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from, Client: client}, func(b ports.LogBatch) error {
479497 for i, l := range b.Lines {
480- if len(out) >= max {
481- return stop
482- }
483- n := b.Offset + int64(i)
484- text := l.Text
485- v := LogLineView{N: n, Step: stepOf(n), Time: l.Time.UTC().Format("15:04:05")}
486- switch {
487- case strings.HasPrefix(text, "##[group]"):
488- v.Group, text = true, strings.TrimPrefix(text, "##[group]")
489- case strings.HasPrefix(text, "##[endgroup]"):
490- v.End, text = true, strings.TrimPrefix(text, "##[endgroup]")
491- case strings.HasPrefix(text, "##[error]"):
492- v.Error, text = true, strings.TrimPrefix(text, "##[error]")
498+ if len(out) >= max && !l.Continued {
499+ return stop // the rest of a cut line still belongs to this page
493500 }
494- if l.Continued && len(out) > 0 {
495- out[len(out)-1].Text += visibleText(text)
496- next = n + 1
497- continue
498- }
499- v.Text = visibleText(text)
500- out = append(out, v)
501- next = n + 1
501+ out = appendLogLine(out, b.Offset+int64(i), l)
502+ next = b.Offset + int64(i) + 1
502503 }
503504 return nil
504505 })
@@ -533,8 +534,9 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) {
533534 }
534535 attempt = int32(n)
535536 }
536- view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt)
537- if err != nil {
537+ // The job is read first: a job that is not there, or not readable, is a 404
538+ // before any header of the log is sent.
539+ if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt); err != nil {
538540 s.fail(ctx, w, r, err)
539541 return
540542 }
@@ -573,7 +575,6 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) {
573575 _, _ = fmt.Fprintln(w)
574576 }
575577 if err != nil && !open && errors.Is(err, domain.ErrNotFound) {
576- _ = view
577578 w.WriteHeader(http.StatusNotFound)
578579 }
579580 }
@@ -714,7 +715,7 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C
714715
715716 // checksSection is the checks of the rail of a merge request, with the rule of
716717 // the repository that applies to them.
717-func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) ChecksSection {
718+func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection {
718719 sec := ChecksSection{}
719720 for _, c := range checks {
720721 row := CheckRow{
@@ -736,7 +737,6 @@ func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) Ch
736737 default:
737738 sec.Note = "checks do not block the merge in this repository."
738739 }
739- _ = now
740740 return sec
741741 }
742742
@@ -770,8 +770,7 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) {
770770 s.denyEvents(ctx, w, errNotFound)
771771 return
772772 }
773- view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0)
774- if err != nil {
773+ if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0); err != nil {
775774 s.denyEvents(ctx, w, err)
776775 return
777776 }
@@ -786,29 +785,17 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) {
786785 if out.raw(": connected\n\n") != nil {
787786 return
788787 }
789- ranges := view.StepRanges()
790- _ = ranges
791788 n := from
792789 err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{
793790 JobID: jobID, Offset: from, Follow: true,
794791 Client: s.logClient(r),
795792 }, func(b ports.LogBatch) error {
796- var html strings.Builder
793+ var views []LogLineView
797794 for i, l := range b.Lines {
798- v := LogLineView{N: b.Offset + int64(i), Time: l.Time.UTC().Format("15:04:05"), Step: l.Step}
799- text := l.Text
800- switch {
801- case strings.HasPrefix(text, "##[group]"):
802- v.Group, text = true, strings.TrimPrefix(text, "##[group]")
803- case strings.HasPrefix(text, "##[endgroup]"):
804- v.End, text = true, strings.TrimPrefix(text, "##[endgroup]")
805- case strings.HasPrefix(text, "##[error]"):
806- v.Error, text = true, strings.TrimPrefix(text, "##[error]")
807- }
808- v.Text = visibleText(text)
809- if l.Continued {
810- continue // joined to its line by the page reload; rare and harmless
811- }
795+ views = appendLogLine(views, b.Offset+int64(i), l)
796+ }
797+ var html strings.Builder
798+ for _, v := range views {
812799 var b2 strings.Builder
813800 if err := LogLine(v).Render(ctx, &b2); err != nil {
814801 return err
@@ -198,6 +198,9 @@ type LogLineView struct {
198 // Group marks "##[group]" and "##[endgroup]" lines and Error the198 // Group marks "##[group]" and "##[endgroup]" lines and Error the
199 // "##[error]" ones; the text is shown without the marker.199 // "##[error]" ones; the text is shown without the marker.
200 Group, End, Error bool200 Group, End, Error bool
201+ // Continued marks a piece of a line whose beginning is in an earlier batch
202+ // of the live feed: the page appends its text to the line before it.
203+ Continued bool
201 }204 }
202 205
203 // JobPage is one job with its steps and its log.206 // JobPage is one job with its steps and its log.
@@ -452,6 +455,30 @@ func (s *Server) job(ctx context.Context, r *http.Request) (templ.Component, int
452 return Job(l, p), http.StatusOK, nil455 return Job(l, p), http.StatusOK, nil
453 }456 }
454 457
458+// appendLogLine adds the line n of a job log to the lines shown. The markers
459+// of the runner ("##[group]", "##[endgroup]", "##[error]") become structure and
460+// are not shown as text. A piece that continues a line too long to send whole
461+// is joined to the line before it; one that opens the list (its line is in an
462+// earlier batch) keeps Continued, and the page appends it to that line.
463+func appendLogLine(out []LogLineView, n int64, l ports.JobLogLine) []LogLineView {
464+ text := l.Text
465+ v := LogLineView{N: n, Step: l.Step, Time: l.Time.UTC().Format("15:04:05"), Continued: l.Continued}
466+ switch {
467+ case strings.HasPrefix(text, "##[group]"):
468+ v.Group, text = true, strings.TrimPrefix(text, "##[group]")
469+ case strings.HasPrefix(text, "##[endgroup]"):
470+ v.End, text = true, strings.TrimPrefix(text, "##[endgroup]")
471+ case strings.HasPrefix(text, "##[error]"):
472+ v.Error, text = true, strings.TrimPrefix(text, "##[error]")
473+ }
474+ v.Text = visibleText(text)
475+ if l.Continued && len(out) > 0 {
476+ out[len(out)-1].Text += v.Text
477+ return out
478+ }
479+ return append(out, v)
480+}
481+
455 // logClient is the key of the visitor for the limit of log streams per client.482 // logClient is the key of the visitor for the limit of log streams per client.
456 func (s *Server) logClient(r *http.Request) string {483 func (s *Server) logClient(r *http.Request) string {
457 return clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies))484 return clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies))
@@ -465,40 +492,14 @@ func (s *Server) readLog(ctx context.Context, client string, repo domain.RepoNam
465 }492 }
466 var out []LogLineView493 var out []LogLineView
467 next := from494 next := from
468- ranges := j.StepRanges()
469- stepOf := func(n int64) int32 {
470- for i, r := range ranges {
471- if n >= r.Start && n < r.End {
472- return j.Steps[i].Number
473- }
474- }
475- return 0
476- }
477 stop := errors.New("enough")495 stop := errors.New("enough")
478 err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from, Client: client}, func(b ports.LogBatch) error {496 err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from, Client: client}, func(b ports.LogBatch) error {
479 for i, l := range b.Lines {497 for i, l := range b.Lines {
480- if len(out) >= max {498+ if len(out) >= max && !l.Continued {
481- return stop499+ return stop // the rest of a cut line still belongs to this page
482- }
483- n := b.Offset + int64(i)
484- text := l.Text
485- v := LogLineView{N: n, Step: stepOf(n), Time: l.Time.UTC().Format("15:04:05")}
486- switch {
487- case strings.HasPrefix(text, "##[group]"):
488- v.Group, text = true, strings.TrimPrefix(text, "##[group]")
489- case strings.HasPrefix(text, "##[endgroup]"):
490- v.End, text = true, strings.TrimPrefix(text, "##[endgroup]")
491- case strings.HasPrefix(text, "##[error]"):
492- v.Error, text = true, strings.TrimPrefix(text, "##[error]")
493 }500 }
494- if l.Continued && len(out) > 0 {501+ out = appendLogLine(out, b.Offset+int64(i), l)
495- out[len(out)-1].Text += visibleText(text)502+ next = b.Offset + int64(i) + 1
496- next = n + 1
497- continue
498- }
499- v.Text = visibleText(text)
500- out = append(out, v)
501- next = n + 1
502 }503 }
503 return nil504 return nil
504 })505 })
@@ -533,8 +534,9 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) {
533 }534 }
534 attempt = int32(n)535 attempt = int32(n)
535 }536 }
536- view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt)537+ // The job is read first: a job that is not there, or not readable, is a 404
537- if err != nil {538+ // before any header of the log is sent.
539+ if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt); err != nil {
538 s.fail(ctx, w, r, err)540 s.fail(ctx, w, r, err)
539 return541 return
540 }542 }
@@ -573,7 +575,6 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) {
573 _, _ = fmt.Fprintln(w)575 _, _ = fmt.Fprintln(w)
574 }576 }
575 if err != nil && !open && errors.Is(err, domain.ErrNotFound) {577 if err != nil && !open && errors.Is(err, domain.ErrNotFound) {
576- _ = view
577 w.WriteHeader(http.StatusNotFound)578 w.WriteHeader(http.StatusNotFound)
578 }579 }
579 }580 }
@@ -714,7 +715,7 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C
714 715
715 // checksSection is the checks of the rail of a merge request, with the rule of716 // checksSection is the checks of the rail of a merge request, with the rule of
716 // the repository that applies to them.717 // the repository that applies to them.
717-func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) ChecksSection {718+func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection {
718 sec := ChecksSection{}719 sec := ChecksSection{}
719 for _, c := range checks {720 for _, c := range checks {
720 row := CheckRow{721 row := CheckRow{
@@ -736,7 +737,6 @@ func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) Ch
736 default:737 default:
737 sec.Note = "checks do not block the merge in this repository."738 sec.Note = "checks do not block the merge in this repository."
738 }739 }
739- _ = now
740 return sec740 return sec
741 }741 }
742 742
@@ -770,8 +770,7 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) {
770 s.denyEvents(ctx, w, errNotFound)770 s.denyEvents(ctx, w, errNotFound)
771 return771 return
772 }772 }
773- view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0)773+ if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0); err != nil {
774- if err != nil {
775 s.denyEvents(ctx, w, err)774 s.denyEvents(ctx, w, err)
776 return775 return
777 }776 }
@@ -786,29 +785,17 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) {
786 if out.raw(": connected\n\n") != nil {785 if out.raw(": connected\n\n") != nil {
787 return786 return
788 }787 }
789- ranges := view.StepRanges()
790- _ = ranges
791 n := from788 n := from
792 err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{789 err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{
793 JobID: jobID, Offset: from, Follow: true,790 JobID: jobID, Offset: from, Follow: true,
794 Client: s.logClient(r),791 Client: s.logClient(r),
795 }, func(b ports.LogBatch) error {792 }, func(b ports.LogBatch) error {
796- var html strings.Builder793+ var views []LogLineView
797 for i, l := range b.Lines {794 for i, l := range b.Lines {
798- v := LogLineView{N: b.Offset + int64(i), Time: l.Time.UTC().Format("15:04:05"), Step: l.Step}795+ views = appendLogLine(views, b.Offset+int64(i), l)
799- text := l.Text796+ }
800- switch {797+ var html strings.Builder
801- case strings.HasPrefix(text, "##[group]"):798+ for _, v := range views {
802- v.Group, text = true, strings.TrimPrefix(text, "##[group]")
803- case strings.HasPrefix(text, "##[endgroup]"):
804- v.End, text = true, strings.TrimPrefix(text, "##[endgroup]")
805- case strings.HasPrefix(text, "##[error]"):
806- v.Error, text = true, strings.TrimPrefix(text, "##[error]")
807- }
808- v.Text = visibleText(text)
809- if l.Continued {
810- continue // joined to its line by the page reload; rare and harmless
811- }
812 var b2 strings.Builder799 var b2 strings.Builder
813 if err := LogLine(v).Render(ctx, &b2); err != nil {800 if err := LogLine(v).Render(ctx, &b2); err != nil {
814 return err801 return err
M internal/adapters/web/actions.templ +1 -1
@@ -208,7 +208,7 @@ templ Job(l Layout, p JobPage) {
208208 // LogLine is a line of the log. Group and error markers of the runner show as
209209 // headings and errors, never as raw text.
210210 templ LogLine(ln LogLineView) {
211- <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li>
211+ <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) } data-cont?={ ln.Continued }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li>
212212 }
213213
214214 templ jobRail(p JobPage) {
@@ -208,7 +208,7 @@ templ Job(l Layout, p JobPage) {
208 // LogLine is a line of the log. Group and error markers of the runner show as208 // LogLine is a line of the log. Group and error markers of the runner show as
209 // headings and errors, never as raw text.209 // headings and errors, never as raw text.
210 templ LogLine(ln LogLineView) {210 templ LogLine(ln LogLineView) {
211- <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li>211+ <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) } data-cont?={ ln.Continued }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li>
212 }212 }
213 213
214 templ jobRail(p JobPage) {214 templ jobRail(p JobPage) {
M internal/adapters/web/actions_templ.go +49 -39 generated file: collapsed
M internal/adapters/web/actions_test.go +42 -0
@@ -68,3 +68,45 @@ func TestLogPagesPassTheClientKey(t *testing.T) {
6868 assert.Equal(t, want, c)
6969 }
7070 }
71+
72+// M4: one helper shapes the lines of the page, the raw feed and the live feed.
73+func TestAppendLogLine(t *testing.T) {
74+ t.Parallel()
75+ at := time.Date(2026, 10, 12, 10, 11, 12, 0, time.UTC)
76+ line := func(text string, cont bool) ports.JobLogLine {
77+ return ports.JobLogLine{Step: 2, CILogLine: domain.CILogLine{Time: at, Text: text, Continued: cont}}
78+ }
79+ var out []LogLineView
80+ out = appendLogLine(out, 10, line("##[group]Run make", false))
81+ out = appendLogLine(out, 11, line("plain <b>", false))
82+ out = appendLogLine(out, 12, line("##[error]Process completed with exit code 1.", false))
83+ out = appendLogLine(out, 13, line("##[endgroup]", false))
84+ require.Len(t, out, 4)
85+ assert.True(t, out[0].Group)
86+ assert.Equal(t, "Run make", out[0].Text, "the marker is structure, not text")
87+ assert.EqualValues(t, 10, out[0].N)
88+ assert.Equal(t, "10:11:12", out[0].Time)
89+ assert.EqualValues(t, 2, out[0].Step)
90+ assert.False(t, out[1].Group || out[1].Error || out[1].End)
91+ assert.True(t, out[2].Error)
92+ assert.Equal(t, "Process completed with exit code 1.", out[2].Text)
93+ assert.True(t, out[3].End)
94+
95+ // A cut line comes back as one line, with the number of its first piece.
96+ var cut []LogLineView
97+ cut = appendLogLine(cut, 20, line("first half, ", false))
98+ cut = appendLogLine(cut, 21, line("second half", true))
99+ require.Len(t, cut, 1)
100+ assert.Equal(t, "first half, second half", cut[0].Text)
101+ assert.EqualValues(t, 20, cut[0].N)
102+
103+ // A piece that opens a batch has no line before it here: it stays, marked,
104+ // for the page to join to the line it already holds.
105+ open := appendLogLine(nil, 30, line("rest", true))
106+ require.Len(t, open, 1)
107+ assert.True(t, open[0].Continued)
108+
109+ // Invisible characters of the runner's text are shown, never applied.
110+ hidden := appendLogLine(nil, 40, line("a\u202eb", false))
111+ assert.NotContains(t, hidden[0].Text, "\u202e")
112+}
@@ -68,3 +68,45 @@ func TestLogPagesPassTheClientKey(t *testing.T) {
68 assert.Equal(t, want, c)68 assert.Equal(t, want, c)
69 }69 }
70 }70 }
71+
72+// M4: one helper shapes the lines of the page, the raw feed and the live feed.
73+func TestAppendLogLine(t *testing.T) {
74+ t.Parallel()
75+ at := time.Date(2026, 10, 12, 10, 11, 12, 0, time.UTC)
76+ line := func(text string, cont bool) ports.JobLogLine {
77+ return ports.JobLogLine{Step: 2, CILogLine: domain.CILogLine{Time: at, Text: text, Continued: cont}}
78+ }
79+ var out []LogLineView
80+ out = appendLogLine(out, 10, line("##[group]Run make", false))
81+ out = appendLogLine(out, 11, line("plain <b>", false))
82+ out = appendLogLine(out, 12, line("##[error]Process completed with exit code 1.", false))
83+ out = appendLogLine(out, 13, line("##[endgroup]", false))
84+ require.Len(t, out, 4)
85+ assert.True(t, out[0].Group)
86+ assert.Equal(t, "Run make", out[0].Text, "the marker is structure, not text")
87+ assert.EqualValues(t, 10, out[0].N)
88+ assert.Equal(t, "10:11:12", out[0].Time)
89+ assert.EqualValues(t, 2, out[0].Step)
90+ assert.False(t, out[1].Group || out[1].Error || out[1].End)
91+ assert.True(t, out[2].Error)
92+ assert.Equal(t, "Process completed with exit code 1.", out[2].Text)
93+ assert.True(t, out[3].End)
94+
95+ // A cut line comes back as one line, with the number of its first piece.
96+ var cut []LogLineView
97+ cut = appendLogLine(cut, 20, line("first half, ", false))
98+ cut = appendLogLine(cut, 21, line("second half", true))
99+ require.Len(t, cut, 1)
100+ assert.Equal(t, "first half, second half", cut[0].Text)
101+ assert.EqualValues(t, 20, cut[0].N)
102+
103+ // A piece that opens a batch has no line before it here: it stays, marked,
104+ // for the page to join to the line it already holds.
105+ open := appendLogLine(nil, 30, line("rest", true))
106+ require.Len(t, open, 1)
107+ assert.True(t, open[0].Continued)
108+
109+ // Invisible characters of the runner's text are shown, never applied.
110+ hidden := appendLogLine(nil, 40, line("a\u202eb", false))
111+ assert.NotContains(t, hidden[0].Text, "\u202e")
112+}
M internal/adapters/web/assets.go +2 -1
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
2323 // Assets are the URLs of the assets a page loads. Their names carry a hash of
2424 // the content, so they can be cached for ever.
2525 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Files, Icon string
26+ CSS, HTMX, Live, SSE, App, Files, Log, Icon string
2727 // Font is the UI font, preloaded by every page.
2828 Font string
2929 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -56,6 +56,7 @@ var scripts = []struct {
5656 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
5757 {"ermite.js", func(a *Assets, u string) { a.App = u }},
5858 {"files.js", func(a *Assets, u string) { a.Files = u }},
59+ {"log.js", func(a *Assets, u string) { a.Log = u }},
5960 }
6061
6162 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
23 // Assets are the URLs of the assets a page loads. Their names carry a hash of23 // Assets are the URLs of the assets a page loads. Their names carry a hash of
24 // the content, so they can be cached for ever.24 // the content, so they can be cached for ever.
25 type Assets struct {25 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Files, Icon string26+ CSS, HTMX, Live, SSE, App, Files, Log, Icon string
27 // Font is the UI font, preloaded by every page.27 // Font is the UI font, preloaded by every page.
28 Font string28 Font string
29 // Rules is the speculation rules file named by the Speculation-Rules header.29 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -56,6 +56,7 @@ var scripts = []struct {
56 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},56 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
57 {"ermite.js", func(a *Assets, u string) { a.App = u }},57 {"ermite.js", func(a *Assets, u string) { a.App = u }},
58 {"files.js", func(a *Assets, u string) { a.Files = u }},58 {"files.js", func(a *Assets, u string) { a.Files = u }},
59+ {"log.js", func(a *Assets, u string) { a.Log = u }},
59 }60 }
60 61
61 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):62 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
A internal/adapters/web/jstest/log.test.mjs +103 -0
@@ -0,0 +1,103 @@
1+// Tests of the live job log of static/js/log.js (window.ermiteLog) with a stub
2+// EventSource on the tiny hand-rolled DOM. Run with
3+// node --test 'internal/adapters/web/jstest/*.test.mjs'
4+import assert from 'node:assert/strict';
5+import { test } from 'node:test';
6+import { el, installDom } from './dom.mjs';
7+
8+// A line is "text" or "+text" for a piece that continues the line before it.
9+function line(text) {
10+ const cont = text.startsWith('+');
11+ const li = el('li', { cls: 'lg', attrs: cont ? { 'data-cont': '' } : {} }, el('span', { cls: 't', text: cont ? text.slice(1) : text }));
12+ return li;
13+}
14+
15+installDom({ parseHTML: (data) => data.split('|').filter(Boolean).map(line) });
16+globalThis.document.addEventListener = () => {}; // log.js starts itself on DOMContentLoaded
17+
18+class StubEventSource {
19+ static CLOSED = 2;
20+ static last = null;
21+ constructor(url) {
22+ this.url = url;
23+ this.readyState = 1;
24+ this.handlers = new Map();
25+ StubEventSource.last = this;
26+ }
27+ addEventListener(name, fn) { this.handlers.set(name, fn); }
28+ close() { this.readyState = StubEventSource.CLOSED; }
29+ emit(name, data) { this.handlers.get(name)?.({ data }); }
30+}
31+globalThis.EventSource = StubEventSource;
32+
33+let scrolls = [];
34+globalThis.scrollTo = (x, y) => scrolls.push(y);
35+Object.assign(globalThis, { innerHeight: 800, scrollY: 0 });
36+
37+await import('../static/js/log.js');
38+const { follow } = window.ermiteLog;
39+
40+// page makes a job page body: the box with its list and the lines already there.
41+function page(attrs, ...lines) {
42+ const list = el('ol', { cls: 'log' }, ...lines.map(line));
43+ const box = el('div', { id: 'job-log', attrs }, list);
44+ const body = document.body;
45+ body.replaceChildren(box);
46+ Object.defineProperty(globalThis.document.body, 'scrollHeight', { value: 1000, configurable: true });
47+ return { box, list };
48+}
49+const running = { 'data-follow': '', 'data-stream': '/r/job/stream', 'data-next': '3' };
50+const texts = (list) => list.children.map((li) => li.querySelector('.t').textContent);
51+
52+test('it does nothing for a job that is over, or a page without a feed', () => {
53+ assert.equal(follow(null), null);
54+ assert.equal(follow(page({ 'data-stream': '/s' }).box), null, 'no data-follow: the job ended');
55+ assert.equal(follow(page({ 'data-follow': '' }).box), null, 'no stream URL');
56+});
57+
58+test('it opens the feed after the last line the page holds', () => {
59+ const { box } = page(running, 'a', 'b', 'c');
60+ const es = follow(box);
61+ assert.equal(es.url, '/r/job/stream?from=3');
62+});
63+
64+test('it appends the lines of a batch', () => {
65+ const { box, list } = page(running, 'a');
66+ const es = follow(box);
67+ es.emit('ci-log', 'b|c');
68+ es.emit('ci-log', 'd');
69+ assert.deepEqual(texts(list), ['a', 'b', 'c', 'd']);
70+});
71+
72+test('a piece that continues a line of an earlier batch joins that line', () => {
73+ const { box, list } = page(running, 'start of a long');
74+ const es = follow(box);
75+ es.emit('ci-log', '+ line|next');
76+ assert.deepEqual(texts(list), ['start of a long line', 'next']);
77+});
78+
79+test('it scrolls with the log only while the reader is at its end', () => {
80+ const { box } = page(running, 'a');
81+ const es = follow(box);
82+ scrolls = [];
83+ Object.assign(globalThis, { scrollY: 190 }); // 800 + 190 >= 1000 - 80: at the end
84+ es.emit('ci-log', 'b');
85+ assert.deepEqual(scrolls, [1000]);
86+ scrolls = [];
87+ Object.assign(globalThis, { scrollY: 0 }); // reading above
88+ es.emit('ci-log', 'c');
89+ assert.deepEqual(scrolls, []);
90+});
91+
92+test('ci-end closes the feed, and a closed connection stays closed', () => {
93+ const { box } = page(running, 'a');
94+ const es = follow(box);
95+ assert.equal(es.readyState, 1);
96+ es.emit('ci-end', 'end');
97+ assert.equal(es.readyState, StubEventSource.CLOSED);
98+
99+ const again = follow(page(running, 'a').box);
100+ again.readyState = StubEventSource.CLOSED;
101+ again.onerror();
102+ assert.equal(again.readyState, StubEventSource.CLOSED);
103+});
@@ -0,0 +1,103 @@
1+// Tests of the live job log of static/js/log.js (window.ermiteLog) with a stub
2+// EventSource on the tiny hand-rolled DOM. Run with
3+// node --test 'internal/adapters/web/jstest/*.test.mjs'
4+import assert from 'node:assert/strict';
5+import { test } from 'node:test';
6+import { el, installDom } from './dom.mjs';
7+
8+// A line is "text" or "+text" for a piece that continues the line before it.
9+function line(text) {
10+ const cont = text.startsWith('+');
11+ const li = el('li', { cls: 'lg', attrs: cont ? { 'data-cont': '' } : {} }, el('span', { cls: 't', text: cont ? text.slice(1) : text }));
12+ return li;
13+}
14+
15+installDom({ parseHTML: (data) => data.split('|').filter(Boolean).map(line) });
16+globalThis.document.addEventListener = () => {}; // log.js starts itself on DOMContentLoaded
17+
18+class StubEventSource {
19+ static CLOSED = 2;
20+ static last = null;
21+ constructor(url) {
22+ this.url = url;
23+ this.readyState = 1;
24+ this.handlers = new Map();
25+ StubEventSource.last = this;
26+ }
27+ addEventListener(name, fn) { this.handlers.set(name, fn); }
28+ close() { this.readyState = StubEventSource.CLOSED; }
29+ emit(name, data) { this.handlers.get(name)?.({ data }); }
30+}
31+globalThis.EventSource = StubEventSource;
32+
33+let scrolls = [];
34+globalThis.scrollTo = (x, y) => scrolls.push(y);
35+Object.assign(globalThis, { innerHeight: 800, scrollY: 0 });
36+
37+await import('../static/js/log.js');
38+const { follow } = window.ermiteLog;
39+
40+// page makes a job page body: the box with its list and the lines already there.
41+function page(attrs, ...lines) {
42+ const list = el('ol', { cls: 'log' }, ...lines.map(line));
43+ const box = el('div', { id: 'job-log', attrs }, list);
44+ const body = document.body;
45+ body.replaceChildren(box);
46+ Object.defineProperty(globalThis.document.body, 'scrollHeight', { value: 1000, configurable: true });
47+ return { box, list };
48+}
49+const running = { 'data-follow': '', 'data-stream': '/r/job/stream', 'data-next': '3' };
50+const texts = (list) => list.children.map((li) => li.querySelector('.t').textContent);
51+
52+test('it does nothing for a job that is over, or a page without a feed', () => {
53+ assert.equal(follow(null), null);
54+ assert.equal(follow(page({ 'data-stream': '/s' }).box), null, 'no data-follow: the job ended');
55+ assert.equal(follow(page({ 'data-follow': '' }).box), null, 'no stream URL');
56+});
57+
58+test('it opens the feed after the last line the page holds', () => {
59+ const { box } = page(running, 'a', 'b', 'c');
60+ const es = follow(box);
61+ assert.equal(es.url, '/r/job/stream?from=3');
62+});
63+
64+test('it appends the lines of a batch', () => {
65+ const { box, list } = page(running, 'a');
66+ const es = follow(box);
67+ es.emit('ci-log', 'b|c');
68+ es.emit('ci-log', 'd');
69+ assert.deepEqual(texts(list), ['a', 'b', 'c', 'd']);
70+});
71+
72+test('a piece that continues a line of an earlier batch joins that line', () => {
73+ const { box, list } = page(running, 'start of a long');
74+ const es = follow(box);
75+ es.emit('ci-log', '+ line|next');
76+ assert.deepEqual(texts(list), ['start of a long line', 'next']);
77+});
78+
79+test('it scrolls with the log only while the reader is at its end', () => {
80+ const { box } = page(running, 'a');
81+ const es = follow(box);
82+ scrolls = [];
83+ Object.assign(globalThis, { scrollY: 190 }); // 800 + 190 >= 1000 - 80: at the end
84+ es.emit('ci-log', 'b');
85+ assert.deepEqual(scrolls, [1000]);
86+ scrolls = [];
87+ Object.assign(globalThis, { scrollY: 0 }); // reading above
88+ es.emit('ci-log', 'c');
89+ assert.deepEqual(scrolls, []);
90+});
91+
92+test('ci-end closes the feed, and a closed connection stays closed', () => {
93+ const { box } = page(running, 'a');
94+ const es = follow(box);
95+ assert.equal(es.readyState, 1);
96+ es.emit('ci-end', 'end');
97+ assert.equal(es.readyState, StubEventSource.CLOSED);
98+
99+ const again = follow(page(running, 'a').box);
100+ again.readyState = StubEventSource.CLOSED;
101+ again.onerror();
102+ assert.equal(again.readyState, StubEventSource.CLOSED);
103+});
M internal/adapters/web/mr.go +2 -2
@@ -459,9 +459,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
459459 }
460460 p.ChecksBlock = d.repo.Settings.FailingChecksBlock
461461 if open && head != "" {
462- p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now)
462+ p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head))
463463 } else if head != "" {
464- p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now)
464+ p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head))
465465 p.Checks.Note = ""
466466 }
467467 p.Note = diffNote(res.Diff, len(p.Files), p.NFiles)
@@ -459,9 +459,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
459 }459 }
460 p.ChecksBlock = d.repo.Settings.FailingChecksBlock460 p.ChecksBlock = d.repo.Settings.FailingChecksBlock
461 if open && head != "" {461 if open && head != "" {
462- p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now)462+ p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head))
463 } else if head != "" {463 } else if head != "" {
464- p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now)464+ p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head))
465 p.Checks.Note = ""465 p.Checks.Note = ""
466 }466 }
467 p.Note = diffNote(res.Diff, len(p.Files), p.NFiles)467 p.Note = diffNote(res.Diff, len(p.Files), p.NFiles)
M internal/adapters/web/shell.templ +1 -0
@@ -22,6 +22,7 @@ templ Shell(l Layout, rail templ.Component) {
2222 <script src={ l.Assets.SSE } defer></script>
2323 <script src={ l.Assets.App } defer></script>
2424 <script src={ l.Assets.Files } defer></script>
25+ <script src={ l.Assets.Log } defer></script>
2526 </head>
2627 <body
2728 data-ref={ l.RefPath }
@@ -22,6 +22,7 @@ templ Shell(l Layout, rail templ.Component) {
22 <script src={ l.Assets.SSE } defer></script>22 <script src={ l.Assets.SSE } defer></script>
23 <script src={ l.Assets.App } defer></script>23 <script src={ l.Assets.App } defer></script>
24 <script src={ l.Assets.Files } defer></script>24 <script src={ l.Assets.Files } defer></script>
25+ <script src={ l.Assets.Log } defer></script>
25 </head>26 </head>
26 <body27 <body
27 data-ref={ l.RefPath }28 data-ref={ l.RefPath }
M internal/adapters/web/shell_templ.go +155 -142
@@ -151,69 +151,82 @@ func Shell(l Layout, rail templ.Component) templ.Component {
151151 if templ_7745c5c3_Err != nil {
152152 return templ_7745c5c3_Err
153153 }
154- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" defer></script></head><body data-ref=\"")
154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" defer></script><script src=\"")
155155 if templ_7745c5c3_Err != nil {
156156 return templ_7745c5c3_Err
157157 }
158158 var templ_7745c5c3_Var11 string
159- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
159+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Log)
160160 if templ_7745c5c3_Err != nil {
161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 27, Col: 23}
161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 29}
162162 }
163163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
164164 if templ_7745c5c3_Err != nil {
165165 return templ_7745c5c3_Err
166166 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-sha=\"")
167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" defer></script></head><body data-ref=\"")
168168 if templ_7745c5c3_Err != nil {
169169 return templ_7745c5c3_Err
170170 }
171171 var templ_7745c5c3_Var12 string
172- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
172+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
173173 if templ_7745c5c3_Err != nil {
174- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 28, Col: 19}
174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 28, Col: 23}
175175 }
176176 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
177177 if templ_7745c5c3_Err != nil {
178178 return templ_7745c5c3_Err
179179 }
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-sel=\"\" data-nav=\"\"")
180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-sha=\"")
181+ if templ_7745c5c3_Err != nil {
182+ return templ_7745c5c3_Err
183+ }
184+ var templ_7745c5c3_Var13 string
185+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
186+ if templ_7745c5c3_Err != nil {
187+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 19}
188+ }
189+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
190+ if templ_7745c5c3_Err != nil {
191+ return templ_7745c5c3_Err
192+ }
193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-sel=\"\" data-nav=\"\"")
181194 if templ_7745c5c3_Err != nil {
182195 return templ_7745c5c3_Err
183196 }
184197 if l.PathsURL != "" {
185- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " data-paths=\"")
198+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " data-paths=\"")
186199 if templ_7745c5c3_Err != nil {
187200 return templ_7745c5c3_Err
188201 }
189- var templ_7745c5c3_Var13 string
190- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
202+ var templ_7745c5c3_Var14 string
203+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
191204 if templ_7745c5c3_Err != nil {
192- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 32, Col: 27}
205+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 33, Col: 27}
193206 }
194- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
207+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
195208 if templ_7745c5c3_Err != nil {
196209 return templ_7745c5c3_Err
197210 }
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" data-blob=\"")
211+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-blob=\"")
199212 if templ_7745c5c3_Err != nil {
200213 return templ_7745c5c3_Err
201214 }
202- var templ_7745c5c3_Var14 string
203- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
215+ var templ_7745c5c3_Var15 string
216+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
204217 if templ_7745c5c3_Err != nil {
205- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 33, Col: 26}
218+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 26}
206219 }
207- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
220+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
208221 if templ_7745c5c3_Err != nil {
209222 return templ_7745c5c3_Err
210223 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"")
224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"")
212225 if templ_7745c5c3_Err != nil {
213226 return templ_7745c5c3_Err
214227 }
215228 }
216- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "><a class=\"skip\" href=\"#main\">skip to content</a>")
229+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "><a class=\"skip\" href=\"#main\">skip to content</a>")
217230 if templ_7745c5c3_Err != nil {
218231 return templ_7745c5c3_Err
219232 }
@@ -221,48 +234,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
221234 if templ_7745c5c3_Err != nil {
222235 return templ_7745c5c3_Err
223236 }
224- var templ_7745c5c3_Var15 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
225- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
237+ var templ_7745c5c3_Var16 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
238+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
226239 if templ_7745c5c3_Err != nil {
227240 return templ_7745c5c3_Err
228241 }
229- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"")
242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"")
230243 if templ_7745c5c3_Err != nil {
231244 return templ_7745c5c3_Err
232245 }
233- var templ_7745c5c3_Var16 string
234- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var15).String())
246+ var templ_7745c5c3_Var17 string
247+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var16).String())
235248 if templ_7745c5c3_Err != nil {
236249 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
237250 }
238- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
251+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
239252 if templ_7745c5c3_Err != nil {
240253 return templ_7745c5c3_Err
241254 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
255+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
243256 if templ_7745c5c3_Err != nil {
244257 return templ_7745c5c3_Err
245258 }
246259 if l.Lede != "" {
247- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
260+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
248261 if templ_7745c5c3_Err != nil {
249262 return templ_7745c5c3_Err
250263 }
251- var templ_7745c5c3_Var17 string
252- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
264+ var templ_7745c5c3_Var18 string
265+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
253266 if templ_7745c5c3_Err != nil {
254- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 40, Col: 81}
267+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 41, Col: 81}
255268 }
256- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
269+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
257270 if templ_7745c5c3_Err != nil {
258271 return templ_7745c5c3_Err
259272 }
260- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</p></section>")
273+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p></section>")
261274 if templ_7745c5c3_Err != nil {
262275 return templ_7745c5c3_Err
263276 }
264277 }
265- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<main id=\"main\" tabindex=\"-1\">")
278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<main id=\"main\" tabindex=\"-1\">")
266279 if templ_7745c5c3_Err != nil {
267280 return templ_7745c5c3_Err
268281 }
@@ -270,12 +283,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
270283 if templ_7745c5c3_Err != nil {
271284 return templ_7745c5c3_Err
272285 }
273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</main>")
286+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</main>")
274287 if templ_7745c5c3_Err != nil {
275288 return templ_7745c5c3_Err
276289 }
277290 if l.HasRail && rail != nil {
278- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<aside class=\"rail\" aria-label=\"Context\">")
291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<aside class=\"rail\" aria-label=\"Context\">")
279292 if templ_7745c5c3_Err != nil {
280293 return templ_7745c5c3_Err
281294 }
@@ -283,12 +296,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
283296 if templ_7745c5c3_Err != nil {
284297 return templ_7745c5c3_Err
285298 }
286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside>")
299+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</aside>")
287300 if templ_7745c5c3_Err != nil {
288301 return templ_7745c5c3_Err
289302 }
290303 }
291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")
304+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div>")
292305 if templ_7745c5c3_Err != nil {
293306 return templ_7745c5c3_Err
294307 }
@@ -316,7 +329,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
316329 if templ_7745c5c3_Err != nil {
317330 return templ_7745c5c3_Err
318331 }
319- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</body></html>")
332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</body></html>")
320333 if templ_7745c5c3_Err != nil {
321334 return templ_7745c5c3_Err
322335 }
@@ -342,273 +355,273 @@ func Header(l Layout) templ.Component {
342355 }()
343356 }
344357 ctx = templ.InitializeContext(ctx)
345- templ_7745c5c3_Var18 := templ.GetChildren(ctx)
346- if templ_7745c5c3_Var18 == nil {
347- templ_7745c5c3_Var18 = templ.NopComponent
358+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
359+ if templ_7745c5c3_Var19 == nil {
360+ templ_7745c5c3_Var19 = templ.NopComponent
348361 }
349362 ctx = templ.ClearChildren(ctx)
350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
363+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
351364 if templ_7745c5c3_Err != nil {
352365 return templ_7745c5c3_Err
353366 }
354- var templ_7745c5c3_Var19 string
355- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
367+ var templ_7745c5c3_Var20 string
368+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
356369 if templ_7745c5c3_Err != nil {
357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 68, Col: 41}
370+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 69, Col: 41}
358371 }
359- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
372+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
360373 if templ_7745c5c3_Err != nil {
361374 return templ_7745c5c3_Err
362375 }
363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</span></a> ")
376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span></a> ")
364377 if templ_7745c5c3_Err != nil {
365378 return templ_7745c5c3_Err
366379 }
367380 if len(l.Crumbs) > 0 {
368- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
369382 if templ_7745c5c3_Err != nil {
370383 return templ_7745c5c3_Err
371384 }
372385 for _, c := range l.Crumbs {
373- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<li>")
386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<li>")
374387 if templ_7745c5c3_Err != nil {
375388 return templ_7745c5c3_Err
376389 }
377390 if c.Href != "" {
378- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"")
391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a href=\"")
379392 if templ_7745c5c3_Err != nil {
380393 return templ_7745c5c3_Err
381394 }
382- var templ_7745c5c3_Var20 templ.SafeURL
383- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
395+ var templ_7745c5c3_Var21 templ.SafeURL
396+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
384397 if templ_7745c5c3_Err != nil {
385- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 77, Col: 38}
398+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 78, Col: 38}
386399 }
387- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
400+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
388401 if templ_7745c5c3_Err != nil {
389402 return templ_7745c5c3_Err
390403 }
391- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"")
404+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"")
392405 if templ_7745c5c3_Err != nil {
393406 return templ_7745c5c3_Err
394407 }
395408 if c.Current {
396- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " aria-current=\"page\"")
409+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " aria-current=\"page\"")
397410 if templ_7745c5c3_Err != nil {
398411 return templ_7745c5c3_Err
399412 }
400413 }
401- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">")
414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">")
402415 if templ_7745c5c3_Err != nil {
403416 return templ_7745c5c3_Err
404417 }
405- var templ_7745c5c3_Var21 string
406- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
418+ var templ_7745c5c3_Var22 string
419+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
407420 if templ_7745c5c3_Err != nil {
408- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 81, Col: 19}
421+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 82, Col: 19}
409422 }
410- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
423+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
411424 if templ_7745c5c3_Err != nil {
412425 return templ_7745c5c3_Err
413426 }
414- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</a>")
427+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</a>")
415428 if templ_7745c5c3_Err != nil {
416429 return templ_7745c5c3_Err
417430 }
418431 } else {
419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<span")
432+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span")
420433 if templ_7745c5c3_Err != nil {
421434 return templ_7745c5c3_Err
422435 }
423436 if c.Current {
424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " aria-current=\"page\"")
437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " aria-current=\"page\"")
425438 if templ_7745c5c3_Err != nil {
426439 return templ_7745c5c3_Err
427440 }
428441 }
429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, ">")
442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">")
430443 if templ_7745c5c3_Err != nil {
431444 return templ_7745c5c3_Err
432445 }
433- var templ_7745c5c3_Var22 string
434- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
446+ var templ_7745c5c3_Var23 string
447+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
435448 if templ_7745c5c3_Err != nil {
436- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 87, Col: 19}
449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 88, Col: 19}
437450 }
438- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
439452 if templ_7745c5c3_Err != nil {
440453 return templ_7745c5c3_Err
441454 }
442- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span>")
455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span>")
443456 if templ_7745c5c3_Err != nil {
444457 return templ_7745c5c3_Err
445458 }
446459 }
447- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</li>")
460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</li>")
448461 if templ_7745c5c3_Err != nil {
449462 return templ_7745c5c3_Err
450463 }
451464 }
452- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</ol></nav>")
465+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</ol></nav>")
453466 if templ_7745c5c3_Err != nil {
454467 return templ_7745c5c3_Err
455468 }
456469 }
457- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"hdr-acts\">")
470+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"hdr-acts\">")
458471 if templ_7745c5c3_Err != nil {
459472 return templ_7745c5c3_Err
460473 }
461474 if l.Ref != "" {
462- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<a class=\"btn ref\" href=\"")
475+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<a class=\"btn ref\" href=\"")
463476 if templ_7745c5c3_Err != nil {
464477 return templ_7745c5c3_Err
465478 }
466- var templ_7745c5c3_Var23 templ.SafeURL
467- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
479+ var templ_7745c5c3_Var24 templ.SafeURL
480+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
468481 if templ_7745c5c3_Err != nil {
469- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 98, Col: 52}
482+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 99, Col: 52}
470483 }
471- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
484+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
472485 if templ_7745c5c3_Err != nil {
473486 return templ_7745c5c3_Err
474487 }
475- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
488+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
476489 if templ_7745c5c3_Err != nil {
477490 return templ_7745c5c3_Err
478491 }
479- var templ_7745c5c3_Var24 string
480- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
492+ var templ_7745c5c3_Var25 string
493+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
481494 if templ_7745c5c3_Err != nil {
482- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 104, Col: 36}
495+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 105, Col: 36}
483496 }
484- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
497+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
485498 if templ_7745c5c3_Err != nil {
486499 return templ_7745c5c3_Err
487500 }
488- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</span> <span class=\"sha\">")
501+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span> <span class=\"sha\">")
489502 if templ_7745c5c3_Err != nil {
490503 return templ_7745c5c3_Err
491504 }
492- var templ_7745c5c3_Var25 string
493- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
505+ var templ_7745c5c3_Var26 string
506+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
494507 if templ_7745c5c3_Err != nil {
495- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 105, Col: 33}
508+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 33}
496509 }
497- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
510+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
498511 if templ_7745c5c3_Err != nil {
499512 return templ_7745c5c3_Err
500513 }
501- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span></a> ")
514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span></a> ")
502515 if templ_7745c5c3_Err != nil {
503516 return templ_7745c5c3_Err
504517 }
505518 }
506- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\">?</button> ")
519+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\">?</button> ")
507520 if templ_7745c5c3_Err != nil {
508521 return templ_7745c5c3_Err
509522 }
510523 if SignedIn(ctx) {
511- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
524+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
512525 if templ_7745c5c3_Err != nil {
513526 return templ_7745c5c3_Err
514527 }
515528 }
516- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></div>")
529+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div></div>")
517530 if templ_7745c5c3_Err != nil {
518531 return templ_7745c5c3_Err
519532 }
520533 if len(l.Sections) > 0 {
521- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
534+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
522535 if templ_7745c5c3_Err != nil {
523536 return templ_7745c5c3_Err
524537 }
525538 for _, s := range l.Sections {
526- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<li><a href=\"")
539+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<li><a href=\"")
527540 if templ_7745c5c3_Err != nil {
528541 return templ_7745c5c3_Err
529542 }
530- var templ_7745c5c3_Var26 templ.SafeURL
531- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
543+ var templ_7745c5c3_Var27 templ.SafeURL
544+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
532545 if templ_7745c5c3_Err != nil {
533- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 36}
546+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 138, Col: 36}
534547 }
535- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
548+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
536549 if templ_7745c5c3_Err != nil {
537550 return templ_7745c5c3_Err
538551 }
539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" data-key=\"")
552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-key=\"")
540553 if templ_7745c5c3_Err != nil {
541554 return templ_7745c5c3_Err
542555 }
543- var templ_7745c5c3_Var27 string
544- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
556+ var templ_7745c5c3_Var28 string
557+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
545558 if templ_7745c5c3_Err != nil {
546- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 138, Col: 24}
559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 139, Col: 24}
547560 }
548- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
561+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
549562 if templ_7745c5c3_Err != nil {
550563 return templ_7745c5c3_Err
551564 }
552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" aria-keyshortcuts=\"")
565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" aria-keyshortcuts=\"")
553566 if templ_7745c5c3_Err != nil {
554567 return templ_7745c5c3_Err
555568 }
556- var templ_7745c5c3_Var28 string
557- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
569+ var templ_7745c5c3_Var29 string
570+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
558571 if templ_7745c5c3_Err != nil {
559- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 139, Col: 33}
572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 140, Col: 33}
560573 }
561- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
574+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
562575 if templ_7745c5c3_Err != nil {
563576 return templ_7745c5c3_Err
564577 }
565- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"")
578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"")
566579 if templ_7745c5c3_Err != nil {
567580 return templ_7745c5c3_Err
568581 }
569582 if s.Current {
570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " aria-current=\"page\"")
583+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " aria-current=\"page\"")
571584 if templ_7745c5c3_Err != nil {
572585 return templ_7745c5c3_Err
573586 }
574587 }
575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">")
588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">")
576589 if templ_7745c5c3_Err != nil {
577590 return templ_7745c5c3_Err
578591 }
579- var templ_7745c5c3_Var29 string
580- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
592+ var templ_7745c5c3_Var30 string
593+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
581594 if templ_7745c5c3_Err != nil {
582- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 143, Col: 17}
595+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 144, Col: 17}
583596 }
584- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
597+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
585598 if templ_7745c5c3_Err != nil {
586599 return templ_7745c5c3_Err
587600 }
588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<kbd aria-hidden=\"true\">")
601+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<kbd aria-hidden=\"true\">")
589602 if templ_7745c5c3_Err != nil {
590603 return templ_7745c5c3_Err
591604 }
592- var templ_7745c5c3_Var30 string
593- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
605+ var templ_7745c5c3_Var31 string
606+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
594607 if templ_7745c5c3_Err != nil {
595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 143, Col: 50}
608+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 144, Col: 50}
596609 }
597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
610+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
598611 if templ_7745c5c3_Err != nil {
599612 return templ_7745c5c3_Err
600613 }
601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</kbd></a></li>")
614+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</kbd></a></li>")
602615 if templ_7745c5c3_Err != nil {
603616 return templ_7745c5c3_Err
604617 }
605618 }
606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</ul></nav>")
619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</ul></nav>")
607620 if templ_7745c5c3_Err != nil {
608621 return templ_7745c5c3_Err
609622 }
610623 }
611- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</header>")
624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</header>")
612625 if templ_7745c5c3_Err != nil {
613626 return templ_7745c5c3_Err
614627 }
@@ -634,38 +647,38 @@ func Footer(l Layout) templ.Component {
634647 }()
635648 }
636649 ctx = templ.InitializeContext(ctx)
637- templ_7745c5c3_Var31 := templ.GetChildren(ctx)
638- if templ_7745c5c3_Var31 == nil {
639- templ_7745c5c3_Var31 = templ.NopComponent
650+ templ_7745c5c3_Var32 := templ.GetChildren(ctx)
651+ if templ_7745c5c3_Var32 == nil {
652+ templ_7745c5c3_Var32 = templ.NopComponent
640653 }
641654 ctx = templ.ClearChildren(ctx)
642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<footer class=\"foot\"><span>ermite ")
655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<footer class=\"foot\"><span>ermite ")
643656 if templ_7745c5c3_Err != nil {
644657 return templ_7745c5c3_Err
645658 }
646- var templ_7745c5c3_Var32 string
647- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
659+ var templ_7745c5c3_Var33 string
660+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
648661 if templ_7745c5c3_Err != nil {
649- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 156, Col: 26}
662+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 26}
650663 }
651- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
664+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
652665 if templ_7745c5c3_Err != nil {
653666 return templ_7745c5c3_Err
654667 }
655- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span> <a href=\"")
668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> <a href=\"")
656669 if templ_7745c5c3_Err != nil {
657670 return templ_7745c5c3_Err
658671 }
659- var templ_7745c5c3_Var33 templ.SafeURL
660- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
672+ var templ_7745c5c3_Var34 templ.SafeURL
673+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
661674 if templ_7745c5c3_Err != nil {
662- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 38}
675+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 158, Col: 38}
663676 }
664- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
677+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
665678 if templ_7745c5c3_Err != nil {
666679 return templ_7745c5c3_Err
667680 }
668- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
681+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
669682 if templ_7745c5c3_Err != nil {
670683 return templ_7745c5c3_Err
671684 }
@@ -693,12 +706,12 @@ func Hint(l Layout) templ.Component {
693706 }()
694707 }
695708 ctx = templ.InitializeContext(ctx)
696- templ_7745c5c3_Var34 := templ.GetChildren(ctx)
697- if templ_7745c5c3_Var34 == nil {
698- templ_7745c5c3_Var34 = templ.NopComponent
709+ templ_7745c5c3_Var35 := templ.GetChildren(ctx)
710+ if templ_7745c5c3_Var35 == nil {
711+ templ_7745c5c3_Var35 = templ.NopComponent
699712 }
700713 ctx = templ.ClearChildren(ctx)
701- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
714+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
702715 if templ_7745c5c3_Err != nil {
703716 return templ_7745c5c3_Err
704717 }
@@ -706,7 +719,7 @@ func Hint(l Layout) templ.Component {
706719 if templ_7745c5c3_Err != nil {
707720 return templ_7745c5c3_Err
708721 }
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</div>")
722+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</div>")
710723 if templ_7745c5c3_Err != nil {
711724 return templ_7745c5c3_Err
712725 }
@@ -151,69 +151,82 @@ func Shell(l Layout, rail templ.Component) templ.Component {
151 if templ_7745c5c3_Err != nil {151 if templ_7745c5c3_Err != nil {
152 return templ_7745c5c3_Err152 return templ_7745c5c3_Err
153 }153 }
154- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" defer></script></head><body data-ref=\"")154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" defer></script><script src=\"")
155 if templ_7745c5c3_Err != nil {155 if templ_7745c5c3_Err != nil {
156 return templ_7745c5c3_Err156 return templ_7745c5c3_Err
157 }157 }
158 var templ_7745c5c3_Var11 string158 var templ_7745c5c3_Var11 string
159- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)159+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Log)
160 if templ_7745c5c3_Err != nil {160 if templ_7745c5c3_Err != nil {
161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 27, Col: 23}161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 29}
162 }162 }
163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
164 if templ_7745c5c3_Err != nil {164 if templ_7745c5c3_Err != nil {
165 return templ_7745c5c3_Err165 return templ_7745c5c3_Err
166 }166 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-sha=\"")167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" defer></script></head><body data-ref=\"")
168 if templ_7745c5c3_Err != nil {168 if templ_7745c5c3_Err != nil {
169 return templ_7745c5c3_Err169 return templ_7745c5c3_Err
170 }170 }
171 var templ_7745c5c3_Var12 string171 var templ_7745c5c3_Var12 string
172- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)172+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
173 if templ_7745c5c3_Err != nil {173 if templ_7745c5c3_Err != nil {
174- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 28, Col: 19}174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 28, Col: 23}
175 }175 }
176 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)176 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
177 if templ_7745c5c3_Err != nil {177 if templ_7745c5c3_Err != nil {
178 return templ_7745c5c3_Err178 return templ_7745c5c3_Err
179 }179 }
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-sel=\"\" data-nav=\"\"")180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-sha=\"")
181+ if templ_7745c5c3_Err != nil {
182+ return templ_7745c5c3_Err
183+ }
184+ var templ_7745c5c3_Var13 string
185+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
186+ if templ_7745c5c3_Err != nil {
187+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 19}
188+ }
189+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
190+ if templ_7745c5c3_Err != nil {
191+ return templ_7745c5c3_Err
192+ }
193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-sel=\"\" data-nav=\"\"")
181 if templ_7745c5c3_Err != nil {194 if templ_7745c5c3_Err != nil {
182 return templ_7745c5c3_Err195 return templ_7745c5c3_Err
183 }196 }
184 if l.PathsURL != "" {197 if l.PathsURL != "" {
185- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " data-paths=\"")198+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " data-paths=\"")
186 if templ_7745c5c3_Err != nil {199 if templ_7745c5c3_Err != nil {
187 return templ_7745c5c3_Err200 return templ_7745c5c3_Err
188 }201 }
189- var templ_7745c5c3_Var13 string202+ var templ_7745c5c3_Var14 string
190- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)203+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
191 if templ_7745c5c3_Err != nil {204 if templ_7745c5c3_Err != nil {
192- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 32, Col: 27}205+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 33, Col: 27}
193 }206 }
194- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)207+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
195 if templ_7745c5c3_Err != nil {208 if templ_7745c5c3_Err != nil {
196 return templ_7745c5c3_Err209 return templ_7745c5c3_Err
197 }210 }
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" data-blob=\"")211+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-blob=\"")
199 if templ_7745c5c3_Err != nil {212 if templ_7745c5c3_Err != nil {
200 return templ_7745c5c3_Err213 return templ_7745c5c3_Err
201 }214 }
202- var templ_7745c5c3_Var14 string215+ var templ_7745c5c3_Var15 string
203- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)216+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
204 if templ_7745c5c3_Err != nil {217 if templ_7745c5c3_Err != nil {
205- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 33, Col: 26}218+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 26}
206 }219 }
207- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)220+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
208 if templ_7745c5c3_Err != nil {221 if templ_7745c5c3_Err != nil {
209 return templ_7745c5c3_Err222 return templ_7745c5c3_Err
210 }223 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"")224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"")
212 if templ_7745c5c3_Err != nil {225 if templ_7745c5c3_Err != nil {
213 return templ_7745c5c3_Err226 return templ_7745c5c3_Err
214 }227 }
215 }228 }
216- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "><a class=\"skip\" href=\"#main\">skip to content</a>")229+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "><a class=\"skip\" href=\"#main\">skip to content</a>")
217 if templ_7745c5c3_Err != nil {230 if templ_7745c5c3_Err != nil {
218 return templ_7745c5c3_Err231 return templ_7745c5c3_Err
219 }232 }
@@ -221,48 +234,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
221 if templ_7745c5c3_Err != nil {234 if templ_7745c5c3_Err != nil {
222 return templ_7745c5c3_Err235 return templ_7745c5c3_Err
223 }236 }
224- var templ_7745c5c3_Var15 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}237+ var templ_7745c5c3_Var16 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
225- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)238+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
226 if templ_7745c5c3_Err != nil {239 if templ_7745c5c3_Err != nil {
227 return templ_7745c5c3_Err240 return templ_7745c5c3_Err
228 }241 }
229- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"")242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"")
230 if templ_7745c5c3_Err != nil {243 if templ_7745c5c3_Err != nil {
231 return templ_7745c5c3_Err244 return templ_7745c5c3_Err
232 }245 }
233- var templ_7745c5c3_Var16 string246+ var templ_7745c5c3_Var17 string
234- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var15).String())247+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var16).String())
235 if templ_7745c5c3_Err != nil {248 if templ_7745c5c3_Err != nil {
236 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}249 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
237 }250 }
238- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)251+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
239 if templ_7745c5c3_Err != nil {252 if templ_7745c5c3_Err != nil {
240 return templ_7745c5c3_Err253 return templ_7745c5c3_Err
241 }254 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")255+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
243 if templ_7745c5c3_Err != nil {256 if templ_7745c5c3_Err != nil {
244 return templ_7745c5c3_Err257 return templ_7745c5c3_Err
245 }258 }
246 if l.Lede != "" {259 if l.Lede != "" {
247- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")260+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
248 if templ_7745c5c3_Err != nil {261 if templ_7745c5c3_Err != nil {
249 return templ_7745c5c3_Err262 return templ_7745c5c3_Err
250 }263 }
251- var templ_7745c5c3_Var17 string264+ var templ_7745c5c3_Var18 string
252- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)265+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
253 if templ_7745c5c3_Err != nil {266 if templ_7745c5c3_Err != nil {
254- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 40, Col: 81}267+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 41, Col: 81}
255 }268 }
256- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))269+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
257 if templ_7745c5c3_Err != nil {270 if templ_7745c5c3_Err != nil {
258 return templ_7745c5c3_Err271 return templ_7745c5c3_Err
259 }272 }
260- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</p></section>")273+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p></section>")
261 if templ_7745c5c3_Err != nil {274 if templ_7745c5c3_Err != nil {
262 return templ_7745c5c3_Err275 return templ_7745c5c3_Err
263 }276 }
264 }277 }
265- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<main id=\"main\" tabindex=\"-1\">")278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<main id=\"main\" tabindex=\"-1\">")
266 if templ_7745c5c3_Err != nil {279 if templ_7745c5c3_Err != nil {
267 return templ_7745c5c3_Err280 return templ_7745c5c3_Err
268 }281 }
@@ -270,12 +283,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
270 if templ_7745c5c3_Err != nil {283 if templ_7745c5c3_Err != nil {
271 return templ_7745c5c3_Err284 return templ_7745c5c3_Err
272 }285 }
273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</main>")286+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</main>")
274 if templ_7745c5c3_Err != nil {287 if templ_7745c5c3_Err != nil {
275 return templ_7745c5c3_Err288 return templ_7745c5c3_Err
276 }289 }
277 if l.HasRail && rail != nil {290 if l.HasRail && rail != nil {
278- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<aside class=\"rail\" aria-label=\"Context\">")291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<aside class=\"rail\" aria-label=\"Context\">")
279 if templ_7745c5c3_Err != nil {292 if templ_7745c5c3_Err != nil {
280 return templ_7745c5c3_Err293 return templ_7745c5c3_Err
281 }294 }
@@ -283,12 +296,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
283 if templ_7745c5c3_Err != nil {296 if templ_7745c5c3_Err != nil {
284 return templ_7745c5c3_Err297 return templ_7745c5c3_Err
285 }298 }
286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside>")299+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</aside>")
287 if templ_7745c5c3_Err != nil {300 if templ_7745c5c3_Err != nil {
288 return templ_7745c5c3_Err301 return templ_7745c5c3_Err
289 }302 }
290 }303 }
291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")304+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div>")
292 if templ_7745c5c3_Err != nil {305 if templ_7745c5c3_Err != nil {
293 return templ_7745c5c3_Err306 return templ_7745c5c3_Err
294 }307 }
@@ -316,7 +329,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
316 if templ_7745c5c3_Err != nil {329 if templ_7745c5c3_Err != nil {
317 return templ_7745c5c3_Err330 return templ_7745c5c3_Err
318 }331 }
319- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</body></html>")332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</body></html>")
320 if templ_7745c5c3_Err != nil {333 if templ_7745c5c3_Err != nil {
321 return templ_7745c5c3_Err334 return templ_7745c5c3_Err
322 }335 }
@@ -342,273 +355,273 @@ func Header(l Layout) templ.Component {
342 }()355 }()
343 }356 }
344 ctx = templ.InitializeContext(ctx)357 ctx = templ.InitializeContext(ctx)
345- templ_7745c5c3_Var18 := templ.GetChildren(ctx)358+ templ_7745c5c3_Var19 := templ.GetChildren(ctx)
346- if templ_7745c5c3_Var18 == nil {359+ if templ_7745c5c3_Var19 == nil {
347- templ_7745c5c3_Var18 = templ.NopComponent360+ templ_7745c5c3_Var19 = templ.NopComponent
348 }361 }
349 ctx = templ.ClearChildren(ctx)362 ctx = templ.ClearChildren(ctx)
350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")363+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
351 if templ_7745c5c3_Err != nil {364 if templ_7745c5c3_Err != nil {
352 return templ_7745c5c3_Err365 return templ_7745c5c3_Err
353 }366 }
354- var templ_7745c5c3_Var19 string367+ var templ_7745c5c3_Var20 string
355- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)368+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
356 if templ_7745c5c3_Err != nil {369 if templ_7745c5c3_Err != nil {
357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 68, Col: 41}370+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 69, Col: 41}
358 }371 }
359- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))372+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
360 if templ_7745c5c3_Err != nil {373 if templ_7745c5c3_Err != nil {
361 return templ_7745c5c3_Err374 return templ_7745c5c3_Err
362 }375 }
363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</span></a> ")376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span></a> ")
364 if templ_7745c5c3_Err != nil {377 if templ_7745c5c3_Err != nil {
365 return templ_7745c5c3_Err378 return templ_7745c5c3_Err
366 }379 }
367 if len(l.Crumbs) > 0 {380 if len(l.Crumbs) > 0 {
368- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
369 if templ_7745c5c3_Err != nil {382 if templ_7745c5c3_Err != nil {
370 return templ_7745c5c3_Err383 return templ_7745c5c3_Err
371 }384 }
372 for _, c := range l.Crumbs {385 for _, c := range l.Crumbs {
373- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<li>")386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<li>")
374 if templ_7745c5c3_Err != nil {387 if templ_7745c5c3_Err != nil {
375 return templ_7745c5c3_Err388 return templ_7745c5c3_Err
376 }389 }
377 if c.Href != "" {390 if c.Href != "" {
378- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"")391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a href=\"")
379 if templ_7745c5c3_Err != nil {392 if templ_7745c5c3_Err != nil {
380 return templ_7745c5c3_Err393 return templ_7745c5c3_Err
381 }394 }
382- var templ_7745c5c3_Var20 templ.SafeURL395+ var templ_7745c5c3_Var21 templ.SafeURL
383- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))396+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
384 if templ_7745c5c3_Err != nil {397 if templ_7745c5c3_Err != nil {
385- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 77, Col: 38}398+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 78, Col: 38}
386 }399 }
387- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))400+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
388 if templ_7745c5c3_Err != nil {401 if templ_7745c5c3_Err != nil {
389 return templ_7745c5c3_Err402 return templ_7745c5c3_Err
390 }403 }
391- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"")404+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"")
392 if templ_7745c5c3_Err != nil {405 if templ_7745c5c3_Err != nil {
393 return templ_7745c5c3_Err406 return templ_7745c5c3_Err
394 }407 }
395 if c.Current {408 if c.Current {
396- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " aria-current=\"page\"")409+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " aria-current=\"page\"")
397 if templ_7745c5c3_Err != nil {410 if templ_7745c5c3_Err != nil {
398 return templ_7745c5c3_Err411 return templ_7745c5c3_Err
399 }412 }
400 }413 }
401- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">")414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">")
402 if templ_7745c5c3_Err != nil {415 if templ_7745c5c3_Err != nil {
403 return templ_7745c5c3_Err416 return templ_7745c5c3_Err
404 }417 }
405- var templ_7745c5c3_Var21 string418+ var templ_7745c5c3_Var22 string
406- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)419+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
407 if templ_7745c5c3_Err != nil {420 if templ_7745c5c3_Err != nil {
408- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 81, Col: 19}421+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 82, Col: 19}
409 }422 }
410- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))423+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
411 if templ_7745c5c3_Err != nil {424 if templ_7745c5c3_Err != nil {
412 return templ_7745c5c3_Err425 return templ_7745c5c3_Err
413 }426 }
414- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</a>")427+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</a>")
415 if templ_7745c5c3_Err != nil {428 if templ_7745c5c3_Err != nil {
416 return templ_7745c5c3_Err429 return templ_7745c5c3_Err
417 }430 }
418 } else {431 } else {
419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<span")432+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span")
420 if templ_7745c5c3_Err != nil {433 if templ_7745c5c3_Err != nil {
421 return templ_7745c5c3_Err434 return templ_7745c5c3_Err
422 }435 }
423 if c.Current {436 if c.Current {
424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " aria-current=\"page\"")437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " aria-current=\"page\"")
425 if templ_7745c5c3_Err != nil {438 if templ_7745c5c3_Err != nil {
426 return templ_7745c5c3_Err439 return templ_7745c5c3_Err
427 }440 }
428 }441 }
429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, ">")442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">")
430 if templ_7745c5c3_Err != nil {443 if templ_7745c5c3_Err != nil {
431 return templ_7745c5c3_Err444 return templ_7745c5c3_Err
432 }445 }
433- var templ_7745c5c3_Var22 string446+ var templ_7745c5c3_Var23 string
434- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)447+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
435 if templ_7745c5c3_Err != nil {448 if templ_7745c5c3_Err != nil {
436- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 87, Col: 19}449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 88, Col: 19}
437 }450 }
438- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
439 if templ_7745c5c3_Err != nil {452 if templ_7745c5c3_Err != nil {
440 return templ_7745c5c3_Err453 return templ_7745c5c3_Err
441 }454 }
442- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span>")455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span>")
443 if templ_7745c5c3_Err != nil {456 if templ_7745c5c3_Err != nil {
444 return templ_7745c5c3_Err457 return templ_7745c5c3_Err
445 }458 }
446 }459 }
447- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</li>")460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</li>")
448 if templ_7745c5c3_Err != nil {461 if templ_7745c5c3_Err != nil {
449 return templ_7745c5c3_Err462 return templ_7745c5c3_Err
450 }463 }
451 }464 }
452- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</ol></nav>")465+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</ol></nav>")
453 if templ_7745c5c3_Err != nil {466 if templ_7745c5c3_Err != nil {
454 return templ_7745c5c3_Err467 return templ_7745c5c3_Err
455 }468 }
456 }469 }
457- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"hdr-acts\">")470+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"hdr-acts\">")
458 if templ_7745c5c3_Err != nil {471 if templ_7745c5c3_Err != nil {
459 return templ_7745c5c3_Err472 return templ_7745c5c3_Err
460 }473 }
461 if l.Ref != "" {474 if l.Ref != "" {
462- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<a class=\"btn ref\" href=\"")475+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<a class=\"btn ref\" href=\"")
463 if templ_7745c5c3_Err != nil {476 if templ_7745c5c3_Err != nil {
464 return templ_7745c5c3_Err477 return templ_7745c5c3_Err
465 }478 }
466- var templ_7745c5c3_Var23 templ.SafeURL479+ var templ_7745c5c3_Var24 templ.SafeURL
467- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))480+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
468 if templ_7745c5c3_Err != nil {481 if templ_7745c5c3_Err != nil {
469- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 98, Col: 52}482+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 99, Col: 52}
470 }483 }
471- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))484+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
472 if templ_7745c5c3_Err != nil {485 if templ_7745c5c3_Err != nil {
473 return templ_7745c5c3_Err486 return templ_7745c5c3_Err
474 }487 }
475- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")488+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
476 if templ_7745c5c3_Err != nil {489 if templ_7745c5c3_Err != nil {
477 return templ_7745c5c3_Err490 return templ_7745c5c3_Err
478 }491 }
479- var templ_7745c5c3_Var24 string492+ var templ_7745c5c3_Var25 string
480- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)493+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
481 if templ_7745c5c3_Err != nil {494 if templ_7745c5c3_Err != nil {
482- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 104, Col: 36}495+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 105, Col: 36}
483 }496 }
484- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))497+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
485 if templ_7745c5c3_Err != nil {498 if templ_7745c5c3_Err != nil {
486 return templ_7745c5c3_Err499 return templ_7745c5c3_Err
487 }500 }
488- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</span> <span class=\"sha\">")501+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span> <span class=\"sha\">")
489 if templ_7745c5c3_Err != nil {502 if templ_7745c5c3_Err != nil {
490 return templ_7745c5c3_Err503 return templ_7745c5c3_Err
491 }504 }
492- var templ_7745c5c3_Var25 string505+ var templ_7745c5c3_Var26 string
493- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)506+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
494 if templ_7745c5c3_Err != nil {507 if templ_7745c5c3_Err != nil {
495- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 105, Col: 33}508+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 33}
496 }509 }
497- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))510+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
498 if templ_7745c5c3_Err != nil {511 if templ_7745c5c3_Err != nil {
499 return templ_7745c5c3_Err512 return templ_7745c5c3_Err
500 }513 }
501- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span></a> ")514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span></a> ")
502 if templ_7745c5c3_Err != nil {515 if templ_7745c5c3_Err != nil {
503 return templ_7745c5c3_Err516 return templ_7745c5c3_Err
504 }517 }
505 }518 }
506- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\">?</button> ")519+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> <button type=\"button\" class=\"btn icon\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\">?</button> ")
507 if templ_7745c5c3_Err != nil {520 if templ_7745c5c3_Err != nil {
508 return templ_7745c5c3_Err521 return templ_7745c5c3_Err
509 }522 }
510 if SignedIn(ctx) {523 if SignedIn(ctx) {
511- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")524+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> <button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
512 if templ_7745c5c3_Err != nil {525 if templ_7745c5c3_Err != nil {
513 return templ_7745c5c3_Err526 return templ_7745c5c3_Err
514 }527 }
515 }528 }
516- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></div>")529+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div></div>")
517 if templ_7745c5c3_Err != nil {530 if templ_7745c5c3_Err != nil {
518 return templ_7745c5c3_Err531 return templ_7745c5c3_Err
519 }532 }
520 if len(l.Sections) > 0 {533 if len(l.Sections) > 0 {
521- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")534+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
522 if templ_7745c5c3_Err != nil {535 if templ_7745c5c3_Err != nil {
523 return templ_7745c5c3_Err536 return templ_7745c5c3_Err
524 }537 }
525 for _, s := range l.Sections {538 for _, s := range l.Sections {
526- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<li><a href=\"")539+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<li><a href=\"")
527 if templ_7745c5c3_Err != nil {540 if templ_7745c5c3_Err != nil {
528 return templ_7745c5c3_Err541 return templ_7745c5c3_Err
529 }542 }
530- var templ_7745c5c3_Var26 templ.SafeURL543+ var templ_7745c5c3_Var27 templ.SafeURL
531- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))544+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
532 if templ_7745c5c3_Err != nil {545 if templ_7745c5c3_Err != nil {
533- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 36}546+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 138, Col: 36}
534 }547 }
535- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))548+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
536 if templ_7745c5c3_Err != nil {549 if templ_7745c5c3_Err != nil {
537 return templ_7745c5c3_Err550 return templ_7745c5c3_Err
538 }551 }
539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" data-key=\"")552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-key=\"")
540 if templ_7745c5c3_Err != nil {553 if templ_7745c5c3_Err != nil {
541 return templ_7745c5c3_Err554 return templ_7745c5c3_Err
542 }555 }
543- var templ_7745c5c3_Var27 string556+ var templ_7745c5c3_Var28 string
544- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)557+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
545 if templ_7745c5c3_Err != nil {558 if templ_7745c5c3_Err != nil {
546- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 138, Col: 24}559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 139, Col: 24}
547 }560 }
548- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)561+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
549 if templ_7745c5c3_Err != nil {562 if templ_7745c5c3_Err != nil {
550 return templ_7745c5c3_Err563 return templ_7745c5c3_Err
551 }564 }
552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" aria-keyshortcuts=\"")565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" aria-keyshortcuts=\"")
553 if templ_7745c5c3_Err != nil {566 if templ_7745c5c3_Err != nil {
554 return templ_7745c5c3_Err567 return templ_7745c5c3_Err
555 }568 }
556- var templ_7745c5c3_Var28 string569+ var templ_7745c5c3_Var29 string
557- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)570+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
558 if templ_7745c5c3_Err != nil {571 if templ_7745c5c3_Err != nil {
559- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 139, Col: 33}572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 140, Col: 33}
560 }573 }
561- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)574+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
562 if templ_7745c5c3_Err != nil {575 if templ_7745c5c3_Err != nil {
563 return templ_7745c5c3_Err576 return templ_7745c5c3_Err
564 }577 }
565- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"")578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"")
566 if templ_7745c5c3_Err != nil {579 if templ_7745c5c3_Err != nil {
567 return templ_7745c5c3_Err580 return templ_7745c5c3_Err
568 }581 }
569 if s.Current {582 if s.Current {
570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " aria-current=\"page\"")583+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " aria-current=\"page\"")
571 if templ_7745c5c3_Err != nil {584 if templ_7745c5c3_Err != nil {
572 return templ_7745c5c3_Err585 return templ_7745c5c3_Err
573 }586 }
574 }587 }
575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">")588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">")
576 if templ_7745c5c3_Err != nil {589 if templ_7745c5c3_Err != nil {
577 return templ_7745c5c3_Err590 return templ_7745c5c3_Err
578 }591 }
579- var templ_7745c5c3_Var29 string592+ var templ_7745c5c3_Var30 string
580- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)593+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
581 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
582- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 143, Col: 17}595+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 144, Col: 17}
583 }596 }
584- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))597+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
585 if templ_7745c5c3_Err != nil {598 if templ_7745c5c3_Err != nil {
586 return templ_7745c5c3_Err599 return templ_7745c5c3_Err
587 }600 }
588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<kbd aria-hidden=\"true\">")601+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<kbd aria-hidden=\"true\">")
589 if templ_7745c5c3_Err != nil {602 if templ_7745c5c3_Err != nil {
590 return templ_7745c5c3_Err603 return templ_7745c5c3_Err
591 }604 }
592- var templ_7745c5c3_Var30 string605+ var templ_7745c5c3_Var31 string
593- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)606+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
594 if templ_7745c5c3_Err != nil {607 if templ_7745c5c3_Err != nil {
595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 143, Col: 50}608+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 144, Col: 50}
596 }609 }
597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))610+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
598 if templ_7745c5c3_Err != nil {611 if templ_7745c5c3_Err != nil {
599 return templ_7745c5c3_Err612 return templ_7745c5c3_Err
600 }613 }
601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</kbd></a></li>")614+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</kbd></a></li>")
602 if templ_7745c5c3_Err != nil {615 if templ_7745c5c3_Err != nil {
603 return templ_7745c5c3_Err616 return templ_7745c5c3_Err
604 }617 }
605 }618 }
606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</ul></nav>")619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</ul></nav>")
607 if templ_7745c5c3_Err != nil {620 if templ_7745c5c3_Err != nil {
608 return templ_7745c5c3_Err621 return templ_7745c5c3_Err
609 }622 }
610 }623 }
611- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</header>")624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</header>")
612 if templ_7745c5c3_Err != nil {625 if templ_7745c5c3_Err != nil {
613 return templ_7745c5c3_Err626 return templ_7745c5c3_Err
614 }627 }
@@ -634,38 +647,38 @@ func Footer(l Layout) templ.Component {
634 }()647 }()
635 }648 }
636 ctx = templ.InitializeContext(ctx)649 ctx = templ.InitializeContext(ctx)
637- templ_7745c5c3_Var31 := templ.GetChildren(ctx)650+ templ_7745c5c3_Var32 := templ.GetChildren(ctx)
638- if templ_7745c5c3_Var31 == nil {651+ if templ_7745c5c3_Var32 == nil {
639- templ_7745c5c3_Var31 = templ.NopComponent652+ templ_7745c5c3_Var32 = templ.NopComponent
640 }653 }
641 ctx = templ.ClearChildren(ctx)654 ctx = templ.ClearChildren(ctx)
642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<footer class=\"foot\"><span>ermite ")655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<footer class=\"foot\"><span>ermite ")
643 if templ_7745c5c3_Err != nil {656 if templ_7745c5c3_Err != nil {
644 return templ_7745c5c3_Err657 return templ_7745c5c3_Err
645 }658 }
646- var templ_7745c5c3_Var32 string659+ var templ_7745c5c3_Var33 string
647- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)660+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
648 if templ_7745c5c3_Err != nil {661 if templ_7745c5c3_Err != nil {
649- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 156, Col: 26}662+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 26}
650 }663 }
651- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))664+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
652 if templ_7745c5c3_Err != nil {665 if templ_7745c5c3_Err != nil {
653 return templ_7745c5c3_Err666 return templ_7745c5c3_Err
654 }667 }
655- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span> <a href=\"")668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> <a href=\"")
656 if templ_7745c5c3_Err != nil {669 if templ_7745c5c3_Err != nil {
657 return templ_7745c5c3_Err670 return templ_7745c5c3_Err
658 }671 }
659- var templ_7745c5c3_Var33 templ.SafeURL672+ var templ_7745c5c3_Var34 templ.SafeURL
660- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))673+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
661 if templ_7745c5c3_Err != nil {674 if templ_7745c5c3_Err != nil {
662- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 38}675+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 158, Col: 38}
663 }676 }
664- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))677+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
665 if templ_7745c5c3_Err != nil {678 if templ_7745c5c3_Err != nil {
666 return templ_7745c5c3_Err679 return templ_7745c5c3_Err
667 }680 }
668- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")681+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
669 if templ_7745c5c3_Err != nil {682 if templ_7745c5c3_Err != nil {
670 return templ_7745c5c3_Err683 return templ_7745c5c3_Err
671 }684 }
@@ -693,12 +706,12 @@ func Hint(l Layout) templ.Component {
693 }()706 }()
694 }707 }
695 ctx = templ.InitializeContext(ctx)708 ctx = templ.InitializeContext(ctx)
696- templ_7745c5c3_Var34 := templ.GetChildren(ctx)709+ templ_7745c5c3_Var35 := templ.GetChildren(ctx)
697- if templ_7745c5c3_Var34 == nil {710+ if templ_7745c5c3_Var35 == nil {
698- templ_7745c5c3_Var34 = templ.NopComponent711+ templ_7745c5c3_Var35 = templ.NopComponent
699 }712 }
700 ctx = templ.ClearChildren(ctx)713 ctx = templ.ClearChildren(ctx)
701- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")714+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
702 if templ_7745c5c3_Err != nil {715 if templ_7745c5c3_Err != nil {
703 return templ_7745c5c3_Err716 return templ_7745c5c3_Err
704 }717 }
@@ -706,7 +719,7 @@ func Hint(l Layout) templ.Component {
706 if templ_7745c5c3_Err != nil {719 if templ_7745c5c3_Err != nil {
707 return templ_7745c5c3_Err720 return templ_7745c5c3_Err
708 }721 }
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</div>")722+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</div>")
710 if templ_7745c5c3_Err != nil {723 if templ_7745c5c3_Err != nil {
711 return templ_7745c5c3_Err724 return templ_7745c5c3_Err
712 }725 }
M internal/adapters/web/static/js/ermite.js +0 -23
@@ -363,26 +363,3 @@ window.ermiteLive = (() => {
363363 },
364364 };
365365 })();
366-
367-// ermiteLog follows the log of a running job on its page: the lines arrive as
368-// "ci-log" events (internal/adapters/web/actions.go, logStream) and are
369-// appended to the list. The browser reconnects by itself with Last-Event-ID,
370-// the number of the next line; "ci-end" closes the feed when the job is over.
371-// The page scrolls with the log only while the reader is at its end.
372-(() => {
373- const box = document.getElementById('job-log');
374- if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return;
375- const list = box.querySelector('ol.log');
376- if (!list) return;
377- const url = box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0');
378- const es = new EventSource(url);
379- es.addEventListener('ci-log', (e) => {
380- const t = document.createElement('template');
381- t.innerHTML = e.data;
382- const atEnd = window.innerHeight + window.scrollY >= document.body.scrollHeight - 80;
383- list.append(...t.content.children);
384- if (atEnd) window.scrollTo(0, document.body.scrollHeight);
385- });
386- es.addEventListener('ci-end', () => es.close());
387- es.onerror = () => { if (es.readyState === EventSource.CLOSED) es.close(); };
388-})();
@@ -363,26 +363,3 @@ window.ermiteLive = (() => {
363 },363 },
364 };364 };
365 })();365 })();
366-
367-// ermiteLog follows the log of a running job on its page: the lines arrive as
368-// "ci-log" events (internal/adapters/web/actions.go, logStream) and are
369-// appended to the list. The browser reconnects by itself with Last-Event-ID,
370-// the number of the next line; "ci-end" closes the feed when the job is over.
371-// The page scrolls with the log only while the reader is at its end.
372-(() => {
373- const box = document.getElementById('job-log');
374- if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return;
375- const list = box.querySelector('ol.log');
376- if (!list) return;
377- const url = box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0');
378- const es = new EventSource(url);
379- es.addEventListener('ci-log', (e) => {
380- const t = document.createElement('template');
381- t.innerHTML = e.data;
382- const atEnd = window.innerHeight + window.scrollY >= document.body.scrollHeight - 80;
383- list.append(...t.content.children);
384- if (atEnd) window.scrollTo(0, document.body.scrollHeight);
385- });
386- es.addEventListener('ci-end', () => es.close());
387- es.onerror = () => { if (es.readyState === EventSource.CLOSED) es.close(); };
388-})();
A internal/adapters/web/static/js/log.js +43 -0
@@ -0,0 +1,43 @@
1+// log.js: the live log of a running job (page /<repo>/actions/runs/<id>/job/<id>).
2+// The lines arrive as "ci-log" events (internal/adapters/web/actions.go,
3+// logStream), each one the <li> of the lines of a batch, and are appended to the
4+// list. The browser reconnects by itself with Last-Event-ID, the number of the
5+// next line; "ci-end" closes the feed when the job is over. The page follows the
6+// log (scrolls to its end) only while the reader is at the end already. A piece
7+// that continues a line begun in an earlier batch (data-cont) is joined to the
8+// last line of the list.
9+window.ermiteLog = (() => {
10+ const nearEnd = () => window.innerHeight + window.scrollY >= document.body.scrollHeight - 80;
11+
12+ // follow starts the feed of the box (#job-log) and returns the EventSource,
13+ // or null when the box is not a running job's.
14+ function follow(box) {
15+ if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return null;
16+ const list = box.querySelector('ol.log');
17+ if (!list) return null;
18+ const es = new window.EventSource(box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0'));
19+ es.addEventListener('ci-log', (e) => {
20+ const t = document.createElement('template');
21+ t.innerHTML = e.data;
22+ const atEnd = nearEnd();
23+ for (const line of [...t.content.children]) {
24+ const last = list.children[list.children.length - 1];
25+ if (line.hasAttribute('data-cont') && last) {
26+ const text = line.querySelector('.t');
27+ const into = last.querySelector('.t');
28+ if (text && into) into.textContent += text.textContent;
29+ } else {
30+ list.append(line);
31+ }
32+ }
33+ if (atEnd) window.scrollTo(0, document.body.scrollHeight);
34+ });
35+ es.addEventListener('ci-end', () => es.close());
36+ es.onerror = () => { if (es.readyState === window.EventSource.CLOSED) es.close(); };
37+ return es;
38+ }
39+
40+ return { follow };
41+})();
42+
43+document.addEventListener('DOMContentLoaded', () => window.ermiteLog.follow(document.getElementById('job-log')));
@@ -0,0 +1,43 @@
1+// log.js: the live log of a running job (page /<repo>/actions/runs/<id>/job/<id>).
2+// The lines arrive as "ci-log" events (internal/adapters/web/actions.go,
3+// logStream), each one the <li> of the lines of a batch, and are appended to the
4+// list. The browser reconnects by itself with Last-Event-ID, the number of the
5+// next line; "ci-end" closes the feed when the job is over. The page follows the
6+// log (scrolls to its end) only while the reader is at the end already. A piece
7+// that continues a line begun in an earlier batch (data-cont) is joined to the
8+// last line of the list.
9+window.ermiteLog = (() => {
10+ const nearEnd = () => window.innerHeight + window.scrollY >= document.body.scrollHeight - 80;
11+
12+ // follow starts the feed of the box (#job-log) and returns the EventSource,
13+ // or null when the box is not a running job's.
14+ function follow(box) {
15+ if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return null;
16+ const list = box.querySelector('ol.log');
17+ if (!list) return null;
18+ const es = new window.EventSource(box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0'));
19+ es.addEventListener('ci-log', (e) => {
20+ const t = document.createElement('template');
21+ t.innerHTML = e.data;
22+ const atEnd = nearEnd();
23+ for (const line of [...t.content.children]) {
24+ const last = list.children[list.children.length - 1];
25+ if (line.hasAttribute('data-cont') && last) {
26+ const text = line.querySelector('.t');
27+ const into = last.querySelector('.t');
28+ if (text && into) into.textContent += text.textContent;
29+ } else {
30+ list.append(line);
31+ }
32+ }
33+ if (atEnd) window.scrollTo(0, document.body.scrollHeight);
34+ });
35+ es.addEventListener('ci-end', () => es.close());
36+ es.onerror = () => { if (es.readyState === window.EventSource.CLOSED) es.close(); };
37+ return es;
38+ }
39+
40+ return { follow };
41+})();
42+
43+document.addEventListener('DOMContentLoaded', () => window.ermiteLog.follow(document.getElementById('job-log')));
M internal/app/testdata/web/blob-binary.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-code.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-empty.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-image.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-markdown.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-svg.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-text.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-too-large.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/blob-trojan.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-big.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-binary.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-merge.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-modify-split.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-modify.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-rename.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-root.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commit-trojan.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">25 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commits-page2.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commits-path.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commits-root.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/commits-small.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/empty.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="" data-sel="" data-nav="">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="" data-sel="" data-nav="">25 <body data-ref="" data-sha="" data-sel="" data-nav="">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/home.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="" data-sel="" data-nav="">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="" data-sel="" data-nav="">25 <body data-ref="" data-sha="" data-sel="" data-nav="">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/not-found-page.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="" data-sel="" data-nav="">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="" data-sel="" data-nav="">25 <body data-ref="" data-sha="" data-sel="" data-nav="">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/not-found.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="" data-sha="" data-sel="" data-nav="">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="" data-sha="" data-sel="" data-nav="">25 <body data-ref="" data-sha="" data-sel="" data-nav="">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/refs.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/summary.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/tree-nested.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/tree-root.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/tree-slash-ref.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/feature/x">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/feature/x">25 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/feature/x">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/app/testdata/web/tree-subdir.html +2 -0
@@ -19,6 +19,8 @@
1919 </script>
2020 <script src="/-/static/js/files.HASH.js" defer>
2121 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
2224 </head>
2325 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
2426 <a class="skip" href="#main">skip to content</a>
@@ -19,6 +19,8 @@
19 </script>19 </script>
20 <script src="/-/static/js/files.HASH.js" defer>20 <script src="/-/static/js/files.HASH.js" defer>
21 </script>21 </script>
22+<script src="/-/static/js/log.HASH.js" defer>
23+</script>
22 </head>24 </head>
23 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">25 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main">
24 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>

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
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close