detached bc6524c

feat(web): owner inbox page and home button

commit
bc6524ce22edf8d1d57debf44b4d4324876a00ec
author
brice · · 2026-10-19 06:37 UTC
parent
ab38ed2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
23 files · +1617 -378
M docs/issues.md +6 -0
@@ -33,3 +33,9 @@ ermite token create claude --preset agent --scope issue:write --repo <repo> --br
3333 ## Numbering
3434
3535 Merge requests and issues of a repository draw `#n` from one sequence, so a number names one thing: `ermite mr show myrepo 1`, `ermite issue show myrepo 2`.
36+
37+## Inbox
38+
39+The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
40+
41+The page updates live: merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
@@ -33,3 +33,9 @@ ermite token create claude --preset agent --scope issue:write --repo <repo> --br
33 ## Numbering33 ## Numbering
34 34
35 Merge requests and issues of a repository draw `#n` from one sequence, so a number names one thing: `ermite mr show myrepo 1`, `ermite issue show myrepo 2`.35 Merge requests and issues of a repository draw `#n` from one sequence, so a number names one thing: `ermite mr show myrepo 1`, `ermite issue show myrepo 2`.
36+
37+## Inbox
38+
39+The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
40+
41+The page updates live: merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
M internal/adapters/web/events.go +31 -2
@@ -27,6 +27,7 @@ import (
2727 // the HTML fragments the page shows, sent as Server-Sent Events:
2828 //
2929 // GET /-/events?topic=home
30+// GET /-/events?topic=owner&view=inbox[&repo=<name>] (the signed-in owner only)
3031 // GET /-/events?topic=repo:<name>&view=summary|refs
3132 // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha>
3233 // GET /-/events?topic=repo:<name>&view=mrs
@@ -84,9 +85,14 @@ const (
8485 retryAfter = 10
8586 )
8687
87-// parseTopic reads "home" or "repo:<name>"; repo is "" for home.
88+// ownerTopic is the stream of the inbox: the merge requests and issues of every
89+// repository, for the owner.
90+const ownerTopic = "owner"
91+
92+// parseTopic reads "home", "owner" or "repo:<name>"; repo is "" for home and
93+// owner.
8894 func parseTopic(topic string) (repo string, ok bool) {
89- if topic == "home" {
95+ if topic == "home" || topic == ownerTopic {
9096 return "", true
9197 }
9298 if name, found := strings.CutPrefix(topic, "repo:"); found && name != "" {
@@ -95,6 +101,21 @@ func parseTopic(topic string) (repo string, ok bool) {
95101 return "", false
96102 }
97103
104+// ownerStreamOK tells whether the request may follow the owner topic: the
105+// signed-in owner, with the inbox view and a valid repository filter, on a
106+// server that has the inbox.
107+func (s *Server) ownerStreamOK(ctx context.Context, q url.Values) bool {
108+ if s.cfg.Inbox == nil || !SignedIn(ctx) || q.Get("view") != "inbox" {
109+ return false
110+ }
111+ if f := q.Get("repo"); f != "" {
112+ if _, err := domain.ParseRepoName(f); err != nil {
113+ return false
114+ }
115+ }
116+ return true
117+}
118+
98119 // events is GET /-/events.
99120 func (s *Server) events(w http.ResponseWriter, r *http.Request) {
100121 if s.cfg.Events == nil {
@@ -104,6 +125,11 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
104125 q := r.URL.Query()
105126 topic := q.Get("topic")
106127 repo, ok := parseTopic(topic)
128+ if topic == ownerTopic {
129+ // Anyone but the signed-in owner gets the answer of an unknown topic.
130+ ok = s.ownerStreamOK(r.Context(), q)
131+ repo = ""
132+ }
107133 if !ok {
108134 denyStream(w, http.StatusBadRequest, "topic must be home or repo:<name>")
109135 return
@@ -227,6 +253,9 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
227253 }
228254
229255 st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n"), job: q.Get("job"), sha: sha}
256+ if topic == ownerTopic {
257+ st.filter = q.Get("repo")
258+ }
230259 handle := func(m recvd) bool {
231260 if m.err != nil {
232261 if ctx.Err() == nil && !errors.Is(m.err, context.Canceled) {
@@ -27,6 +27,7 @@ import (
27 // the HTML fragments the page shows, sent as Server-Sent Events:27 // the HTML fragments the page shows, sent as Server-Sent Events:
28 //28 //
29 // GET /-/events?topic=home29 // GET /-/events?topic=home
30+// GET /-/events?topic=owner&view=inbox[&repo=<name>] (the signed-in owner only)
30 // GET /-/events?topic=repo:<name>&view=summary|refs31 // GET /-/events?topic=repo:<name>&view=summary|refs
31 // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha>32 // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha>
32 // GET /-/events?topic=repo:<name>&view=mrs33 // GET /-/events?topic=repo:<name>&view=mrs
@@ -84,9 +85,14 @@ const (
84 retryAfter = 1085 retryAfter = 10
85 )86 )
86 87
87-// parseTopic reads "home" or "repo:<name>"; repo is "" for home.88+// ownerTopic is the stream of the inbox: the merge requests and issues of every
89+// repository, for the owner.
90+const ownerTopic = "owner"
91+
92+// parseTopic reads "home", "owner" or "repo:<name>"; repo is "" for home and
93+// owner.
88 func parseTopic(topic string) (repo string, ok bool) {94 func parseTopic(topic string) (repo string, ok bool) {
89- if topic == "home" {95+ if topic == "home" || topic == ownerTopic {
90 return "", true96 return "", true
91 }97 }
92 if name, found := strings.CutPrefix(topic, "repo:"); found && name != "" {98 if name, found := strings.CutPrefix(topic, "repo:"); found && name != "" {
@@ -95,6 +101,21 @@ func parseTopic(topic string) (repo string, ok bool) {
95 return "", false101 return "", false
96 }102 }
97 103
104+// ownerStreamOK tells whether the request may follow the owner topic: the
105+// signed-in owner, with the inbox view and a valid repository filter, on a
106+// server that has the inbox.
107+func (s *Server) ownerStreamOK(ctx context.Context, q url.Values) bool {
108+ if s.cfg.Inbox == nil || !SignedIn(ctx) || q.Get("view") != "inbox" {
109+ return false
110+ }
111+ if f := q.Get("repo"); f != "" {
112+ if _, err := domain.ParseRepoName(f); err != nil {
113+ return false
114+ }
115+ }
116+ return true
117+}
118+
98 // events is GET /-/events.119 // events is GET /-/events.
99 func (s *Server) events(w http.ResponseWriter, r *http.Request) {120 func (s *Server) events(w http.ResponseWriter, r *http.Request) {
100 if s.cfg.Events == nil {121 if s.cfg.Events == nil {
@@ -104,6 +125,11 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
104 q := r.URL.Query()125 q := r.URL.Query()
105 topic := q.Get("topic")126 topic := q.Get("topic")
106 repo, ok := parseTopic(topic)127 repo, ok := parseTopic(topic)
128+ if topic == ownerTopic {
129+ // Anyone but the signed-in owner gets the answer of an unknown topic.
130+ ok = s.ownerStreamOK(r.Context(), q)
131+ repo = ""
132+ }
107 if !ok {133 if !ok {
108 denyStream(w, http.StatusBadRequest, "topic must be home or repo:<name>")134 denyStream(w, http.StatusBadRequest, "topic must be home or repo:<name>")
109 return135 return
@@ -227,6 +253,9 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
227 }253 }
228 254
229 st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n"), job: q.Get("job"), sha: sha}255 st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n"), job: q.Get("job"), sha: sha}
256+ if topic == ownerTopic {
257+ st.filter = q.Get("repo")
258+ }
230 handle := func(m recvd) bool {259 handle := func(m recvd) bool {
231 if m.err != nil {260 if m.err != nil {
232 if ctx.Err() == nil && !errors.Is(m.err, context.Canceled) {261 if ctx.Err() == nil && !errors.Is(m.err, context.Canceled) {
M internal/adapters/web/events_test.go +1 -1
@@ -36,7 +36,7 @@ func TestFormatEvent(t *testing.T) {
3636
3737 func TestParseTopic(t *testing.T) {
3838 t.Parallel()
39- for in, want := range map[string]string{"home": "", "repo:x": "x", "repo:a.b-c": "a.b-c"} {
39+ for in, want := range map[string]string{"home": "", "owner": "", "repo:x": "x", "repo:a.b-c": "a.b-c"} {
4040 repo, ok := parseTopic(in)
4141 assert.True(t, ok, in)
4242 assert.Equal(t, want, repo, in)
@@ -36,7 +36,7 @@ func TestFormatEvent(t *testing.T) {
36 36
37 func TestParseTopic(t *testing.T) {37 func TestParseTopic(t *testing.T) {
38 t.Parallel()38 t.Parallel()
39- for in, want := range map[string]string{"home": "", "repo:x": "x", "repo:a.b-c": "a.b-c"} {39+ for in, want := range map[string]string{"home": "", "owner": "", "repo:x": "x", "repo:a.b-c": "a.b-c"} {
40 repo, ok := parseTopic(in)40 repo, ok := parseTopic(in)
41 assert.True(t, ok, in)41 assert.True(t, ok, in)
42 assert.Equal(t, want, repo, in)42 assert.Equal(t, want, repo, in)
M internal/adapters/web/fragments.go +10 -5
@@ -21,11 +21,13 @@ import (
2121 type liveStream struct {
2222 s *Server
2323 repo string // "" on the home page
24- view string // "summary", "refs" or "commits"
25- ref string // commits: the branch listed
26- head string // commits: the commit listed first
27- n string // mr: the number of the merge request shown; run: the run id
28- job string // job: the id of the job shown
24+ // filter is the repository the inbox is filtered on, "" for all.
25+ filter string
26+ view string // "summary", "refs" or "commits"
27+ ref string // commits: the branch listed
28+ head string // commits: the commit listed first
29+ n string // mr: the number of the merge request shown; run: the run id
30+ job string // job: the id of the job shown
2931 // sha is the commit of the run of the job shown, read once when the stream
3032 // is set up: the annotations link to files at it, and it never changes.
3133 sha string
@@ -75,6 +77,9 @@ func (l *liveStream) render(ctx context.Context, we ports.WatchEvent) ([]fragmen
7577 if we.Event == nil {
7678 return nil, nil
7779 }
80+ if l.view == "inbox" {
81+ return l.inbox(ctx, we)
82+ }
7883 if l.repo == "" {
7984 return l.home(ctx, we)
8085 }
@@ -21,11 +21,13 @@ import (
21 type liveStream struct {21 type liveStream struct {
22 s *Server22 s *Server
23 repo string // "" on the home page23 repo string // "" on the home page
24- view string // "summary", "refs" or "commits"24+ // filter is the repository the inbox is filtered on, "" for all.
25- ref string // commits: the branch listed25+ filter string
26- head string // commits: the commit listed first26+ view string // "summary", "refs" or "commits"
27- n string // mr: the number of the merge request shown; run: the run id27+ ref string // commits: the branch listed
28- job string // job: the id of the job shown28+ head string // commits: the commit listed first
29+ n string // mr: the number of the merge request shown; run: the run id
30+ job string // job: the id of the job shown
29 // sha is the commit of the run of the job shown, read once when the stream31 // sha is the commit of the run of the job shown, read once when the stream
30 // is set up: the annotations link to files at it, and it never changes.32 // is set up: the annotations link to files at it, and it never changes.
31 sha string33 sha string
@@ -75,6 +77,9 @@ func (l *liveStream) render(ctx context.Context, we ports.WatchEvent) ([]fragmen
75 if we.Event == nil {77 if we.Event == nil {
76 return nil, nil78 return nil, nil
77 }79 }
80+ if l.view == "inbox" {
81+ return l.inbox(ctx, we)
82+ }
78 if l.repo == "" {83 if l.repo == "" {
79 return l.home(ctx, we)84 return l.home(ctx, we)
80 }85 }
M internal/adapters/web/fragments.templ +1 -1
@@ -102,7 +102,7 @@ templ Live(l Layout) {
102102 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
103103 htmx:sse:error -> ermiteLive.error(event.detail.status);
104104 htmx:sse:before:connection -> ermiteLive.gate(event);
105- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
105+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
106106 gap -> ermiteLive.reload()"
107107 ></span>
108108 }
@@ -102,7 +102,7 @@ templ Live(l Layout) {
102 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();102 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
103 htmx:sse:error -> ermiteLive.error(event.detail.status);103 htmx:sse:error -> ermiteLive.error(event.detail.status);
104 htmx:sse:before:connection -> ermiteLive.gate(event);104 htmx:sse:before:connection -> ermiteLive.gate(event);
105- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);105+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
106 gap -> ermiteLive.reload()"106 gap -> ermiteLive.reload()"
107 ></span>107 ></span>
108 }108 }
M internal/adapters/web/fragments_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/home.go +3 -0
@@ -121,6 +121,9 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in
121121 p.Sorts = append(p.Sorts, sl)
122122 }
123123 p.Rows = s.homeRows(ctx, repos, now)
124+ if n, show := s.inboxCount(ctx); show {
125+ p.Inbox = &InboxButton{Href: inboxPath, Count: n}
126+ }
124127
125128 l := s.base()
126129 l.Title = s.instance
@@ -121,6 +121,9 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in
121 p.Sorts = append(p.Sorts, sl)121 p.Sorts = append(p.Sorts, sl)
122 }122 }
123 p.Rows = s.homeRows(ctx, repos, now)123 p.Rows = s.homeRows(ctx, repos, now)
124+ if n, show := s.inboxCount(ctx); show {
125+ p.Inbox = &InboxButton{Href: inboxPath, Count: n}
126+ }
124 127
125 l := s.base()128 l := s.base()
126 l.Title = s.instance129 l.Title = s.instance
A internal/adapters/web/inbox.go +188 -0
@@ -0,0 +1,188 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "net/http"
8+ "net/url"
9+ "strconv"
10+ "time"
11+
12+ "github.com/a-h/templ"
13+
14+ "bnema.dev/ermite/internal/domain"
15+ "bnema.dev/ermite/internal/ports"
16+)
17+
18+// The owner's inbox (ADR 0031): GET /-/inbox lists the open merge requests and
19+// issues of every repository, newest activity first, with a filter by
20+// repository (?repo=<name>, a plain link, so it works without scripts). Only a
21+// signed-in owner sees it: for anyone else the path is as missing as any other.
22+// It follows the stream "owner" with view=inbox: each event of a merge request
23+// or an issue sends the counters, the two lists and the filter again.
24+
25+// inboxPath is the page of the inbox.
26+const inboxPath = "/-/inbox"
27+
28+// inboxEvent is the name of the live events of the inbox.
29+const inboxEvent = "inbox"
30+
31+// InboxRow is one open item of the inbox.
32+type InboxRow struct {
33+ Repo string
34+ RepoHref string
35+ Number string
36+ Href string
37+ Title string
38+ When string
39+ WhenAt string
40+ Cursor bool
41+}
42+
43+// InboxRepoLink is an entry of the repository filter.
44+type InboxRepoLink struct {
45+ Label string
46+ Href string
47+ Current bool
48+}
49+
50+// InboxPage is the inbox: the counters, the filter and the two lists.
51+type InboxPage struct {
52+ // MergeRequests and Issues count every open item, whatever the filter.
53+ MergeRequests, Issues int
54+ Filter string // the repository filtered on, "" for all
55+ Repos []InboxRepoLink
56+ MRs []InboxRow
57+ IssueRows []InboxRow
58+}
59+
60+// inboxHref is the inbox page, filtered on repo when it is not "".
61+func inboxHref(repo string) string {
62+ if repo == "" {
63+ return inboxPath
64+ }
65+ return inboxPath + "?repo=" + url.QueryEscape(repo)
66+}
67+
68+// inboxFilter reads ?repo=. A name that is not valid is a page that is not
69+// there.
70+func inboxFilter(r *http.Request) (domain.RepoName, error) {
71+ raw := r.URL.Query().Get("repo")
72+ if raw == "" {
73+ return domain.RepoName{}, nil
74+ }
75+ name, err := domain.ParseRepoName(raw)
76+ if err != nil {
77+ return domain.RepoName{}, errNotFound
78+ }
79+ return name, nil
80+}
81+
82+// inboxPage builds the page from the overview.
83+func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {
84+ now := s.cfg.Now()
85+ p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, Filter: filter.String()}
86+ row := func(repo domain.RepoName, n int64, href, title string, updated, created time.Time) InboxRow {
87+ ts := updated
88+ if ts.IsZero() {
89+ ts = created
90+ }
91+ r := InboxRow{
92+ Repo: repo.String(), RepoHref: repoHref(repo.String()),
93+ Number: "#" + strconv.FormatInt(n, 10), Href: href, Title: visibleText(title),
94+ }
95+ r.When, r.WhenAt = whenOf(now, ts)
96+ return r
97+ }
98+ for _, m := range ov.MergeRequests {
99+ p.MRs = append(p.MRs, row(m.Repo, m.Number, mrHref(m.Repo.String(), m.Number), m.Title.String(), m.UpdatedAt, m.CreatedAt))
100+ }
101+ for _, i := range ov.Issues {
102+ p.IssueRows = append(p.IssueRows, row(i.Repo, i.Number, issueHref(i.Repo.String(), i.Number), i.Title.String(), i.UpdatedAt, i.CreatedAt))
103+ }
104+ switch {
105+ case len(p.MRs) > 0:
106+ p.MRs[0].Cursor = true
107+ case len(p.IssueRows) > 0:
108+ p.IssueRows[0].Cursor = true
109+ }
110+ p.Repos = append(p.Repos, InboxRepoLink{Label: "all", Href: inboxHref(""), Current: filter.IsZero()})
111+ for _, r := range ov.Repos {
112+ p.Repos = append(p.Repos, InboxRepoLink{Label: r.String(), Href: inboxHref(r.String()), Current: r == filter})
113+ }
114+ return p
115+}
116+
117+// inbox is GET /-/inbox.
118+func (s *Server) inbox(ctx context.Context, r *http.Request) (templ.Component, int, error) {
119+ if !SignedIn(ctx) {
120+ return nil, 0, errNotFound
121+ }
122+ filter, err := inboxFilter(r)
123+ if err != nil {
124+ return nil, 0, err
125+ }
126+ ov, err := s.cfg.Inbox.Overview(ctx, principal(ctx), filter)
127+ if err != nil {
128+ return nil, 0, err
129+ }
130+ l := s.base()
131+ l.Title = "inbox · " + s.instance
132+ l.Crumbs = []Crumb{{Label: "inbox", Current: true}}
133+ l.Cmd = []CmdItem{{Kind: "go", Label: "home", Href: "/"}}
134+ if s.cfg.Events != nil {
135+ q := "view=inbox"
136+ if !filter.IsZero() {
137+ q += "&repo=" + url.QueryEscape(filter.String())
138+ }
139+ l.Live, l.LiveURL = true, liveURL("owner", q)
140+ }
141+ return Inbox(l, s.inboxPage(ov, filter)), http.StatusOK, nil
142+}
143+
144+// inboxCount is the number of open items the home button shows, and whether
145+// there is a button: only for the signed-in owner, and only when the inbox is
146+// there. A failed read leaves the button without a number.
147+func (s *Server) inboxCount(ctx context.Context) (n int, show bool) {
148+ if s.cfg.Inbox == nil || !SignedIn(ctx) {
149+ return 0, false
150+ }
151+ ov, err := s.cfg.Inbox.Overview(ctx, principal(ctx), domain.RepoName{})
152+ if err != nil {
153+ return -1, true
154+ }
155+ return ov.Counts.MergeRequests + ov.Counts.Issues, true
156+}
157+
158+// inbox renders the fragments an event of a merge request or an issue changes
159+// on the inbox page: the counters, the filter and the two lists.
160+func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
161+ switch we.Event.(type) {
162+ case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded,
163+ domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented:
164+ default:
165+ return nil, nil
166+ }
167+ if l.s.cfg.Inbox == nil {
168+ return nil, nil
169+ }
170+ var filter domain.RepoName
171+ if l.filter != "" {
172+ filter, _ = domain.ParseRepoName(l.filter)
173+ }
174+ ov, err := l.s.cfg.Inbox.Overview(ctx, principal(ctx), filter)
175+ if err != nil {
176+ return nil, err
177+ }
178+ p := l.s.inboxPage(ov, filter)
179+ var out []fragment
180+ for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxMRs(p), InboxIssues(p)} {
181+ f, err := renderFragment(ctx, inboxEvent, c)
182+ if err != nil {
183+ return nil, err
184+ }
185+ out = append(out, f)
186+ }
187+ return out, nil
188+}
@@ -0,0 +1,188 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "net/http"
8+ "net/url"
9+ "strconv"
10+ "time"
11+
12+ "github.com/a-h/templ"
13+
14+ "bnema.dev/ermite/internal/domain"
15+ "bnema.dev/ermite/internal/ports"
16+)
17+
18+// The owner's inbox (ADR 0031): GET /-/inbox lists the open merge requests and
19+// issues of every repository, newest activity first, with a filter by
20+// repository (?repo=<name>, a plain link, so it works without scripts). Only a
21+// signed-in owner sees it: for anyone else the path is as missing as any other.
22+// It follows the stream "owner" with view=inbox: each event of a merge request
23+// or an issue sends the counters, the two lists and the filter again.
24+
25+// inboxPath is the page of the inbox.
26+const inboxPath = "/-/inbox"
27+
28+// inboxEvent is the name of the live events of the inbox.
29+const inboxEvent = "inbox"
30+
31+// InboxRow is one open item of the inbox.
32+type InboxRow struct {
33+ Repo string
34+ RepoHref string
35+ Number string
36+ Href string
37+ Title string
38+ When string
39+ WhenAt string
40+ Cursor bool
41+}
42+
43+// InboxRepoLink is an entry of the repository filter.
44+type InboxRepoLink struct {
45+ Label string
46+ Href string
47+ Current bool
48+}
49+
50+// InboxPage is the inbox: the counters, the filter and the two lists.
51+type InboxPage struct {
52+ // MergeRequests and Issues count every open item, whatever the filter.
53+ MergeRequests, Issues int
54+ Filter string // the repository filtered on, "" for all
55+ Repos []InboxRepoLink
56+ MRs []InboxRow
57+ IssueRows []InboxRow
58+}
59+
60+// inboxHref is the inbox page, filtered on repo when it is not "".
61+func inboxHref(repo string) string {
62+ if repo == "" {
63+ return inboxPath
64+ }
65+ return inboxPath + "?repo=" + url.QueryEscape(repo)
66+}
67+
68+// inboxFilter reads ?repo=. A name that is not valid is a page that is not
69+// there.
70+func inboxFilter(r *http.Request) (domain.RepoName, error) {
71+ raw := r.URL.Query().Get("repo")
72+ if raw == "" {
73+ return domain.RepoName{}, nil
74+ }
75+ name, err := domain.ParseRepoName(raw)
76+ if err != nil {
77+ return domain.RepoName{}, errNotFound
78+ }
79+ return name, nil
80+}
81+
82+// inboxPage builds the page from the overview.
83+func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {
84+ now := s.cfg.Now()
85+ p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, Filter: filter.String()}
86+ row := func(repo domain.RepoName, n int64, href, title string, updated, created time.Time) InboxRow {
87+ ts := updated
88+ if ts.IsZero() {
89+ ts = created
90+ }
91+ r := InboxRow{
92+ Repo: repo.String(), RepoHref: repoHref(repo.String()),
93+ Number: "#" + strconv.FormatInt(n, 10), Href: href, Title: visibleText(title),
94+ }
95+ r.When, r.WhenAt = whenOf(now, ts)
96+ return r
97+ }
98+ for _, m := range ov.MergeRequests {
99+ p.MRs = append(p.MRs, row(m.Repo, m.Number, mrHref(m.Repo.String(), m.Number), m.Title.String(), m.UpdatedAt, m.CreatedAt))
100+ }
101+ for _, i := range ov.Issues {
102+ p.IssueRows = append(p.IssueRows, row(i.Repo, i.Number, issueHref(i.Repo.String(), i.Number), i.Title.String(), i.UpdatedAt, i.CreatedAt))
103+ }
104+ switch {
105+ case len(p.MRs) > 0:
106+ p.MRs[0].Cursor = true
107+ case len(p.IssueRows) > 0:
108+ p.IssueRows[0].Cursor = true
109+ }
110+ p.Repos = append(p.Repos, InboxRepoLink{Label: "all", Href: inboxHref(""), Current: filter.IsZero()})
111+ for _, r := range ov.Repos {
112+ p.Repos = append(p.Repos, InboxRepoLink{Label: r.String(), Href: inboxHref(r.String()), Current: r == filter})
113+ }
114+ return p
115+}
116+
117+// inbox is GET /-/inbox.
118+func (s *Server) inbox(ctx context.Context, r *http.Request) (templ.Component, int, error) {
119+ if !SignedIn(ctx) {
120+ return nil, 0, errNotFound
121+ }
122+ filter, err := inboxFilter(r)
123+ if err != nil {
124+ return nil, 0, err
125+ }
126+ ov, err := s.cfg.Inbox.Overview(ctx, principal(ctx), filter)
127+ if err != nil {
128+ return nil, 0, err
129+ }
130+ l := s.base()
131+ l.Title = "inbox · " + s.instance
132+ l.Crumbs = []Crumb{{Label: "inbox", Current: true}}
133+ l.Cmd = []CmdItem{{Kind: "go", Label: "home", Href: "/"}}
134+ if s.cfg.Events != nil {
135+ q := "view=inbox"
136+ if !filter.IsZero() {
137+ q += "&repo=" + url.QueryEscape(filter.String())
138+ }
139+ l.Live, l.LiveURL = true, liveURL("owner", q)
140+ }
141+ return Inbox(l, s.inboxPage(ov, filter)), http.StatusOK, nil
142+}
143+
144+// inboxCount is the number of open items the home button shows, and whether
145+// there is a button: only for the signed-in owner, and only when the inbox is
146+// there. A failed read leaves the button without a number.
147+func (s *Server) inboxCount(ctx context.Context) (n int, show bool) {
148+ if s.cfg.Inbox == nil || !SignedIn(ctx) {
149+ return 0, false
150+ }
151+ ov, err := s.cfg.Inbox.Overview(ctx, principal(ctx), domain.RepoName{})
152+ if err != nil {
153+ return -1, true
154+ }
155+ return ov.Counts.MergeRequests + ov.Counts.Issues, true
156+}
157+
158+// inbox renders the fragments an event of a merge request or an issue changes
159+// on the inbox page: the counters, the filter and the two lists.
160+func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
161+ switch we.Event.(type) {
162+ case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded,
163+ domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented:
164+ default:
165+ return nil, nil
166+ }
167+ if l.s.cfg.Inbox == nil {
168+ return nil, nil
169+ }
170+ var filter domain.RepoName
171+ if l.filter != "" {
172+ filter, _ = domain.ParseRepoName(l.filter)
173+ }
174+ ov, err := l.s.cfg.Inbox.Overview(ctx, principal(ctx), filter)
175+ if err != nil {
176+ return nil, err
177+ }
178+ p := l.s.inboxPage(ov, filter)
179+ var out []fragment
180+ for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxMRs(p), InboxIssues(p)} {
181+ f, err := renderFragment(ctx, inboxEvent, c)
182+ if err != nil {
183+ return nil, err
184+ }
185+ out = append(out, f)
186+ }
187+ return out, nil
188+}
A internal/adapters/web/inbox.templ +77 -0
@@ -0,0 +1,77 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+// Inbox is the owner's inbox: what is open across every repository. The
6+// counters, the filter and the lists are fragments with an id that the live
7+// stream replaces as a whole (ermiteLive.swap).
8+templ Inbox(l Layout, p InboxPage) {
9+ @Shell(l, nil) {
10+ <h1 class="sr">Inbox</h1>
11+ <div class="inbox">
12+ @InboxCounters(p)
13+ @InboxRepos(p)
14+ @InboxMRs(p)
15+ @InboxIssues(p)
16+ </div>
17+ }
18+}
19+
20+templ InboxCounters(p InboxPage) {
21+ <p id="inbox-counts" class="inbox-counts">
22+ <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues
23+ </p>
24+}
25+
26+templ InboxRepos(p InboxPage) {
27+ <nav id="inbox-repos" class="since" aria-label="Repository">
28+ for _, r := range p.Repos {
29+ <a
30+ href={ templ.SafeURL(r.Href) }
31+ if r.Current {
32+ aria-current="page"
33+ }
34+ >{ r.Label }</a>
35+ }
36+ </nav>
37+}
38+
39+templ InboxMRs(p InboxPage) {
40+ <section id="inbox-mrs" aria-labelledby="inbox-mrs-h">
41+ <h2 id="inbox-mrs-h" class="rule-h"><span>Open merge requests</span><span class="count">{ itoa(len(p.MRs)) }</span></h2>
42+ @inboxRows(p.MRs, "Open merge requests")
43+ if len(p.MRs) == 0 {
44+ <p class="lede dim">no open merge requests.</p>
45+ }
46+ </section>
47+}
48+
49+templ InboxIssues(p InboxPage) {
50+ <section id="inbox-issues" aria-labelledby="inbox-issues-h">
51+ <h2 id="inbox-issues-h" class="rule-h"><span>Open issues</span><span class="count">{ itoa(len(p.IssueRows)) }</span></h2>
52+ @inboxRows(p.IssueRows, "Open issues")
53+ if len(p.IssueRows) == 0 {
54+ <p class="lede dim">no open issues.</p>
55+ }
56+ </section>
57+}
58+
59+templ inboxRows(rows []InboxRow, label string) {
60+ if len(rows) > 0 {
61+ <table class="list mrs" aria-label={ label }>
62+ <tbody data-rows>
63+ for _, m := range rows {
64+ <tr data-cursor?={ m.Cursor } data-state="open">
65+ <td class="c-n mute">{ m.Number }</td>
66+ <td class="c-subject"><a href={ templ.SafeURL(m.Href) }>{ m.Title }</a> <a class="mute" href={ templ.SafeURL(m.RepoHref) }>{ m.Repo }</a></td>
67+ <td class="c-when nowrap dim">
68+ if m.WhenAt != "" {
69+ <time datetime={ m.WhenAt } title={ m.WhenAt }>{ m.When }</time>
70+ }
71+ </td>
72+ </tr>
73+ }
74+ </tbody>
75+ </table>
76+ }
77+}
@@ -0,0 +1,77 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+// Inbox is the owner's inbox: what is open across every repository. The
6+// counters, the filter and the lists are fragments with an id that the live
7+// stream replaces as a whole (ermiteLive.swap).
8+templ Inbox(l Layout, p InboxPage) {
9+ @Shell(l, nil) {
10+ <h1 class="sr">Inbox</h1>
11+ <div class="inbox">
12+ @InboxCounters(p)
13+ @InboxRepos(p)
14+ @InboxMRs(p)
15+ @InboxIssues(p)
16+ </div>
17+ }
18+}
19+
20+templ InboxCounters(p InboxPage) {
21+ <p id="inbox-counts" class="inbox-counts">
22+ <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues
23+ </p>
24+}
25+
26+templ InboxRepos(p InboxPage) {
27+ <nav id="inbox-repos" class="since" aria-label="Repository">
28+ for _, r := range p.Repos {
29+ <a
30+ href={ templ.SafeURL(r.Href) }
31+ if r.Current {
32+ aria-current="page"
33+ }
34+ >{ r.Label }</a>
35+ }
36+ </nav>
37+}
38+
39+templ InboxMRs(p InboxPage) {
40+ <section id="inbox-mrs" aria-labelledby="inbox-mrs-h">
41+ <h2 id="inbox-mrs-h" class="rule-h"><span>Open merge requests</span><span class="count">{ itoa(len(p.MRs)) }</span></h2>
42+ @inboxRows(p.MRs, "Open merge requests")
43+ if len(p.MRs) == 0 {
44+ <p class="lede dim">no open merge requests.</p>
45+ }
46+ </section>
47+}
48+
49+templ InboxIssues(p InboxPage) {
50+ <section id="inbox-issues" aria-labelledby="inbox-issues-h">
51+ <h2 id="inbox-issues-h" class="rule-h"><span>Open issues</span><span class="count">{ itoa(len(p.IssueRows)) }</span></h2>
52+ @inboxRows(p.IssueRows, "Open issues")
53+ if len(p.IssueRows) == 0 {
54+ <p class="lede dim">no open issues.</p>
55+ }
56+ </section>
57+}
58+
59+templ inboxRows(rows []InboxRow, label string) {
60+ if len(rows) > 0 {
61+ <table class="list mrs" aria-label={ label }>
62+ <tbody data-rows>
63+ for _, m := range rows {
64+ <tr data-cursor?={ m.Cursor } data-state="open">
65+ <td class="c-n mute">{ m.Number }</td>
66+ <td class="c-subject"><a href={ templ.SafeURL(m.Href) }>{ m.Title }</a> <a class="mute" href={ templ.SafeURL(m.RepoHref) }>{ m.Repo }</a></td>
67+ <td class="c-when nowrap dim">
68+ if m.WhenAt != "" {
69+ <time datetime={ m.WhenAt } title={ m.WhenAt }>{ m.When }</time>
70+ }
71+ </td>
72+ </tr>
73+ }
74+ </tbody>
75+ </table>
76+ }
77+}
A internal/adapters/web/inbox_templ.go +503 -0 generated file: collapsed
A internal/adapters/web/inbox_test.go +195 -0
@@ -0,0 +1,195 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "io"
8+ "net/http"
9+ "net/http/httptest"
10+ "testing"
11+ "time"
12+
13+ "github.com/stretchr/testify/assert"
14+ "github.com/stretchr/testify/mock"
15+ "github.com/stretchr/testify/require"
16+
17+ "bnema.dev/ermite/internal/domain"
18+ "bnema.dev/ermite/internal/ports"
19+ "bnema.dev/ermite/internal/ports/mocks"
20+)
21+
22+func inboxOverview(t testing.TB) ports.InboxOverview {
23+ t.Helper()
24+ mrTitle, err := domain.ParseMRTitle("fix <b>search</b>")
25+ require.NoError(t, err)
26+ isTitle, err := domain.ParseIssueTitle("crash on start")
27+ require.NoError(t, err)
28+ at := time.Now().Add(-2 * time.Hour)
29+ ov := ports.InboxOverview{
30+ MergeRequests: []ports.InboxMergeRequest{{MergeRequest: domain.MergeRequest{
31+ Repo: repoName(t, "alpha"), Number: 4, Title: mrTitle, State: domain.MRStateOpen, UpdatedAt: at,
32+ }}},
33+ Issues: []domain.Issue{{Repo: repoName(t, "beta"), Number: 7, Title: isTitle, State: domain.IssueStateOpen, UpdatedAt: at}},
34+ Repos: []domain.RepoName{repoName(t, "alpha"), repoName(t, "beta")},
35+ }
36+ ov.Counts = ports.InboxCounts{MergeRequests: 1, Issues: 1}
37+ return ov
38+}
39+
40+func inboxServer(t *testing.T, inbox ports.InboxService) *Server {
41+ t.Helper()
42+ return newTestServer(t, Config{
43+ Repos: reposServing(t, publicRepo(t, "alpha")), Browse: homeBrowse(t), Inbox: inbox,
44+ Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{},
45+ })
46+}
47+
48+func inboxGet(srv *Server, path string, owner bool) *httptest.ResponseRecorder {
49+ req := httptest.NewRequest(http.MethodGet, path, nil)
50+ if owner {
51+ req.AddCookie(&http.Cookie{Name: CookieName, Value: "tok"})
52+ }
53+ rec := httptest.NewRecorder()
54+ srv.ServeHTTP(rec, req)
55+ return rec
56+}
57+
58+func TestInboxPageForTheOwner(t *testing.T) {
59+ t.Parallel()
60+ svc := mocks.NewInboxService(t)
61+ svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(inboxOverview(t), nil)
62+ srv := inboxServer(t, svc)
63+
64+ rec := inboxGet(srv, "/-/inbox", true)
65+ require.Equal(t, 200, rec.Code, rec.Body.String())
66+ body := rec.Body.String()
67+ assert.Contains(t, body, "<strong>1</strong> merge requests · <strong>1</strong> issues")
68+ assert.Contains(t, body, "Open merge requests")
69+ assert.Contains(t, body, "Open issues")
70+ assert.Contains(t, body, `href="/alpha/merge-requests/4"`)
71+ assert.Contains(t, body, "fix &lt;b&gt;search&lt;/b&gt;")
72+ assert.Contains(t, body, `href="/beta/issues/7"`)
73+ assert.Contains(t, body, "#7")
74+ assert.Contains(t, body, "2h ago")
75+ assert.Contains(t, body, `href="/-/inbox?repo=beta"`, "the filter")
76+ assert.Equal(t, "private, no-store", rec.Header().Get("Cache-Control"))
77+}
78+
79+func TestInboxPageIsMissingForEveryoneElse(t *testing.T) {
80+ t.Parallel()
81+ svc := mocks.NewInboxService(t) // never asked
82+ srv := inboxServer(t, svc)
83+ missing := inboxGet(srv, "/-/nothing-here", false)
84+ require.Equal(t, 404, missing.Code)
85+
86+ anon := inboxGet(srv, "/-/inbox", false)
87+ assert.Equal(t, 404, anon.Code)
88+ assert.Equal(t, missing.Body.String(), anon.Body.String(), "the same page as an unknown one")
89+ assert.NotContains(t, anon.Body.String(), "Open merge requests")
90+
91+ // A bad cookie is anonymous as well.
92+ req := httptest.NewRequest(http.MethodGet, "/-/inbox", nil)
93+ req.AddCookie(&http.Cookie{Name: CookieName, Value: "other"})
94+ rec := httptest.NewRecorder()
95+ srv.ServeHTTP(rec, req)
96+ assert.Equal(t, 404, rec.Code)
97+}
98+
99+func TestInboxNotMountedWithoutTheService(t *testing.T) {
100+ t.Parallel()
101+ srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "alpha")), Browse: homeBrowse(t), Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{}})
102+ assert.Equal(t, 404, inboxGet(srv, "/-/inbox", true).Code)
103+}
104+
105+func TestInboxFilterAndEmptyStates(t *testing.T) {
106+ t.Parallel()
107+ svc := mocks.NewInboxService(t)
108+ ov := inboxOverview(t)
109+ filtered := ov
110+ filtered.MergeRequests, filtered.Issues = nil, nil
111+ svc.EXPECT().Overview(mock.Anything, mock.Anything, repoName(t, "gamma")).Return(filtered, nil)
112+ srv := inboxServer(t, svc)
113+
114+ rec := inboxGet(srv, "/-/inbox?repo=gamma", true)
115+ require.Equal(t, 200, rec.Code)
116+ body := rec.Body.String()
117+ assert.Contains(t, body, "no open merge requests.")
118+ assert.Contains(t, body, "no open issues.")
119+ assert.Contains(t, body, `href="/-/inbox"`, "the link back to all")
120+
121+ assert.Equal(t, 404, inboxGet(srv, "/-/inbox?repo=Bad%20Name", true).Code, "an invalid name is a missing page")
122+}
123+
124+func TestInboxButtonOnHomeIsForTheOwnerOnly(t *testing.T) {
125+ t.Parallel()
126+ svc := mocks.NewInboxService(t)
127+ ov := inboxOverview(t)
128+ ov.Counts = ports.InboxCounts{MergeRequests: 3, Issues: 2}
129+ svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(ov, nil).Once() // the owner's home only
130+ srv := inboxServer(t, svc)
131+
132+ anon := inboxGet(srv, "/", false)
133+ require.Equal(t, 200, anon.Code)
134+ assert.NotContains(t, anon.Body.String(), "/-/inbox")
135+ assert.NotContains(t, anon.Body.String(), "Inbox")
136+
137+ owner := inboxGet(srv, "/", true)
138+ require.Equal(t, 200, owner.Code)
139+ assert.Contains(t, owner.Body.String(), `href="/-/inbox"`)
140+ assert.Contains(t, owner.Body.String(), `<span class="count">5</span>`)
141+}
142+
143+func TestInboxStreamIsTheOwnersAlone(t *testing.T) {
144+ t.Parallel()
145+ f := &watchProbe{release: make(chan struct{})}
146+ svc := mocks.NewInboxService(t)
147+ srv := newTestServer(t, Config{
148+ Repos: reposServing(t, publicRepo(t, "x")), Browse: mocks.NewBrowseService(t), Events: f.service(t), Inbox: svc,
149+ Heartbeat: time.Hour, Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{},
150+ })
151+ ts := httptest.NewServer(srv)
152+ t.Cleanup(ts.Close)
153+
154+ unknown, _ := open(t, ts, "topic=nope&view=inbox", nil)
155+ want, _ := io.ReadAll(unknown.Body)
156+ for name, hdr := range map[string]map[string]string{
157+ "anonymous": nil,
158+ "another token": {"Cookie": CookieName + "=other"},
159+ } {
160+ resp, _ := open(t, ts, "topic=owner&view=inbox", hdr)
161+ body, _ := io.ReadAll(resp.Body)
162+ assert.Equal(t, unknown.StatusCode, resp.StatusCode, name)
163+ assert.Equal(t, string(want), string(body), name)
164+ assert.Equal(t, unknown.Header.Get("Cache-Control"), resp.Header.Get("Cache-Control"), name)
165+ }
166+ assert.Zero(t, f.opened.Load(), "no watch is opened for them")
167+
168+ resp, _ := open(t, ts, "topic=owner&view=inbox", map[string]string{"Cookie": CookieName + "=tok"})
169+ assert.Equal(t, http.StatusOK, resp.StatusCode)
170+ assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type"))
171+}
172+
173+func TestInboxLiveFragments(t *testing.T) {
174+ t.Parallel()
175+ svc := mocks.NewInboxService(t)
176+ svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(inboxOverview(t), nil)
177+ srv := inboxServer(t, svc)
178+ ls := &liveStream{s: srv, view: "inbox"}
179+ frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
180+ require.NoError(t, err)
181+ require.Len(t, frags, 4)
182+ assert.Contains(t, frags[0].html, `id="inbox-counts"`)
183+ assert.Contains(t, frags[1].html, `id="inbox-repos"`)
184+ assert.Contains(t, frags[2].html, `id="inbox-mrs"`)
185+ assert.Contains(t, frags[3].html, `id="inbox-issues"`)
186+
187+ none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
188+ require.NoError(t, err)
189+ assert.Empty(t, none, "a push is not inbox news")
190+}
191+
192+// withOwner is a context as the session middleware leaves it for the owner.
193+func withOwner(ctx context.Context) context.Context {
194+ return domain.ContextWithPrincipal(context.WithValue(ctx, signedInKey{}, struct{}{}), domain.Owner())
195+}
@@ -0,0 +1,195 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "io"
8+ "net/http"
9+ "net/http/httptest"
10+ "testing"
11+ "time"
12+
13+ "github.com/stretchr/testify/assert"
14+ "github.com/stretchr/testify/mock"
15+ "github.com/stretchr/testify/require"
16+
17+ "bnema.dev/ermite/internal/domain"
18+ "bnema.dev/ermite/internal/ports"
19+ "bnema.dev/ermite/internal/ports/mocks"
20+)
21+
22+func inboxOverview(t testing.TB) ports.InboxOverview {
23+ t.Helper()
24+ mrTitle, err := domain.ParseMRTitle("fix <b>search</b>")
25+ require.NoError(t, err)
26+ isTitle, err := domain.ParseIssueTitle("crash on start")
27+ require.NoError(t, err)
28+ at := time.Now().Add(-2 * time.Hour)
29+ ov := ports.InboxOverview{
30+ MergeRequests: []ports.InboxMergeRequest{{MergeRequest: domain.MergeRequest{
31+ Repo: repoName(t, "alpha"), Number: 4, Title: mrTitle, State: domain.MRStateOpen, UpdatedAt: at,
32+ }}},
33+ Issues: []domain.Issue{{Repo: repoName(t, "beta"), Number: 7, Title: isTitle, State: domain.IssueStateOpen, UpdatedAt: at}},
34+ Repos: []domain.RepoName{repoName(t, "alpha"), repoName(t, "beta")},
35+ }
36+ ov.Counts = ports.InboxCounts{MergeRequests: 1, Issues: 1}
37+ return ov
38+}
39+
40+func inboxServer(t *testing.T, inbox ports.InboxService) *Server {
41+ t.Helper()
42+ return newTestServer(t, Config{
43+ Repos: reposServing(t, publicRepo(t, "alpha")), Browse: homeBrowse(t), Inbox: inbox,
44+ Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{},
45+ })
46+}
47+
48+func inboxGet(srv *Server, path string, owner bool) *httptest.ResponseRecorder {
49+ req := httptest.NewRequest(http.MethodGet, path, nil)
50+ if owner {
51+ req.AddCookie(&http.Cookie{Name: CookieName, Value: "tok"})
52+ }
53+ rec := httptest.NewRecorder()
54+ srv.ServeHTTP(rec, req)
55+ return rec
56+}
57+
58+func TestInboxPageForTheOwner(t *testing.T) {
59+ t.Parallel()
60+ svc := mocks.NewInboxService(t)
61+ svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(inboxOverview(t), nil)
62+ srv := inboxServer(t, svc)
63+
64+ rec := inboxGet(srv, "/-/inbox", true)
65+ require.Equal(t, 200, rec.Code, rec.Body.String())
66+ body := rec.Body.String()
67+ assert.Contains(t, body, "<strong>1</strong> merge requests · <strong>1</strong> issues")
68+ assert.Contains(t, body, "Open merge requests")
69+ assert.Contains(t, body, "Open issues")
70+ assert.Contains(t, body, `href="/alpha/merge-requests/4"`)
71+ assert.Contains(t, body, "fix &lt;b&gt;search&lt;/b&gt;")
72+ assert.Contains(t, body, `href="/beta/issues/7"`)
73+ assert.Contains(t, body, "#7")
74+ assert.Contains(t, body, "2h ago")
75+ assert.Contains(t, body, `href="/-/inbox?repo=beta"`, "the filter")
76+ assert.Equal(t, "private, no-store", rec.Header().Get("Cache-Control"))
77+}
78+
79+func TestInboxPageIsMissingForEveryoneElse(t *testing.T) {
80+ t.Parallel()
81+ svc := mocks.NewInboxService(t) // never asked
82+ srv := inboxServer(t, svc)
83+ missing := inboxGet(srv, "/-/nothing-here", false)
84+ require.Equal(t, 404, missing.Code)
85+
86+ anon := inboxGet(srv, "/-/inbox", false)
87+ assert.Equal(t, 404, anon.Code)
88+ assert.Equal(t, missing.Body.String(), anon.Body.String(), "the same page as an unknown one")
89+ assert.NotContains(t, anon.Body.String(), "Open merge requests")
90+
91+ // A bad cookie is anonymous as well.
92+ req := httptest.NewRequest(http.MethodGet, "/-/inbox", nil)
93+ req.AddCookie(&http.Cookie{Name: CookieName, Value: "other"})
94+ rec := httptest.NewRecorder()
95+ srv.ServeHTTP(rec, req)
96+ assert.Equal(t, 404, rec.Code)
97+}
98+
99+func TestInboxNotMountedWithoutTheService(t *testing.T) {
100+ t.Parallel()
101+ srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "alpha")), Browse: homeBrowse(t), Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{}})
102+ assert.Equal(t, 404, inboxGet(srv, "/-/inbox", true).Code)
103+}
104+
105+func TestInboxFilterAndEmptyStates(t *testing.T) {
106+ t.Parallel()
107+ svc := mocks.NewInboxService(t)
108+ ov := inboxOverview(t)
109+ filtered := ov
110+ filtered.MergeRequests, filtered.Issues = nil, nil
111+ svc.EXPECT().Overview(mock.Anything, mock.Anything, repoName(t, "gamma")).Return(filtered, nil)
112+ srv := inboxServer(t, svc)
113+
114+ rec := inboxGet(srv, "/-/inbox?repo=gamma", true)
115+ require.Equal(t, 200, rec.Code)
116+ body := rec.Body.String()
117+ assert.Contains(t, body, "no open merge requests.")
118+ assert.Contains(t, body, "no open issues.")
119+ assert.Contains(t, body, `href="/-/inbox"`, "the link back to all")
120+
121+ assert.Equal(t, 404, inboxGet(srv, "/-/inbox?repo=Bad%20Name", true).Code, "an invalid name is a missing page")
122+}
123+
124+func TestInboxButtonOnHomeIsForTheOwnerOnly(t *testing.T) {
125+ t.Parallel()
126+ svc := mocks.NewInboxService(t)
127+ ov := inboxOverview(t)
128+ ov.Counts = ports.InboxCounts{MergeRequests: 3, Issues: 2}
129+ svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(ov, nil).Once() // the owner's home only
130+ srv := inboxServer(t, svc)
131+
132+ anon := inboxGet(srv, "/", false)
133+ require.Equal(t, 200, anon.Code)
134+ assert.NotContains(t, anon.Body.String(), "/-/inbox")
135+ assert.NotContains(t, anon.Body.String(), "Inbox")
136+
137+ owner := inboxGet(srv, "/", true)
138+ require.Equal(t, 200, owner.Code)
139+ assert.Contains(t, owner.Body.String(), `href="/-/inbox"`)
140+ assert.Contains(t, owner.Body.String(), `<span class="count">5</span>`)
141+}
142+
143+func TestInboxStreamIsTheOwnersAlone(t *testing.T) {
144+ t.Parallel()
145+ f := &watchProbe{release: make(chan struct{})}
146+ svc := mocks.NewInboxService(t)
147+ srv := newTestServer(t, Config{
148+ Repos: reposServing(t, publicRepo(t, "x")), Browse: mocks.NewBrowseService(t), Events: f.service(t), Inbox: svc,
149+ Heartbeat: time.Hour, Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{},
150+ })
151+ ts := httptest.NewServer(srv)
152+ t.Cleanup(ts.Close)
153+
154+ unknown, _ := open(t, ts, "topic=nope&view=inbox", nil)
155+ want, _ := io.ReadAll(unknown.Body)
156+ for name, hdr := range map[string]map[string]string{
157+ "anonymous": nil,
158+ "another token": {"Cookie": CookieName + "=other"},
159+ } {
160+ resp, _ := open(t, ts, "topic=owner&view=inbox", hdr)
161+ body, _ := io.ReadAll(resp.Body)
162+ assert.Equal(t, unknown.StatusCode, resp.StatusCode, name)
163+ assert.Equal(t, string(want), string(body), name)
164+ assert.Equal(t, unknown.Header.Get("Cache-Control"), resp.Header.Get("Cache-Control"), name)
165+ }
166+ assert.Zero(t, f.opened.Load(), "no watch is opened for them")
167+
168+ resp, _ := open(t, ts, "topic=owner&view=inbox", map[string]string{"Cookie": CookieName + "=tok"})
169+ assert.Equal(t, http.StatusOK, resp.StatusCode)
170+ assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type"))
171+}
172+
173+func TestInboxLiveFragments(t *testing.T) {
174+ t.Parallel()
175+ svc := mocks.NewInboxService(t)
176+ svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(inboxOverview(t), nil)
177+ srv := inboxServer(t, svc)
178+ ls := &liveStream{s: srv, view: "inbox"}
179+ frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
180+ require.NoError(t, err)
181+ require.Len(t, frags, 4)
182+ assert.Contains(t, frags[0].html, `id="inbox-counts"`)
183+ assert.Contains(t, frags[1].html, `id="inbox-repos"`)
184+ assert.Contains(t, frags[2].html, `id="inbox-mrs"`)
185+ assert.Contains(t, frags[3].html, `id="inbox-issues"`)
186+
187+ none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
188+ require.NoError(t, err)
189+ assert.Empty(t, none, "a push is not inbox news")
190+}
191+
192+// withOwner is a context as the session middleware leaves it for the owner.
193+func withOwner(ctx context.Context) context.Context {
194+ return domain.ContextWithPrincipal(context.WithValue(ctx, signedInKey{}, struct{}{}), domain.Owner())
195+}
M internal/adapters/web/pages.templ +8 -0
@@ -9,6 +9,14 @@ templ Home(l Layout, p HomePage) {
99 <div class="home" data-layout="cards">
1010 <section class="home-repos" aria-label="Repositories">
1111 <div class="home-bar">
12+ if p.Inbox != nil {
13+ <a class="btn inbox-btn" href={ templ.SafeURL(p.Inbox.Href) } title="inbox">
14+ Inbox
15+ if p.Inbox.Count >= 0 {
16+ <span class="count">{ itoa(p.Inbox.Count) }</span>
17+ }
18+ </a>
19+ }
1220 <div class="filter">
1321 <label for="home-filter" class="sr">filter</label>
1422 <input
@@ -9,6 +9,14 @@ templ Home(l Layout, p HomePage) {
9 <div class="home" data-layout="cards">9 <div class="home" data-layout="cards">
10 <section class="home-repos" aria-label="Repositories">10 <section class="home-repos" aria-label="Repositories">
11 <div class="home-bar">11 <div class="home-bar">
12+ if p.Inbox != nil {
13+ <a class="btn inbox-btn" href={ templ.SafeURL(p.Inbox.Href) } title="inbox">
14+ Inbox
15+ if p.Inbox.Count >= 0 {
16+ <span class="count">{ itoa(p.Inbox.Count) }</span>
17+ }
18+ </a>
19+ }
12 <div class="filter">20 <div class="filter">
13 <label for="home-filter" class="sr">filter</label>21 <label for="home-filter" class="sr">filter</label>
14 <input22 <input
M internal/adapters/web/pages_templ.go +409 -363
@@ -44,89 +44,135 @@ func Home(l Layout, p HomePage) templ.Component {
4444 }()
4545 }
4646 ctx = templ.InitializeContext(ctx)
47- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"sr\">Repositories</h1><div class=\"home\" data-layout=\"cards\"><section class=\"home-repos\" aria-label=\"Repositories\"><div class=\"home-bar\"><div class=\"filter\"><label for=\"home-filter\" class=\"sr\">filter</label> <input id=\"home-filter\" type=\"search\" data-filter=\"#repo-rows > li\" placeholder=\"Filter repositories…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><nav class=\"sort\" aria-label=\"Sort repositories\"><span class=\"mute\">sort</span> ")
47+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"sr\">Repositories</h1><div class=\"home\" data-layout=\"cards\"><section class=\"home-repos\" aria-label=\"Repositories\"><div class=\"home-bar\">")
4848 if templ_7745c5c3_Err != nil {
4949 return templ_7745c5c3_Err
5050 }
51- for _, s := range p.Sorts {
52- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"")
51+ if p.Inbox != nil {
52+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a class=\"btn inbox-btn\" href=\"")
5353 if templ_7745c5c3_Err != nil {
5454 return templ_7745c5c3_Err
5555 }
5656 var templ_7745c5c3_Var3 templ.SafeURL
57- templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
57+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Inbox.Href))
5858 if templ_7745c5c3_Err != nil {
59- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 27, Col: 36}
59+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 13, Col: 65}
6060 }
6161 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
6262 if templ_7745c5c3_Err != nil {
6363 return templ_7745c5c3_Err
6464 }
65- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"")
65+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" title=\"inbox\">Inbox ")
66+ if templ_7745c5c3_Err != nil {
67+ return templ_7745c5c3_Err
68+ }
69+ if p.Inbox.Count >= 0 {
70+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"count\">")
71+ if templ_7745c5c3_Err != nil {
72+ return templ_7745c5c3_Err
73+ }
74+ var templ_7745c5c3_Var4 string
75+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Inbox.Count))
76+ if templ_7745c5c3_Err != nil {
77+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 16, Col: 49}
78+ }
79+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
80+ if templ_7745c5c3_Err != nil {
81+ return templ_7745c5c3_Err
82+ }
83+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span>")
84+ if templ_7745c5c3_Err != nil {
85+ return templ_7745c5c3_Err
86+ }
87+ }
88+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a>")
89+ if templ_7745c5c3_Err != nil {
90+ return templ_7745c5c3_Err
91+ }
92+ }
93+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"filter\"><label for=\"home-filter\" class=\"sr\">filter</label> <input id=\"home-filter\" type=\"search\" data-filter=\"#repo-rows > li\" placeholder=\"Filter repositories…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><nav class=\"sort\" aria-label=\"Sort repositories\"><span class=\"mute\">sort</span> ")
94+ if templ_7745c5c3_Err != nil {
95+ return templ_7745c5c3_Err
96+ }
97+ for _, s := range p.Sorts {
98+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
99+ if templ_7745c5c3_Err != nil {
100+ return templ_7745c5c3_Err
101+ }
102+ var templ_7745c5c3_Var5 templ.SafeURL
103+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
104+ if templ_7745c5c3_Err != nil {
105+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 35, Col: 36}
106+ }
107+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
108+ if templ_7745c5c3_Err != nil {
109+ return templ_7745c5c3_Err
110+ }
111+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"")
66112 if templ_7745c5c3_Err != nil {
67113 return templ_7745c5c3_Err
68114 }
69115 if s.Mark != "" {
70- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " aria-current=\"true\"")
116+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " aria-current=\"true\"")
71117 if templ_7745c5c3_Err != nil {
72118 return templ_7745c5c3_Err
73119 }
74120 }
75- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">")
121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">")
76122 if templ_7745c5c3_Err != nil {
77123 return templ_7745c5c3_Err
78124 }
79- var templ_7745c5c3_Var4 string
80- templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
125+ var templ_7745c5c3_Var6 string
126+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
81127 if templ_7745c5c3_Err != nil {
82- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 32, Col: 17}
128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 40, Col: 17}
83129 }
84- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
130+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
85131 if templ_7745c5c3_Err != nil {
86132 return templ_7745c5c3_Err
87133 }
88- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
89135 if templ_7745c5c3_Err != nil {
90136 return templ_7745c5c3_Err
91137 }
92138 if s.Mark != "" {
93- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span aria-hidden=\"true\">")
139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span aria-hidden=\"true\">")
94140 if templ_7745c5c3_Err != nil {
95141 return templ_7745c5c3_Err
96142 }
97- var templ_7745c5c3_Var5 string
98- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(s.Mark)
143+ var templ_7745c5c3_Var7 string
144+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(s.Mark)
99145 if templ_7745c5c3_Err != nil {
100- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 34, Col: 42}
146+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 42, Col: 42}
101147 }
102- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
148+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
103149 if templ_7745c5c3_Err != nil {
104150 return templ_7745c5c3_Err
105151 }
106- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span> <span class=\"sr\">")
152+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span class=\"sr\">")
107153 if templ_7745c5c3_Err != nil {
108154 return templ_7745c5c3_Err
109155 }
110- var templ_7745c5c3_Var6 string
111- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(", " + s.State)
156+ var templ_7745c5c3_Var8 string
157+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(", " + s.State)
112158 if templ_7745c5c3_Err != nil {
113- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 35, Col: 42}
159+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 43, Col: 42}
114160 }
115- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
161+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
116162 if templ_7745c5c3_Err != nil {
117163 return templ_7745c5c3_Err
118164 }
119- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
165+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span>")
120166 if templ_7745c5c3_Err != nil {
121167 return templ_7745c5c3_Err
122168 }
123169 }
124- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</a>")
170+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a>")
125171 if templ_7745c5c3_Err != nil {
126172 return templ_7745c5c3_Err
127173 }
128174 }
129- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</nav><div class=\"layout-switch\" role=\"group\" aria-label=\"Layout\" data-js><button type=\"button\" class=\"btn icon\" data-set-layout=\"cards\" aria-pressed=\"true\" title=\"Cards\" aria-label=\"Cards\"><span class=\"ico i-cards\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"split\" aria-pressed=\"false\" title=\"Two columns\" aria-label=\"Two columns\"><span class=\"ico i-split\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"compact\" aria-pressed=\"false\" title=\"Compact list\" aria-label=\"Compact list\"><span class=\"ico i-compact\" aria-hidden=\"true\"></span></button></div></div><ul class=\"repos\" id=\"repo-rows\" data-rows>")
175+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</nav><div class=\"layout-switch\" role=\"group\" aria-label=\"Layout\" data-js><button type=\"button\" class=\"btn icon\" data-set-layout=\"cards\" aria-pressed=\"true\" title=\"Cards\" aria-label=\"Cards\"><span class=\"ico i-cards\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"split\" aria-pressed=\"false\" title=\"Two columns\" aria-label=\"Two columns\"><span class=\"ico i-split\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"compact\" aria-pressed=\"false\" title=\"Compact list\" aria-label=\"Compact list\"><span class=\"ico i-compact\" aria-hidden=\"true\"></span></button></div></div><ul class=\"repos\" id=\"repo-rows\" data-rows>")
130176 if templ_7745c5c3_Err != nil {
131177 return templ_7745c5c3_Err
132178 }
@@ -136,17 +182,17 @@ func Home(l Layout, p HomePage) templ.Component {
136182 return templ_7745c5c3_Err
137183 }
138184 }
139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</ul>")
185+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</ul>")
140186 if templ_7745c5c3_Err != nil {
141187 return templ_7745c5c3_Err
142188 }
143189 if len(p.Rows) == 0 {
144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">no repositories yet.</p>")
190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"lede dim\">no repositories yet.</p>")
145191 if templ_7745c5c3_Err != nil {
146192 return templ_7745c5c3_Err
147193 }
148194 }
149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</section></div>")
195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</section></div>")
150196 if templ_7745c5c3_Err != nil {
151197 return templ_7745c5c3_Err
152198 }
@@ -178,12 +224,12 @@ func Summary(l Layout, p SummaryPage) templ.Component {
178224 }()
179225 }
180226 ctx = templ.InitializeContext(ctx)
181- templ_7745c5c3_Var7 := templ.GetChildren(ctx)
182- if templ_7745c5c3_Var7 == nil {
183- templ_7745c5c3_Var7 = templ.NopComponent
227+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
228+ if templ_7745c5c3_Var9 == nil {
229+ templ_7745c5c3_Var9 = templ.NopComponent
184230 }
185231 ctx = templ.ClearChildren(ctx)
186- templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
232+ templ_7745c5c3_Var10 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
187233 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
188234 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
189235 if !templ_7745c5c3_IsBuffer {
@@ -203,7 +249,7 @@ func Summary(l Layout, p SummaryPage) templ.Component {
203249 }
204250 return nil
205251 })
206- templ_7745c5c3_Err = Shell(l, summaryRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
252+ templ_7745c5c3_Err = Shell(l, summaryRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
207253 if templ_7745c5c3_Err != nil {
208254 return templ_7745c5c3_Err
209255 }
@@ -230,158 +276,158 @@ func latestTagFact(t LatestTag) templ.Component {
230276 }()
231277 }
232278 ctx = templ.InitializeContext(ctx)
233- templ_7745c5c3_Var9 := templ.GetChildren(ctx)
234- if templ_7745c5c3_Var9 == nil {
235- templ_7745c5c3_Var9 = templ.NopComponent
279+ templ_7745c5c3_Var11 := templ.GetChildren(ctx)
280+ if templ_7745c5c3_Var11 == nil {
281+ templ_7745c5c3_Var11 = templ.NopComponent
236282 }
237283 ctx = templ.ClearChildren(ctx)
238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<dt>latest tag</dt><dd class=\"latest-tag\">")
284+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<dt>latest tag</dt><dd class=\"latest-tag\">")
239285 if templ_7745c5c3_Err != nil {
240286 return templ_7745c5c3_Err
241287 }
242288 if t.Name != "" {
243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a class=\"tag-name\" href=\"")
289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a class=\"tag-name\" href=\"")
244290 if templ_7745c5c3_Err != nil {
245291 return templ_7745c5c3_Err
246292 }
247- var templ_7745c5c3_Var10 templ.SafeURL
248- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.Href))
293+ var templ_7745c5c3_Var12 templ.SafeURL
294+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.Href))
249295 if templ_7745c5c3_Err != nil {
250- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 82, Col: 51}
296+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 90, Col: 51}
251297 }
252- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
298+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
253299 if templ_7745c5c3_Err != nil {
254300 return templ_7745c5c3_Err
255301 }
256- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" title=\"")
302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" title=\"")
257303 if templ_7745c5c3_Err != nil {
258304 return templ_7745c5c3_Err
259305 }
260- var templ_7745c5c3_Var11 string
261- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.Subject)
306+ var templ_7745c5c3_Var13 string
307+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.Subject)
262308 if templ_7745c5c3_Err != nil {
263- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 82, Col: 71}
309+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 90, Col: 71}
264310 }
265- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
311+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
266312 if templ_7745c5c3_Err != nil {
267313 return templ_7745c5c3_Err
268314 }
269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
270316 if templ_7745c5c3_Err != nil {
271317 return templ_7745c5c3_Err
272318 }
273- var templ_7745c5c3_Var12 string
274- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)
319+ var templ_7745c5c3_Var14 string
320+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)
275321 if templ_7745c5c3_Err != nil {
276- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 82, Col: 82}
322+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 90, Col: 82}
277323 }
278- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
324+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
279325 if templ_7745c5c3_Err != nil {
280326 return templ_7745c5c3_Err
281327 }
282- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a> ")
328+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</a> ")
283329 if templ_7745c5c3_Err != nil {
284330 return templ_7745c5c3_Err
285331 }
286332 if t.WhenAt != "" {
287- var templ_7745c5c3_Var13 string
288- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
333+ var templ_7745c5c3_Var15 string
334+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
289335 if templ_7745c5c3_Err != nil {
290- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 12}
336+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 12}
291337 }
292- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
338+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
293339 if templ_7745c5c3_Err != nil {
294340 return templ_7745c5c3_Err
295341 }
296- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<time datetime=\"")
342+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<time datetime=\"")
297343 if templ_7745c5c3_Err != nil {
298344 return templ_7745c5c3_Err
299345 }
300- var templ_7745c5c3_Var14 string
301- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)
346+ var templ_7745c5c3_Var16 string
347+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)
302348 if templ_7745c5c3_Err != nil {
303- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 39}
349+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 39}
304350 }
305- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
351+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
306352 if templ_7745c5c3_Err != nil {
307353 return templ_7745c5c3_Err
308354 }
309- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" title=\"")
355+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" title=\"")
310356 if templ_7745c5c3_Err != nil {
311357 return templ_7745c5c3_Err
312358 }
313- var templ_7745c5c3_Var15 string
314- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)
359+ var templ_7745c5c3_Var17 string
360+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)
315361 if templ_7745c5c3_Err != nil {
316- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 58}
362+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 58}
317363 }
318- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
364+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
319365 if templ_7745c5c3_Err != nil {
320366 return templ_7745c5c3_Err
321367 }
322- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
368+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
323369 if templ_7745c5c3_Err != nil {
324370 return templ_7745c5c3_Err
325371 }
326- var templ_7745c5c3_Var16 string
327- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(t.When)
372+ var templ_7745c5c3_Var18 string
373+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(t.When)
328374 if templ_7745c5c3_Err != nil {
329- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 69}
375+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 69}
330376 }
331- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
377+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
332378 if templ_7745c5c3_Err != nil {
333379 return templ_7745c5c3_Err
334380 }
335- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</time>")
381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</time>")
336382 if templ_7745c5c3_Err != nil {
337383 return templ_7745c5c3_Err
338384 }
339385 }
340- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ")
341387 if templ_7745c5c3_Err != nil {
342388 return templ_7745c5c3_Err
343389 }
344- var templ_7745c5c3_Var17 string
345- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
390+ var templ_7745c5c3_Var19 string
391+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
346392 if templ_7745c5c3_Err != nil {
347- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 86, Col: 11}
393+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 94, Col: 11}
348394 }
349- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
395+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
350396 if templ_7745c5c3_Err != nil {
351397 return templ_7745c5c3_Err
352398 }
353- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ")
399+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
354400 if templ_7745c5c3_Err != nil {
355401 return templ_7745c5c3_Err
356402 }
357403 }
358- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a class=\"all-tags\" href=\"")
404+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a class=\"all-tags\" href=\"")
359405 if templ_7745c5c3_Err != nil {
360406 return templ_7745c5c3_Err
361407 }
362- var templ_7745c5c3_Var18 templ.SafeURL
363- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.RefsHref))
408+ var templ_7745c5c3_Var20 templ.SafeURL
409+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.RefsHref))
364410 if templ_7745c5c3_Err != nil {
365- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 88, Col: 54}
411+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 96, Col: 54}
366412 }
367- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
413+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
368414 if templ_7745c5c3_Err != nil {
369415 return templ_7745c5c3_Err
370416 }
371- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")
417+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
372418 if templ_7745c5c3_Err != nil {
373419 return templ_7745c5c3_Err
374420 }
375- var templ_7745c5c3_Var19 string
376- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(plural(t.Count, "tag", "tags"))
421+ var templ_7745c5c3_Var21 string
422+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(plural(t.Count, "tag", "tags"))
377423 if templ_7745c5c3_Err != nil {
378- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 88, Col: 89}
424+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 96, Col: 89}
379425 }
380- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
426+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
381427 if templ_7745c5c3_Err != nil {
382428 return templ_7745c5c3_Err
383429 }
384- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a></dd>")
430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a></dd>")
385431 if templ_7745c5c3_Err != nil {
386432 return templ_7745c5c3_Err
387433 }
@@ -405,12 +451,12 @@ func summaryRail(p SummaryPage) templ.Component {
405451 }()
406452 }
407453 ctx = templ.InitializeContext(ctx)
408- templ_7745c5c3_Var20 := templ.GetChildren(ctx)
409- if templ_7745c5c3_Var20 == nil {
410- templ_7745c5c3_Var20 = templ.NopComponent
454+ templ_7745c5c3_Var22 := templ.GetChildren(ctx)
455+ if templ_7745c5c3_Var22 == nil {
456+ templ_7745c5c3_Var22 = templ.NopComponent
411457 }
412458 ctx = templ.ClearChildren(ctx)
413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section aria-labelledby=\"r-clone\"><h2 id=\"r-clone\">Clone</h2>")
459+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<section aria-labelledby=\"r-clone\"><h2 id=\"r-clone\">Clone</h2>")
414460 if templ_7745c5c3_Err != nil {
415461 return templ_7745c5c3_Err
416462 }
@@ -418,25 +464,25 @@ func summaryRail(p SummaryPage) templ.Component {
418464 if templ_7745c5c3_Err != nil {
419465 return templ_7745c5c3_Err
420466 }
421- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</section><section aria-labelledby=\"r-facts\"><h2 id=\"r-facts\">About</h2><dl class=\"kv\">")
467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</section><section aria-labelledby=\"r-facts\"><h2 id=\"r-facts\">About</h2><dl class=\"kv\">")
422468 if templ_7745c5c3_Err != nil {
423469 return templ_7745c5c3_Err
424470 }
425471 for _, f := range p.Facts {
426- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<dt>")
472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<dt>")
427473 if templ_7745c5c3_Err != nil {
428474 return templ_7745c5c3_Err
429475 }
430- var templ_7745c5c3_Var21 string
431- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(f.Key)
476+ var templ_7745c5c3_Var23 string
477+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(f.Key)
432478 if templ_7745c5c3_Err != nil {
433- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 99, Col: 15}
479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 107, Col: 15}
434480 }
435- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
481+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
436482 if templ_7745c5c3_Err != nil {
437483 return templ_7745c5c3_Err
438484 }
439- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</dt>")
485+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</dt>")
440486 if templ_7745c5c3_Err != nil {
441487 return templ_7745c5c3_Err
442488 }
@@ -451,25 +497,25 @@ func summaryRail(p SummaryPage) templ.Component {
451497 return templ_7745c5c3_Err
452498 }
453499 }
454- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</dl></section>")
500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</dl></section>")
455501 if templ_7745c5c3_Err != nil {
456502 return templ_7745c5c3_Err
457503 }
458504 if len(p.Recent) > 0 {
459- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")
505+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")
460506 if templ_7745c5c3_Err != nil {
461507 return templ_7745c5c3_Err
462508 }
463- var templ_7745c5c3_Var22 templ.SafeURL
464- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))
509+ var templ_7745c5c3_Var24 templ.SafeURL
510+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))
465511 if templ_7745c5c3_Err != nil {
466- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 108, Col: 105}
512+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 116, Col: 105}
467513 }
468- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
514+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
469515 if templ_7745c5c3_Err != nil {
470516 return templ_7745c5c3_Err
471517 }
472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">View all</a></h2>")
518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">View all</a></h2>")
473519 if templ_7745c5c3_Err != nil {
474520 return templ_7745c5c3_Err
475521 }
@@ -477,7 +523,7 @@ func summaryRail(p SummaryPage) templ.Component {
477523 if templ_7745c5c3_Err != nil {
478524 return templ_7745c5c3_Err
479525 }
480- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</section>")
526+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</section>")
481527 if templ_7745c5c3_Err != nil {
482528 return templ_7745c5c3_Err
483529 }
@@ -489,7 +535,7 @@ func summaryRail(p SummaryPage) templ.Component {
489535 }
490536 }
491537 if len(p.Outline) > 0 && p.Readme != nil {
492- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")
538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")
493539 if templ_7745c5c3_Err != nil {
494540 return templ_7745c5c3_Err
495541 }
@@ -497,7 +543,7 @@ func summaryRail(p SummaryPage) templ.Component {
497543 if templ_7745c5c3_Err != nil {
498544 return templ_7745c5c3_Err
499545 }
500- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</section>")
546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</section>")
501547 if templ_7745c5c3_Err != nil {
502548 return templ_7745c5c3_Err
503549 }
@@ -523,12 +569,12 @@ func Refs(l Layout, p RefsPage) templ.Component {
523569 }()
524570 }
525571 ctx = templ.InitializeContext(ctx)
526- templ_7745c5c3_Var23 := templ.GetChildren(ctx)
527- if templ_7745c5c3_Var23 == nil {
528- templ_7745c5c3_Var23 = templ.NopComponent
572+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
573+ if templ_7745c5c3_Var25 == nil {
574+ templ_7745c5c3_Var25 = templ.NopComponent
529575 }
530576 ctx = templ.ClearChildren(ctx)
531- templ_7745c5c3_Var24 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
577+ templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
532578 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
533579 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
534580 if !templ_7745c5c3_IsBuffer {
@@ -540,7 +586,7 @@ func Refs(l Layout, p RefsPage) templ.Component {
540586 }()
541587 }
542588 ctx = templ.InitializeContext(ctx)
543- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")
589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")
544590 if templ_7745c5c3_Err != nil {
545591 return templ_7745c5c3_Err
546592 }
@@ -548,32 +594,32 @@ func Refs(l Layout, p RefsPage) templ.Component {
548594 if templ_7745c5c3_Err != nil {
549595 return templ_7745c5c3_Err
550596 }
551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</table>")
597+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</table>")
552598 if templ_7745c5c3_Err != nil {
553599 return templ_7745c5c3_Err
554600 }
555601 if p.Note != "" {
556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p class=\"note\">")
602+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<p class=\"note\">")
557603 if templ_7745c5c3_Err != nil {
558604 return templ_7745c5c3_Err
559605 }
560- var templ_7745c5c3_Var25 string
561- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
606+ var templ_7745c5c3_Var27 string
607+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
562608 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 141, Col: 27}
609+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 149, Col: 27}
564610 }
565- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
611+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
566612 if templ_7745c5c3_Err != nil {
567613 return templ_7745c5c3_Err
568614 }
569- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</p>")
615+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</p>")
570616 if templ_7745c5c3_Err != nil {
571617 return templ_7745c5c3_Err
572618 }
573619 }
574620 return nil
575621 })
576- templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var24), templ_7745c5c3_Buffer)
622+ templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer)
577623 if templ_7745c5c3_Err != nil {
578624 return templ_7745c5c3_Err
579625 }
@@ -597,112 +643,112 @@ func refRow(kind string, r RefRow) templ.Component {
597643 }()
598644 }
599645 ctx = templ.InitializeContext(ctx)
600- templ_7745c5c3_Var26 := templ.GetChildren(ctx)
601- if templ_7745c5c3_Var26 == nil {
602- templ_7745c5c3_Var26 = templ.NopComponent
646+ templ_7745c5c3_Var28 := templ.GetChildren(ctx)
647+ if templ_7745c5c3_Var28 == nil {
648+ templ_7745c5c3_Var28 = templ.NopComponent
603649 }
604650 ctx = templ.ClearChildren(ctx)
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<tr")
651+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<tr")
606652 if templ_7745c5c3_Err != nil {
607653 return templ_7745c5c3_Err
608654 }
609655 if r.Cursor {
610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " data-cursor")
656+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " data-cursor")
611657 if templ_7745c5c3_Err != nil {
612658 return templ_7745c5c3_Err
613659 }
614660 }
615- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "><td class=\"c-kind mute\">")
661+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "><td class=\"c-kind mute\">")
616662 if templ_7745c5c3_Err != nil {
617663 return templ_7745c5c3_Err
618664 }
619- var templ_7745c5c3_Var27 string
620- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
665+ var templ_7745c5c3_Var29 string
666+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
621667 if templ_7745c5c3_Err != nil {
622- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 148, Col: 32}
668+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 156, Col: 32}
623669 }
624- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
670+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
625671 if templ_7745c5c3_Err != nil {
626672 return templ_7745c5c3_Err
627673 }
628- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</td><td class=\"c-name\"><a href=\"")
674+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</td><td class=\"c-name\"><a href=\"")
629675 if templ_7745c5c3_Err != nil {
630676 return templ_7745c5c3_Err
631677 }
632- var templ_7745c5c3_Var28 templ.SafeURL
633- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
678+ var templ_7745c5c3_Var30 templ.SafeURL
679+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
634680 if templ_7745c5c3_Err != nil {
635- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 150, Col: 34}
681+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 158, Col: 34}
636682 }
637- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
683+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
638684 if templ_7745c5c3_Err != nil {
639685 return templ_7745c5c3_Err
640686 }
641- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">")
687+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
642688 if templ_7745c5c3_Err != nil {
643689 return templ_7745c5c3_Err
644690 }
645- var templ_7745c5c3_Var29 string
646- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
691+ var templ_7745c5c3_Var31 string
692+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
647693 if templ_7745c5c3_Err != nil {
648- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 150, Col: 45}
694+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 158, Col: 45}
649695 }
650- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
696+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
651697 if templ_7745c5c3_Err != nil {
652698 return templ_7745c5c3_Err
653699 }
654- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</a> ")
700+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</a> ")
655701 if templ_7745c5c3_Err != nil {
656702 return templ_7745c5c3_Err
657703 }
658704 if r.Default {
659- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"pill\">default</span>")
705+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"pill\">default</span>")
660706 if templ_7745c5c3_Err != nil {
661707 return templ_7745c5c3_Err
662708 }
663709 }
664- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td class=\"c-sha\">")
710+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</td><td class=\"c-sha\">")
665711 if templ_7745c5c3_Err != nil {
666712 return templ_7745c5c3_Err
667713 }
668714 if r.CHref != "" {
669- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<a href=\"")
715+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a href=\"")
670716 if templ_7745c5c3_Err != nil {
671717 return templ_7745c5c3_Err
672718 }
673- var templ_7745c5c3_Var30 templ.SafeURL
674- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
719+ var templ_7745c5c3_Var32 templ.SafeURL
720+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
675721 if templ_7745c5c3_Err != nil {
676- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 157, Col: 36}
722+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 165, Col: 36}
677723 }
678- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
724+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
679725 if templ_7745c5c3_Err != nil {
680726 return templ_7745c5c3_Err
681727 }
682- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">")
728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">")
683729 if templ_7745c5c3_Err != nil {
684730 return templ_7745c5c3_Err
685731 }
686- var templ_7745c5c3_Var31 string
687- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
732+ var templ_7745c5c3_Var33 string
733+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
688734 if templ_7745c5c3_Err != nil {
689- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 157, Col: 48}
735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 165, Col: 48}
690736 }
691- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
692738 if templ_7745c5c3_Err != nil {
693739 return templ_7745c5c3_Err
694740 }
695- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</a>")
741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</a>")
696742 if templ_7745c5c3_Err != nil {
697743 return templ_7745c5c3_Err
698744 }
699745 } else {
700- var templ_7745c5c3_Var32 string
701- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
746+ var templ_7745c5c3_Var34 string
747+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
702748 if templ_7745c5c3_Err != nil {
703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 159, Col: 13}
749+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 167, Col: 13}
704750 }
705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
751+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
706752 if templ_7745c5c3_Err != nil {
707753 return templ_7745c5c3_Err
708754 }
@@ -711,69 +757,69 @@ func refRow(kind string, r RefRow) templ.Component {
711757 if templ_7745c5c3_Err != nil {
712758 return templ_7745c5c3_Err
713759 }
714- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</td><td class=\"c-sub\">")
760+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td><td class=\"c-sub\">")
715761 if templ_7745c5c3_Err != nil {
716762 return templ_7745c5c3_Err
717763 }
718- var templ_7745c5c3_Var33 string
719- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
764+ var templ_7745c5c3_Var35 string
765+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
720766 if templ_7745c5c3_Err != nil {
721- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 163, Col: 31}
767+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 31}
722768 }
723- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
769+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
724770 if templ_7745c5c3_Err != nil {
725771 return templ_7745c5c3_Err
726772 }
727- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</td><td class=\"c-when dim\">")
773+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</td><td class=\"c-when dim\">")
728774 if templ_7745c5c3_Err != nil {
729775 return templ_7745c5c3_Err
730776 }
731777 if r.WhenAt != "" {
732- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<time datetime=\"")
778+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<time datetime=\"")
733779 if templ_7745c5c3_Err != nil {
734780 return templ_7745c5c3_Err
735781 }
736- var templ_7745c5c3_Var34 string
737- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
782+ var templ_7745c5c3_Var36 string
783+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
738784 if templ_7745c5c3_Err != nil {
739- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 29}
785+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 174, Col: 29}
740786 }
741- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
787+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
742788 if templ_7745c5c3_Err != nil {
743789 return templ_7745c5c3_Err
744790 }
745- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" title=\"")
791+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" title=\"")
746792 if templ_7745c5c3_Err != nil {
747793 return templ_7745c5c3_Err
748794 }
749- var templ_7745c5c3_Var35 string
750- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
795+ var templ_7745c5c3_Var37 string
796+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
751797 if templ_7745c5c3_Err != nil {
752- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 48}
798+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 174, Col: 48}
753799 }
754- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
800+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
755801 if templ_7745c5c3_Err != nil {
756802 return templ_7745c5c3_Err
757803 }
758- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">")
804+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
759805 if templ_7745c5c3_Err != nil {
760806 return templ_7745c5c3_Err
761807 }
762- var templ_7745c5c3_Var36 string
763- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
808+ var templ_7745c5c3_Var38 string
809+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
764810 if templ_7745c5c3_Err != nil {
765- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 59}
811+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 174, Col: 59}
766812 }
767- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
813+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
768814 if templ_7745c5c3_Err != nil {
769815 return templ_7745c5c3_Err
770816 }
771- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</time>")
817+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</time>")
772818 if templ_7745c5c3_Err != nil {
773819 return templ_7745c5c3_Err
774820 }
775821 }
776- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td></tr>")
822+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</td></tr>")
777823 if templ_7745c5c3_Err != nil {
778824 return templ_7745c5c3_Err
779825 }
@@ -797,12 +843,12 @@ func refsRail(p RefsPage) templ.Component {
797843 }()
798844 }
799845 ctx = templ.InitializeContext(ctx)
800- templ_7745c5c3_Var37 := templ.GetChildren(ctx)
801- if templ_7745c5c3_Var37 == nil {
802- templ_7745c5c3_Var37 = templ.NopComponent
846+ templ_7745c5c3_Var39 := templ.GetChildren(ctx)
847+ if templ_7745c5c3_Var39 == nil {
848+ templ_7745c5c3_Var39 = templ.NopComponent
803849 }
804850 ctx = templ.ClearChildren(ctx)
805- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")
851+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")
806852 if templ_7745c5c3_Err != nil {
807853 return templ_7745c5c3_Err
808854 }
@@ -810,7 +856,7 @@ func refsRail(p RefsPage) templ.Component {
810856 if templ_7745c5c3_Err != nil {
811857 return templ_7745c5c3_Err
812858 }
813- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</section>")
859+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</section>")
814860 if templ_7745c5c3_Err != nil {
815861 return templ_7745c5c3_Err
816862 }
@@ -835,12 +881,12 @@ func State(l Layout, p StatePage) templ.Component {
835881 }()
836882 }
837883 ctx = templ.InitializeContext(ctx)
838- templ_7745c5c3_Var38 := templ.GetChildren(ctx)
839- if templ_7745c5c3_Var38 == nil {
840- templ_7745c5c3_Var38 = templ.NopComponent
884+ templ_7745c5c3_Var40 := templ.GetChildren(ctx)
885+ if templ_7745c5c3_Var40 == nil {
886+ templ_7745c5c3_Var40 = templ.NopComponent
841887 }
842888 ctx = templ.ClearChildren(ctx)
843- templ_7745c5c3_Var39 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
889+ templ_7745c5c3_Var41 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
844890 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
845891 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
846892 if !templ_7745c5c3_IsBuffer {
@@ -852,51 +898,51 @@ func State(l Layout, p StatePage) templ.Component {
852898 }()
853899 }
854900 ctx = templ.InitializeContext(ctx)
855- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")
901+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")
856902 if templ_7745c5c3_Err != nil {
857903 return templ_7745c5c3_Err
858904 }
859- var templ_7745c5c3_Var40 string
860- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)
905+ var templ_7745c5c3_Var42 string
906+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)
861907 if templ_7745c5c3_Err != nil {
862- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 183, Col: 30}
908+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 30}
863909 }
864- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
910+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
865911 if templ_7745c5c3_Err != nil {
866912 return templ_7745c5c3_Err
867913 }
868- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</p><h1 id=\"state-title\">")
914+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</p><h1 id=\"state-title\">")
869915 if templ_7745c5c3_Err != nil {
870916 return templ_7745c5c3_Err
871917 }
872- var templ_7745c5c3_Var41 string
873- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
918+ var templ_7745c5c3_Var43 string
919+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
874920 if templ_7745c5c3_Err != nil {
875- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 33}
921+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 192, Col: 33}
876922 }
877- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
923+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
878924 if templ_7745c5c3_Err != nil {
879925 return templ_7745c5c3_Err
880926 }
881- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</h1>")
927+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</h1>")
882928 if templ_7745c5c3_Err != nil {
883929 return templ_7745c5c3_Err
884930 }
885931 for _, line := range p.Lines {
886- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<p class=\"dim\">")
932+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<p class=\"dim\">")
887933 if templ_7745c5c3_Err != nil {
888934 return templ_7745c5c3_Err
889935 }
890- var templ_7745c5c3_Var42 string
891- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(line)
936+ var templ_7745c5c3_Var44 string
937+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(line)
892938 if templ_7745c5c3_Err != nil {
893- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 186, Col: 25}
939+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 194, Col: 25}
894940 }
895- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
941+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
896942 if templ_7745c5c3_Err != nil {
897943 return templ_7745c5c3_Err
898944 }
899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</p>")
945+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</p>")
900946 if templ_7745c5c3_Err != nil {
901947 return templ_7745c5c3_Err
902948 }
@@ -907,49 +953,49 @@ func State(l Layout, p StatePage) templ.Component {
907953 return templ_7745c5c3_Err
908954 }
909955 }
910- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<p class=\"state-back\">")
956+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<p class=\"state-back\">")
911957 if templ_7745c5c3_Err != nil {
912958 return templ_7745c5c3_Err
913959 }
914960 for _, b := range p.Back {
915- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<a href=\"")
961+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<a href=\"")
916962 if templ_7745c5c3_Err != nil {
917963 return templ_7745c5c3_Err
918964 }
919- var templ_7745c5c3_Var43 templ.SafeURL
920- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
965+ var templ_7745c5c3_Var45 templ.SafeURL
966+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
921967 if templ_7745c5c3_Err != nil {
922- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 193, Col: 36}
968+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 201, Col: 36}
923969 }
924- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
970+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
925971 if templ_7745c5c3_Err != nil {
926972 return templ_7745c5c3_Err
927973 }
928- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\">")
974+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\">")
929975 if templ_7745c5c3_Err != nil {
930976 return templ_7745c5c3_Err
931977 }
932- var templ_7745c5c3_Var44 string
933- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)
978+ var templ_7745c5c3_Var46 string
979+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)
934980 if templ_7745c5c3_Err != nil {
935- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 193, Col: 48}
981+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 201, Col: 48}
936982 }
937- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
983+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
938984 if templ_7745c5c3_Err != nil {
939985 return templ_7745c5c3_Err
940986 }
941- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</a>")
987+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</a>")
942988 if templ_7745c5c3_Err != nil {
943989 return templ_7745c5c3_Err
944990 }
945991 }
946- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</p></section>")
992+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</p></section>")
947993 if templ_7745c5c3_Err != nil {
948994 return templ_7745c5c3_Err
949995 }
950996 return nil
951997 })
952- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer)
998+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var41), templ_7745c5c3_Buffer)
953999 if templ_7745c5c3_Err != nil {
9541000 return templ_7745c5c3_Err
9551001 }
@@ -978,12 +1024,12 @@ func Login(l Layout, p LoginPage) templ.Component {
9781024 }()
9791025 }
9801026 ctx = templ.InitializeContext(ctx)
981- templ_7745c5c3_Var45 := templ.GetChildren(ctx)
982- if templ_7745c5c3_Var45 == nil {
983- templ_7745c5c3_Var45 = templ.NopComponent
1027+ templ_7745c5c3_Var47 := templ.GetChildren(ctx)
1028+ if templ_7745c5c3_Var47 == nil {
1029+ templ_7745c5c3_Var47 = templ.NopComponent
9841030 }
9851031 ctx = templ.ClearChildren(ctx)
986- templ_7745c5c3_Var46 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1032+ templ_7745c5c3_Var48 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
9871033 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
9881034 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
9891035 if !templ_7745c5c3_IsBuffer {
@@ -995,75 +1041,75 @@ func Login(l Layout, p LoginPage) templ.Component {
9951041 }()
9961042 }
9971043 ctx = templ.InitializeContext(ctx)
998- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1044+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
9991045 if templ_7745c5c3_Err != nil {
10001046 return templ_7745c5c3_Err
10011047 }
1002- var templ_7745c5c3_Var47 string
1003- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1048+ var templ_7745c5c3_Var49 string
1049+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
10041050 if templ_7745c5c3_Err != nil {
1005- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 208, Col: 33}
1051+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 216, Col: 33}
10061052 }
1007- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
1053+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
10081054 if templ_7745c5c3_Err != nil {
10091055 return templ_7745c5c3_Err
10101056 }
1011- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</h1>")
1057+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</h1>")
10121058 if templ_7745c5c3_Err != nil {
10131059 return templ_7745c5c3_Err
10141060 }
10151061 for _, line := range p.Lines {
1016- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"dim\">")
1062+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<p class=\"dim\">")
10171063 if templ_7745c5c3_Err != nil {
10181064 return templ_7745c5c3_Err
10191065 }
1020- var templ_7745c5c3_Var48 string
1021- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1066+ var templ_7745c5c3_Var50 string
1067+ templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(line)
10221068 if templ_7745c5c3_Err != nil {
1023- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 210, Col: 25}
1069+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 218, Col: 25}
10241070 }
1025- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
1071+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
10261072 if templ_7745c5c3_Err != nil {
10271073 return templ_7745c5c3_Err
10281074 }
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</p>")
1075+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</p>")
10301076 if templ_7745c5c3_Err != nil {
10311077 return templ_7745c5c3_Err
10321078 }
10331079 }
1034- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<form class=\"state-act\" method=\"post\" action=\"")
1080+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<form class=\"state-act\" method=\"post\" action=\"")
10351081 if templ_7745c5c3_Err != nil {
10361082 return templ_7745c5c3_Err
10371083 }
1038- var templ_7745c5c3_Var49 templ.SafeURL
1039- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
1084+ var templ_7745c5c3_Var51 templ.SafeURL
1085+ templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
10401086 if templ_7745c5c3_Err != nil {
1041- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 212, Col: 73}
1087+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 220, Col: 73}
10421088 }
1043- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
1089+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
10441090 if templ_7745c5c3_Err != nil {
10451091 return templ_7745c5c3_Err
10461092 }
1047- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")
1093+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")
10481094 if templ_7745c5c3_Err != nil {
10491095 return templ_7745c5c3_Err
10501096 }
1051- var templ_7745c5c3_Var50 string
1052- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1097+ var templ_7745c5c3_Var52 string
1098+ templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
10531099 if templ_7745c5c3_Err != nil {
1054- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 213, Col: 56}
1100+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 221, Col: 56}
10551101 }
1056- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
1102+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
10571103 if templ_7745c5c3_Err != nil {
10581104 return templ_7745c5c3_Err
10591105 }
1060- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1106+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
10611107 if templ_7745c5c3_Err != nil {
10621108 return templ_7745c5c3_Err
10631109 }
10641110 return nil
10651111 })
1066- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var46), templ_7745c5c3_Buffer)
1112+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var48), templ_7745c5c3_Buffer)
10671113 if templ_7745c5c3_Err != nil {
10681114 return templ_7745c5c3_Err
10691115 }
@@ -1089,12 +1135,12 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
10891135 }()
10901136 }
10911137 ctx = templ.InitializeContext(ctx)
1092- templ_7745c5c3_Var51 := templ.GetChildren(ctx)
1093- if templ_7745c5c3_Var51 == nil {
1094- templ_7745c5c3_Var51 = templ.NopComponent
1138+ templ_7745c5c3_Var53 := templ.GetChildren(ctx)
1139+ if templ_7745c5c3_Var53 == nil {
1140+ templ_7745c5c3_Var53 = templ.NopComponent
10951141 }
10961142 ctx = templ.ClearChildren(ctx)
1097- templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1143+ templ_7745c5c3_Var54 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
10981144 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
10991145 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
11001146 if !templ_7745c5c3_IsBuffer {
@@ -1106,88 +1152,88 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
11061152 }()
11071153 }
11081154 ctx = templ.InitializeContext(ctx)
1109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1155+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
11101156 if templ_7745c5c3_Err != nil {
11111157 return templ_7745c5c3_Err
11121158 }
1113- var templ_7745c5c3_Var53 string
1114- templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1159+ var templ_7745c5c3_Var55 string
1160+ templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
11151161 if templ_7745c5c3_Err != nil {
1116- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 225, Col: 33}
1162+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 33}
11171163 }
1118- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
1164+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
11191165 if templ_7745c5c3_Err != nil {
11201166 return templ_7745c5c3_Err
11211167 }
1122- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")
1168+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")
11231169 if templ_7745c5c3_Err != nil {
11241170 return templ_7745c5c3_Err
11251171 }
1126- var templ_7745c5c3_Var54 string
1127- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1172+ var templ_7745c5c3_Var56 string
1173+ templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
11281174 if templ_7745c5c3_Err != nil {
1129- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 227, Col: 87}
1175+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 87}
11301176 }
1131- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54)
1177+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56)
11321178 if templ_7745c5c3_Err != nil {
11331179 return templ_7745c5c3_Err
11341180 }
1135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" data-finish=\"")
1181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" data-finish=\"")
11361182 if templ_7745c5c3_Err != nil {
11371183 return templ_7745c5c3_Err
11381184 }
1139- var templ_7745c5c3_Var55 string
1140- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1185+ var templ_7745c5c3_Var57 string
1186+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
11411187 if templ_7745c5c3_Err != nil {
1142- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 227, Col: 112}
1188+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 112}
11431189 }
1144- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var55)
1190+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57)
11451191 if templ_7745c5c3_Err != nil {
11461192 return templ_7745c5c3_Err
11471193 }
1148- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\">")
1194+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\">")
11491195 if templ_7745c5c3_Err != nil {
11501196 return templ_7745c5c3_Err
11511197 }
1152- var templ_7745c5c3_Var56 string
1153- templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1198+ var templ_7745c5c3_Var58 string
1199+ templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
11541200 if templ_7745c5c3_Err != nil {
1155- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 227, Col: 125}
1201+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 125}
11561202 }
1157- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
1203+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
11581204 if templ_7745c5c3_Err != nil {
11591205 return templ_7745c5c3_Err
11601206 }
1161- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
11621208 if templ_7745c5c3_Err != nil {
11631209 return templ_7745c5c3_Err
11641210 }
11651211 for _, line := range p.Lines {
1166- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"dim\">")
1212+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<p class=\"dim\">")
11671213 if templ_7745c5c3_Err != nil {
11681214 return templ_7745c5c3_Err
11691215 }
1170- var templ_7745c5c3_Var57 string
1171- templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1216+ var templ_7745c5c3_Var59 string
1217+ templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(line)
11721218 if templ_7745c5c3_Err != nil {
1173- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 231, Col: 25}
1219+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 239, Col: 25}
11741220 }
1175- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
1221+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
11761222 if templ_7745c5c3_Err != nil {
11771223 return templ_7745c5c3_Err
11781224 }
1179- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</p>")
1225+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</p>")
11801226 if templ_7745c5c3_Err != nil {
11811227 return templ_7745c5c3_Err
11821228 }
11831229 }
1184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
11851231 if templ_7745c5c3_Err != nil {
11861232 return templ_7745c5c3_Err
11871233 }
11881234 return nil
11891235 })
1190- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer)
1236+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var54), templ_7745c5c3_Buffer)
11911237 if templ_7745c5c3_Err != nil {
11921238 return templ_7745c5c3_Err
11931239 }
@@ -1212,12 +1258,12 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
12121258 }()
12131259 }
12141260 ctx = templ.InitializeContext(ctx)
1215- templ_7745c5c3_Var58 := templ.GetChildren(ctx)
1216- if templ_7745c5c3_Var58 == nil {
1217- templ_7745c5c3_Var58 = templ.NopComponent
1261+ templ_7745c5c3_Var60 := templ.GetChildren(ctx)
1262+ if templ_7745c5c3_Var60 == nil {
1263+ templ_7745c5c3_Var60 = templ.NopComponent
12181264 }
12191265 ctx = templ.ClearChildren(ctx)
1220- templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1266+ templ_7745c5c3_Var61 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
12211267 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
12221268 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
12231269 if !templ_7745c5c3_IsBuffer {
@@ -1229,231 +1275,231 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
12291275 }()
12301276 }
12311277 ctx = templ.InitializeContext(ctx)
1232- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")
1278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")
12331279 if templ_7745c5c3_Err != nil {
12341280 return templ_7745c5c3_Err
12351281 }
1236- var templ_7745c5c3_Var60 string
1237- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
1282+ var templ_7745c5c3_Var62 string
1283+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
12381284 if templ_7745c5c3_Err != nil {
1239- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 241, Col: 81}
1285+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 249, Col: 81}
12401286 }
1241- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))
1287+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
12421288 if templ_7745c5c3_Err != nil {
12431289 return templ_7745c5c3_Err
12441290 }
1245- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")
1291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")
12461292 if templ_7745c5c3_Err != nil {
12471293 return templ_7745c5c3_Err
12481294 }
12491295 if len(p.Rows) > 0 {
1250- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")
1296+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")
12511297 if templ_7745c5c3_Err != nil {
12521298 return templ_7745c5c3_Err
12531299 }
12541300 for _, r := range p.Rows {
1255- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<tr><td class=\"c-name\">")
1301+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<tr><td class=\"c-name\">")
12561302 if templ_7745c5c3_Err != nil {
12571303 return templ_7745c5c3_Err
12581304 }
1259- var templ_7745c5c3_Var61 string
1260- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
1305+ var templ_7745c5c3_Var63 string
1306+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
12611307 if templ_7745c5c3_Err != nil {
1262- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 257, Col: 34}
1308+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 265, Col: 34}
12631309 }
1264- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1310+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
12651311 if templ_7745c5c3_Err != nil {
12661312 return templ_7745c5c3_Err
12671313 }
1268- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</td><td class=\"c-when nowrap\"><time")
1314+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</td><td class=\"c-when nowrap\"><time")
12691315 if templ_7745c5c3_Err != nil {
12701316 return templ_7745c5c3_Err
12711317 }
12721318 if r.CreatedAt != "" {
1273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " datetime=\"")
1319+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, " datetime=\"")
12741320 if templ_7745c5c3_Err != nil {
12751321 return templ_7745c5c3_Err
12761322 }
1277- var templ_7745c5c3_Var62 string
1278- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)
1323+ var templ_7745c5c3_Var64 string
1324+ templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)
12791325 if templ_7745c5c3_Err != nil {
1280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 261, Col: 32}
1326+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 269, Col: 32}
12811327 }
1282- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
1328+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
12831329 if templ_7745c5c3_Err != nil {
12841330 return templ_7745c5c3_Err
12851331 }
1286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\"")
1332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\"")
12871333 if templ_7745c5c3_Err != nil {
12881334 return templ_7745c5c3_Err
12891335 }
12901336 }
1291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, ">")
1337+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, ">")
12921338 if templ_7745c5c3_Err != nil {
12931339 return templ_7745c5c3_Err
12941340 }
1295- var templ_7745c5c3_Var63 string
1296- templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)
1341+ var templ_7745c5c3_Var65 string
1342+ templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)
12971343 if templ_7745c5c3_Err != nil {
1298- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 263, Col: 20}
1344+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 271, Col: 20}
12991345 }
1300- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
1346+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
13011347 if templ_7745c5c3_Err != nil {
13021348 return templ_7745c5c3_Err
13031349 }
1304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</time></td><td class=\"c-when nowrap\"><time")
1350+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</time></td><td class=\"c-when nowrap\"><time")
13051351 if templ_7745c5c3_Err != nil {
13061352 return templ_7745c5c3_Err
13071353 }
13081354 if r.LastUsedAt != "" {
1309- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " datetime=\"")
1355+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " datetime=\"")
13101356 if templ_7745c5c3_Err != nil {
13111357 return templ_7745c5c3_Err
13121358 }
1313- var templ_7745c5c3_Var64 string
1314- templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)
1359+ var templ_7745c5c3_Var66 string
1360+ templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)
13151361 if templ_7745c5c3_Err != nil {
1316- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 268, Col: 33}
1362+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 276, Col: 33}
13171363 }
1318- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
1364+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
13191365 if templ_7745c5c3_Err != nil {
13201366 return templ_7745c5c3_Err
13211367 }
1322- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\"")
1368+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"")
13231369 if templ_7745c5c3_Err != nil {
13241370 return templ_7745c5c3_Err
13251371 }
13261372 }
1327- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, ">")
1373+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, ">")
13281374 if templ_7745c5c3_Err != nil {
13291375 return templ_7745c5c3_Err
13301376 }
1331- var templ_7745c5c3_Var65 string
1332- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)
1377+ var templ_7745c5c3_Var67 string
1378+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)
13331379 if templ_7745c5c3_Err != nil {
1334- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 270, Col: 21}
1380+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 278, Col: 21}
13351381 }
1336- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
1382+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
13371383 if templ_7745c5c3_Err != nil {
13381384 return templ_7745c5c3_Err
13391385 }
1340- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</time></td><td class=\"c-sync\">")
1386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</time></td><td class=\"c-sync\">")
13411387 if templ_7745c5c3_Err != nil {
13421388 return templ_7745c5c3_Err
13431389 }
13441390 if r.Synced {
1345- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "yes")
1391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "yes")
13461392 if templ_7745c5c3_Err != nil {
13471393 return templ_7745c5c3_Err
13481394 }
13491395 } else {
1350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "no")
1396+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "no")
13511397 if templ_7745c5c3_Err != nil {
13521398 return templ_7745c5c3_Err
13531399 }
13541400 }
1355- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</td><td class=\"c-act\"><form method=\"post\" action=\"")
1401+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</td><td class=\"c-act\"><form method=\"post\" action=\"")
13561402 if templ_7745c5c3_Err != nil {
13571403 return templ_7745c5c3_Err
13581404 }
1359- var templ_7745c5c3_Var66 templ.SafeURL
1360- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))
1405+ var templ_7745c5c3_Var68 templ.SafeURL
1406+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))
13611407 if templ_7745c5c3_Err != nil {
1362- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 280, Col: 60}
1408+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 288, Col: 60}
13631409 }
1364- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
1410+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
13651411 if templ_7745c5c3_Err != nil {
13661412 return templ_7745c5c3_Err
13671413 }
1368- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")
1414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")
13691415 if templ_7745c5c3_Err != nil {
13701416 return templ_7745c5c3_Err
13711417 }
1372- var templ_7745c5c3_Var67 string
1373- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)
1418+ var templ_7745c5c3_Var69 string
1419+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)
13741420 if templ_7745c5c3_Err != nil {
1375- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 281, Col: 89}
1421+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 289, Col: 89}
13761422 }
1377- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
1423+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
13781424 if templ_7745c5c3_Err != nil {
13791425 return templ_7745c5c3_Err
13801426 }
1381- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\">Remove</button></form></td></tr>")
1427+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\">Remove</button></form></td></tr>")
13821428 if templ_7745c5c3_Err != nil {
13831429 return templ_7745c5c3_Err
13841430 }
13851431 }
1386- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</tbody></table>")
1432+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</tbody></table>")
13871433 if templ_7745c5c3_Err != nil {
13881434 return templ_7745c5c3_Err
13891435 }
13901436 } else {
1391- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"lede dim\">no passkeys yet.</p>")
1437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<p class=\"lede dim\">no passkeys yet.</p>")
13921438 if templ_7745c5c3_Err != nil {
13931439 return templ_7745c5c3_Err
13941440 }
13951441 }
1396- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " ")
1442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " ")
13971443 if templ_7745c5c3_Err != nil {
13981444 return templ_7745c5c3_Err
13991445 }
14001446 if p.Full {
1401- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<p class=\"dim\">")
1447+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"dim\">")
14021448 if templ_7745c5c3_Err != nil {
14031449 return templ_7745c5c3_Err
14041450 }
1405- var templ_7745c5c3_Var68 string
1406- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))
1451+ var templ_7745c5c3_Var70 string
1452+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))
14071453 if templ_7745c5c3_Err != nil {
1408- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 292, Col: 31}
1454+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 300, Col: 31}
14091455 }
1410- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1456+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
14111457 if templ_7745c5c3_Err != nil {
14121458 return templ_7745c5c3_Err
14131459 }
1414- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " passkeys are registered, the most there can be. Remove one to add another.</p>")
1460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, " passkeys are registered, the most there can be. Remove one to add another.</p>")
14151461 if templ_7745c5c3_Err != nil {
14161462 return templ_7745c5c3_Err
14171463 }
14181464 } else {
1419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")
1465+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")
14201466 if templ_7745c5c3_Err != nil {
14211467 return templ_7745c5c3_Err
14221468 }
1423- var templ_7745c5c3_Var69 string
1424- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1469+ var templ_7745c5c3_Var71 string
1470+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
14251471 if templ_7745c5c3_Err != nil {
1426- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 297, Col: 90}
1472+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 90}
14271473 }
1428- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
1474+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
14291475 if templ_7745c5c3_Err != nil {
14301476 return templ_7745c5c3_Err
14311477 }
1432- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "\" data-finish=\"")
1478+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" data-finish=\"")
14331479 if templ_7745c5c3_Err != nil {
14341480 return templ_7745c5c3_Err
14351481 }
1436- var templ_7745c5c3_Var70 string
1437- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1482+ var templ_7745c5c3_Var72 string
1483+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
14381484 if templ_7745c5c3_Err != nil {
1439- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 297, Col: 115}
1485+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 115}
14401486 }
1441- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70)
1487+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
14421488 if templ_7745c5c3_Err != nil {
14431489 return templ_7745c5c3_Err
14441490 }
1445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">Add a passkey</button></form>")
1491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">Add a passkey</button></form>")
14461492 if templ_7745c5c3_Err != nil {
14471493 return templ_7745c5c3_Err
14481494 }
14491495 }
1450- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
14511497 if templ_7745c5c3_Err != nil {
14521498 return templ_7745c5c3_Err
14531499 }
14541500 return nil
14551501 })
1456- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer)
1502+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var61), templ_7745c5c3_Buffer)
14571503 if templ_7745c5c3_Err != nil {
14581504 return templ_7745c5c3_Err
14591505 }
@@ -44,89 +44,135 @@ func Home(l Layout, p HomePage) templ.Component {
44 }()44 }()
45 }45 }
46 ctx = templ.InitializeContext(ctx)46 ctx = templ.InitializeContext(ctx)
47- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"sr\">Repositories</h1><div class=\"home\" data-layout=\"cards\"><section class=\"home-repos\" aria-label=\"Repositories\"><div class=\"home-bar\"><div class=\"filter\"><label for=\"home-filter\" class=\"sr\">filter</label> <input id=\"home-filter\" type=\"search\" data-filter=\"#repo-rows > li\" placeholder=\"Filter repositories…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><nav class=\"sort\" aria-label=\"Sort repositories\"><span class=\"mute\">sort</span> ")47+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"sr\">Repositories</h1><div class=\"home\" data-layout=\"cards\"><section class=\"home-repos\" aria-label=\"Repositories\"><div class=\"home-bar\">")
48 if templ_7745c5c3_Err != nil {48 if templ_7745c5c3_Err != nil {
49 return templ_7745c5c3_Err49 return templ_7745c5c3_Err
50 }50 }
51- for _, s := range p.Sorts {51+ if p.Inbox != nil {
52- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"")52+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a class=\"btn inbox-btn\" href=\"")
53 if templ_7745c5c3_Err != nil {53 if templ_7745c5c3_Err != nil {
54 return templ_7745c5c3_Err54 return templ_7745c5c3_Err
55 }55 }
56 var templ_7745c5c3_Var3 templ.SafeURL56 var templ_7745c5c3_Var3 templ.SafeURL
57- templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))57+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Inbox.Href))
58 if templ_7745c5c3_Err != nil {58 if templ_7745c5c3_Err != nil {
59- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 27, Col: 36}59+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 13, Col: 65}
60 }60 }
61 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))61 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
62 if templ_7745c5c3_Err != nil {62 if templ_7745c5c3_Err != nil {
63 return templ_7745c5c3_Err63 return templ_7745c5c3_Err
64 }64 }
65- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"")65+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" title=\"inbox\">Inbox ")
66+ if templ_7745c5c3_Err != nil {
67+ return templ_7745c5c3_Err
68+ }
69+ if p.Inbox.Count >= 0 {
70+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"count\">")
71+ if templ_7745c5c3_Err != nil {
72+ return templ_7745c5c3_Err
73+ }
74+ var templ_7745c5c3_Var4 string
75+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Inbox.Count))
76+ if templ_7745c5c3_Err != nil {
77+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 16, Col: 49}
78+ }
79+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
80+ if templ_7745c5c3_Err != nil {
81+ return templ_7745c5c3_Err
82+ }
83+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span>")
84+ if templ_7745c5c3_Err != nil {
85+ return templ_7745c5c3_Err
86+ }
87+ }
88+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a>")
89+ if templ_7745c5c3_Err != nil {
90+ return templ_7745c5c3_Err
91+ }
92+ }
93+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"filter\"><label for=\"home-filter\" class=\"sr\">filter</label> <input id=\"home-filter\" type=\"search\" data-filter=\"#repo-rows > li\" placeholder=\"Filter repositories…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><nav class=\"sort\" aria-label=\"Sort repositories\"><span class=\"mute\">sort</span> ")
94+ if templ_7745c5c3_Err != nil {
95+ return templ_7745c5c3_Err
96+ }
97+ for _, s := range p.Sorts {
98+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
99+ if templ_7745c5c3_Err != nil {
100+ return templ_7745c5c3_Err
101+ }
102+ var templ_7745c5c3_Var5 templ.SafeURL
103+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
104+ if templ_7745c5c3_Err != nil {
105+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 35, Col: 36}
106+ }
107+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
108+ if templ_7745c5c3_Err != nil {
109+ return templ_7745c5c3_Err
110+ }
111+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"")
66 if templ_7745c5c3_Err != nil {112 if templ_7745c5c3_Err != nil {
67 return templ_7745c5c3_Err113 return templ_7745c5c3_Err
68 }114 }
69 if s.Mark != "" {115 if s.Mark != "" {
70- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " aria-current=\"true\"")116+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " aria-current=\"true\"")
71 if templ_7745c5c3_Err != nil {117 if templ_7745c5c3_Err != nil {
72 return templ_7745c5c3_Err118 return templ_7745c5c3_Err
73 }119 }
74 }120 }
75- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">")121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">")
76 if templ_7745c5c3_Err != nil {122 if templ_7745c5c3_Err != nil {
77 return templ_7745c5c3_Err123 return templ_7745c5c3_Err
78 }124 }
79- var templ_7745c5c3_Var4 string125+ var templ_7745c5c3_Var6 string
80- templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)126+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
81 if templ_7745c5c3_Err != nil {127 if templ_7745c5c3_Err != nil {
82- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 32, Col: 17}128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 40, Col: 17}
83 }129 }
84- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))130+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
85 if templ_7745c5c3_Err != nil {131 if templ_7745c5c3_Err != nil {
86 return templ_7745c5c3_Err132 return templ_7745c5c3_Err
87 }133 }
88- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
89 if templ_7745c5c3_Err != nil {135 if templ_7745c5c3_Err != nil {
90 return templ_7745c5c3_Err136 return templ_7745c5c3_Err
91 }137 }
92 if s.Mark != "" {138 if s.Mark != "" {
93- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span aria-hidden=\"true\">")139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span aria-hidden=\"true\">")
94 if templ_7745c5c3_Err != nil {140 if templ_7745c5c3_Err != nil {
95 return templ_7745c5c3_Err141 return templ_7745c5c3_Err
96 }142 }
97- var templ_7745c5c3_Var5 string143+ var templ_7745c5c3_Var7 string
98- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(s.Mark)144+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(s.Mark)
99 if templ_7745c5c3_Err != nil {145 if templ_7745c5c3_Err != nil {
100- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 34, Col: 42}146+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 42, Col: 42}
101 }147 }
102- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))148+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
103 if templ_7745c5c3_Err != nil {149 if templ_7745c5c3_Err != nil {
104 return templ_7745c5c3_Err150 return templ_7745c5c3_Err
105 }151 }
106- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span> <span class=\"sr\">")152+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span class=\"sr\">")
107 if templ_7745c5c3_Err != nil {153 if templ_7745c5c3_Err != nil {
108 return templ_7745c5c3_Err154 return templ_7745c5c3_Err
109 }155 }
110- var templ_7745c5c3_Var6 string156+ var templ_7745c5c3_Var8 string
111- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(", " + s.State)157+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(", " + s.State)
112 if templ_7745c5c3_Err != nil {158 if templ_7745c5c3_Err != nil {
113- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 35, Col: 42}159+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 43, Col: 42}
114 }160 }
115- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))161+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
116 if templ_7745c5c3_Err != nil {162 if templ_7745c5c3_Err != nil {
117 return templ_7745c5c3_Err163 return templ_7745c5c3_Err
118 }164 }
119- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")165+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span>")
120 if templ_7745c5c3_Err != nil {166 if templ_7745c5c3_Err != nil {
121 return templ_7745c5c3_Err167 return templ_7745c5c3_Err
122 }168 }
123 }169 }
124- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</a>")170+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a>")
125 if templ_7745c5c3_Err != nil {171 if templ_7745c5c3_Err != nil {
126 return templ_7745c5c3_Err172 return templ_7745c5c3_Err
127 }173 }
128 }174 }
129- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</nav><div class=\"layout-switch\" role=\"group\" aria-label=\"Layout\" data-js><button type=\"button\" class=\"btn icon\" data-set-layout=\"cards\" aria-pressed=\"true\" title=\"Cards\" aria-label=\"Cards\"><span class=\"ico i-cards\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"split\" aria-pressed=\"false\" title=\"Two columns\" aria-label=\"Two columns\"><span class=\"ico i-split\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"compact\" aria-pressed=\"false\" title=\"Compact list\" aria-label=\"Compact list\"><span class=\"ico i-compact\" aria-hidden=\"true\"></span></button></div></div><ul class=\"repos\" id=\"repo-rows\" data-rows>")175+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</nav><div class=\"layout-switch\" role=\"group\" aria-label=\"Layout\" data-js><button type=\"button\" class=\"btn icon\" data-set-layout=\"cards\" aria-pressed=\"true\" title=\"Cards\" aria-label=\"Cards\"><span class=\"ico i-cards\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"split\" aria-pressed=\"false\" title=\"Two columns\" aria-label=\"Two columns\"><span class=\"ico i-split\" aria-hidden=\"true\"></span></button> <button type=\"button\" class=\"btn icon\" data-set-layout=\"compact\" aria-pressed=\"false\" title=\"Compact list\" aria-label=\"Compact list\"><span class=\"ico i-compact\" aria-hidden=\"true\"></span></button></div></div><ul class=\"repos\" id=\"repo-rows\" data-rows>")
130 if templ_7745c5c3_Err != nil {176 if templ_7745c5c3_Err != nil {
131 return templ_7745c5c3_Err177 return templ_7745c5c3_Err
132 }178 }
@@ -136,17 +182,17 @@ func Home(l Layout, p HomePage) templ.Component {
136 return templ_7745c5c3_Err182 return templ_7745c5c3_Err
137 }183 }
138 }184 }
139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</ul>")185+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</ul>")
140 if templ_7745c5c3_Err != nil {186 if templ_7745c5c3_Err != nil {
141 return templ_7745c5c3_Err187 return templ_7745c5c3_Err
142 }188 }
143 if len(p.Rows) == 0 {189 if len(p.Rows) == 0 {
144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">no repositories yet.</p>")190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"lede dim\">no repositories yet.</p>")
145 if templ_7745c5c3_Err != nil {191 if templ_7745c5c3_Err != nil {
146 return templ_7745c5c3_Err192 return templ_7745c5c3_Err
147 }193 }
148 }194 }
149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</section></div>")195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</section></div>")
150 if templ_7745c5c3_Err != nil {196 if templ_7745c5c3_Err != nil {
151 return templ_7745c5c3_Err197 return templ_7745c5c3_Err
152 }198 }
@@ -178,12 +224,12 @@ func Summary(l Layout, p SummaryPage) templ.Component {
178 }()224 }()
179 }225 }
180 ctx = templ.InitializeContext(ctx)226 ctx = templ.InitializeContext(ctx)
181- templ_7745c5c3_Var7 := templ.GetChildren(ctx)227+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
182- if templ_7745c5c3_Var7 == nil {228+ if templ_7745c5c3_Var9 == nil {
183- templ_7745c5c3_Var7 = templ.NopComponent229+ templ_7745c5c3_Var9 = templ.NopComponent
184 }230 }
185 ctx = templ.ClearChildren(ctx)231 ctx = templ.ClearChildren(ctx)
186- templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {232+ templ_7745c5c3_Var10 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
187 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context233 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
188 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)234 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
189 if !templ_7745c5c3_IsBuffer {235 if !templ_7745c5c3_IsBuffer {
@@ -203,7 +249,7 @@ func Summary(l Layout, p SummaryPage) templ.Component {
203 }249 }
204 return nil250 return nil
205 })251 })
206- templ_7745c5c3_Err = Shell(l, summaryRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)252+ templ_7745c5c3_Err = Shell(l, summaryRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
207 if templ_7745c5c3_Err != nil {253 if templ_7745c5c3_Err != nil {
208 return templ_7745c5c3_Err254 return templ_7745c5c3_Err
209 }255 }
@@ -230,158 +276,158 @@ func latestTagFact(t LatestTag) templ.Component {
230 }()276 }()
231 }277 }
232 ctx = templ.InitializeContext(ctx)278 ctx = templ.InitializeContext(ctx)
233- templ_7745c5c3_Var9 := templ.GetChildren(ctx)279+ templ_7745c5c3_Var11 := templ.GetChildren(ctx)
234- if templ_7745c5c3_Var9 == nil {280+ if templ_7745c5c3_Var11 == nil {
235- templ_7745c5c3_Var9 = templ.NopComponent281+ templ_7745c5c3_Var11 = templ.NopComponent
236 }282 }
237 ctx = templ.ClearChildren(ctx)283 ctx = templ.ClearChildren(ctx)
238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<dt>latest tag</dt><dd class=\"latest-tag\">")284+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<dt>latest tag</dt><dd class=\"latest-tag\">")
239 if templ_7745c5c3_Err != nil {285 if templ_7745c5c3_Err != nil {
240 return templ_7745c5c3_Err286 return templ_7745c5c3_Err
241 }287 }
242 if t.Name != "" {288 if t.Name != "" {
243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a class=\"tag-name\" href=\"")289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a class=\"tag-name\" href=\"")
244 if templ_7745c5c3_Err != nil {290 if templ_7745c5c3_Err != nil {
245 return templ_7745c5c3_Err291 return templ_7745c5c3_Err
246 }292 }
247- var templ_7745c5c3_Var10 templ.SafeURL293+ var templ_7745c5c3_Var12 templ.SafeURL
248- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.Href))294+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.Href))
249 if templ_7745c5c3_Err != nil {295 if templ_7745c5c3_Err != nil {
250- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 82, Col: 51}296+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 90, Col: 51}
251 }297 }
252- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))298+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
253 if templ_7745c5c3_Err != nil {299 if templ_7745c5c3_Err != nil {
254 return templ_7745c5c3_Err300 return templ_7745c5c3_Err
255 }301 }
256- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" title=\"")302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" title=\"")
257 if templ_7745c5c3_Err != nil {303 if templ_7745c5c3_Err != nil {
258 return templ_7745c5c3_Err304 return templ_7745c5c3_Err
259 }305 }
260- var templ_7745c5c3_Var11 string306+ var templ_7745c5c3_Var13 string
261- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.Subject)307+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.Subject)
262 if templ_7745c5c3_Err != nil {308 if templ_7745c5c3_Err != nil {
263- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 82, Col: 71}309+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 90, Col: 71}
264 }310 }
265- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)311+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
266 if templ_7745c5c3_Err != nil {312 if templ_7745c5c3_Err != nil {
267 return templ_7745c5c3_Err313 return templ_7745c5c3_Err
268 }314 }
269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
270 if templ_7745c5c3_Err != nil {316 if templ_7745c5c3_Err != nil {
271 return templ_7745c5c3_Err317 return templ_7745c5c3_Err
272 }318 }
273- var templ_7745c5c3_Var12 string319+ var templ_7745c5c3_Var14 string
274- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)320+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)
275 if templ_7745c5c3_Err != nil {321 if templ_7745c5c3_Err != nil {
276- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 82, Col: 82}322+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 90, Col: 82}
277 }323 }
278- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))324+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
279 if templ_7745c5c3_Err != nil {325 if templ_7745c5c3_Err != nil {
280 return templ_7745c5c3_Err326 return templ_7745c5c3_Err
281 }327 }
282- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a> ")328+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</a> ")
283 if templ_7745c5c3_Err != nil {329 if templ_7745c5c3_Err != nil {
284 return templ_7745c5c3_Err330 return templ_7745c5c3_Err
285 }331 }
286 if t.WhenAt != "" {332 if t.WhenAt != "" {
287- var templ_7745c5c3_Var13 string333+ var templ_7745c5c3_Var15 string
288- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")334+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
289 if templ_7745c5c3_Err != nil {335 if templ_7745c5c3_Err != nil {
290- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 12}336+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 12}
291 }337 }
292- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))338+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
293 if templ_7745c5c3_Err != nil {339 if templ_7745c5c3_Err != nil {
294 return templ_7745c5c3_Err340 return templ_7745c5c3_Err
295 }341 }
296- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<time datetime=\"")342+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<time datetime=\"")
297 if templ_7745c5c3_Err != nil {343 if templ_7745c5c3_Err != nil {
298 return templ_7745c5c3_Err344 return templ_7745c5c3_Err
299 }345 }
300- var templ_7745c5c3_Var14 string346+ var templ_7745c5c3_Var16 string
301- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)347+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)
302 if templ_7745c5c3_Err != nil {348 if templ_7745c5c3_Err != nil {
303- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 39}349+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 39}
304 }350 }
305- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)351+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
306 if templ_7745c5c3_Err != nil {352 if templ_7745c5c3_Err != nil {
307 return templ_7745c5c3_Err353 return templ_7745c5c3_Err
308 }354 }
309- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" title=\"")355+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" title=\"")
310 if templ_7745c5c3_Err != nil {356 if templ_7745c5c3_Err != nil {
311 return templ_7745c5c3_Err357 return templ_7745c5c3_Err
312 }358 }
313- var templ_7745c5c3_Var15 string359+ var templ_7745c5c3_Var17 string
314- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)360+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.WhenAt)
315 if templ_7745c5c3_Err != nil {361 if templ_7745c5c3_Err != nil {
316- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 58}362+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 58}
317 }363 }
318- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)364+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
319 if templ_7745c5c3_Err != nil {365 if templ_7745c5c3_Err != nil {
320 return templ_7745c5c3_Err366 return templ_7745c5c3_Err
321 }367 }
322- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")368+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
323 if templ_7745c5c3_Err != nil {369 if templ_7745c5c3_Err != nil {
324 return templ_7745c5c3_Err370 return templ_7745c5c3_Err
325 }371 }
326- var templ_7745c5c3_Var16 string372+ var templ_7745c5c3_Var18 string
327- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(t.When)373+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(t.When)
328 if templ_7745c5c3_Err != nil {374 if templ_7745c5c3_Err != nil {
329- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 84, Col: 69}375+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 92, Col: 69}
330 }376 }
331- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))377+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
332 if templ_7745c5c3_Err != nil {378 if templ_7745c5c3_Err != nil {
333 return templ_7745c5c3_Err379 return templ_7745c5c3_Err
334 }380 }
335- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</time>")381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</time>")
336 if templ_7745c5c3_Err != nil {382 if templ_7745c5c3_Err != nil {
337 return templ_7745c5c3_Err383 return templ_7745c5c3_Err
338 }384 }
339 }385 }
340- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ")
341 if templ_7745c5c3_Err != nil {387 if templ_7745c5c3_Err != nil {
342 return templ_7745c5c3_Err388 return templ_7745c5c3_Err
343 }389 }
344- var templ_7745c5c3_Var17 string390+ var templ_7745c5c3_Var19 string
345- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")391+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
346 if templ_7745c5c3_Err != nil {392 if templ_7745c5c3_Err != nil {
347- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 86, Col: 11}393+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 94, Col: 11}
348 }394 }
349- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))395+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
350 if templ_7745c5c3_Err != nil {396 if templ_7745c5c3_Err != nil {
351 return templ_7745c5c3_Err397 return templ_7745c5c3_Err
352 }398 }
353- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ")399+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
354 if templ_7745c5c3_Err != nil {400 if templ_7745c5c3_Err != nil {
355 return templ_7745c5c3_Err401 return templ_7745c5c3_Err
356 }402 }
357 }403 }
358- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a class=\"all-tags\" href=\"")404+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a class=\"all-tags\" href=\"")
359 if templ_7745c5c3_Err != nil {405 if templ_7745c5c3_Err != nil {
360 return templ_7745c5c3_Err406 return templ_7745c5c3_Err
361 }407 }
362- var templ_7745c5c3_Var18 templ.SafeURL408+ var templ_7745c5c3_Var20 templ.SafeURL
363- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.RefsHref))409+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.RefsHref))
364 if templ_7745c5c3_Err != nil {410 if templ_7745c5c3_Err != nil {
365- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 88, Col: 54}411+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 96, Col: 54}
366 }412 }
367- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))413+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
368 if templ_7745c5c3_Err != nil {414 if templ_7745c5c3_Err != nil {
369 return templ_7745c5c3_Err415 return templ_7745c5c3_Err
370 }416 }
371- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")417+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
372 if templ_7745c5c3_Err != nil {418 if templ_7745c5c3_Err != nil {
373 return templ_7745c5c3_Err419 return templ_7745c5c3_Err
374 }420 }
375- var templ_7745c5c3_Var19 string421+ var templ_7745c5c3_Var21 string
376- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(plural(t.Count, "tag", "tags"))422+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(plural(t.Count, "tag", "tags"))
377 if templ_7745c5c3_Err != nil {423 if templ_7745c5c3_Err != nil {
378- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 88, Col: 89}424+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 96, Col: 89}
379 }425 }
380- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))426+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
381 if templ_7745c5c3_Err != nil {427 if templ_7745c5c3_Err != nil {
382 return templ_7745c5c3_Err428 return templ_7745c5c3_Err
383 }429 }
384- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a></dd>")430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a></dd>")
385 if templ_7745c5c3_Err != nil {431 if templ_7745c5c3_Err != nil {
386 return templ_7745c5c3_Err432 return templ_7745c5c3_Err
387 }433 }
@@ -405,12 +451,12 @@ func summaryRail(p SummaryPage) templ.Component {
405 }()451 }()
406 }452 }
407 ctx = templ.InitializeContext(ctx)453 ctx = templ.InitializeContext(ctx)
408- templ_7745c5c3_Var20 := templ.GetChildren(ctx)454+ templ_7745c5c3_Var22 := templ.GetChildren(ctx)
409- if templ_7745c5c3_Var20 == nil {455+ if templ_7745c5c3_Var22 == nil {
410- templ_7745c5c3_Var20 = templ.NopComponent456+ templ_7745c5c3_Var22 = templ.NopComponent
411 }457 }
412 ctx = templ.ClearChildren(ctx)458 ctx = templ.ClearChildren(ctx)
413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section aria-labelledby=\"r-clone\"><h2 id=\"r-clone\">Clone</h2>")459+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<section aria-labelledby=\"r-clone\"><h2 id=\"r-clone\">Clone</h2>")
414 if templ_7745c5c3_Err != nil {460 if templ_7745c5c3_Err != nil {
415 return templ_7745c5c3_Err461 return templ_7745c5c3_Err
416 }462 }
@@ -418,25 +464,25 @@ func summaryRail(p SummaryPage) templ.Component {
418 if templ_7745c5c3_Err != nil {464 if templ_7745c5c3_Err != nil {
419 return templ_7745c5c3_Err465 return templ_7745c5c3_Err
420 }466 }
421- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</section><section aria-labelledby=\"r-facts\"><h2 id=\"r-facts\">About</h2><dl class=\"kv\">")467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</section><section aria-labelledby=\"r-facts\"><h2 id=\"r-facts\">About</h2><dl class=\"kv\">")
422 if templ_7745c5c3_Err != nil {468 if templ_7745c5c3_Err != nil {
423 return templ_7745c5c3_Err469 return templ_7745c5c3_Err
424 }470 }
425 for _, f := range p.Facts {471 for _, f := range p.Facts {
426- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<dt>")472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<dt>")
427 if templ_7745c5c3_Err != nil {473 if templ_7745c5c3_Err != nil {
428 return templ_7745c5c3_Err474 return templ_7745c5c3_Err
429 }475 }
430- var templ_7745c5c3_Var21 string476+ var templ_7745c5c3_Var23 string
431- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(f.Key)477+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(f.Key)
432 if templ_7745c5c3_Err != nil {478 if templ_7745c5c3_Err != nil {
433- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 99, Col: 15}479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 107, Col: 15}
434 }480 }
435- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))481+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
436 if templ_7745c5c3_Err != nil {482 if templ_7745c5c3_Err != nil {
437 return templ_7745c5c3_Err483 return templ_7745c5c3_Err
438 }484 }
439- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</dt>")485+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</dt>")
440 if templ_7745c5c3_Err != nil {486 if templ_7745c5c3_Err != nil {
441 return templ_7745c5c3_Err487 return templ_7745c5c3_Err
442 }488 }
@@ -451,25 +497,25 @@ func summaryRail(p SummaryPage) templ.Component {
451 return templ_7745c5c3_Err497 return templ_7745c5c3_Err
452 }498 }
453 }499 }
454- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</dl></section>")500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</dl></section>")
455 if templ_7745c5c3_Err != nil {501 if templ_7745c5c3_Err != nil {
456 return templ_7745c5c3_Err502 return templ_7745c5c3_Err
457 }503 }
458 if len(p.Recent) > 0 {504 if len(p.Recent) > 0 {
459- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")505+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<section aria-labelledby=\"r-recent\"><h2 id=\"r-recent\">Recent commits <a href=\"")
460 if templ_7745c5c3_Err != nil {506 if templ_7745c5c3_Err != nil {
461 return templ_7745c5c3_Err507 return templ_7745c5c3_Err
462 }508 }
463- var templ_7745c5c3_Var22 templ.SafeURL509+ var templ_7745c5c3_Var24 templ.SafeURL
464- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))510+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.AllHref))
465 if templ_7745c5c3_Err != nil {511 if templ_7745c5c3_Err != nil {
466- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 108, Col: 105}512+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 116, Col: 105}
467 }513 }
468- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))514+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
469 if templ_7745c5c3_Err != nil {515 if templ_7745c5c3_Err != nil {
470 return templ_7745c5c3_Err516 return templ_7745c5c3_Err
471 }517 }
472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">View all</a></h2>")518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">View all</a></h2>")
473 if templ_7745c5c3_Err != nil {519 if templ_7745c5c3_Err != nil {
474 return templ_7745c5c3_Err520 return templ_7745c5c3_Err
475 }521 }
@@ -477,7 +523,7 @@ func summaryRail(p SummaryPage) templ.Component {
477 if templ_7745c5c3_Err != nil {523 if templ_7745c5c3_Err != nil {
478 return templ_7745c5c3_Err524 return templ_7745c5c3_Err
479 }525 }
480- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</section>")526+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</section>")
481 if templ_7745c5c3_Err != nil {527 if templ_7745c5c3_Err != nil {
482 return templ_7745c5c3_Err528 return templ_7745c5c3_Err
483 }529 }
@@ -489,7 +535,7 @@ func summaryRail(p SummaryPage) templ.Component {
489 }535 }
490 }536 }
491 if len(p.Outline) > 0 && p.Readme != nil {537 if len(p.Outline) > 0 && p.Readme != nil {
492- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<section aria-labelledby=\"r-outline\"><h2 id=\"r-outline\">On this page</h2>")
493 if templ_7745c5c3_Err != nil {539 if templ_7745c5c3_Err != nil {
494 return templ_7745c5c3_Err540 return templ_7745c5c3_Err
495 }541 }
@@ -497,7 +543,7 @@ func summaryRail(p SummaryPage) templ.Component {
497 if templ_7745c5c3_Err != nil {543 if templ_7745c5c3_Err != nil {
498 return templ_7745c5c3_Err544 return templ_7745c5c3_Err
499 }545 }
500- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</section>")546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</section>")
501 if templ_7745c5c3_Err != nil {547 if templ_7745c5c3_Err != nil {
502 return templ_7745c5c3_Err548 return templ_7745c5c3_Err
503 }549 }
@@ -523,12 +569,12 @@ func Refs(l Layout, p RefsPage) templ.Component {
523 }()569 }()
524 }570 }
525 ctx = templ.InitializeContext(ctx)571 ctx = templ.InitializeContext(ctx)
526- templ_7745c5c3_Var23 := templ.GetChildren(ctx)572+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
527- if templ_7745c5c3_Var23 == nil {573+ if templ_7745c5c3_Var25 == nil {
528- templ_7745c5c3_Var23 = templ.NopComponent574+ templ_7745c5c3_Var25 = templ.NopComponent
529 }575 }
530 ctx = templ.ClearChildren(ctx)576 ctx = templ.ClearChildren(ctx)
531- templ_7745c5c3_Var24 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {577+ templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
532 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context578 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
533 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)579 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
534 if !templ_7745c5c3_IsBuffer {580 if !templ_7745c5c3_IsBuffer {
@@ -540,7 +586,7 @@ func Refs(l Layout, p RefsPage) templ.Component {
540 }()586 }()
541 }587 }
542 ctx = templ.InitializeContext(ctx)588 ctx = templ.InitializeContext(ctx)
543- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"filter\"><label for=\"ref-filter\" class=\"sr\">filter</label> <input id=\"ref-filter\" type=\"search\" data-filter=\"#ref-rows > tr\" placeholder=\"Filter branches and tags…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div><table class=\"list refs\" aria-label=\"Branches and tags\"><thead><tr><th class=\"c-kind\">kind</th><th class=\"c-name\">name</th><th class=\"c-sha\">commit</th><th class=\"c-sub\">subject</th><th class=\"c-when\">date</th></tr></thead>")
544 if templ_7745c5c3_Err != nil {590 if templ_7745c5c3_Err != nil {
545 return templ_7745c5c3_Err591 return templ_7745c5c3_Err
546 }592 }
@@ -548,32 +594,32 @@ func Refs(l Layout, p RefsPage) templ.Component {
548 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
549 return templ_7745c5c3_Err595 return templ_7745c5c3_Err
550 }596 }
551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</table>")597+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</table>")
552 if templ_7745c5c3_Err != nil {598 if templ_7745c5c3_Err != nil {
553 return templ_7745c5c3_Err599 return templ_7745c5c3_Err
554 }600 }
555 if p.Note != "" {601 if p.Note != "" {
556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p class=\"note\">")602+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<p class=\"note\">")
557 if templ_7745c5c3_Err != nil {603 if templ_7745c5c3_Err != nil {
558 return templ_7745c5c3_Err604 return templ_7745c5c3_Err
559 }605 }
560- var templ_7745c5c3_Var25 string606+ var templ_7745c5c3_Var27 string
561- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)607+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
562 if templ_7745c5c3_Err != nil {608 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 141, Col: 27}609+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 149, Col: 27}
564 }610 }
565- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))611+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
566 if templ_7745c5c3_Err != nil {612 if templ_7745c5c3_Err != nil {
567 return templ_7745c5c3_Err613 return templ_7745c5c3_Err
568 }614 }
569- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</p>")615+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</p>")
570 if templ_7745c5c3_Err != nil {616 if templ_7745c5c3_Err != nil {
571 return templ_7745c5c3_Err617 return templ_7745c5c3_Err
572 }618 }
573 }619 }
574 return nil620 return nil
575 })621 })
576- templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var24), templ_7745c5c3_Buffer)622+ templ_7745c5c3_Err = Shell(l, refsRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer)
577 if templ_7745c5c3_Err != nil {623 if templ_7745c5c3_Err != nil {
578 return templ_7745c5c3_Err624 return templ_7745c5c3_Err
579 }625 }
@@ -597,112 +643,112 @@ func refRow(kind string, r RefRow) templ.Component {
597 }()643 }()
598 }644 }
599 ctx = templ.InitializeContext(ctx)645 ctx = templ.InitializeContext(ctx)
600- templ_7745c5c3_Var26 := templ.GetChildren(ctx)646+ templ_7745c5c3_Var28 := templ.GetChildren(ctx)
601- if templ_7745c5c3_Var26 == nil {647+ if templ_7745c5c3_Var28 == nil {
602- templ_7745c5c3_Var26 = templ.NopComponent648+ templ_7745c5c3_Var28 = templ.NopComponent
603 }649 }
604 ctx = templ.ClearChildren(ctx)650 ctx = templ.ClearChildren(ctx)
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<tr")651+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<tr")
606 if templ_7745c5c3_Err != nil {652 if templ_7745c5c3_Err != nil {
607 return templ_7745c5c3_Err653 return templ_7745c5c3_Err
608 }654 }
609 if r.Cursor {655 if r.Cursor {
610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " data-cursor")656+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " data-cursor")
611 if templ_7745c5c3_Err != nil {657 if templ_7745c5c3_Err != nil {
612 return templ_7745c5c3_Err658 return templ_7745c5c3_Err
613 }659 }
614 }660 }
615- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "><td class=\"c-kind mute\">")661+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "><td class=\"c-kind mute\">")
616 if templ_7745c5c3_Err != nil {662 if templ_7745c5c3_Err != nil {
617 return templ_7745c5c3_Err663 return templ_7745c5c3_Err
618 }664 }
619- var templ_7745c5c3_Var27 string665+ var templ_7745c5c3_Var29 string
620- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(kind)666+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
621 if templ_7745c5c3_Err != nil {667 if templ_7745c5c3_Err != nil {
622- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 148, Col: 32}668+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 156, Col: 32}
623 }669 }
624- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))670+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
625 if templ_7745c5c3_Err != nil {671 if templ_7745c5c3_Err != nil {
626 return templ_7745c5c3_Err672 return templ_7745c5c3_Err
627 }673 }
628- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</td><td class=\"c-name\"><a href=\"")674+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</td><td class=\"c-name\"><a href=\"")
629 if templ_7745c5c3_Err != nil {675 if templ_7745c5c3_Err != nil {
630 return templ_7745c5c3_Err676 return templ_7745c5c3_Err
631 }677 }
632- var templ_7745c5c3_Var28 templ.SafeURL678+ var templ_7745c5c3_Var30 templ.SafeURL
633- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))679+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
634 if templ_7745c5c3_Err != nil {680 if templ_7745c5c3_Err != nil {
635- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 150, Col: 34}681+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 158, Col: 34}
636 }682 }
637- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))683+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
638 if templ_7745c5c3_Err != nil {684 if templ_7745c5c3_Err != nil {
639 return templ_7745c5c3_Err685 return templ_7745c5c3_Err
640 }686 }
641- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">")687+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
642 if templ_7745c5c3_Err != nil {688 if templ_7745c5c3_Err != nil {
643 return templ_7745c5c3_Err689 return templ_7745c5c3_Err
644 }690 }
645- var templ_7745c5c3_Var29 string691+ var templ_7745c5c3_Var31 string
646- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)692+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
647 if templ_7745c5c3_Err != nil {693 if templ_7745c5c3_Err != nil {
648- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 150, Col: 45}694+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 158, Col: 45}
649 }695 }
650- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))696+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
651 if templ_7745c5c3_Err != nil {697 if templ_7745c5c3_Err != nil {
652 return templ_7745c5c3_Err698 return templ_7745c5c3_Err
653 }699 }
654- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</a> ")700+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</a> ")
655 if templ_7745c5c3_Err != nil {701 if templ_7745c5c3_Err != nil {
656 return templ_7745c5c3_Err702 return templ_7745c5c3_Err
657 }703 }
658 if r.Default {704 if r.Default {
659- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"pill\">default</span>")705+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"pill\">default</span>")
660 if templ_7745c5c3_Err != nil {706 if templ_7745c5c3_Err != nil {
661 return templ_7745c5c3_Err707 return templ_7745c5c3_Err
662 }708 }
663 }709 }
664- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td class=\"c-sha\">")710+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</td><td class=\"c-sha\">")
665 if templ_7745c5c3_Err != nil {711 if templ_7745c5c3_Err != nil {
666 return templ_7745c5c3_Err712 return templ_7745c5c3_Err
667 }713 }
668 if r.CHref != "" {714 if r.CHref != "" {
669- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<a href=\"")715+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a href=\"")
670 if templ_7745c5c3_Err != nil {716 if templ_7745c5c3_Err != nil {
671 return templ_7745c5c3_Err717 return templ_7745c5c3_Err
672 }718 }
673- var templ_7745c5c3_Var30 templ.SafeURL719+ var templ_7745c5c3_Var32 templ.SafeURL
674- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))720+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
675 if templ_7745c5c3_Err != nil {721 if templ_7745c5c3_Err != nil {
676- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 157, Col: 36}722+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 165, Col: 36}
677 }723 }
678- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))724+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
679 if templ_7745c5c3_Err != nil {725 if templ_7745c5c3_Err != nil {
680 return templ_7745c5c3_Err726 return templ_7745c5c3_Err
681 }727 }
682- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">")728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">")
683 if templ_7745c5c3_Err != nil {729 if templ_7745c5c3_Err != nil {
684 return templ_7745c5c3_Err730 return templ_7745c5c3_Err
685 }731 }
686- var templ_7745c5c3_Var31 string732+ var templ_7745c5c3_Var33 string
687- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)733+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
688 if templ_7745c5c3_Err != nil {734 if templ_7745c5c3_Err != nil {
689- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 157, Col: 48}735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 165, Col: 48}
690 }736 }
691- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
692 if templ_7745c5c3_Err != nil {738 if templ_7745c5c3_Err != nil {
693 return templ_7745c5c3_Err739 return templ_7745c5c3_Err
694 }740 }
695- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</a>")741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</a>")
696 if templ_7745c5c3_Err != nil {742 if templ_7745c5c3_Err != nil {
697 return templ_7745c5c3_Err743 return templ_7745c5c3_Err
698 }744 }
699 } else {745 } else {
700- var templ_7745c5c3_Var32 string746+ var templ_7745c5c3_Var34 string
701- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)747+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
702 if templ_7745c5c3_Err != nil {748 if templ_7745c5c3_Err != nil {
703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 159, Col: 13}749+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 167, Col: 13}
704 }750 }
705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))751+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
706 if templ_7745c5c3_Err != nil {752 if templ_7745c5c3_Err != nil {
707 return templ_7745c5c3_Err753 return templ_7745c5c3_Err
708 }754 }
@@ -711,69 +757,69 @@ func refRow(kind string, r RefRow) templ.Component {
711 if templ_7745c5c3_Err != nil {757 if templ_7745c5c3_Err != nil {
712 return templ_7745c5c3_Err758 return templ_7745c5c3_Err
713 }759 }
714- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</td><td class=\"c-sub\">")760+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td><td class=\"c-sub\">")
715 if templ_7745c5c3_Err != nil {761 if templ_7745c5c3_Err != nil {
716 return templ_7745c5c3_Err762 return templ_7745c5c3_Err
717 }763 }
718- var templ_7745c5c3_Var33 string764+ var templ_7745c5c3_Var35 string
719- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)765+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
720 if templ_7745c5c3_Err != nil {766 if templ_7745c5c3_Err != nil {
721- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 163, Col: 31}767+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 171, Col: 31}
722 }768 }
723- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))769+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
724 if templ_7745c5c3_Err != nil {770 if templ_7745c5c3_Err != nil {
725 return templ_7745c5c3_Err771 return templ_7745c5c3_Err
726 }772 }
727- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</td><td class=\"c-when dim\">")773+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</td><td class=\"c-when dim\">")
728 if templ_7745c5c3_Err != nil {774 if templ_7745c5c3_Err != nil {
729 return templ_7745c5c3_Err775 return templ_7745c5c3_Err
730 }776 }
731 if r.WhenAt != "" {777 if r.WhenAt != "" {
732- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<time datetime=\"")778+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<time datetime=\"")
733 if templ_7745c5c3_Err != nil {779 if templ_7745c5c3_Err != nil {
734 return templ_7745c5c3_Err780 return templ_7745c5c3_Err
735 }781 }
736- var templ_7745c5c3_Var34 string782+ var templ_7745c5c3_Var36 string
737- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)783+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
738 if templ_7745c5c3_Err != nil {784 if templ_7745c5c3_Err != nil {
739- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 29}785+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 174, Col: 29}
740 }786 }
741- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)787+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
742 if templ_7745c5c3_Err != nil {788 if templ_7745c5c3_Err != nil {
743 return templ_7745c5c3_Err789 return templ_7745c5c3_Err
744 }790 }
745- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" title=\"")791+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" title=\"")
746 if templ_7745c5c3_Err != nil {792 if templ_7745c5c3_Err != nil {
747 return templ_7745c5c3_Err793 return templ_7745c5c3_Err
748 }794 }
749- var templ_7745c5c3_Var35 string795+ var templ_7745c5c3_Var37 string
750- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)796+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
751 if templ_7745c5c3_Err != nil {797 if templ_7745c5c3_Err != nil {
752- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 48}798+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 174, Col: 48}
753 }799 }
754- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)800+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
755 if templ_7745c5c3_Err != nil {801 if templ_7745c5c3_Err != nil {
756 return templ_7745c5c3_Err802 return templ_7745c5c3_Err
757 }803 }
758- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">")804+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
759 if templ_7745c5c3_Err != nil {805 if templ_7745c5c3_Err != nil {
760 return templ_7745c5c3_Err806 return templ_7745c5c3_Err
761 }807 }
762- var templ_7745c5c3_Var36 string808+ var templ_7745c5c3_Var38 string
763- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)809+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
764 if templ_7745c5c3_Err != nil {810 if templ_7745c5c3_Err != nil {
765- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 166, Col: 59}811+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 174, Col: 59}
766 }812 }
767- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))813+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
768 if templ_7745c5c3_Err != nil {814 if templ_7745c5c3_Err != nil {
769 return templ_7745c5c3_Err815 return templ_7745c5c3_Err
770 }816 }
771- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</time>")817+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</time>")
772 if templ_7745c5c3_Err != nil {818 if templ_7745c5c3_Err != nil {
773 return templ_7745c5c3_Err819 return templ_7745c5c3_Err
774 }820 }
775 }821 }
776- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td></tr>")822+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</td></tr>")
777 if templ_7745c5c3_Err != nil {823 if templ_7745c5c3_Err != nil {
778 return templ_7745c5c3_Err824 return templ_7745c5c3_Err
779 }825 }
@@ -797,12 +843,12 @@ func refsRail(p RefsPage) templ.Component {
797 }()843 }()
798 }844 }
799 ctx = templ.InitializeContext(ctx)845 ctx = templ.InitializeContext(ctx)
800- templ_7745c5c3_Var37 := templ.GetChildren(ctx)846+ templ_7745c5c3_Var39 := templ.GetChildren(ctx)
801- if templ_7745c5c3_Var37 == nil {847+ if templ_7745c5c3_Var39 == nil {
802- templ_7745c5c3_Var37 = templ.NopComponent848+ templ_7745c5c3_Var39 = templ.NopComponent
803 }849 }
804 ctx = templ.ClearChildren(ctx)850 ctx = templ.ClearChildren(ctx)
805- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")851+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<section aria-labelledby=\"r-refs\"><h2 id=\"r-refs\">Overview</h2>")
806 if templ_7745c5c3_Err != nil {852 if templ_7745c5c3_Err != nil {
807 return templ_7745c5c3_Err853 return templ_7745c5c3_Err
808 }854 }
@@ -810,7 +856,7 @@ func refsRail(p RefsPage) templ.Component {
810 if templ_7745c5c3_Err != nil {856 if templ_7745c5c3_Err != nil {
811 return templ_7745c5c3_Err857 return templ_7745c5c3_Err
812 }858 }
813- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</section>")859+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</section>")
814 if templ_7745c5c3_Err != nil {860 if templ_7745c5c3_Err != nil {
815 return templ_7745c5c3_Err861 return templ_7745c5c3_Err
816 }862 }
@@ -835,12 +881,12 @@ func State(l Layout, p StatePage) templ.Component {
835 }()881 }()
836 }882 }
837 ctx = templ.InitializeContext(ctx)883 ctx = templ.InitializeContext(ctx)
838- templ_7745c5c3_Var38 := templ.GetChildren(ctx)884+ templ_7745c5c3_Var40 := templ.GetChildren(ctx)
839- if templ_7745c5c3_Var38 == nil {885+ if templ_7745c5c3_Var40 == nil {
840- templ_7745c5c3_Var38 = templ.NopComponent886+ templ_7745c5c3_Var40 = templ.NopComponent
841 }887 }
842 ctx = templ.ClearChildren(ctx)888 ctx = templ.ClearChildren(ctx)
843- templ_7745c5c3_Var39 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {889+ templ_7745c5c3_Var41 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
844 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context890 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
845 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)891 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
846 if !templ_7745c5c3_IsBuffer {892 if !templ_7745c5c3_IsBuffer {
@@ -852,51 +898,51 @@ func State(l Layout, p StatePage) templ.Component {
852 }()898 }()
853 }899 }
854 ctx = templ.InitializeContext(ctx)900 ctx = templ.InitializeContext(ctx)
855- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")901+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<section class=\"state\" aria-labelledby=\"state-title\"><p class=\"eyebrow\">")
856 if templ_7745c5c3_Err != nil {902 if templ_7745c5c3_Err != nil {
857 return templ_7745c5c3_Err903 return templ_7745c5c3_Err
858 }904 }
859- var templ_7745c5c3_Var40 string905+ var templ_7745c5c3_Var42 string
860- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)906+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(p.Code)
861 if templ_7745c5c3_Err != nil {907 if templ_7745c5c3_Err != nil {
862- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 183, Col: 30}908+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 191, Col: 30}
863 }909 }
864- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))910+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
865 if templ_7745c5c3_Err != nil {911 if templ_7745c5c3_Err != nil {
866 return templ_7745c5c3_Err912 return templ_7745c5c3_Err
867 }913 }
868- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</p><h1 id=\"state-title\">")914+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</p><h1 id=\"state-title\">")
869 if templ_7745c5c3_Err != nil {915 if templ_7745c5c3_Err != nil {
870 return templ_7745c5c3_Err916 return templ_7745c5c3_Err
871 }917 }
872- var templ_7745c5c3_Var41 string918+ var templ_7745c5c3_Var43 string
873- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)919+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
874 if templ_7745c5c3_Err != nil {920 if templ_7745c5c3_Err != nil {
875- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 184, Col: 33}921+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 192, Col: 33}
876 }922 }
877- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))923+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
878 if templ_7745c5c3_Err != nil {924 if templ_7745c5c3_Err != nil {
879 return templ_7745c5c3_Err925 return templ_7745c5c3_Err
880 }926 }
881- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</h1>")927+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</h1>")
882 if templ_7745c5c3_Err != nil {928 if templ_7745c5c3_Err != nil {
883 return templ_7745c5c3_Err929 return templ_7745c5c3_Err
884 }930 }
885 for _, line := range p.Lines {931 for _, line := range p.Lines {
886- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<p class=\"dim\">")932+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<p class=\"dim\">")
887 if templ_7745c5c3_Err != nil {933 if templ_7745c5c3_Err != nil {
888 return templ_7745c5c3_Err934 return templ_7745c5c3_Err
889 }935 }
890- var templ_7745c5c3_Var42 string936+ var templ_7745c5c3_Var44 string
891- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(line)937+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(line)
892 if templ_7745c5c3_Err != nil {938 if templ_7745c5c3_Err != nil {
893- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 186, Col: 25}939+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 194, Col: 25}
894 }940 }
895- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))941+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
896 if templ_7745c5c3_Err != nil {942 if templ_7745c5c3_Err != nil {
897 return templ_7745c5c3_Err943 return templ_7745c5c3_Err
898 }944 }
899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</p>")945+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</p>")
900 if templ_7745c5c3_Err != nil {946 if templ_7745c5c3_Err != nil {
901 return templ_7745c5c3_Err947 return templ_7745c5c3_Err
902 }948 }
@@ -907,49 +953,49 @@ func State(l Layout, p StatePage) templ.Component {
907 return templ_7745c5c3_Err953 return templ_7745c5c3_Err
908 }954 }
909 }955 }
910- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<p class=\"state-back\">")956+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<p class=\"state-back\">")
911 if templ_7745c5c3_Err != nil {957 if templ_7745c5c3_Err != nil {
912 return templ_7745c5c3_Err958 return templ_7745c5c3_Err
913 }959 }
914 for _, b := range p.Back {960 for _, b := range p.Back {
915- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<a href=\"")961+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<a href=\"")
916 if templ_7745c5c3_Err != nil {962 if templ_7745c5c3_Err != nil {
917 return templ_7745c5c3_Err963 return templ_7745c5c3_Err
918 }964 }
919- var templ_7745c5c3_Var43 templ.SafeURL965+ var templ_7745c5c3_Var45 templ.SafeURL
920- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))966+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
921 if templ_7745c5c3_Err != nil {967 if templ_7745c5c3_Err != nil {
922- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 193, Col: 36}968+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 201, Col: 36}
923 }969 }
924- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))970+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
925 if templ_7745c5c3_Err != nil {971 if templ_7745c5c3_Err != nil {
926 return templ_7745c5c3_Err972 return templ_7745c5c3_Err
927 }973 }
928- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\">")974+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\">")
929 if templ_7745c5c3_Err != nil {975 if templ_7745c5c3_Err != nil {
930 return templ_7745c5c3_Err976 return templ_7745c5c3_Err
931 }977 }
932- var templ_7745c5c3_Var44 string978+ var templ_7745c5c3_Var46 string
933- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)979+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(b.Label)
934 if templ_7745c5c3_Err != nil {980 if templ_7745c5c3_Err != nil {
935- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 193, Col: 48}981+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 201, Col: 48}
936 }982 }
937- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))983+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
938 if templ_7745c5c3_Err != nil {984 if templ_7745c5c3_Err != nil {
939 return templ_7745c5c3_Err985 return templ_7745c5c3_Err
940 }986 }
941- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</a>")987+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</a>")
942 if templ_7745c5c3_Err != nil {988 if templ_7745c5c3_Err != nil {
943 return templ_7745c5c3_Err989 return templ_7745c5c3_Err
944 }990 }
945 }991 }
946- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</p></section>")992+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</p></section>")
947 if templ_7745c5c3_Err != nil {993 if templ_7745c5c3_Err != nil {
948 return templ_7745c5c3_Err994 return templ_7745c5c3_Err
949 }995 }
950 return nil996 return nil
951 })997 })
952- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer)998+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var41), templ_7745c5c3_Buffer)
953 if templ_7745c5c3_Err != nil {999 if templ_7745c5c3_Err != nil {
954 return templ_7745c5c3_Err1000 return templ_7745c5c3_Err
955 }1001 }
@@ -978,12 +1024,12 @@ func Login(l Layout, p LoginPage) templ.Component {
978 }()1024 }()
979 }1025 }
980 ctx = templ.InitializeContext(ctx)1026 ctx = templ.InitializeContext(ctx)
981- templ_7745c5c3_Var45 := templ.GetChildren(ctx)1027+ templ_7745c5c3_Var47 := templ.GetChildren(ctx)
982- if templ_7745c5c3_Var45 == nil {1028+ if templ_7745c5c3_Var47 == nil {
983- templ_7745c5c3_Var45 = templ.NopComponent1029+ templ_7745c5c3_Var47 = templ.NopComponent
984 }1030 }
985 ctx = templ.ClearChildren(ctx)1031 ctx = templ.ClearChildren(ctx)
986- templ_7745c5c3_Var46 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1032+ templ_7745c5c3_Var48 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
987 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1033 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
988 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1034 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
989 if !templ_7745c5c3_IsBuffer {1035 if !templ_7745c5c3_IsBuffer {
@@ -995,75 +1041,75 @@ func Login(l Layout, p LoginPage) templ.Component {
995 }()1041 }()
996 }1042 }
997 ctx = templ.InitializeContext(ctx)1043 ctx = templ.InitializeContext(ctx)
998- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")1044+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
999 if templ_7745c5c3_Err != nil {1045 if templ_7745c5c3_Err != nil {
1000 return templ_7745c5c3_Err1046 return templ_7745c5c3_Err
1001 }1047 }
1002- var templ_7745c5c3_Var47 string1048+ var templ_7745c5c3_Var49 string
1003- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)1049+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1004 if templ_7745c5c3_Err != nil {1050 if templ_7745c5c3_Err != nil {
1005- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 208, Col: 33}1051+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 216, Col: 33}
1006 }1052 }
1007- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))1053+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
1008 if templ_7745c5c3_Err != nil {1054 if templ_7745c5c3_Err != nil {
1009 return templ_7745c5c3_Err1055 return templ_7745c5c3_Err
1010 }1056 }
1011- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</h1>")1057+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</h1>")
1012 if templ_7745c5c3_Err != nil {1058 if templ_7745c5c3_Err != nil {
1013 return templ_7745c5c3_Err1059 return templ_7745c5c3_Err
1014 }1060 }
1015 for _, line := range p.Lines {1061 for _, line := range p.Lines {
1016- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"dim\">")1062+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<p class=\"dim\">")
1017 if templ_7745c5c3_Err != nil {1063 if templ_7745c5c3_Err != nil {
1018 return templ_7745c5c3_Err1064 return templ_7745c5c3_Err
1019 }1065 }
1020- var templ_7745c5c3_Var48 string1066+ var templ_7745c5c3_Var50 string
1021- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(line)1067+ templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1022 if templ_7745c5c3_Err != nil {1068 if templ_7745c5c3_Err != nil {
1023- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 210, Col: 25}1069+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 218, Col: 25}
1024 }1070 }
1025- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))1071+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
1026 if templ_7745c5c3_Err != nil {1072 if templ_7745c5c3_Err != nil {
1027 return templ_7745c5c3_Err1073 return templ_7745c5c3_Err
1028 }1074 }
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</p>")1075+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</p>")
1030 if templ_7745c5c3_Err != nil {1076 if templ_7745c5c3_Err != nil {
1031 return templ_7745c5c3_Err1077 return templ_7745c5c3_Err
1032 }1078 }
1033 }1079 }
1034- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<form class=\"state-act\" method=\"post\" action=\"")1080+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<form class=\"state-act\" method=\"post\" action=\"")
1035 if templ_7745c5c3_Err != nil {1081 if templ_7745c5c3_Err != nil {
1036 return templ_7745c5c3_Err1082 return templ_7745c5c3_Err
1037 }1083 }
1038- var templ_7745c5c3_Var49 templ.SafeURL1084+ var templ_7745c5c3_Var51 templ.SafeURL
1039- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))1085+ templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Action))
1040 if templ_7745c5c3_Err != nil {1086 if templ_7745c5c3_Err != nil {
1041- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 212, Col: 73}1087+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 220, Col: 73}
1042 }1088 }
1043- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))1089+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
1044 if templ_7745c5c3_Err != nil {1090 if templ_7745c5c3_Err != nil {
1045 return templ_7745c5c3_Err1091 return templ_7745c5c3_Err
1046 }1092 }
1047- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")1093+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" data-autosubmit><button type=\"submit\" class=\"btn primary\">")
1048 if templ_7745c5c3_Err != nil {1094 if templ_7745c5c3_Err != nil {
1049 return templ_7745c5c3_Err1095 return templ_7745c5c3_Err
1050 }1096 }
1051- var templ_7745c5c3_Var50 string1097+ var templ_7745c5c3_Var52 string
1052- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)1098+ templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1053 if templ_7745c5c3_Err != nil {1099 if templ_7745c5c3_Err != nil {
1054- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 213, Col: 56}1100+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 221, Col: 56}
1055 }1101 }
1056- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))1102+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1057 if templ_7745c5c3_Err != nil {1103 if templ_7745c5c3_Err != nil {
1058 return templ_7745c5c3_Err1104 return templ_7745c5c3_Err
1059 }1105 }
1060- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")1106+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</button></form><p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1061 if templ_7745c5c3_Err != nil {1107 if templ_7745c5c3_Err != nil {
1062 return templ_7745c5c3_Err1108 return templ_7745c5c3_Err
1063 }1109 }
1064 return nil1110 return nil
1065 })1111 })
1066- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var46), templ_7745c5c3_Buffer)1112+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var48), templ_7745c5c3_Buffer)
1067 if templ_7745c5c3_Err != nil {1113 if templ_7745c5c3_Err != nil {
1068 return templ_7745c5c3_Err1114 return templ_7745c5c3_Err
1069 }1115 }
@@ -1089,12 +1135,12 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
1089 }()1135 }()
1090 }1136 }
1091 ctx = templ.InitializeContext(ctx)1137 ctx = templ.InitializeContext(ctx)
1092- templ_7745c5c3_Var51 := templ.GetChildren(ctx)1138+ templ_7745c5c3_Var53 := templ.GetChildren(ctx)
1093- if templ_7745c5c3_Var51 == nil {1139+ if templ_7745c5c3_Var53 == nil {
1094- templ_7745c5c3_Var51 = templ.NopComponent1140+ templ_7745c5c3_Var53 = templ.NopComponent
1095 }1141 }
1096 ctx = templ.ClearChildren(ctx)1142 ctx = templ.ClearChildren(ctx)
1097- templ_7745c5c3_Var52 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1143+ templ_7745c5c3_Var54 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1098 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1144 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
1099 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1145 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
1100 if !templ_7745c5c3_IsBuffer {1146 if !templ_7745c5c3_IsBuffer {
@@ -1106,88 +1152,88 @@ func PasskeyLogin(l Layout, p PasskeyLoginPage) templ.Component {
1106 }()1152 }()
1107 }1153 }
1108 ctx = templ.InitializeContext(ctx)1154 ctx = templ.InitializeContext(ctx)
1109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")1155+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<section class=\"state\" aria-labelledby=\"state-title\"><h1 id=\"state-title\">")
1110 if templ_7745c5c3_Err != nil {1156 if templ_7745c5c3_Err != nil {
1111 return templ_7745c5c3_Err1157 return templ_7745c5c3_Err
1112 }1158 }
1113- var templ_7745c5c3_Var53 string1159+ var templ_7745c5c3_Var55 string
1114- templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)1160+ templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
1115 if templ_7745c5c3_Err != nil {1161 if templ_7745c5c3_Err != nil {
1116- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 225, Col: 33}1162+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 233, Col: 33}
1117 }1163 }
1118- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))1164+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
1119 if templ_7745c5c3_Err != nil {1165 if templ_7745c5c3_Err != nil {
1120 return templ_7745c5c3_Err1166 return templ_7745c5c3_Err
1121 }1167 }
1122- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")1168+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</h1><form class=\"state-act\" data-js data-passkey-form><button type=\"submit\" class=\"btn primary\" data-passkey=\"login\" data-begin=\"")
1123 if templ_7745c5c3_Err != nil {1169 if templ_7745c5c3_Err != nil {
1124 return templ_7745c5c3_Err1170 return templ_7745c5c3_Err
1125 }1171 }
1126- var templ_7745c5c3_Var54 string1172+ var templ_7745c5c3_Var56 string
1127- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)1173+ templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1128 if templ_7745c5c3_Err != nil {1174 if templ_7745c5c3_Err != nil {
1129- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 227, Col: 87}1175+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 87}
1130 }1176 }
1131- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54)1177+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56)
1132 if templ_7745c5c3_Err != nil {1178 if templ_7745c5c3_Err != nil {
1133 return templ_7745c5c3_Err1179 return templ_7745c5c3_Err
1134 }1180 }
1135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" data-finish=\"")1181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" data-finish=\"")
1136 if templ_7745c5c3_Err != nil {1182 if templ_7745c5c3_Err != nil {
1137 return templ_7745c5c3_Err1183 return templ_7745c5c3_Err
1138 }1184 }
1139- var templ_7745c5c3_Var55 string1185+ var templ_7745c5c3_Var57 string
1140- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)1186+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1141 if templ_7745c5c3_Err != nil {1187 if templ_7745c5c3_Err != nil {
1142- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 227, Col: 112}1188+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 112}
1143 }1189 }
1144- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var55)1190+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57)
1145 if templ_7745c5c3_Err != nil {1191 if templ_7745c5c3_Err != nil {
1146 return templ_7745c5c3_Err1192 return templ_7745c5c3_Err
1147 }1193 }
1148- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\">")1194+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\">")
1149 if templ_7745c5c3_Err != nil {1195 if templ_7745c5c3_Err != nil {
1150 return templ_7745c5c3_Err1196 return templ_7745c5c3_Err
1151 }1197 }
1152- var templ_7745c5c3_Var56 string1198+ var templ_7745c5c3_Var58 string
1153- templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)1199+ templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(p.Button)
1154 if templ_7745c5c3_Err != nil {1200 if templ_7745c5c3_Err != nil {
1155- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 227, Col: 125}1201+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 235, Col: 125}
1156 }1202 }
1157- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))1203+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
1158 if templ_7745c5c3_Err != nil {1204 if templ_7745c5c3_Err != nil {
1159 return templ_7745c5c3_Err1205 return templ_7745c5c3_Err
1160 }1206 }
1161- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")1207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</button></form><p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1162 if templ_7745c5c3_Err != nil {1208 if templ_7745c5c3_Err != nil {
1163 return templ_7745c5c3_Err1209 return templ_7745c5c3_Err
1164 }1210 }
1165 for _, line := range p.Lines {1211 for _, line := range p.Lines {
1166- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"dim\">")1212+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<p class=\"dim\">")
1167 if templ_7745c5c3_Err != nil {1213 if templ_7745c5c3_Err != nil {
1168 return templ_7745c5c3_Err1214 return templ_7745c5c3_Err
1169 }1215 }
1170- var templ_7745c5c3_Var57 string1216+ var templ_7745c5c3_Var59 string
1171- templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(line)1217+ templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(line)
1172 if templ_7745c5c3_Err != nil {1218 if templ_7745c5c3_Err != nil {
1173- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 231, Col: 25}1219+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 239, Col: 25}
1174 }1220 }
1175- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))1221+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
1176 if templ_7745c5c3_Err != nil {1222 if templ_7745c5c3_Err != nil {
1177 return templ_7745c5c3_Err1223 return templ_7745c5c3_Err
1178 }1224 }
1179- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</p>")1225+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</p>")
1180 if templ_7745c5c3_Err != nil {1226 if templ_7745c5c3_Err != nil {
1181 return templ_7745c5c3_Err1227 return templ_7745c5c3_Err
1182 }1228 }
1183 }1229 }
1184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")1230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<p class=\"state-back\"><a href=\"/\">Back to home</a></p></section>")
1185 if templ_7745c5c3_Err != nil {1231 if templ_7745c5c3_Err != nil {
1186 return templ_7745c5c3_Err1232 return templ_7745c5c3_Err
1187 }1233 }
1188 return nil1234 return nil
1189 })1235 })
1190- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var52), templ_7745c5c3_Buffer)1236+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var54), templ_7745c5c3_Buffer)
1191 if templ_7745c5c3_Err != nil {1237 if templ_7745c5c3_Err != nil {
1192 return templ_7745c5c3_Err1238 return templ_7745c5c3_Err
1193 }1239 }
@@ -1212,12 +1258,12 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
1212 }()1258 }()
1213 }1259 }
1214 ctx = templ.InitializeContext(ctx)1260 ctx = templ.InitializeContext(ctx)
1215- templ_7745c5c3_Var58 := templ.GetChildren(ctx)1261+ templ_7745c5c3_Var60 := templ.GetChildren(ctx)
1216- if templ_7745c5c3_Var58 == nil {1262+ if templ_7745c5c3_Var60 == nil {
1217- templ_7745c5c3_Var58 = templ.NopComponent1263+ templ_7745c5c3_Var60 = templ.NopComponent
1218 }1264 }
1219 ctx = templ.ClearChildren(ctx)1265 ctx = templ.ClearChildren(ctx)
1220- templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1266+ templ_7745c5c3_Var61 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1221 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1267 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
1222 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)1268 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
1223 if !templ_7745c5c3_IsBuffer {1269 if !templ_7745c5c3_IsBuffer {
@@ -1229,231 +1275,231 @@ func Passkeys(l Layout, p PasskeysPage) templ.Component {
1229 }()1275 }()
1230 }1276 }
1231 ctx = templ.InitializeContext(ctx)1277 ctx = templ.InitializeContext(ctx)
1232- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")1278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<h2 class=\"rule-h\"><span>Passkeys</span><span class=\"count\">")
1233 if templ_7745c5c3_Err != nil {1279 if templ_7745c5c3_Err != nil {
1234 return templ_7745c5c3_Err1280 return templ_7745c5c3_Err
1235 }1281 }
1236- var templ_7745c5c3_Var60 string1282+ var templ_7745c5c3_Var62 string
1237- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))1283+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Rows)))
1238 if templ_7745c5c3_Err != nil {1284 if templ_7745c5c3_Err != nil {
1239- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 241, Col: 81}1285+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 249, Col: 81}
1240 }1286 }
1241- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))1287+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1242 if templ_7745c5c3_Err != nil {1288 if templ_7745c5c3_Err != nil {
1243 return templ_7745c5c3_Err1289 return templ_7745c5c3_Err
1244 }1290 }
1245- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")1291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</span></h2><p class=\"dim\">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>")
1246 if templ_7745c5c3_Err != nil {1292 if templ_7745c5c3_Err != nil {
1247 return templ_7745c5c3_Err1293 return templ_7745c5c3_Err
1248 }1294 }
1249 if len(p.Rows) > 0 {1295 if len(p.Rows) > 0 {
1250- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")1296+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<table class=\"list passkeys\" aria-label=\"Passkeys\"><thead><tr><th class=\"c-name\">Name</th><th class=\"c-when\">Created</th><th class=\"c-when\">Last used</th><th class=\"c-sync\">Synced</th><th class=\"c-act\"><span class=\"sr\">Remove</span></th></tr></thead> <tbody>")
1251 if templ_7745c5c3_Err != nil {1297 if templ_7745c5c3_Err != nil {
1252 return templ_7745c5c3_Err1298 return templ_7745c5c3_Err
1253 }1299 }
1254 for _, r := range p.Rows {1300 for _, r := range p.Rows {
1255- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<tr><td class=\"c-name\">")1301+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<tr><td class=\"c-name\">")
1256 if templ_7745c5c3_Err != nil {1302 if templ_7745c5c3_Err != nil {
1257 return templ_7745c5c3_Err1303 return templ_7745c5c3_Err
1258 }1304 }
1259- var templ_7745c5c3_Var61 string1305+ var templ_7745c5c3_Var63 string
1260- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)1306+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
1261 if templ_7745c5c3_Err != nil {1307 if templ_7745c5c3_Err != nil {
1262- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 257, Col: 34}1308+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 265, Col: 34}
1263 }1309 }
1264- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))1310+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
1265 if templ_7745c5c3_Err != nil {1311 if templ_7745c5c3_Err != nil {
1266 return templ_7745c5c3_Err1312 return templ_7745c5c3_Err
1267 }1313 }
1268- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</td><td class=\"c-when nowrap\"><time")1314+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</td><td class=\"c-when nowrap\"><time")
1269 if templ_7745c5c3_Err != nil {1315 if templ_7745c5c3_Err != nil {
1270 return templ_7745c5c3_Err1316 return templ_7745c5c3_Err
1271 }1317 }
1272 if r.CreatedAt != "" {1318 if r.CreatedAt != "" {
1273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " datetime=\"")1319+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, " datetime=\"")
1274 if templ_7745c5c3_Err != nil {1320 if templ_7745c5c3_Err != nil {
1275 return templ_7745c5c3_Err1321 return templ_7745c5c3_Err
1276 }1322 }
1277- var templ_7745c5c3_Var62 string1323+ var templ_7745c5c3_Var64 string
1278- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)1324+ templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.CreatedAt)
1279 if templ_7745c5c3_Err != nil {1325 if templ_7745c5c3_Err != nil {
1280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 261, Col: 32}1326+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 269, Col: 32}
1281 }1327 }
1282- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)1328+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
1283 if templ_7745c5c3_Err != nil {1329 if templ_7745c5c3_Err != nil {
1284 return templ_7745c5c3_Err1330 return templ_7745c5c3_Err
1285 }1331 }
1286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\"")1332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\"")
1287 if templ_7745c5c3_Err != nil {1333 if templ_7745c5c3_Err != nil {
1288 return templ_7745c5c3_Err1334 return templ_7745c5c3_Err
1289 }1335 }
1290 }1336 }
1291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, ">")1337+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, ">")
1292 if templ_7745c5c3_Err != nil {1338 if templ_7745c5c3_Err != nil {
1293 return templ_7745c5c3_Err1339 return templ_7745c5c3_Err
1294 }1340 }
1295- var templ_7745c5c3_Var63 string1341+ var templ_7745c5c3_Var65 string
1296- templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)1342+ templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(r.Created)
1297 if templ_7745c5c3_Err != nil {1343 if templ_7745c5c3_Err != nil {
1298- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 263, Col: 20}1344+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 271, Col: 20}
1299 }1345 }
1300- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))1346+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
1301 if templ_7745c5c3_Err != nil {1347 if templ_7745c5c3_Err != nil {
1302 return templ_7745c5c3_Err1348 return templ_7745c5c3_Err
1303 }1349 }
1304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</time></td><td class=\"c-when nowrap\"><time")1350+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</time></td><td class=\"c-when nowrap\"><time")
1305 if templ_7745c5c3_Err != nil {1351 if templ_7745c5c3_Err != nil {
1306 return templ_7745c5c3_Err1352 return templ_7745c5c3_Err
1307 }1353 }
1308 if r.LastUsedAt != "" {1354 if r.LastUsedAt != "" {
1309- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " datetime=\"")1355+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " datetime=\"")
1310 if templ_7745c5c3_Err != nil {1356 if templ_7745c5c3_Err != nil {
1311 return templ_7745c5c3_Err1357 return templ_7745c5c3_Err
1312 }1358 }
1313- var templ_7745c5c3_Var64 string1359+ var templ_7745c5c3_Var66 string
1314- templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)1360+ templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.LastUsedAt)
1315 if templ_7745c5c3_Err != nil {1361 if templ_7745c5c3_Err != nil {
1316- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 268, Col: 33}1362+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 276, Col: 33}
1317 }1363 }
1318- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)1364+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
1319 if templ_7745c5c3_Err != nil {1365 if templ_7745c5c3_Err != nil {
1320 return templ_7745c5c3_Err1366 return templ_7745c5c3_Err
1321 }1367 }
1322- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\"")1368+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"")
1323 if templ_7745c5c3_Err != nil {1369 if templ_7745c5c3_Err != nil {
1324 return templ_7745c5c3_Err1370 return templ_7745c5c3_Err
1325 }1371 }
1326 }1372 }
1327- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, ">")1373+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, ">")
1328 if templ_7745c5c3_Err != nil {1374 if templ_7745c5c3_Err != nil {
1329 return templ_7745c5c3_Err1375 return templ_7745c5c3_Err
1330 }1376 }
1331- var templ_7745c5c3_Var65 string1377+ var templ_7745c5c3_Var67 string
1332- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)1378+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(r.LastUsed)
1333 if templ_7745c5c3_Err != nil {1379 if templ_7745c5c3_Err != nil {
1334- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 270, Col: 21}1380+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 278, Col: 21}
1335 }1381 }
1336- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))1382+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
1337 if templ_7745c5c3_Err != nil {1383 if templ_7745c5c3_Err != nil {
1338 return templ_7745c5c3_Err1384 return templ_7745c5c3_Err
1339 }1385 }
1340- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</time></td><td class=\"c-sync\">")1386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</time></td><td class=\"c-sync\">")
1341 if templ_7745c5c3_Err != nil {1387 if templ_7745c5c3_Err != nil {
1342 return templ_7745c5c3_Err1388 return templ_7745c5c3_Err
1343 }1389 }
1344 if r.Synced {1390 if r.Synced {
1345- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "yes")1391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "yes")
1346 if templ_7745c5c3_Err != nil {1392 if templ_7745c5c3_Err != nil {
1347 return templ_7745c5c3_Err1393 return templ_7745c5c3_Err
1348 }1394 }
1349 } else {1395 } else {
1350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "no")1396+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "no")
1351 if templ_7745c5c3_Err != nil {1397 if templ_7745c5c3_Err != nil {
1352 return templ_7745c5c3_Err1398 return templ_7745c5c3_Err
1353 }1399 }
1354 }1400 }
1355- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</td><td class=\"c-act\"><form method=\"post\" action=\"")1401+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</td><td class=\"c-act\"><form method=\"post\" action=\"")
1356 if templ_7745c5c3_Err != nil {1402 if templ_7745c5c3_Err != nil {
1357 return templ_7745c5c3_Err1403 return templ_7745c5c3_Err
1358 }1404 }
1359- var templ_7745c5c3_Var66 templ.SafeURL1405+ var templ_7745c5c3_Var68 templ.SafeURL
1360- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))1406+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Remove))
1361 if templ_7745c5c3_Err != nil {1407 if templ_7745c5c3_Err != nil {
1362- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 280, Col: 60}1408+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 288, Col: 60}
1363 }1409 }
1364- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))1410+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1365 if templ_7745c5c3_Err != nil {1411 if templ_7745c5c3_Err != nil {
1366 return templ_7745c5c3_Err1412 return templ_7745c5c3_Err
1367 }1413 }
1368- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")1414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\"><button type=\"submit\" class=\"btn danger\" aria-label=\"")
1369 if templ_7745c5c3_Err != nil {1415 if templ_7745c5c3_Err != nil {
1370 return templ_7745c5c3_Err1416 return templ_7745c5c3_Err
1371 }1417 }
1372- var templ_7745c5c3_Var67 string1418+ var templ_7745c5c3_Var69 string
1373- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)1419+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove passkey " + r.Name)
1374 if templ_7745c5c3_Err != nil {1420 if templ_7745c5c3_Err != nil {
1375- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 281, Col: 89}1421+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 289, Col: 89}
1376 }1422 }
1377- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)1423+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
1378 if templ_7745c5c3_Err != nil {1424 if templ_7745c5c3_Err != nil {
1379 return templ_7745c5c3_Err1425 return templ_7745c5c3_Err
1380 }1426 }
1381- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\">Remove</button></form></td></tr>")1427+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\">Remove</button></form></td></tr>")
1382 if templ_7745c5c3_Err != nil {1428 if templ_7745c5c3_Err != nil {
1383 return templ_7745c5c3_Err1429 return templ_7745c5c3_Err
1384 }1430 }
1385 }1431 }
1386- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</tbody></table>")1432+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</tbody></table>")
1387 if templ_7745c5c3_Err != nil {1433 if templ_7745c5c3_Err != nil {
1388 return templ_7745c5c3_Err1434 return templ_7745c5c3_Err
1389 }1435 }
1390 } else {1436 } else {
1391- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"lede dim\">no passkeys yet.</p>")1437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<p class=\"lede dim\">no passkeys yet.</p>")
1392 if templ_7745c5c3_Err != nil {1438 if templ_7745c5c3_Err != nil {
1393 return templ_7745c5c3_Err1439 return templ_7745c5c3_Err
1394 }1440 }
1395 }1441 }
1396- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " ")1442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " ")
1397 if templ_7745c5c3_Err != nil {1443 if templ_7745c5c3_Err != nil {
1398 return templ_7745c5c3_Err1444 return templ_7745c5c3_Err
1399 }1445 }
1400 if p.Full {1446 if p.Full {
1401- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<p class=\"dim\">")1447+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<p class=\"dim\">")
1402 if templ_7745c5c3_Err != nil {1448 if templ_7745c5c3_Err != nil {
1403 return templ_7745c5c3_Err1449 return templ_7745c5c3_Err
1404 }1450 }
1405- var templ_7745c5c3_Var68 string1451+ var templ_7745c5c3_Var70 string
1406- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))1452+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Max))
1407 if templ_7745c5c3_Err != nil {1453 if templ_7745c5c3_Err != nil {
1408- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 292, Col: 31}1454+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 300, Col: 31}
1409 }1455 }
1410- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))1456+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1411 if templ_7745c5c3_Err != nil {1457 if templ_7745c5c3_Err != nil {
1412 return templ_7745c5c3_Err1458 return templ_7745c5c3_Err
1413 }1459 }
1414- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " passkeys are registered, the most there can be. Remove one to add another.</p>")1460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, " passkeys are registered, the most there can be. Remove one to add another.</p>")
1415 if templ_7745c5c3_Err != nil {1461 if templ_7745c5c3_Err != nil {
1416 return templ_7745c5c3_Err1462 return templ_7745c5c3_Err
1417 }1463 }
1418 } else {1464 } else {
1419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")1465+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<form class=\"passkey-add\" data-js data-passkey-form><label for=\"passkey-name\" class=\"sr\">Name of the new passkey</label> <input id=\"passkey-name\" type=\"text\" placeholder=\"Name, for example Pixel 9\" autocomplete=\"off\" data-passkey-name> <button type=\"submit\" class=\"btn primary\" data-passkey=\"register\" data-begin=\"")
1420 if templ_7745c5c3_Err != nil {1466 if templ_7745c5c3_Err != nil {
1421 return templ_7745c5c3_Err1467 return templ_7745c5c3_Err
1422 }1468 }
1423- var templ_7745c5c3_Var69 string1469+ var templ_7745c5c3_Var71 string
1424- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)1470+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Begin)
1425 if templ_7745c5c3_Err != nil {1471 if templ_7745c5c3_Err != nil {
1426- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 297, Col: 90}1472+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 90}
1427 }1473 }
1428- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)1474+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
1429 if templ_7745c5c3_Err != nil {1475 if templ_7745c5c3_Err != nil {
1430 return templ_7745c5c3_Err1476 return templ_7745c5c3_Err
1431 }1477 }
1432- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "\" data-finish=\"")1478+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" data-finish=\"")
1433 if templ_7745c5c3_Err != nil {1479 if templ_7745c5c3_Err != nil {
1434 return templ_7745c5c3_Err1480 return templ_7745c5c3_Err
1435 }1481 }
1436- var templ_7745c5c3_Var70 string1482+ var templ_7745c5c3_Var72 string
1437- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)1483+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Finish)
1438 if templ_7745c5c3_Err != nil {1484 if templ_7745c5c3_Err != nil {
1439- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 297, Col: 115}1485+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/pages.templ`, Line: 305, Col: 115}
1440 }1486 }
1441- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70)1487+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
1442 if templ_7745c5c3_Err != nil {1488 if templ_7745c5c3_Err != nil {
1443 return templ_7745c5c3_Err1489 return templ_7745c5c3_Err
1444 }1490 }
1445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">Add a passkey</button></form>")1491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">Add a passkey</button></form>")
1446 if templ_7745c5c3_Err != nil {1492 if templ_7745c5c3_Err != nil {
1447 return templ_7745c5c3_Err1493 return templ_7745c5c3_Err
1448 }1494 }
1449 }1495 }
1450- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")1496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, " <p class=\"dim\" role=\"status\" aria-live=\"polite\" data-passkey-status></p>")
1451 if templ_7745c5c3_Err != nil {1497 if templ_7745c5c3_Err != nil {
1452 return templ_7745c5c3_Err1498 return templ_7745c5c3_Err
1453 }1499 }
1454 return nil1500 return nil
1455 })1501 })
1456- templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer)1502+ templ_7745c5c3_Err = Shell(l, nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var61), templ_7745c5c3_Buffer)
1457 if templ_7745c5c3_Err != nil {1503 if templ_7745c5c3_Err != nil {
1458 return templ_7745c5c3_Err1504 return templ_7745c5c3_Err
1459 }1505 }
M internal/adapters/web/static/css/60-mr.css +9 -0
@@ -186,6 +186,15 @@
186186 }
187187 #mr-notice:empty { display: none; }
188188
189+ /* the owner's inbox: counters, repository filter, then the two lists */
190+ .inbox {
191+ display: flex; flex-direction: column; gap: var(--s4);
192+ & .inbox-counts { font-size: var(--fs-l, var(--fs)); color: var(--ink-dim); }
193+ & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); }
194+ & .since { align-self: flex-start; max-width: 100%; }
195+ }
196+ .inbox-btn { gap: var(--s2); }
197+
189198 @media (max-width: 700px) {
190199 .list.mrs .c-who { display: none; }
191200 }
@@ -186,6 +186,15 @@
186 }186 }
187 #mr-notice:empty { display: none; }187 #mr-notice:empty { display: none; }
188 188
189+ /* the owner's inbox: counters, repository filter, then the two lists */
190+ .inbox {
191+ display: flex; flex-direction: column; gap: var(--s4);
192+ & .inbox-counts { font-size: var(--fs-l, var(--fs)); color: var(--ink-dim); }
193+ & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); }
194+ & .since { align-self: flex-start; max-width: 100%; }
195+ }
196+ .inbox-btn { gap: var(--s2); }
197+
189 @media (max-width: 700px) {198 @media (max-width: 700px) {
190 .list.mrs .c-who { display: none; }199 .list.mrs .c-who { display: none; }
191 }200 }
M internal/adapters/web/view.go +10 -0
@@ -135,6 +135,16 @@ type SortLink struct {
135135 type HomePage struct {
136136 Sorts []SortLink
137137 Rows []HomeRow
138+ // Inbox is the button to the owner's inbox; nil unless the owner is signed
139+ // in.
140+ Inbox *InboxButton
141+}
142+
143+// InboxButton is the link to the inbox with the number of open items; Count is
144+// negative when it could not be read.
145+type InboxButton struct {
146+ Href string
147+ Count int
138148 }
139149
140150 // Fact is one key/value row of the facts block.
@@ -135,6 +135,16 @@ type SortLink struct {
135 type HomePage struct {135 type HomePage struct {
136 Sorts []SortLink136 Sorts []SortLink
137 Rows []HomeRow137 Rows []HomeRow
138+ // Inbox is the button to the owner's inbox; nil unless the owner is signed
139+ // in.
140+ Inbox *InboxButton
141+}
142+
143+// InboxButton is the link to the inbox with the number of open items; Count is
144+// negative when it could not be read.
145+type InboxButton struct {
146+ Href string
147+ Count int
138 }148 }
139 149
140 // Fact is one key/value row of the facts block.150 // Fact is one key/value row of the facts block.
M internal/adapters/web/web.go +7 -0
@@ -63,6 +63,10 @@ type Config struct {
6363 // it there is no such redirect. Leave it unset rather than assigning a
6464 // typed nil pointer.
6565 IssueNumbers ports.IssueNumberResolver
66+ // Inbox serves the owner's inbox page (/-/inbox) and the button that
67+ // leads to it. Without it there is neither. Leave it unset rather than
68+ // assigning a typed nil pointer.
69+ Inbox ports.InboxService
6670 // Runs serves the CI pages (/<repo>/actions) and the checks shown next to
6771 // commits and merge requests. Without it there are no such pages. Leave it
6872 // unset rather than assigning a typed nil pointer.
@@ -175,6 +179,9 @@ func (s *Server) routes() *http.ServeMux {
175179 if s.passkeysOn() {
176180 s.passkeyRoutes(mux)
177181 }
182+ if s.cfg.Inbox != nil {
183+ mux.HandleFunc("GET "+inboxPath, s.page(s.inbox))
184+ }
178185 mux.HandleFunc("GET /{$}", s.page(s.home))
179186 mux.HandleFunc("GET /{repo}", s.page(s.summary))
180187 mux.HandleFunc("GET /{repo}/{$}", s.page(s.summary))
@@ -63,6 +63,10 @@ type Config struct {
63 // it there is no such redirect. Leave it unset rather than assigning a63 // it there is no such redirect. Leave it unset rather than assigning a
64 // typed nil pointer.64 // typed nil pointer.
65 IssueNumbers ports.IssueNumberResolver65 IssueNumbers ports.IssueNumberResolver
66+ // Inbox serves the owner's inbox page (/-/inbox) and the button that
67+ // leads to it. Without it there is neither. Leave it unset rather than
68+ // assigning a typed nil pointer.
69+ Inbox ports.InboxService
66 // Runs serves the CI pages (/<repo>/actions) and the checks shown next to70 // Runs serves the CI pages (/<repo>/actions) and the checks shown next to
67 // commits and merge requests. Without it there are no such pages. Leave it71 // commits and merge requests. Without it there are no such pages. Leave it
68 // unset rather than assigning a typed nil pointer.72 // unset rather than assigning a typed nil pointer.
@@ -175,6 +179,9 @@ func (s *Server) routes() *http.ServeMux {
175 if s.passkeysOn() {179 if s.passkeysOn() {
176 s.passkeyRoutes(mux)180 s.passkeyRoutes(mux)
177 }181 }
182+ if s.cfg.Inbox != nil {
183+ mux.HandleFunc("GET "+inboxPath, s.page(s.inbox))
184+ }
178 mux.HandleFunc("GET /{$}", s.page(s.home))185 mux.HandleFunc("GET /{$}", s.page(s.home))
179 mux.HandleFunc("GET /{repo}", s.page(s.summary))186 mux.HandleFunc("GET /{repo}", s.page(s.summary))
180 mux.HandleFunc("GET /{repo}/{$}", s.page(s.summary))187 mux.HandleFunc("GET /{repo}/{$}", s.page(s.summary))
M internal/app/testdata/web/commits-root.html +1 -1
@@ -972,7 +972,7 @@
972972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
973973 htmx:sse:error -> ermiteLive.error(event.detail.status);
974974 htmx:sse:before:connection -> ermiteLive.gate(event);
975- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
975+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
976976 gap -> ermiteLive.reload()">
977977 </span>
978978 </div>
@@ -972,7 +972,7 @@
972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
973 htmx:sse:error -> ermiteLive.error(event.detail.status);973 htmx:sse:error -> ermiteLive.error(event.detail.status);
974 htmx:sse:before:connection -> ermiteLive.gate(event);974 htmx:sse:before:connection -> ermiteLive.gate(event);
975- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);975+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
976 gap -> ermiteLive.reload()">976 gap -> ermiteLive.reload()">
977 </span>977 </span>
978 </div>978 </div>
M internal/app/testdata/web/commits-small.html +1 -1
@@ -317,7 +317,7 @@
317317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
318318 htmx:sse:error -> ermiteLive.error(event.detail.status);
319319 htmx:sse:before:connection -> ermiteLive.gate(event);
320- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
320+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
321321 gap -> ermiteLive.reload()">
322322 </span>
323323 </div>
@@ -317,7 +317,7 @@
317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
318 htmx:sse:error -> ermiteLive.error(event.detail.status);318 htmx:sse:error -> ermiteLive.error(event.detail.status);
319 htmx:sse:before:connection -> ermiteLive.gate(event);319 htmx:sse:before:connection -> ermiteLive.gate(event);
320- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);320+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
321 gap -> ermiteLive.reload()">321 gap -> ermiteLive.reload()">
322 </span>322 </span>
323 </div>323 </div>
M internal/app/testdata/web/home.html +1 -1
@@ -191,7 +191,7 @@
191191 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
192192 htmx:sse:error -> ermiteLive.error(event.detail.status);
193193 htmx:sse:before:connection -> ermiteLive.gate(event);
194- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
194+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
195195 gap -> ermiteLive.reload()">
196196 </span>
197197 </div>
@@ -191,7 +191,7 @@
191 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();191 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
192 htmx:sse:error -> ermiteLive.error(event.detail.status);192 htmx:sse:error -> ermiteLive.error(event.detail.status);
193 htmx:sse:before:connection -> ermiteLive.gate(event);193 htmx:sse:before:connection -> ermiteLive.gate(event);
194- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);194+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
195 gap -> ermiteLive.reload()">195 gap -> ermiteLive.reload()">
196 </span>196 </span>
197 </div>197 </div>
M internal/app/testdata/web/refs.html +1 -1
@@ -199,7 +199,7 @@
199199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
200200 htmx:sse:error -> ermiteLive.error(event.detail.status);
201201 htmx:sse:before:connection -> ermiteLive.gate(event);
202- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
202+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
203203 gap -> ermiteLive.reload()">
204204 </span>
205205 </div>
@@ -199,7 +199,7 @@
199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
200 htmx:sse:error -> ermiteLive.error(event.detail.status);200 htmx:sse:error -> ermiteLive.error(event.detail.status);
201 htmx:sse:before:connection -> ermiteLive.gate(event);201 htmx:sse:before:connection -> ermiteLive.gate(event);
202- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);202+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
203 gap -> ermiteLive.reload()">203 gap -> ermiteLive.reload()">
204 </span>204 </span>
205 </div>205 </div>
M internal/app/testdata/web/summary.html +1 -1
@@ -225,7 +225,7 @@
225225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
226226 htmx:sse:error -> ermiteLive.error(event.detail.status);
227227 htmx:sse:before:connection -> ermiteLive.gate(event);
228- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
228+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
229229 gap -> ermiteLive.reload()">
230230 </span>
231231 </div>
@@ -225,7 +225,7 @@
225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
226 htmx:sse:error -> ermiteLive.error(event.detail.status);226 htmx:sse:error -> ermiteLive.error(event.detail.status);
227 htmx:sse:before:connection -> ermiteLive.gate(event);227 htmx:sse:before:connection -> ermiteLive.gate(event);
228- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);228+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
229 gap -> ermiteLive.reload()">229 gap -> ermiteLive.reload()">
230 </span>230 </span>
231 </div>231 </div>
M internal/app/web.go +1 -0
@@ -47,6 +47,7 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han
4747 MergeRequests: f.MergeRequests,
4848 Issues: f.Issues,
4949 IssueNumbers: f.Issues,
50+ Inbox: f.Inbox,
5051 Runs: f.CI,
5152 Artifacts: artifacts,
5253 Events: f.EventLog,
@@ -47,6 +47,7 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han
47 MergeRequests: f.MergeRequests,47 MergeRequests: f.MergeRequests,
48 Issues: f.Issues,48 Issues: f.Issues,
49 IssueNumbers: f.Issues,49 IssueNumbers: f.Issues,
50+ Inbox: f.Inbox,
50 Runs: f.CI,51 Runs: f.CI,
51 Artifacts: artifacts,52 Artifacts: artifacts,
52 Events: f.EventLog,53 Events: f.EventLog,
A internal/app/web_inbox_test.go +152 -0
@@ -0,0 +1,152 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package app
4+
5+import (
6+ "bufio"
7+ "context"
8+ "net/http"
9+ "strings"
10+ "testing"
11+
12+ "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/require"
14+
15+ "bnema.dev/ermite/internal/domain"
16+ "bnema.dev/ermite/internal/ports"
17+)
18+
19+// The owner's inbox end to end: two repositories with open merge requests and
20+// issues, the filter, the home button, the owner stream, and a 404 for
21+// everyone who is not the signed-in owner.
22+func TestWebInboxEndToEnd(t *testing.T) {
23+ r := newMRRepo(t, domain.VisibilityPublic)
24+ b := browser{r.harness}
25+ token := r.signIn(t)
26+
27+ _, err := r.forge.Repos.Create(r.ctx, domain.Root(), createParams(r.harness, "other", domain.VisibilityPrivate))
28+ require.NoError(t, err)
29+ otherURL := r.urlAuth(adminToken, "/other.git")
30+ r.mustGit(r.wt, "push", "-q", otherURL, "main")
31+ r.mustGit(r.wt, "push", "-q", otherURL, "feat/search")
32+
33+ owner := domain.IssuesOwner
34+ for _, n := range []string{"proj", "other"} {
35+ _, err := r.forge.Repos.Update(r.ctx, domain.Root(), r.name(n), ports.UpdateRepoParams{Issues: &owner})
36+ require.NoError(t, err)
37+ }
38+
39+ // Nothing is open yet.
40+ empty := b.get("/-/inbox", as(token))
41+ require.Equal(t, 200, empty.status, empty.body)
42+ assert.Contains(t, empty.body, "no open merge requests.")
43+ assert.Contains(t, empty.body, "no open issues.")
44+
45+ // The owner stream sees both repositories.
46+ stream := ownerStream(t, r.harness, token)
47+
48+ root := domain.Root()
49+ _, err = r.forge.MergeRequests.Create(r.ctx, root, r.name("proj"), ports.CreateMergeRequestParams{Title: "add search", Source: "feat/search", Target: "main"})
50+ require.NoError(t, err)
51+ ev := stream.next(t, watchWait)
52+ assert.Equal(t, "inbox", ev.name)
53+ assert.Contains(t, ev.data, `id="inbox-counts"`)
54+ for range 3 { // the other fragments of this event
55+ stream.next(t, watchWait)
56+ }
57+ _, err = r.forge.Issues.Create(r.ctx, root, r.name("other"), ports.CreateIssueParams{Title: "search is slow"})
58+ require.NoError(t, err)
59+ var seen strings.Builder
60+ for range 4 { // the four fragments of this event
61+ seen.WriteString(stream.next(t, watchWait).data)
62+ }
63+ assert.Contains(t, seen.String(), "search is slow", "an event of the second repository")
64+ _, err = r.forge.MergeRequests.Create(r.ctx, root, r.name("other"), ports.CreateMergeRequestParams{Title: "other work", Source: "feat/search", Target: "main"})
65+ require.NoError(t, err)
66+ _, err = r.forge.Issues.Create(r.ctx, root, r.name("proj"), ports.CreateIssueParams{Title: "proj bug"})
67+ require.NoError(t, err)
68+
69+ all := b.get("/-/inbox", as(token))
70+ require.Equal(t, 200, all.status, all.body)
71+ assert.Equal(t, "private, no-store", all.hdr.Get("Cache-Control"))
72+ assert.Contains(t, all.body, "<strong>2</strong> merge requests · <strong>2</strong> issues")
73+ for _, want := range []string{"add search", "other work", "search is slow", "proj bug", `href="/other/issues/`, `href="/proj/merge-requests/`} {
74+ assert.Contains(t, all.body, want)
75+ }
76+
77+ only := b.get("/-/inbox?repo=other", as(token))
78+ require.Equal(t, 200, only.status)
79+ assert.Contains(t, only.body, "<strong>2</strong> merge requests · <strong>2</strong> issues", "the counters ignore the filter")
80+ assert.Contains(t, only.body, "other work")
81+ assert.Contains(t, only.body, "search is slow")
82+ assert.NotContains(t, only.body, "add search")
83+ assert.NotContains(t, only.body, "proj bug")
84+
85+ // Issues off: that repository's issues leave the inbox, its merge requests stay.
86+ off := domain.IssuesOff
87+ _, err = r.forge.Repos.Update(r.ctx, root, r.name("other"), ports.UpdateRepoParams{Issues: &off})
88+ require.NoError(t, err)
89+ hidden := b.get("/-/inbox", as(token))
90+ assert.Contains(t, hidden.body, "<strong>2</strong> merge requests · <strong>1</strong> issues")
91+ assert.NotContains(t, hidden.body, "search is slow")
92+
93+ // The home button, for the owner only.
94+ assert.Contains(t, b.get("/", as(token)).body, `href="/-/inbox"`)
95+ anonHome := b.get("/", nil)
96+ assert.NotContains(t, anonHome.body, "/-/inbox")
97+ assert.NotContains(t, anonHome.body, "Inbox")
98+
99+ // Anyone else: the 404 of an unknown page, and no stream.
100+ missing := b.get("/-/no-such-page", nil)
101+ anon := b.get("/-/inbox", nil)
102+ assert.Equal(t, 404, anon.status)
103+ assert.Equal(t, missing.body, anon.body)
104+ assert.Equal(t, 404, b.get("/-/inbox", map[string]string{"Authorization": "Bearer " + adminToken}).status, "a token is not a session")
105+ unknown := b.get("/-/events?topic=nope&view=inbox", nil)
106+ ownerAnon := b.get("/-/events?topic=owner&view=inbox", nil)
107+ assert.Equal(t, unknown.status, ownerAnon.status, "the answer to an unknown topic")
108+ assert.Equal(t, strings.ReplaceAll(unknown.body, "nope", "owner"), ownerAnon.body)
109+ assert.Equal(t, unknown.status, b.get("/-/events?topic=owner&view=inbox", map[string]string{"Authorization": "Bearer " + adminToken}).status)
110+}
111+
112+// ownerStream opens the owner topic with the session cookie.
113+func ownerStream(t *testing.T, h *harness, token string) *sseStream {
114+ t.Helper()
115+ ctx, cancel := context.WithCancel(h.ctx)
116+ t.Cleanup(cancel)
117+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.url("/-/events?topic=owner&view=inbox"), nil)
118+ require.NoError(t, err)
119+ req.Header.Set("Cookie", sessionCookie+"="+token)
120+ resp, err := http.DefaultClient.Do(req)
121+ require.NoError(t, err)
122+ t.Cleanup(func() { _ = resp.Body.Close() })
123+ require.Equal(t, http.StatusOK, resp.StatusCode)
124+ s := &sseStream{resp: resp, events: make(chan sseEvent, 256), cancel: cancel}
125+ go func() {
126+ defer close(s.events)
127+ sc := bufio.NewScanner(resp.Body)
128+ sc.Buffer(make([]byte, 0, 64<<10), 1<<20)
129+ var cur sseEvent
130+ var data []string
131+ for sc.Scan() {
132+ line := sc.Text()
133+ switch {
134+ case line == "":
135+ if cur.id != "" || cur.name != "" || len(data) > 0 {
136+ cur.data = strings.Join(data, "\n")
137+ s.events <- cur
138+ }
139+ cur, data = sseEvent{}, nil
140+ case strings.HasPrefix(line, ":"):
141+ cur.comment = true
142+ case strings.HasPrefix(line, "event: "):
143+ cur.name = strings.TrimPrefix(line, "event: ")
144+ case strings.HasPrefix(line, "id: "):
145+ cur.id = strings.TrimPrefix(line, "id: ")
146+ case strings.HasPrefix(line, "data: "):
147+ data = append(data, strings.TrimPrefix(line, "data: "))
148+ }
149+ }
150+ }()
151+ return s
152+}
@@ -0,0 +1,152 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package app
4+
5+import (
6+ "bufio"
7+ "context"
8+ "net/http"
9+ "strings"
10+ "testing"
11+
12+ "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/require"
14+
15+ "bnema.dev/ermite/internal/domain"
16+ "bnema.dev/ermite/internal/ports"
17+)
18+
19+// The owner's inbox end to end: two repositories with open merge requests and
20+// issues, the filter, the home button, the owner stream, and a 404 for
21+// everyone who is not the signed-in owner.
22+func TestWebInboxEndToEnd(t *testing.T) {
23+ r := newMRRepo(t, domain.VisibilityPublic)
24+ b := browser{r.harness}
25+ token := r.signIn(t)
26+
27+ _, err := r.forge.Repos.Create(r.ctx, domain.Root(), createParams(r.harness, "other", domain.VisibilityPrivate))
28+ require.NoError(t, err)
29+ otherURL := r.urlAuth(adminToken, "/other.git")
30+ r.mustGit(r.wt, "push", "-q", otherURL, "main")
31+ r.mustGit(r.wt, "push", "-q", otherURL, "feat/search")
32+
33+ owner := domain.IssuesOwner
34+ for _, n := range []string{"proj", "other"} {
35+ _, err := r.forge.Repos.Update(r.ctx, domain.Root(), r.name(n), ports.UpdateRepoParams{Issues: &owner})
36+ require.NoError(t, err)
37+ }
38+
39+ // Nothing is open yet.
40+ empty := b.get("/-/inbox", as(token))
41+ require.Equal(t, 200, empty.status, empty.body)
42+ assert.Contains(t, empty.body, "no open merge requests.")
43+ assert.Contains(t, empty.body, "no open issues.")
44+
45+ // The owner stream sees both repositories.
46+ stream := ownerStream(t, r.harness, token)
47+
48+ root := domain.Root()
49+ _, err = r.forge.MergeRequests.Create(r.ctx, root, r.name("proj"), ports.CreateMergeRequestParams{Title: "add search", Source: "feat/search", Target: "main"})
50+ require.NoError(t, err)
51+ ev := stream.next(t, watchWait)
52+ assert.Equal(t, "inbox", ev.name)
53+ assert.Contains(t, ev.data, `id="inbox-counts"`)
54+ for range 3 { // the other fragments of this event
55+ stream.next(t, watchWait)
56+ }
57+ _, err = r.forge.Issues.Create(r.ctx, root, r.name("other"), ports.CreateIssueParams{Title: "search is slow"})
58+ require.NoError(t, err)
59+ var seen strings.Builder
60+ for range 4 { // the four fragments of this event
61+ seen.WriteString(stream.next(t, watchWait).data)
62+ }
63+ assert.Contains(t, seen.String(), "search is slow", "an event of the second repository")
64+ _, err = r.forge.MergeRequests.Create(r.ctx, root, r.name("other"), ports.CreateMergeRequestParams{Title: "other work", Source: "feat/search", Target: "main"})
65+ require.NoError(t, err)
66+ _, err = r.forge.Issues.Create(r.ctx, root, r.name("proj"), ports.CreateIssueParams{Title: "proj bug"})
67+ require.NoError(t, err)
68+
69+ all := b.get("/-/inbox", as(token))
70+ require.Equal(t, 200, all.status, all.body)
71+ assert.Equal(t, "private, no-store", all.hdr.Get("Cache-Control"))
72+ assert.Contains(t, all.body, "<strong>2</strong> merge requests · <strong>2</strong> issues")
73+ for _, want := range []string{"add search", "other work", "search is slow", "proj bug", `href="/other/issues/`, `href="/proj/merge-requests/`} {
74+ assert.Contains(t, all.body, want)
75+ }
76+
77+ only := b.get("/-/inbox?repo=other", as(token))
78+ require.Equal(t, 200, only.status)
79+ assert.Contains(t, only.body, "<strong>2</strong> merge requests · <strong>2</strong> issues", "the counters ignore the filter")
80+ assert.Contains(t, only.body, "other work")
81+ assert.Contains(t, only.body, "search is slow")
82+ assert.NotContains(t, only.body, "add search")
83+ assert.NotContains(t, only.body, "proj bug")
84+
85+ // Issues off: that repository's issues leave the inbox, its merge requests stay.
86+ off := domain.IssuesOff
87+ _, err = r.forge.Repos.Update(r.ctx, root, r.name("other"), ports.UpdateRepoParams{Issues: &off})
88+ require.NoError(t, err)
89+ hidden := b.get("/-/inbox", as(token))
90+ assert.Contains(t, hidden.body, "<strong>2</strong> merge requests · <strong>1</strong> issues")
91+ assert.NotContains(t, hidden.body, "search is slow")
92+
93+ // The home button, for the owner only.
94+ assert.Contains(t, b.get("/", as(token)).body, `href="/-/inbox"`)
95+ anonHome := b.get("/", nil)
96+ assert.NotContains(t, anonHome.body, "/-/inbox")
97+ assert.NotContains(t, anonHome.body, "Inbox")
98+
99+ // Anyone else: the 404 of an unknown page, and no stream.
100+ missing := b.get("/-/no-such-page", nil)
101+ anon := b.get("/-/inbox", nil)
102+ assert.Equal(t, 404, anon.status)
103+ assert.Equal(t, missing.body, anon.body)
104+ assert.Equal(t, 404, b.get("/-/inbox", map[string]string{"Authorization": "Bearer " + adminToken}).status, "a token is not a session")
105+ unknown := b.get("/-/events?topic=nope&view=inbox", nil)
106+ ownerAnon := b.get("/-/events?topic=owner&view=inbox", nil)
107+ assert.Equal(t, unknown.status, ownerAnon.status, "the answer to an unknown topic")
108+ assert.Equal(t, strings.ReplaceAll(unknown.body, "nope", "owner"), ownerAnon.body)
109+ assert.Equal(t, unknown.status, b.get("/-/events?topic=owner&view=inbox", map[string]string{"Authorization": "Bearer " + adminToken}).status)
110+}
111+
112+// ownerStream opens the owner topic with the session cookie.
113+func ownerStream(t *testing.T, h *harness, token string) *sseStream {
114+ t.Helper()
115+ ctx, cancel := context.WithCancel(h.ctx)
116+ t.Cleanup(cancel)
117+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.url("/-/events?topic=owner&view=inbox"), nil)
118+ require.NoError(t, err)
119+ req.Header.Set("Cookie", sessionCookie+"="+token)
120+ resp, err := http.DefaultClient.Do(req)
121+ require.NoError(t, err)
122+ t.Cleanup(func() { _ = resp.Body.Close() })
123+ require.Equal(t, http.StatusOK, resp.StatusCode)
124+ s := &sseStream{resp: resp, events: make(chan sseEvent, 256), cancel: cancel}
125+ go func() {
126+ defer close(s.events)
127+ sc := bufio.NewScanner(resp.Body)
128+ sc.Buffer(make([]byte, 0, 64<<10), 1<<20)
129+ var cur sseEvent
130+ var data []string
131+ for sc.Scan() {
132+ line := sc.Text()
133+ switch {
134+ case line == "":
135+ if cur.id != "" || cur.name != "" || len(data) > 0 {
136+ cur.data = strings.Join(data, "\n")
137+ s.events <- cur
138+ }
139+ cur, data = sseEvent{}, nil
140+ case strings.HasPrefix(line, ":"):
141+ cur.comment = true
142+ case strings.HasPrefix(line, "event: "):
143+ cur.name = strings.TrimPrefix(line, "event: ")
144+ case strings.HasPrefix(line, "id: "):
145+ cur.id = strings.TrimPrefix(line, "id: ")
146+ case strings.HasPrefix(line, "data: "):
147+ data = append(data, strings.TrimPrefix(line, "data: "))
148+ }
149+ }
150+ }()
151+ return s
152+}

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 43

↑↓ moveenter openesc close