detached 4277264

feat(web): order the home page by last activity and move repositories up live

commit
427726483415e5fdfc0b1c4aae86aea88a80a1fa
author
brice · · 2026-10-19 15:48 UTC
parent
4fc291e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
28 files · +816 -525
M gen/ermite/storage/repo.pb.go +22 -11 generated file: collapsed
M internal/adapters/kvstore/repos.go +2 -0
@@ -208,6 +208,7 @@ func encodeRepo(r domain.Repo) ([]byte, error) {
208208 Visibility: vis,
209209 CreatedAt: encodeTime(r.CreatedAt),
210210 PushedAt: encodeTime(r.PushedAt),
211+ ActiveAt: encodeTime(r.ActiveAt),
211212 Settings: pbSettings,
212213 }
213214 return mustMarshal(pb), nil
@@ -229,6 +230,7 @@ func decodeRepo(val []byte) (domain.Repo, error) {
229230 Visibility: decodeVisibility(pb.GetVisibility()),
230231 CreatedAt: decodeTime(pb.GetCreatedAt()),
231232 PushedAt: decodeTime(pb.GetPushedAt()),
233+ ActiveAt: decodeTime(pb.GetActiveAt()),
232234 Settings: decodeSettings(pb.GetSettings()),
233235 }, nil
234236 }
@@ -208,6 +208,7 @@ func encodeRepo(r domain.Repo) ([]byte, error) {
208 Visibility: vis,208 Visibility: vis,
209 CreatedAt: encodeTime(r.CreatedAt),209 CreatedAt: encodeTime(r.CreatedAt),
210 PushedAt: encodeTime(r.PushedAt),210 PushedAt: encodeTime(r.PushedAt),
211+ ActiveAt: encodeTime(r.ActiveAt),
211 Settings: pbSettings,212 Settings: pbSettings,
212 }213 }
213 return mustMarshal(pb), nil214 return mustMarshal(pb), nil
@@ -229,6 +230,7 @@ func decodeRepo(val []byte) (domain.Repo, error) {
229 Visibility: decodeVisibility(pb.GetVisibility()),230 Visibility: decodeVisibility(pb.GetVisibility()),
230 CreatedAt: decodeTime(pb.GetCreatedAt()),231 CreatedAt: decodeTime(pb.GetCreatedAt()),
231 PushedAt: decodeTime(pb.GetPushedAt()),232 PushedAt: decodeTime(pb.GetPushedAt()),
233+ ActiveAt: decodeTime(pb.GetActiveAt()),
232 Settings: decodeSettings(pb.GetSettings()),234 Settings: decodeSettings(pb.GetSettings()),
233 }, nil235 }, nil
234 }236 }
M internal/adapters/kvstore/store_test.go +2 -1
@@ -43,7 +43,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) {
4343 s := openStore(t, t.TempDir())
4444
4545 b := domain.Repo{Name: name(t, "b"), Description: "second", Topics: []string{"cli", "go"}, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()}
46- a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), Settings: domain.DefaultRepoSettings()}
46+ a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), ActiveAt: created.Add(2 * time.Hour), Settings: domain.DefaultRepoSettings()}
4747 require.NoError(t, s.Create(ctx, b))
4848 require.NoError(t, s.Create(ctx, a))
4949
@@ -54,6 +54,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) {
5454 require.NoError(t, err)
5555 assert.Equal(t, a, got)
5656 assert.True(t, got.PushedAt.Equal(a.PushedAt))
57+ assert.True(t, got.ActiveAt.Equal(a.ActiveAt))
5758
5859 list, err := s.List(ctx)
5960 require.NoError(t, err)
@@ -43,7 +43,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) {
43 s := openStore(t, t.TempDir())43 s := openStore(t, t.TempDir())
44 44
45 b := domain.Repo{Name: name(t, "b"), Description: "second", Topics: []string{"cli", "go"}, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()}45 b := domain.Repo{Name: name(t, "b"), Description: "second", Topics: []string{"cli", "go"}, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()}
46- a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), Settings: domain.DefaultRepoSettings()}46+ a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), ActiveAt: created.Add(2 * time.Hour), Settings: domain.DefaultRepoSettings()}
47 require.NoError(t, s.Create(ctx, b))47 require.NoError(t, s.Create(ctx, b))
48 require.NoError(t, s.Create(ctx, a))48 require.NoError(t, s.Create(ctx, a))
49 49
@@ -54,6 +54,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) {
54 require.NoError(t, err)54 require.NoError(t, err)
55 assert.Equal(t, a, got)55 assert.Equal(t, a, got)
56 assert.True(t, got.PushedAt.Equal(a.PushedAt))56 assert.True(t, got.PushedAt.Equal(a.PushedAt))
57+ assert.True(t, got.ActiveAt.Equal(a.ActiveAt))
57 58
58 list, err := s.List(ctx)59 list, err := s.List(ctx)
59 require.NoError(t, err)60 require.NoError(t, err)
M internal/adapters/web/components.templ +2 -2
@@ -37,7 +37,7 @@ templ CloneBlock(c Clone) {
3737 // right its figures and CI state over its newest commits. Its id is the
3838 // target of SSE swaps; the name link comes first, `enter` follows it.
3939 templ HomeRowView(r HomeRow) {
40- <li id={ r.ID } class="repo" data-cursor?={ r.Cursor }>
40+ <li id={ r.ID } class="repo" data-cursor?={ r.Cursor } data-activity?={ r.Fresh }>
4141 <div class="repo-main">
4242 <h2 class="repo-name">
4343 <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
@@ -45,7 +45,7 @@ templ HomeRowView(r HomeRow) {
4545 <span class="pill">private</span>
4646 }
4747 if r.Fresh {
48- <span class="pill new">new push</span>
48+ <span class="pill new">new activity</span>
4949 }
5050 </h2>
5151 if r.Desc != "" {
@@ -37,7 +37,7 @@ templ CloneBlock(c Clone) {
37 // right its figures and CI state over its newest commits. Its id is the37 // right its figures and CI state over its newest commits. Its id is the
38 // target of SSE swaps; the name link comes first, `enter` follows it.38 // target of SSE swaps; the name link comes first, `enter` follows it.
39 templ HomeRowView(r HomeRow) {39 templ HomeRowView(r HomeRow) {
40- <li id={ r.ID } class="repo" data-cursor?={ r.Cursor }>40+ <li id={ r.ID } class="repo" data-cursor?={ r.Cursor } data-activity?={ r.Fresh }>
41 <div class="repo-main">41 <div class="repo-main">
42 <h2 class="repo-name">42 <h2 class="repo-name">
43 <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>43 <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
@@ -45,7 +45,7 @@ templ HomeRowView(r HomeRow) {
45 <span class="pill">private</span>45 <span class="pill">private</span>
46 }46 }
47 if r.Fresh {47 if r.Fresh {
48- <span class="pill new">new push</span>48+ <span class="pill new">new activity</span>
49 }49 }
50 </h2>50 </h2>
51 if r.Desc != "" {51 if r.Desc != "" {
M internal/adapters/web/components_templ.go +119 -113
@@ -195,7 +195,13 @@ func HomeRowView(r HomeRow) templ.Component {
195195 return templ_7745c5c3_Err
196196 }
197197 }
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "><div class=\"repo-main\"><h2 class=\"repo-name\"><a href=\"")
198+ if r.Fresh {
199+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " data-activity")
200+ if templ_7745c5c3_Err != nil {
201+ return templ_7745c5c3_Err
202+ }
203+ }
204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "><div class=\"repo-main\"><h2 class=\"repo-name\"><a href=\"")
199205 if templ_7745c5c3_Err != nil {
200206 return templ_7745c5c3_Err
201207 }
@@ -208,7 +214,7 @@ func HomeRowView(r HomeRow) templ.Component {
208214 if templ_7745c5c3_Err != nil {
209215 return templ_7745c5c3_Err
210216 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
212218 if templ_7745c5c3_Err != nil {
213219 return templ_7745c5c3_Err
214220 }
@@ -221,28 +227,28 @@ func HomeRowView(r HomeRow) templ.Component {
221227 if templ_7745c5c3_Err != nil {
222228 return templ_7745c5c3_Err
223229 }
224- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a> ")
230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a> ")
225231 if templ_7745c5c3_Err != nil {
226232 return templ_7745c5c3_Err
227233 }
228234 if r.Private {
229- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"pill\">private</span> ")
235+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"pill\">private</span> ")
230236 if templ_7745c5c3_Err != nil {
231237 return templ_7745c5c3_Err
232238 }
233239 }
234240 if r.Fresh {
235- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"pill new\">new push</span>")
241+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"pill new\">new activity</span>")
236242 if templ_7745c5c3_Err != nil {
237243 return templ_7745c5c3_Err
238244 }
239245 }
240- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</h2>")
246+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h2>")
241247 if templ_7745c5c3_Err != nil {
242248 return templ_7745c5c3_Err
243249 }
244250 if r.Desc != "" {
245- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"repo-desc\">")
251+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"repo-desc\">")
246252 if templ_7745c5c3_Err != nil {
247253 return templ_7745c5c3_Err
248254 }
@@ -255,18 +261,18 @@ func HomeRowView(r HomeRow) templ.Component {
255261 if templ_7745c5c3_Err != nil {
256262 return templ_7745c5c3_Err
257263 }
258- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p>")
264+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p>")
259265 if templ_7745c5c3_Err != nil {
260266 return templ_7745c5c3_Err
261267 }
262268 } else {
263- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"repo-desc mute\">no description</p>")
269+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"repo-desc mute\">no description</p>")
264270 if templ_7745c5c3_Err != nil {
265271 return templ_7745c5c3_Err
266272 }
267273 }
268274 if len(r.Topics) > 0 {
269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<ul class=\"repo-topics\" aria-label=\"")
275+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<ul class=\"repo-topics\" aria-label=\"")
270276 if templ_7745c5c3_Err != nil {
271277 return templ_7745c5c3_Err
272278 }
@@ -279,12 +285,12 @@ func HomeRowView(r HomeRow) templ.Component {
279285 if templ_7745c5c3_Err != nil {
280286 return templ_7745c5c3_Err
281287 }
282- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-fit>")
288+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-fit>")
283289 if templ_7745c5c3_Err != nil {
284290 return templ_7745c5c3_Err
285291 }
286292 for _, t := range r.Topics {
287- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<li>")
293+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li>")
288294 if templ_7745c5c3_Err != nil {
289295 return templ_7745c5c3_Err
290296 }
@@ -297,17 +303,17 @@ func HomeRowView(r HomeRow) templ.Component {
297303 if templ_7745c5c3_Err != nil {
298304 return templ_7745c5c3_Err
299305 }
300- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</li>")
306+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</li>")
301307 if templ_7745c5c3_Err != nil {
302308 return templ_7745c5c3_Err
303309 }
304310 }
305- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</ul>")
311+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</ul>")
306312 if templ_7745c5c3_Err != nil {
307313 return templ_7745c5c3_Err
308314 }
309315 } else {
310- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>")
316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>")
311317 if templ_7745c5c3_Err != nil {
312318 return templ_7745c5c3_Err
313319 }
@@ -318,12 +324,12 @@ func HomeRowView(r HomeRow) templ.Component {
318324 return templ_7745c5c3_Err
319325 }
320326 } else {
321- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>")
327+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>")
322328 if templ_7745c5c3_Err != nil {
323329 return templ_7745c5c3_Err
324330 }
325331 }
326- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"")
332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"")
327333 if templ_7745c5c3_Err != nil {
328334 return templ_7745c5c3_Err
329335 }
@@ -336,12 +342,12 @@ func HomeRowView(r HomeRow) templ.Component {
336342 if templ_7745c5c3_Err != nil {
337343 return templ_7745c5c3_Err
338344 }
339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")
345+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">")
340346 if templ_7745c5c3_Err != nil {
341347 return templ_7745c5c3_Err
342348 }
343349 if s := r.Stats; s != nil {
344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div><dt>commits</dt><dd>")
350+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div><dt>commits</dt><dd>")
345351 if templ_7745c5c3_Err != nil {
346352 return templ_7745c5c3_Err
347353 }
@@ -354,7 +360,7 @@ func HomeRowView(r HomeRow) templ.Component {
354360 if templ_7745c5c3_Err != nil {
355361 return templ_7745c5c3_Err
356362 }
357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</dd></div><div><dt>last 30 days</dt><dd>")
363+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</dd></div><div><dt>last 30 days</dt><dd>")
358364 if templ_7745c5c3_Err != nil {
359365 return templ_7745c5c3_Err
360366 }
@@ -367,17 +373,17 @@ func HomeRowView(r HomeRow) templ.Component {
367373 if templ_7745c5c3_Err != nil {
368374 return templ_7745c5c3_Err
369375 }
370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</dd></div>")
376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</dd></div>")
371377 if templ_7745c5c3_Err != nil {
372378 return templ_7745c5c3_Err
373379 }
374380 }
375- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"rs-active\"><dt>last activity</dt>")
381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"rs-active\"><dt>last activity</dt>")
376382 if templ_7745c5c3_Err != nil {
377383 return templ_7745c5c3_Err
378384 }
379385 if r.ActiveAt != "" {
380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<dd><time datetime=\"")
386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<dd><time datetime=\"")
381387 if templ_7745c5c3_Err != nil {
382388 return templ_7745c5c3_Err
383389 }
@@ -390,7 +396,7 @@ func HomeRowView(r HomeRow) templ.Component {
390396 if templ_7745c5c3_Err != nil {
391397 return templ_7745c5c3_Err
392398 }
393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" title=\"")
399+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" title=\"")
394400 if templ_7745c5c3_Err != nil {
395401 return templ_7745c5c3_Err
396402 }
@@ -403,7 +409,7 @@ func HomeRowView(r HomeRow) templ.Component {
403409 if templ_7745c5c3_Err != nil {
404410 return templ_7745c5c3_Err
405411 }
406- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")
412+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">")
407413 if templ_7745c5c3_Err != nil {
408414 return templ_7745c5c3_Err
409415 }
@@ -416,27 +422,27 @@ func HomeRowView(r HomeRow) templ.Component {
416422 if templ_7745c5c3_Err != nil {
417423 return templ_7745c5c3_Err
418424 }
419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</time></dd>")
425+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</time></dd>")
420426 if templ_7745c5c3_Err != nil {
421427 return templ_7745c5c3_Err
422428 }
423429 } else {
424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<dd class=\"mute\">never</dd>")
430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dd class=\"mute\">never</dd>")
425431 if templ_7745c5c3_Err != nil {
426432 return templ_7745c5c3_Err
427433 }
428434 }
429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div>")
435+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div>")
430436 if templ_7745c5c3_Err != nil {
431437 return templ_7745c5c3_Err
432438 }
433439 if s := r.Stats; s != nil && s.Tag != "" {
434- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"rs-tag\"><dt>last tag</dt>")
440+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"rs-tag\"><dt>last tag</dt>")
435441 if templ_7745c5c3_Err != nil {
436442 return templ_7745c5c3_Err
437443 }
438444 if s.TagHref != "" {
439- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<dd><a href=\"")
445+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<dd><a href=\"")
440446 if templ_7745c5c3_Err != nil {
441447 return templ_7745c5c3_Err
442448 }
@@ -449,7 +455,7 @@ func HomeRowView(r HomeRow) templ.Component {
449455 if templ_7745c5c3_Err != nil {
450456 return templ_7745c5c3_Err
451457 }
452- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" title=\"")
458+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"")
453459 if templ_7745c5c3_Err != nil {
454460 return templ_7745c5c3_Err
455461 }
@@ -462,7 +468,7 @@ func HomeRowView(r HomeRow) templ.Component {
462468 if templ_7745c5c3_Err != nil {
463469 return templ_7745c5c3_Err
464470 }
465- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">")
471+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
466472 if templ_7745c5c3_Err != nil {
467473 return templ_7745c5c3_Err
468474 }
@@ -475,12 +481,12 @@ func HomeRowView(r HomeRow) templ.Component {
475481 if templ_7745c5c3_Err != nil {
476482 return templ_7745c5c3_Err
477483 }
478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a></dd>")
484+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</a></dd>")
479485 if templ_7745c5c3_Err != nil {
480486 return templ_7745c5c3_Err
481487 }
482488 } else {
483- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<dd title=\"")
489+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<dd title=\"")
484490 if templ_7745c5c3_Err != nil {
485491 return templ_7745c5c3_Err
486492 }
@@ -493,7 +499,7 @@ func HomeRowView(r HomeRow) templ.Component {
493499 if templ_7745c5c3_Err != nil {
494500 return templ_7745c5c3_Err
495501 }
496- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\">")
502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">")
497503 if templ_7745c5c3_Err != nil {
498504 return templ_7745c5c3_Err
499505 }
@@ -506,22 +512,22 @@ func HomeRowView(r HomeRow) templ.Component {
506512 if templ_7745c5c3_Err != nil {
507513 return templ_7745c5c3_Err
508514 }
509- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</dd>")
515+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dd>")
510516 if templ_7745c5c3_Err != nil {
511517 return templ_7745c5c3_Err
512518 }
513519 }
514- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div>")
520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div>")
515521 if templ_7745c5c3_Err != nil {
516522 return templ_7745c5c3_Err
517523 }
518524 }
519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</dl>")
525+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</dl>")
520526 if templ_7745c5c3_Err != nil {
521527 return templ_7745c5c3_Err
522528 }
523529 if r.CI != nil {
524- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<a class=\"repo-ci\" href=\"")
530+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a class=\"repo-ci\" href=\"")
525531 if templ_7745c5c3_Err != nil {
526532 return templ_7745c5c3_Err
527533 }
@@ -534,7 +540,7 @@ func HomeRowView(r HomeRow) templ.Component {
534540 if templ_7745c5c3_Err != nil {
535541 return templ_7745c5c3_Err
536542 }
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" title=\"")
543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" title=\"")
538544 if templ_7745c5c3_Err != nil {
539545 return templ_7745c5c3_Err
540546 }
@@ -547,7 +553,7 @@ func HomeRowView(r HomeRow) templ.Component {
547553 if templ_7745c5c3_Err != nil {
548554 return templ_7745c5c3_Err
549555 }
550- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">")
556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
551557 if templ_7745c5c3_Err != nil {
552558 return templ_7745c5c3_Err
553559 }
@@ -556,7 +562,7 @@ func HomeRowView(r HomeRow) templ.Component {
556562 if templ_7745c5c3_Err != nil {
557563 return templ_7745c5c3_Err
558564 }
559- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"")
565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<span class=\"")
560566 if templ_7745c5c3_Err != nil {
561567 return templ_7745c5c3_Err
562568 }
@@ -569,7 +575,7 @@ func HomeRowView(r HomeRow) templ.Component {
569575 if templ_7745c5c3_Err != nil {
570576 return templ_7745c5c3_Err
571577 }
572- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" aria-hidden=\"true\"></span> <span class=\"sr\">CI: </span><span class=\"ci-label\">")
578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" aria-hidden=\"true\"></span> <span class=\"sr\">CI: </span><span class=\"ci-label\">")
573579 if templ_7745c5c3_Err != nil {
574580 return templ_7745c5c3_Err
575581 }
@@ -582,27 +588,27 @@ func HomeRowView(r HomeRow) templ.Component {
582588 if templ_7745c5c3_Err != nil {
583589 return templ_7745c5c3_Err
584590 }
585- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span></a>")
591+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span></a>")
586592 if templ_7745c5c3_Err != nil {
587593 return templ_7745c5c3_Err
588594 }
589595 }
590- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div>")
596+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>")
591597 if templ_7745c5c3_Err != nil {
592598 return templ_7745c5c3_Err
593599 }
594600 if len(r.Commits) > 0 {
595- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<ul class=\"repo-commits\"")
601+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<ul class=\"repo-commits\"")
596602 if templ_7745c5c3_Err != nil {
597603 return templ_7745c5c3_Err
598604 }
599605 if len(r.Commits) == homeRecent {
600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " data-full")
606+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-full")
601607 if templ_7745c5c3_Err != nil {
602608 return templ_7745c5c3_Err
603609 }
604610 }
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " aria-label=\"")
611+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " aria-label=\"")
606612 if templ_7745c5c3_Err != nil {
607613 return templ_7745c5c3_Err
608614 }
@@ -615,12 +621,12 @@ func HomeRowView(r HomeRow) templ.Component {
615621 if templ_7745c5c3_Err != nil {
616622 return templ_7745c5c3_Err
617623 }
618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">")
624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">")
619625 if templ_7745c5c3_Err != nil {
620626 return templ_7745c5c3_Err
621627 }
622628 for _, c := range r.Commits {
623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<li><a class=\"rc-sha\" href=\"")
629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a class=\"rc-sha\" href=\"")
624630 if templ_7745c5c3_Err != nil {
625631 return templ_7745c5c3_Err
626632 }
@@ -633,7 +639,7 @@ func HomeRowView(r HomeRow) templ.Component {
633639 if templ_7745c5c3_Err != nil {
634640 return templ_7745c5c3_Err
635641 }
636- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">")
637643 if templ_7745c5c3_Err != nil {
638644 return templ_7745c5c3_Err
639645 }
@@ -646,7 +652,7 @@ func HomeRowView(r HomeRow) templ.Component {
646652 if templ_7745c5c3_Err != nil {
647653 return templ_7745c5c3_Err
648654 }
649- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a> <span class=\"rc-subject\" title=\"")
655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</a> <span class=\"rc-subject\" title=\"")
650656 if templ_7745c5c3_Err != nil {
651657 return templ_7745c5c3_Err
652658 }
@@ -659,7 +665,7 @@ func HomeRowView(r HomeRow) templ.Component {
659665 if templ_7745c5c3_Err != nil {
660666 return templ_7745c5c3_Err
661667 }
662- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">")
668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">")
663669 if templ_7745c5c3_Err != nil {
664670 return templ_7745c5c3_Err
665671 }
@@ -672,12 +678,12 @@ func HomeRowView(r HomeRow) templ.Component {
672678 if templ_7745c5c3_Err != nil {
673679 return templ_7745c5c3_Err
674680 }
675- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span> ")
681+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</span> ")
676682 if templ_7745c5c3_Err != nil {
677683 return templ_7745c5c3_Err
678684 }
679685 if c.WhenAt != "" {
680- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<time class=\"dim\" datetime=\"")
686+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<time class=\"dim\" datetime=\"")
681687 if templ_7745c5c3_Err != nil {
682688 return templ_7745c5c3_Err
683689 }
@@ -690,7 +696,7 @@ func HomeRowView(r HomeRow) templ.Component {
690696 if templ_7745c5c3_Err != nil {
691697 return templ_7745c5c3_Err
692698 }
693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" title=\"")
699+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" title=\"")
694700 if templ_7745c5c3_Err != nil {
695701 return templ_7745c5c3_Err
696702 }
@@ -703,7 +709,7 @@ func HomeRowView(r HomeRow) templ.Component {
703709 if templ_7745c5c3_Err != nil {
704710 return templ_7745c5c3_Err
705711 }
706- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\">")
712+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\">")
707713 if templ_7745c5c3_Err != nil {
708714 return templ_7745c5c3_Err
709715 }
@@ -716,27 +722,27 @@ func HomeRowView(r HomeRow) templ.Component {
716722 if templ_7745c5c3_Err != nil {
717723 return templ_7745c5c3_Err
718724 }
719- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</time>")
725+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</time>")
720726 if templ_7745c5c3_Err != nil {
721727 return templ_7745c5c3_Err
722728 }
723729 }
724- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</li>")
730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</li>")
725731 if templ_7745c5c3_Err != nil {
726732 return templ_7745c5c3_Err
727733 }
728734 }
729- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</ul>")
735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</ul>")
730736 if templ_7745c5c3_Err != nil {
731737 return templ_7745c5c3_Err
732738 }
733739 } else if r.NoCommits {
734- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<p class=\"repo-commits mute\">no commits yet</p>")
740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"repo-commits mute\">no commits yet</p>")
735741 if templ_7745c5c3_Err != nil {
736742 return templ_7745c5c3_Err
737743 }
738744 }
739- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</li>")
745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</li>")
740746 if templ_7745c5c3_Err != nil {
741747 return templ_7745c5c3_Err
742748 }
@@ -769,17 +775,17 @@ func pulse(h *Heartbeat) templ.Component {
769775 templ_7745c5c3_Var38 = templ.NopComponent
770776 }
771777 ctx = templ.ClearChildren(ctx)
772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<figure class=\"repo-pulse\"")
778+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<figure class=\"repo-pulse\"")
773779 if templ_7745c5c3_Err != nil {
774780 return templ_7745c5c3_Err
775781 }
776782 if h.Live {
777- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " data-live")
783+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " data-live")
778784 if templ_7745c5c3_Err != nil {
779785 return templ_7745c5c3_Err
780786 }
781787 }
782- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"")
788+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"")
783789 if templ_7745c5c3_Err != nil {
784790 return templ_7745c5c3_Err
785791 }
@@ -792,7 +798,7 @@ func pulse(h *Heartbeat) templ.Component {
792798 if templ_7745c5c3_Err != nil {
793799 return templ_7745c5c3_Err
794800 }
795- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" focusable=\"false\"><defs><linearGradient id=\"")
801+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" focusable=\"false\"><defs><linearGradient id=\"")
796802 if templ_7745c5c3_Err != nil {
797803 return templ_7745c5c3_Err
798804 }
@@ -805,7 +811,7 @@ func pulse(h *Heartbeat) templ.Component {
805811 if templ_7745c5c3_Err != nil {
806812 return templ_7745c5c3_Err
807813 }
808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"")
814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"")
809815 if templ_7745c5c3_Err != nil {
810816 return templ_7745c5c3_Err
811817 }
@@ -818,7 +824,7 @@ func pulse(h *Heartbeat) templ.Component {
818824 if templ_7745c5c3_Err != nil {
819825 return templ_7745c5c3_Err
820826 }
821- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\" fill=\"")
827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" fill=\"")
822828 if templ_7745c5c3_Err != nil {
823829 return templ_7745c5c3_Err
824830 }
@@ -831,7 +837,7 @@ func pulse(h *Heartbeat) templ.Component {
831837 if templ_7745c5c3_Err != nil {
832838 return templ_7745c5c3_Err
833839 }
834- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\"></path> <path class=\"pulse-line\" d=\"")
840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"></path> <path class=\"pulse-line\" d=\"")
835841 if templ_7745c5c3_Err != nil {
836842 return templ_7745c5c3_Err
837843 }
@@ -844,7 +850,7 @@ func pulse(h *Heartbeat) templ.Component {
844850 if templ_7745c5c3_Err != nil {
845851 return templ_7745c5c3_Err
846852 }
847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"")
853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"")
848854 if templ_7745c5c3_Err != nil {
849855 return templ_7745c5c3_Err
850856 }
@@ -857,7 +863,7 @@ func pulse(h *Heartbeat) templ.Component {
857863 if templ_7745c5c3_Err != nil {
858864 return templ_7745c5c3_Err
859865 }
860- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" cy=\"")
866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" cy=\"")
861867 if templ_7745c5c3_Err != nil {
862868 return templ_7745c5c3_Err
863869 }
@@ -870,7 +876,7 @@ func pulse(h *Heartbeat) templ.Component {
870876 if templ_7745c5c3_Err != nil {
871877 return templ_7745c5c3_Err
872878 }
873- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" r=\"3.5\"></circle></svg></figure>")
879+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" r=\"3.5\"></circle></svg></figure>")
874880 if templ_7745c5c3_Err != nil {
875881 return templ_7745c5c3_Err
876882 }
@@ -900,7 +906,7 @@ func Outline(headings []Heading) templ.Component {
900906 templ_7745c5c3_Var46 = templ.NopComponent
901907 }
902908 ctx = templ.ClearChildren(ctx)
903- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<nav class=\"outline\" aria-label=\"Outline\">")
909+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<nav class=\"outline\" aria-label=\"Outline\">")
904910 if templ_7745c5c3_Err != nil {
905911 return templ_7745c5c3_Err
906912 }
@@ -910,7 +916,7 @@ func Outline(headings []Heading) templ.Component {
910916 if templ_7745c5c3_Err != nil {
911917 return templ_7745c5c3_Err
912918 }
913- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<a class=\"")
919+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<a class=\"")
914920 if templ_7745c5c3_Err != nil {
915921 return templ_7745c5c3_Err
916922 }
@@ -923,7 +929,7 @@ func Outline(headings []Heading) templ.Component {
923929 if templ_7745c5c3_Err != nil {
924930 return templ_7745c5c3_Err
925931 }
926- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" href=\"")
932+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" href=\"")
927933 if templ_7745c5c3_Err != nil {
928934 return templ_7745c5c3_Err
929935 }
@@ -936,7 +942,7 @@ func Outline(headings []Heading) templ.Component {
936942 if templ_7745c5c3_Err != nil {
937943 return templ_7745c5c3_Err
938944 }
939- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\">")
945+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">")
940946 if templ_7745c5c3_Err != nil {
941947 return templ_7745c5c3_Err
942948 }
@@ -949,12 +955,12 @@ func Outline(headings []Heading) templ.Component {
949955 if templ_7745c5c3_Err != nil {
950956 return templ_7745c5c3_Err
951957 }
952- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</a>")
958+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a>")
953959 if templ_7745c5c3_Err != nil {
954960 return templ_7745c5c3_Err
955961 }
956962 }
957- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</nav>")
963+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</nav>")
958964 if templ_7745c5c3_Err != nil {
959965 return templ_7745c5c3_Err
960966 }
@@ -984,7 +990,7 @@ func ReadmeCard(r Readme) templ.Component {
984990 templ_7745c5c3_Var51 = templ.NopComponent
985991 }
986992 ctx = templ.ClearChildren(ctx)
987- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<section class=\"card readme\" aria-labelledby=\"readme-h\"><h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ")
993+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<section class=\"card readme\" aria-labelledby=\"readme-h\"><h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ")
988994 if templ_7745c5c3_Err != nil {
989995 return templ_7745c5c3_Err
990996 }
@@ -997,7 +1003,7 @@ func ReadmeCard(r Readme) templ.Component {
9971003 if templ_7745c5c3_Err != nil {
9981004 return templ_7745c5c3_Err
9991005 }
1000- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</h2>")
1006+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</h2>")
10011007 if templ_7745c5c3_Err != nil {
10021008 return templ_7745c5c3_Err
10031009 }
@@ -1005,7 +1011,7 @@ func ReadmeCard(r Readme) templ.Component {
10051011 if templ_7745c5c3_Err != nil {
10061012 return templ_7745c5c3_Err
10071013 }
1008- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</section>")
1014+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</section>")
10091015 if templ_7745c5c3_Err != nil {
10101016 return templ_7745c5c3_Err
10111017 }
@@ -1036,7 +1042,7 @@ func ReadmeView(r Readme) templ.Component {
10361042 templ_7745c5c3_Var53 = templ.NopComponent
10371043 }
10381044 ctx = templ.ClearChildren(ctx)
1039- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<article class=\"prose\" aria-label=\"")
1045+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<article class=\"prose\" aria-label=\"")
10401046 if templ_7745c5c3_Err != nil {
10411047 return templ_7745c5c3_Err
10421048 }
@@ -1049,12 +1055,12 @@ func ReadmeView(r Readme) templ.Component {
10491055 if templ_7745c5c3_Err != nil {
10501056 return templ_7745c5c3_Err
10511057 }
1052- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\" hx-ignore>")
1058+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" hx-ignore>")
10531059 if templ_7745c5c3_Err != nil {
10541060 return templ_7745c5c3_Err
10551061 }
10561062 if r.Note != "" {
1057- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<p class=\"dim\">")
1063+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<p class=\"dim\">")
10581064 if templ_7745c5c3_Err != nil {
10591065 return templ_7745c5c3_Err
10601066 }
@@ -1067,7 +1073,7 @@ func ReadmeView(r Readme) templ.Component {
10671073 if templ_7745c5c3_Err != nil {
10681074 return templ_7745c5c3_Err
10691075 }
1070- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</p>")
1076+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</p>")
10711077 if templ_7745c5c3_Err != nil {
10721078 return templ_7745c5c3_Err
10731079 }
@@ -1077,7 +1083,7 @@ func ReadmeView(r Readme) templ.Component {
10771083 return templ_7745c5c3_Err
10781084 }
10791085 }
1080- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</article>")
1086+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</article>")
10811087 if templ_7745c5c3_Err != nil {
10821088 return templ_7745c5c3_Err
10831089 }
@@ -1108,17 +1114,17 @@ func CommitRows(rows []CommitRow) templ.Component {
11081114 templ_7745c5c3_Var56 = templ.NopComponent
11091115 }
11101116 ctx = templ.ClearChildren(ctx)
1111- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>")
1117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>")
11121118 if templ_7745c5c3_Err != nil {
11131119 return templ_7745c5c3_Err
11141120 }
11151121 for _, c := range rows {
1116- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<tr")
1122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<tr")
11171123 if templ_7745c5c3_Err != nil {
11181124 return templ_7745c5c3_Err
11191125 }
11201126 if c.ID != "" {
1121- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, " id=\"")
1127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " id=\"")
11221128 if templ_7745c5c3_Err != nil {
11231129 return templ_7745c5c3_Err
11241130 }
@@ -1131,18 +1137,18 @@ func CommitRows(rows []CommitRow) templ.Component {
11311137 if templ_7745c5c3_Err != nil {
11321138 return templ_7745c5c3_Err
11331139 }
1134- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\"")
1140+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"")
11351141 if templ_7745c5c3_Err != nil {
11361142 return templ_7745c5c3_Err
11371143 }
11381144 }
11391145 if c.Cursor {
1140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " data-cursor")
1146+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " data-cursor")
11411147 if templ_7745c5c3_Err != nil {
11421148 return templ_7745c5c3_Err
11431149 }
11441150 }
1145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "><td class=\"c-sha mute\">")
1151+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "><td class=\"c-sha mute\">")
11461152 if templ_7745c5c3_Err != nil {
11471153 return templ_7745c5c3_Err
11481154 }
@@ -1155,7 +1161,7 @@ func CommitRows(rows []CommitRow) templ.Component {
11551161 if templ_7745c5c3_Err != nil {
11561162 return templ_7745c5c3_Err
11571163 }
1158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</td><td class=\"c-subject\"><a href=\"")
1164+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "</td><td class=\"c-subject\"><a href=\"")
11591165 if templ_7745c5c3_Err != nil {
11601166 return templ_7745c5c3_Err
11611167 }
@@ -1168,7 +1174,7 @@ func CommitRows(rows []CommitRow) templ.Component {
11681174 if templ_7745c5c3_Err != nil {
11691175 return templ_7745c5c3_Err
11701176 }
1171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "\">")
1177+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "\">")
11721178 if templ_7745c5c3_Err != nil {
11731179 return templ_7745c5c3_Err
11741180 }
@@ -1181,12 +1187,12 @@ func CommitRows(rows []CommitRow) templ.Component {
11811187 if templ_7745c5c3_Err != nil {
11821188 return templ_7745c5c3_Err
11831189 }
1184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a> ")
1190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</a> ")
11851191 if templ_7745c5c3_Err != nil {
11861192 return templ_7745c5c3_Err
11871193 }
11881194 if c.Merge {
1189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<span class=\"pill\">merge</span> ")
1195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span class=\"pill\">merge</span> ")
11901196 if templ_7745c5c3_Err != nil {
11911197 return templ_7745c5c3_Err
11921198 }
@@ -1198,7 +1204,7 @@ func CommitRows(rows []CommitRow) templ.Component {
11981204 }
11991205 }
12001206 for _, b := range c.Branches {
1201- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span class=\"pill\">")
1207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<span class=\"pill\">")
12021208 if templ_7745c5c3_Err != nil {
12031209 return templ_7745c5c3_Err
12041210 }
@@ -1211,13 +1217,13 @@ func CommitRows(rows []CommitRow) templ.Component {
12111217 if templ_7745c5c3_Err != nil {
12121218 return templ_7745c5c3_Err
12131219 }
1214- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</span> ")
1220+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ")
12151221 if templ_7745c5c3_Err != nil {
12161222 return templ_7745c5c3_Err
12171223 }
12181224 }
12191225 for _, t := range c.Tags {
1220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<span class=\"pill tag\"><span class=\"sr\">tag </span>")
1226+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<span class=\"pill tag\"><span class=\"sr\">tag </span>")
12211227 if templ_7745c5c3_Err != nil {
12221228 return templ_7745c5c3_Err
12231229 }
@@ -1230,12 +1236,12 @@ func CommitRows(rows []CommitRow) templ.Component {
12301236 if templ_7745c5c3_Err != nil {
12311237 return templ_7745c5c3_Err
12321238 }
1233- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</span>")
1239+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span>")
12341240 if templ_7745c5c3_Err != nil {
12351241 return templ_7745c5c3_Err
12361242 }
12371243 }
1238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</td><td class=\"c-ck\">")
1244+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</td><td class=\"c-ck\">")
12391245 if templ_7745c5c3_Err != nil {
12401246 return templ_7745c5c3_Err
12411247 }
@@ -1243,7 +1249,7 @@ func CommitRows(rows []CommitRow) templ.Component {
12431249 if templ_7745c5c3_Err != nil {
12441250 return templ_7745c5c3_Err
12451251 }
1246- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</td><td class=\"c-who dim\">")
1252+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</td><td class=\"c-who dim\">")
12471253 if templ_7745c5c3_Err != nil {
12481254 return templ_7745c5c3_Err
12491255 }
@@ -1256,7 +1262,7 @@ func CommitRows(rows []CommitRow) templ.Component {
12561262 if templ_7745c5c3_Err != nil {
12571263 return templ_7745c5c3_Err
12581264 }
1259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</td><td class=\"c-when nowrap dim\"><time datetime=\"")
1265+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</td><td class=\"c-when nowrap dim\"><time datetime=\"")
12601266 if templ_7745c5c3_Err != nil {
12611267 return templ_7745c5c3_Err
12621268 }
@@ -1269,7 +1275,7 @@ func CommitRows(rows []CommitRow) templ.Component {
12691275 if templ_7745c5c3_Err != nil {
12701276 return templ_7745c5c3_Err
12711277 }
1272- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\" title=\"")
1278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" title=\"")
12731279 if templ_7745c5c3_Err != nil {
12741280 return templ_7745c5c3_Err
12751281 }
@@ -1282,7 +1288,7 @@ func CommitRows(rows []CommitRow) templ.Component {
12821288 if templ_7745c5c3_Err != nil {
12831289 return templ_7745c5c3_Err
12841290 }
1285- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\">")
1291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">")
12861292 if templ_7745c5c3_Err != nil {
12871293 return templ_7745c5c3_Err
12881294 }
@@ -1295,12 +1301,12 @@ func CommitRows(rows []CommitRow) templ.Component {
12951301 if templ_7745c5c3_Err != nil {
12961302 return templ_7745c5c3_Err
12971303 }
1298- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "</time></td></tr>")
1304+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</time></td></tr>")
12991305 if templ_7745c5c3_Err != nil {
13001306 return templ_7745c5c3_Err
13011307 }
13021308 }
1303- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</tbody></table>")
1309+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</tbody></table>")
13041310 if templ_7745c5c3_Err != nil {
13051311 return templ_7745c5c3_Err
13061312 }
@@ -1336,7 +1342,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
13361342 if templ_7745c5c3_Err != nil {
13371343 return templ_7745c5c3_Err
13381344 }
1339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"")
1345+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span class=\"")
13401346 if templ_7745c5c3_Err != nil {
13411347 return templ_7745c5c3_Err
13421348 }
@@ -1349,7 +1355,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
13491355 if templ_7745c5c3_Err != nil {
13501356 return templ_7745c5c3_Err
13511357 }
1352- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\" title=\"")
1358+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\" title=\"")
13531359 if templ_7745c5c3_Err != nil {
13541360 return templ_7745c5c3_Err
13551361 }
@@ -1362,12 +1368,12 @@ func signaturePill(b *SignatureBadge) templ.Component {
13621368 if templ_7745c5c3_Err != nil {
13631369 return templ_7745c5c3_Err
13641370 }
1365- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\">")
1371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\">")
13661372 if templ_7745c5c3_Err != nil {
13671373 return templ_7745c5c3_Err
13681374 }
13691375 if b.Proto != "" {
1370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<span class=\"sig-proto\">")
1376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<span class=\"sig-proto\">")
13711377 if templ_7745c5c3_Err != nil {
13721378 return templ_7745c5c3_Err
13731379 }
@@ -1380,7 +1386,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
13801386 if templ_7745c5c3_Err != nil {
13811387 return templ_7745c5c3_Err
13821388 }
1383- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</span> ")
1389+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span> ")
13841390 if templ_7745c5c3_Err != nil {
13851391 return templ_7745c5c3_Err
13861392 }
@@ -1394,7 +1400,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
13941400 if templ_7745c5c3_Err != nil {
13951401 return templ_7745c5c3_Err
13961402 }
1397- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span>")
1403+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</span>")
13981404 if templ_7745c5c3_Err != nil {
13991405 return templ_7745c5c3_Err
14001406 }
@@ -195,7 +195,13 @@ func HomeRowView(r HomeRow) templ.Component {
195 return templ_7745c5c3_Err195 return templ_7745c5c3_Err
196 }196 }
197 }197 }
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "><div class=\"repo-main\"><h2 class=\"repo-name\"><a href=\"")198+ if r.Fresh {
199+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " data-activity")
200+ if templ_7745c5c3_Err != nil {
201+ return templ_7745c5c3_Err
202+ }
203+ }
204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "><div class=\"repo-main\"><h2 class=\"repo-name\"><a href=\"")
199 if templ_7745c5c3_Err != nil {205 if templ_7745c5c3_Err != nil {
200 return templ_7745c5c3_Err206 return templ_7745c5c3_Err
201 }207 }
@@ -208,7 +214,7 @@ func HomeRowView(r HomeRow) templ.Component {
208 if templ_7745c5c3_Err != nil {214 if templ_7745c5c3_Err != nil {
209 return templ_7745c5c3_Err215 return templ_7745c5c3_Err
210 }216 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
212 if templ_7745c5c3_Err != nil {218 if templ_7745c5c3_Err != nil {
213 return templ_7745c5c3_Err219 return templ_7745c5c3_Err
214 }220 }
@@ -221,28 +227,28 @@ func HomeRowView(r HomeRow) templ.Component {
221 if templ_7745c5c3_Err != nil {227 if templ_7745c5c3_Err != nil {
222 return templ_7745c5c3_Err228 return templ_7745c5c3_Err
223 }229 }
224- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a> ")230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a> ")
225 if templ_7745c5c3_Err != nil {231 if templ_7745c5c3_Err != nil {
226 return templ_7745c5c3_Err232 return templ_7745c5c3_Err
227 }233 }
228 if r.Private {234 if r.Private {
229- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"pill\">private</span> ")235+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"pill\">private</span> ")
230 if templ_7745c5c3_Err != nil {236 if templ_7745c5c3_Err != nil {
231 return templ_7745c5c3_Err237 return templ_7745c5c3_Err
232 }238 }
233 }239 }
234 if r.Fresh {240 if r.Fresh {
235- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"pill new\">new push</span>")241+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"pill new\">new activity</span>")
236 if templ_7745c5c3_Err != nil {242 if templ_7745c5c3_Err != nil {
237 return templ_7745c5c3_Err243 return templ_7745c5c3_Err
238 }244 }
239 }245 }
240- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</h2>")246+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h2>")
241 if templ_7745c5c3_Err != nil {247 if templ_7745c5c3_Err != nil {
242 return templ_7745c5c3_Err248 return templ_7745c5c3_Err
243 }249 }
244 if r.Desc != "" {250 if r.Desc != "" {
245- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"repo-desc\">")251+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"repo-desc\">")
246 if templ_7745c5c3_Err != nil {252 if templ_7745c5c3_Err != nil {
247 return templ_7745c5c3_Err253 return templ_7745c5c3_Err
248 }254 }
@@ -255,18 +261,18 @@ func HomeRowView(r HomeRow) templ.Component {
255 if templ_7745c5c3_Err != nil {261 if templ_7745c5c3_Err != nil {
256 return templ_7745c5c3_Err262 return templ_7745c5c3_Err
257 }263 }
258- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p>")264+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p>")
259 if templ_7745c5c3_Err != nil {265 if templ_7745c5c3_Err != nil {
260 return templ_7745c5c3_Err266 return templ_7745c5c3_Err
261 }267 }
262 } else {268 } else {
263- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"repo-desc mute\">no description</p>")269+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"repo-desc mute\">no description</p>")
264 if templ_7745c5c3_Err != nil {270 if templ_7745c5c3_Err != nil {
265 return templ_7745c5c3_Err271 return templ_7745c5c3_Err
266 }272 }
267 }273 }
268 if len(r.Topics) > 0 {274 if len(r.Topics) > 0 {
269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<ul class=\"repo-topics\" aria-label=\"")275+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<ul class=\"repo-topics\" aria-label=\"")
270 if templ_7745c5c3_Err != nil {276 if templ_7745c5c3_Err != nil {
271 return templ_7745c5c3_Err277 return templ_7745c5c3_Err
272 }278 }
@@ -279,12 +285,12 @@ func HomeRowView(r HomeRow) templ.Component {
279 if templ_7745c5c3_Err != nil {285 if templ_7745c5c3_Err != nil {
280 return templ_7745c5c3_Err286 return templ_7745c5c3_Err
281 }287 }
282- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-fit>")288+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-fit>")
283 if templ_7745c5c3_Err != nil {289 if templ_7745c5c3_Err != nil {
284 return templ_7745c5c3_Err290 return templ_7745c5c3_Err
285 }291 }
286 for _, t := range r.Topics {292 for _, t := range r.Topics {
287- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<li>")293+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li>")
288 if templ_7745c5c3_Err != nil {294 if templ_7745c5c3_Err != nil {
289 return templ_7745c5c3_Err295 return templ_7745c5c3_Err
290 }296 }
@@ -297,17 +303,17 @@ func HomeRowView(r HomeRow) templ.Component {
297 if templ_7745c5c3_Err != nil {303 if templ_7745c5c3_Err != nil {
298 return templ_7745c5c3_Err304 return templ_7745c5c3_Err
299 }305 }
300- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</li>")306+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</li>")
301 if templ_7745c5c3_Err != nil {307 if templ_7745c5c3_Err != nil {
302 return templ_7745c5c3_Err308 return templ_7745c5c3_Err
303 }309 }
304 }310 }
305- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</ul>")311+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</ul>")
306 if templ_7745c5c3_Err != nil {312 if templ_7745c5c3_Err != nil {
307 return templ_7745c5c3_Err313 return templ_7745c5c3_Err
308 }314 }
309 } else {315 } else {
310- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>")316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"repo-topics\" aria-hidden=\"true\"></div>")
311 if templ_7745c5c3_Err != nil {317 if templ_7745c5c3_Err != nil {
312 return templ_7745c5c3_Err318 return templ_7745c5c3_Err
313 }319 }
@@ -318,12 +324,12 @@ func HomeRowView(r HomeRow) templ.Component {
318 return templ_7745c5c3_Err324 return templ_7745c5c3_Err
319 }325 }
320 } else {326 } else {
321- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>")327+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"repo-pulse\" aria-hidden=\"true\"></div>")
322 if templ_7745c5c3_Err != nil {328 if templ_7745c5c3_Err != nil {
323 return templ_7745c5c3_Err329 return templ_7745c5c3_Err
324 }330 }
325 }331 }
326- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"")332+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><div class=\"repo-head\"><dl class=\"repo-stats\" aria-label=\"")
327 if templ_7745c5c3_Err != nil {333 if templ_7745c5c3_Err != nil {
328 return templ_7745c5c3_Err334 return templ_7745c5c3_Err
329 }335 }
@@ -336,12 +342,12 @@ func HomeRowView(r HomeRow) templ.Component {
336 if templ_7745c5c3_Err != nil {342 if templ_7745c5c3_Err != nil {
337 return templ_7745c5c3_Err343 return templ_7745c5c3_Err
338 }344 }
339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")345+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">")
340 if templ_7745c5c3_Err != nil {346 if templ_7745c5c3_Err != nil {
341 return templ_7745c5c3_Err347 return templ_7745c5c3_Err
342 }348 }
343 if s := r.Stats; s != nil {349 if s := r.Stats; s != nil {
344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div><dt>commits</dt><dd>")350+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div><dt>commits</dt><dd>")
345 if templ_7745c5c3_Err != nil {351 if templ_7745c5c3_Err != nil {
346 return templ_7745c5c3_Err352 return templ_7745c5c3_Err
347 }353 }
@@ -354,7 +360,7 @@ func HomeRowView(r HomeRow) templ.Component {
354 if templ_7745c5c3_Err != nil {360 if templ_7745c5c3_Err != nil {
355 return templ_7745c5c3_Err361 return templ_7745c5c3_Err
356 }362 }
357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</dd></div><div><dt>last 30 days</dt><dd>")363+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</dd></div><div><dt>last 30 days</dt><dd>")
358 if templ_7745c5c3_Err != nil {364 if templ_7745c5c3_Err != nil {
359 return templ_7745c5c3_Err365 return templ_7745c5c3_Err
360 }366 }
@@ -367,17 +373,17 @@ func HomeRowView(r HomeRow) templ.Component {
367 if templ_7745c5c3_Err != nil {373 if templ_7745c5c3_Err != nil {
368 return templ_7745c5c3_Err374 return templ_7745c5c3_Err
369 }375 }
370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</dd></div>")376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</dd></div>")
371 if templ_7745c5c3_Err != nil {377 if templ_7745c5c3_Err != nil {
372 return templ_7745c5c3_Err378 return templ_7745c5c3_Err
373 }379 }
374 }380 }
375- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"rs-active\"><dt>last activity</dt>")381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"rs-active\"><dt>last activity</dt>")
376 if templ_7745c5c3_Err != nil {382 if templ_7745c5c3_Err != nil {
377 return templ_7745c5c3_Err383 return templ_7745c5c3_Err
378 }384 }
379 if r.ActiveAt != "" {385 if r.ActiveAt != "" {
380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<dd><time datetime=\"")386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<dd><time datetime=\"")
381 if templ_7745c5c3_Err != nil {387 if templ_7745c5c3_Err != nil {
382 return templ_7745c5c3_Err388 return templ_7745c5c3_Err
383 }389 }
@@ -390,7 +396,7 @@ func HomeRowView(r HomeRow) templ.Component {
390 if templ_7745c5c3_Err != nil {396 if templ_7745c5c3_Err != nil {
391 return templ_7745c5c3_Err397 return templ_7745c5c3_Err
392 }398 }
393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" title=\"")399+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" title=\"")
394 if templ_7745c5c3_Err != nil {400 if templ_7745c5c3_Err != nil {
395 return templ_7745c5c3_Err401 return templ_7745c5c3_Err
396 }402 }
@@ -403,7 +409,7 @@ func HomeRowView(r HomeRow) templ.Component {
403 if templ_7745c5c3_Err != nil {409 if templ_7745c5c3_Err != nil {
404 return templ_7745c5c3_Err410 return templ_7745c5c3_Err
405 }411 }
406- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")412+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">")
407 if templ_7745c5c3_Err != nil {413 if templ_7745c5c3_Err != nil {
408 return templ_7745c5c3_Err414 return templ_7745c5c3_Err
409 }415 }
@@ -416,27 +422,27 @@ func HomeRowView(r HomeRow) templ.Component {
416 if templ_7745c5c3_Err != nil {422 if templ_7745c5c3_Err != nil {
417 return templ_7745c5c3_Err423 return templ_7745c5c3_Err
418 }424 }
419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</time></dd>")425+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</time></dd>")
420 if templ_7745c5c3_Err != nil {426 if templ_7745c5c3_Err != nil {
421 return templ_7745c5c3_Err427 return templ_7745c5c3_Err
422 }428 }
423 } else {429 } else {
424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<dd class=\"mute\">never</dd>")430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<dd class=\"mute\">never</dd>")
425 if templ_7745c5c3_Err != nil {431 if templ_7745c5c3_Err != nil {
426 return templ_7745c5c3_Err432 return templ_7745c5c3_Err
427 }433 }
428 }434 }
429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div>")435+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div>")
430 if templ_7745c5c3_Err != nil {436 if templ_7745c5c3_Err != nil {
431 return templ_7745c5c3_Err437 return templ_7745c5c3_Err
432 }438 }
433 if s := r.Stats; s != nil && s.Tag != "" {439 if s := r.Stats; s != nil && s.Tag != "" {
434- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"rs-tag\"><dt>last tag</dt>")440+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"rs-tag\"><dt>last tag</dt>")
435 if templ_7745c5c3_Err != nil {441 if templ_7745c5c3_Err != nil {
436 return templ_7745c5c3_Err442 return templ_7745c5c3_Err
437 }443 }
438 if s.TagHref != "" {444 if s.TagHref != "" {
439- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<dd><a href=\"")445+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<dd><a href=\"")
440 if templ_7745c5c3_Err != nil {446 if templ_7745c5c3_Err != nil {
441 return templ_7745c5c3_Err447 return templ_7745c5c3_Err
442 }448 }
@@ -449,7 +455,7 @@ func HomeRowView(r HomeRow) templ.Component {
449 if templ_7745c5c3_Err != nil {455 if templ_7745c5c3_Err != nil {
450 return templ_7745c5c3_Err456 return templ_7745c5c3_Err
451 }457 }
452- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" title=\"")458+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"")
453 if templ_7745c5c3_Err != nil {459 if templ_7745c5c3_Err != nil {
454 return templ_7745c5c3_Err460 return templ_7745c5c3_Err
455 }461 }
@@ -462,7 +468,7 @@ func HomeRowView(r HomeRow) templ.Component {
462 if templ_7745c5c3_Err != nil {468 if templ_7745c5c3_Err != nil {
463 return templ_7745c5c3_Err469 return templ_7745c5c3_Err
464 }470 }
465- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">")471+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
466 if templ_7745c5c3_Err != nil {472 if templ_7745c5c3_Err != nil {
467 return templ_7745c5c3_Err473 return templ_7745c5c3_Err
468 }474 }
@@ -475,12 +481,12 @@ func HomeRowView(r HomeRow) templ.Component {
475 if templ_7745c5c3_Err != nil {481 if templ_7745c5c3_Err != nil {
476 return templ_7745c5c3_Err482 return templ_7745c5c3_Err
477 }483 }
478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a></dd>")484+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</a></dd>")
479 if templ_7745c5c3_Err != nil {485 if templ_7745c5c3_Err != nil {
480 return templ_7745c5c3_Err486 return templ_7745c5c3_Err
481 }487 }
482 } else {488 } else {
483- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<dd title=\"")489+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<dd title=\"")
484 if templ_7745c5c3_Err != nil {490 if templ_7745c5c3_Err != nil {
485 return templ_7745c5c3_Err491 return templ_7745c5c3_Err
486 }492 }
@@ -493,7 +499,7 @@ func HomeRowView(r HomeRow) templ.Component {
493 if templ_7745c5c3_Err != nil {499 if templ_7745c5c3_Err != nil {
494 return templ_7745c5c3_Err500 return templ_7745c5c3_Err
495 }501 }
496- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\">")502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">")
497 if templ_7745c5c3_Err != nil {503 if templ_7745c5c3_Err != nil {
498 return templ_7745c5c3_Err504 return templ_7745c5c3_Err
499 }505 }
@@ -506,22 +512,22 @@ func HomeRowView(r HomeRow) templ.Component {
506 if templ_7745c5c3_Err != nil {512 if templ_7745c5c3_Err != nil {
507 return templ_7745c5c3_Err513 return templ_7745c5c3_Err
508 }514 }
509- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</dd>")515+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</dd>")
510 if templ_7745c5c3_Err != nil {516 if templ_7745c5c3_Err != nil {
511 return templ_7745c5c3_Err517 return templ_7745c5c3_Err
512 }518 }
513 }519 }
514- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div>")520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div>")
515 if templ_7745c5c3_Err != nil {521 if templ_7745c5c3_Err != nil {
516 return templ_7745c5c3_Err522 return templ_7745c5c3_Err
517 }523 }
518 }524 }
519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</dl>")525+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</dl>")
520 if templ_7745c5c3_Err != nil {526 if templ_7745c5c3_Err != nil {
521 return templ_7745c5c3_Err527 return templ_7745c5c3_Err
522 }528 }
523 if r.CI != nil {529 if r.CI != nil {
524- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<a class=\"repo-ci\" href=\"")530+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a class=\"repo-ci\" href=\"")
525 if templ_7745c5c3_Err != nil {531 if templ_7745c5c3_Err != nil {
526 return templ_7745c5c3_Err532 return templ_7745c5c3_Err
527 }533 }
@@ -534,7 +540,7 @@ func HomeRowView(r HomeRow) templ.Component {
534 if templ_7745c5c3_Err != nil {540 if templ_7745c5c3_Err != nil {
535 return templ_7745c5c3_Err541 return templ_7745c5c3_Err
536 }542 }
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" title=\"")543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" title=\"")
538 if templ_7745c5c3_Err != nil {544 if templ_7745c5c3_Err != nil {
539 return templ_7745c5c3_Err545 return templ_7745c5c3_Err
540 }546 }
@@ -547,7 +553,7 @@ func HomeRowView(r HomeRow) templ.Component {
547 if templ_7745c5c3_Err != nil {553 if templ_7745c5c3_Err != nil {
548 return templ_7745c5c3_Err554 return templ_7745c5c3_Err
549 }555 }
550- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">")556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
551 if templ_7745c5c3_Err != nil {557 if templ_7745c5c3_Err != nil {
552 return templ_7745c5c3_Err558 return templ_7745c5c3_Err
553 }559 }
@@ -556,7 +562,7 @@ func HomeRowView(r HomeRow) templ.Component {
556 if templ_7745c5c3_Err != nil {562 if templ_7745c5c3_Err != nil {
557 return templ_7745c5c3_Err563 return templ_7745c5c3_Err
558 }564 }
559- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"")565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<span class=\"")
560 if templ_7745c5c3_Err != nil {566 if templ_7745c5c3_Err != nil {
561 return templ_7745c5c3_Err567 return templ_7745c5c3_Err
562 }568 }
@@ -569,7 +575,7 @@ func HomeRowView(r HomeRow) templ.Component {
569 if templ_7745c5c3_Err != nil {575 if templ_7745c5c3_Err != nil {
570 return templ_7745c5c3_Err576 return templ_7745c5c3_Err
571 }577 }
572- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" aria-hidden=\"true\"></span> <span class=\"sr\">CI: </span><span class=\"ci-label\">")578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" aria-hidden=\"true\"></span> <span class=\"sr\">CI: </span><span class=\"ci-label\">")
573 if templ_7745c5c3_Err != nil {579 if templ_7745c5c3_Err != nil {
574 return templ_7745c5c3_Err580 return templ_7745c5c3_Err
575 }581 }
@@ -582,27 +588,27 @@ func HomeRowView(r HomeRow) templ.Component {
582 if templ_7745c5c3_Err != nil {588 if templ_7745c5c3_Err != nil {
583 return templ_7745c5c3_Err589 return templ_7745c5c3_Err
584 }590 }
585- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span></a>")591+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span></a>")
586 if templ_7745c5c3_Err != nil {592 if templ_7745c5c3_Err != nil {
587 return templ_7745c5c3_Err593 return templ_7745c5c3_Err
588 }594 }
589 }595 }
590- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div>")596+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>")
591 if templ_7745c5c3_Err != nil {597 if templ_7745c5c3_Err != nil {
592 return templ_7745c5c3_Err598 return templ_7745c5c3_Err
593 }599 }
594 if len(r.Commits) > 0 {600 if len(r.Commits) > 0 {
595- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<ul class=\"repo-commits\"")601+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<ul class=\"repo-commits\"")
596 if templ_7745c5c3_Err != nil {602 if templ_7745c5c3_Err != nil {
597 return templ_7745c5c3_Err603 return templ_7745c5c3_Err
598 }604 }
599 if len(r.Commits) == homeRecent {605 if len(r.Commits) == homeRecent {
600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " data-full")606+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " data-full")
601 if templ_7745c5c3_Err != nil {607 if templ_7745c5c3_Err != nil {
602 return templ_7745c5c3_Err608 return templ_7745c5c3_Err
603 }609 }
604 }610 }
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " aria-label=\"")611+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " aria-label=\"")
606 if templ_7745c5c3_Err != nil {612 if templ_7745c5c3_Err != nil {
607 return templ_7745c5c3_Err613 return templ_7745c5c3_Err
608 }614 }
@@ -615,12 +621,12 @@ func HomeRowView(r HomeRow) templ.Component {
615 if templ_7745c5c3_Err != nil {621 if templ_7745c5c3_Err != nil {
616 return templ_7745c5c3_Err622 return templ_7745c5c3_Err
617 }623 }
618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">")624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">")
619 if templ_7745c5c3_Err != nil {625 if templ_7745c5c3_Err != nil {
620 return templ_7745c5c3_Err626 return templ_7745c5c3_Err
621 }627 }
622 for _, c := range r.Commits {628 for _, c := range r.Commits {
623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<li><a class=\"rc-sha\" href=\"")629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a class=\"rc-sha\" href=\"")
624 if templ_7745c5c3_Err != nil {630 if templ_7745c5c3_Err != nil {
625 return templ_7745c5c3_Err631 return templ_7745c5c3_Err
626 }632 }
@@ -633,7 +639,7 @@ func HomeRowView(r HomeRow) templ.Component {
633 if templ_7745c5c3_Err != nil {639 if templ_7745c5c3_Err != nil {
634 return templ_7745c5c3_Err640 return templ_7745c5c3_Err
635 }641 }
636- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">")
637 if templ_7745c5c3_Err != nil {643 if templ_7745c5c3_Err != nil {
638 return templ_7745c5c3_Err644 return templ_7745c5c3_Err
639 }645 }
@@ -646,7 +652,7 @@ func HomeRowView(r HomeRow) templ.Component {
646 if templ_7745c5c3_Err != nil {652 if templ_7745c5c3_Err != nil {
647 return templ_7745c5c3_Err653 return templ_7745c5c3_Err
648 }654 }
649- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</a> <span class=\"rc-subject\" title=\"")655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</a> <span class=\"rc-subject\" title=\"")
650 if templ_7745c5c3_Err != nil {656 if templ_7745c5c3_Err != nil {
651 return templ_7745c5c3_Err657 return templ_7745c5c3_Err
652 }658 }
@@ -659,7 +665,7 @@ func HomeRowView(r HomeRow) templ.Component {
659 if templ_7745c5c3_Err != nil {665 if templ_7745c5c3_Err != nil {
660 return templ_7745c5c3_Err666 return templ_7745c5c3_Err
661 }667 }
662- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">")668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">")
663 if templ_7745c5c3_Err != nil {669 if templ_7745c5c3_Err != nil {
664 return templ_7745c5c3_Err670 return templ_7745c5c3_Err
665 }671 }
@@ -672,12 +678,12 @@ func HomeRowView(r HomeRow) templ.Component {
672 if templ_7745c5c3_Err != nil {678 if templ_7745c5c3_Err != nil {
673 return templ_7745c5c3_Err679 return templ_7745c5c3_Err
674 }680 }
675- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span> ")681+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</span> ")
676 if templ_7745c5c3_Err != nil {682 if templ_7745c5c3_Err != nil {
677 return templ_7745c5c3_Err683 return templ_7745c5c3_Err
678 }684 }
679 if c.WhenAt != "" {685 if c.WhenAt != "" {
680- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<time class=\"dim\" datetime=\"")686+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<time class=\"dim\" datetime=\"")
681 if templ_7745c5c3_Err != nil {687 if templ_7745c5c3_Err != nil {
682 return templ_7745c5c3_Err688 return templ_7745c5c3_Err
683 }689 }
@@ -690,7 +696,7 @@ func HomeRowView(r HomeRow) templ.Component {
690 if templ_7745c5c3_Err != nil {696 if templ_7745c5c3_Err != nil {
691 return templ_7745c5c3_Err697 return templ_7745c5c3_Err
692 }698 }
693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" title=\"")699+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" title=\"")
694 if templ_7745c5c3_Err != nil {700 if templ_7745c5c3_Err != nil {
695 return templ_7745c5c3_Err701 return templ_7745c5c3_Err
696 }702 }
@@ -703,7 +709,7 @@ func HomeRowView(r HomeRow) templ.Component {
703 if templ_7745c5c3_Err != nil {709 if templ_7745c5c3_Err != nil {
704 return templ_7745c5c3_Err710 return templ_7745c5c3_Err
705 }711 }
706- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\">")712+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\">")
707 if templ_7745c5c3_Err != nil {713 if templ_7745c5c3_Err != nil {
708 return templ_7745c5c3_Err714 return templ_7745c5c3_Err
709 }715 }
@@ -716,27 +722,27 @@ func HomeRowView(r HomeRow) templ.Component {
716 if templ_7745c5c3_Err != nil {722 if templ_7745c5c3_Err != nil {
717 return templ_7745c5c3_Err723 return templ_7745c5c3_Err
718 }724 }
719- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</time>")725+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</time>")
720 if templ_7745c5c3_Err != nil {726 if templ_7745c5c3_Err != nil {
721 return templ_7745c5c3_Err727 return templ_7745c5c3_Err
722 }728 }
723 }729 }
724- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</li>")730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</li>")
725 if templ_7745c5c3_Err != nil {731 if templ_7745c5c3_Err != nil {
726 return templ_7745c5c3_Err732 return templ_7745c5c3_Err
727 }733 }
728 }734 }
729- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</ul>")735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</ul>")
730 if templ_7745c5c3_Err != nil {736 if templ_7745c5c3_Err != nil {
731 return templ_7745c5c3_Err737 return templ_7745c5c3_Err
732 }738 }
733 } else if r.NoCommits {739 } else if r.NoCommits {
734- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<p class=\"repo-commits mute\">no commits yet</p>")740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"repo-commits mute\">no commits yet</p>")
735 if templ_7745c5c3_Err != nil {741 if templ_7745c5c3_Err != nil {
736 return templ_7745c5c3_Err742 return templ_7745c5c3_Err
737 }743 }
738 }744 }
739- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</li>")745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</li>")
740 if templ_7745c5c3_Err != nil {746 if templ_7745c5c3_Err != nil {
741 return templ_7745c5c3_Err747 return templ_7745c5c3_Err
742 }748 }
@@ -769,17 +775,17 @@ func pulse(h *Heartbeat) templ.Component {
769 templ_7745c5c3_Var38 = templ.NopComponent775 templ_7745c5c3_Var38 = templ.NopComponent
770 }776 }
771 ctx = templ.ClearChildren(ctx)777 ctx = templ.ClearChildren(ctx)
772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<figure class=\"repo-pulse\"")778+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<figure class=\"repo-pulse\"")
773 if templ_7745c5c3_Err != nil {779 if templ_7745c5c3_Err != nil {
774 return templ_7745c5c3_Err780 return templ_7745c5c3_Err
775 }781 }
776 if h.Live {782 if h.Live {
777- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " data-live")783+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " data-live")
778 if templ_7745c5c3_Err != nil {784 if templ_7745c5c3_Err != nil {
779 return templ_7745c5c3_Err785 return templ_7745c5c3_Err
780 }786 }
781 }787 }
782- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"")788+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "><svg viewBox=\"0 0 600 64\" role=\"img\" aria-label=\"")
783 if templ_7745c5c3_Err != nil {789 if templ_7745c5c3_Err != nil {
784 return templ_7745c5c3_Err790 return templ_7745c5c3_Err
785 }791 }
@@ -792,7 +798,7 @@ func pulse(h *Heartbeat) templ.Component {
792 if templ_7745c5c3_Err != nil {798 if templ_7745c5c3_Err != nil {
793 return templ_7745c5c3_Err799 return templ_7745c5c3_Err
794 }800 }
795- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" focusable=\"false\"><defs><linearGradient id=\"")801+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" focusable=\"false\"><defs><linearGradient id=\"")
796 if templ_7745c5c3_Err != nil {802 if templ_7745c5c3_Err != nil {
797 return templ_7745c5c3_Err803 return templ_7745c5c3_Err
798 }804 }
@@ -805,7 +811,7 @@ func pulse(h *Heartbeat) templ.Component {
805 if templ_7745c5c3_Err != nil {811 if templ_7745c5c3_Err != nil {
806 return templ_7745c5c3_Err812 return templ_7745c5c3_Err
807 }813 }
808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"")814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop class=\"pulse-stop-top\" offset=\"0\"></stop> <stop class=\"pulse-stop-bottom\" offset=\"1\"></stop></linearGradient></defs> <path class=\"pulse-grid\" d=\"M0 21.5 H600 M0 42.5 H600 M0 63.5 H600\" vector-effect=\"non-scaling-stroke\"></path> <path class=\"pulse-area\" d=\"")
809 if templ_7745c5c3_Err != nil {815 if templ_7745c5c3_Err != nil {
810 return templ_7745c5c3_Err816 return templ_7745c5c3_Err
811 }817 }
@@ -818,7 +824,7 @@ func pulse(h *Heartbeat) templ.Component {
818 if templ_7745c5c3_Err != nil {824 if templ_7745c5c3_Err != nil {
819 return templ_7745c5c3_Err825 return templ_7745c5c3_Err
820 }826 }
821- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\" fill=\"")827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" fill=\"")
822 if templ_7745c5c3_Err != nil {828 if templ_7745c5c3_Err != nil {
823 return templ_7745c5c3_Err829 return templ_7745c5c3_Err
824 }830 }
@@ -831,7 +837,7 @@ func pulse(h *Heartbeat) templ.Component {
831 if templ_7745c5c3_Err != nil {837 if templ_7745c5c3_Err != nil {
832 return templ_7745c5c3_Err838 return templ_7745c5c3_Err
833 }839 }
834- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\"></path> <path class=\"pulse-line\" d=\"")840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"></path> <path class=\"pulse-line\" d=\"")
835 if templ_7745c5c3_Err != nil {841 if templ_7745c5c3_Err != nil {
836 return templ_7745c5c3_Err842 return templ_7745c5c3_Err
837 }843 }
@@ -844,7 +850,7 @@ func pulse(h *Heartbeat) templ.Component {
844 if templ_7745c5c3_Err != nil {850 if templ_7745c5c3_Err != nil {
845 return templ_7745c5c3_Err851 return templ_7745c5c3_Err
846 }852 }
847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"")853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" vector-effect=\"non-scaling-stroke\"></path> <circle class=\"pulse-dot\" cx=\"")
848 if templ_7745c5c3_Err != nil {854 if templ_7745c5c3_Err != nil {
849 return templ_7745c5c3_Err855 return templ_7745c5c3_Err
850 }856 }
@@ -857,7 +863,7 @@ func pulse(h *Heartbeat) templ.Component {
857 if templ_7745c5c3_Err != nil {863 if templ_7745c5c3_Err != nil {
858 return templ_7745c5c3_Err864 return templ_7745c5c3_Err
859 }865 }
860- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" cy=\"")866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" cy=\"")
861 if templ_7745c5c3_Err != nil {867 if templ_7745c5c3_Err != nil {
862 return templ_7745c5c3_Err868 return templ_7745c5c3_Err
863 }869 }
@@ -870,7 +876,7 @@ func pulse(h *Heartbeat) templ.Component {
870 if templ_7745c5c3_Err != nil {876 if templ_7745c5c3_Err != nil {
871 return templ_7745c5c3_Err877 return templ_7745c5c3_Err
872 }878 }
873- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" r=\"3.5\"></circle></svg></figure>")879+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" r=\"3.5\"></circle></svg></figure>")
874 if templ_7745c5c3_Err != nil {880 if templ_7745c5c3_Err != nil {
875 return templ_7745c5c3_Err881 return templ_7745c5c3_Err
876 }882 }
@@ -900,7 +906,7 @@ func Outline(headings []Heading) templ.Component {
900 templ_7745c5c3_Var46 = templ.NopComponent906 templ_7745c5c3_Var46 = templ.NopComponent
901 }907 }
902 ctx = templ.ClearChildren(ctx)908 ctx = templ.ClearChildren(ctx)
903- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<nav class=\"outline\" aria-label=\"Outline\">")909+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<nav class=\"outline\" aria-label=\"Outline\">")
904 if templ_7745c5c3_Err != nil {910 if templ_7745c5c3_Err != nil {
905 return templ_7745c5c3_Err911 return templ_7745c5c3_Err
906 }912 }
@@ -910,7 +916,7 @@ func Outline(headings []Heading) templ.Component {
910 if templ_7745c5c3_Err != nil {916 if templ_7745c5c3_Err != nil {
911 return templ_7745c5c3_Err917 return templ_7745c5c3_Err
912 }918 }
913- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<a class=\"")919+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<a class=\"")
914 if templ_7745c5c3_Err != nil {920 if templ_7745c5c3_Err != nil {
915 return templ_7745c5c3_Err921 return templ_7745c5c3_Err
916 }922 }
@@ -923,7 +929,7 @@ func Outline(headings []Heading) templ.Component {
923 if templ_7745c5c3_Err != nil {929 if templ_7745c5c3_Err != nil {
924 return templ_7745c5c3_Err930 return templ_7745c5c3_Err
925 }931 }
926- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" href=\"")932+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" href=\"")
927 if templ_7745c5c3_Err != nil {933 if templ_7745c5c3_Err != nil {
928 return templ_7745c5c3_Err934 return templ_7745c5c3_Err
929 }935 }
@@ -936,7 +942,7 @@ func Outline(headings []Heading) templ.Component {
936 if templ_7745c5c3_Err != nil {942 if templ_7745c5c3_Err != nil {
937 return templ_7745c5c3_Err943 return templ_7745c5c3_Err
938 }944 }
939- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\">")945+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">")
940 if templ_7745c5c3_Err != nil {946 if templ_7745c5c3_Err != nil {
941 return templ_7745c5c3_Err947 return templ_7745c5c3_Err
942 }948 }
@@ -949,12 +955,12 @@ func Outline(headings []Heading) templ.Component {
949 if templ_7745c5c3_Err != nil {955 if templ_7745c5c3_Err != nil {
950 return templ_7745c5c3_Err956 return templ_7745c5c3_Err
951 }957 }
952- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</a>")958+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a>")
953 if templ_7745c5c3_Err != nil {959 if templ_7745c5c3_Err != nil {
954 return templ_7745c5c3_Err960 return templ_7745c5c3_Err
955 }961 }
956 }962 }
957- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</nav>")963+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</nav>")
958 if templ_7745c5c3_Err != nil {964 if templ_7745c5c3_Err != nil {
959 return templ_7745c5c3_Err965 return templ_7745c5c3_Err
960 }966 }
@@ -984,7 +990,7 @@ func ReadmeCard(r Readme) templ.Component {
984 templ_7745c5c3_Var51 = templ.NopComponent990 templ_7745c5c3_Var51 = templ.NopComponent
985 }991 }
986 ctx = templ.ClearChildren(ctx)992 ctx = templ.ClearChildren(ctx)
987- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<section class=\"card readme\" aria-labelledby=\"readme-h\"><h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ")993+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<section class=\"card readme\" aria-labelledby=\"readme-h\"><h2 class=\"card-h\" id=\"readme-h\"><span class=\"ico i-file\" aria-hidden=\"true\"></span> ")
988 if templ_7745c5c3_Err != nil {994 if templ_7745c5c3_Err != nil {
989 return templ_7745c5c3_Err995 return templ_7745c5c3_Err
990 }996 }
@@ -997,7 +1003,7 @@ func ReadmeCard(r Readme) templ.Component {
997 if templ_7745c5c3_Err != nil {1003 if templ_7745c5c3_Err != nil {
998 return templ_7745c5c3_Err1004 return templ_7745c5c3_Err
999 }1005 }
1000- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</h2>")1006+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</h2>")
1001 if templ_7745c5c3_Err != nil {1007 if templ_7745c5c3_Err != nil {
1002 return templ_7745c5c3_Err1008 return templ_7745c5c3_Err
1003 }1009 }
@@ -1005,7 +1011,7 @@ func ReadmeCard(r Readme) templ.Component {
1005 if templ_7745c5c3_Err != nil {1011 if templ_7745c5c3_Err != nil {
1006 return templ_7745c5c3_Err1012 return templ_7745c5c3_Err
1007 }1013 }
1008- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</section>")1014+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</section>")
1009 if templ_7745c5c3_Err != nil {1015 if templ_7745c5c3_Err != nil {
1010 return templ_7745c5c3_Err1016 return templ_7745c5c3_Err
1011 }1017 }
@@ -1036,7 +1042,7 @@ func ReadmeView(r Readme) templ.Component {
1036 templ_7745c5c3_Var53 = templ.NopComponent1042 templ_7745c5c3_Var53 = templ.NopComponent
1037 }1043 }
1038 ctx = templ.ClearChildren(ctx)1044 ctx = templ.ClearChildren(ctx)
1039- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<article class=\"prose\" aria-label=\"")1045+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<article class=\"prose\" aria-label=\"")
1040 if templ_7745c5c3_Err != nil {1046 if templ_7745c5c3_Err != nil {
1041 return templ_7745c5c3_Err1047 return templ_7745c5c3_Err
1042 }1048 }
@@ -1049,12 +1055,12 @@ func ReadmeView(r Readme) templ.Component {
1049 if templ_7745c5c3_Err != nil {1055 if templ_7745c5c3_Err != nil {
1050 return templ_7745c5c3_Err1056 return templ_7745c5c3_Err
1051 }1057 }
1052- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\" hx-ignore>")1058+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" hx-ignore>")
1053 if templ_7745c5c3_Err != nil {1059 if templ_7745c5c3_Err != nil {
1054 return templ_7745c5c3_Err1060 return templ_7745c5c3_Err
1055 }1061 }
1056 if r.Note != "" {1062 if r.Note != "" {
1057- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<p class=\"dim\">")1063+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<p class=\"dim\">")
1058 if templ_7745c5c3_Err != nil {1064 if templ_7745c5c3_Err != nil {
1059 return templ_7745c5c3_Err1065 return templ_7745c5c3_Err
1060 }1066 }
@@ -1067,7 +1073,7 @@ func ReadmeView(r Readme) templ.Component {
1067 if templ_7745c5c3_Err != nil {1073 if templ_7745c5c3_Err != nil {
1068 return templ_7745c5c3_Err1074 return templ_7745c5c3_Err
1069 }1075 }
1070- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</p>")1076+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</p>")
1071 if templ_7745c5c3_Err != nil {1077 if templ_7745c5c3_Err != nil {
1072 return templ_7745c5c3_Err1078 return templ_7745c5c3_Err
1073 }1079 }
@@ -1077,7 +1083,7 @@ func ReadmeView(r Readme) templ.Component {
1077 return templ_7745c5c3_Err1083 return templ_7745c5c3_Err
1078 }1084 }
1079 }1085 }
1080- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</article>")1086+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</article>")
1081 if templ_7745c5c3_Err != nil {1087 if templ_7745c5c3_Err != nil {
1082 return templ_7745c5c3_Err1088 return templ_7745c5c3_Err
1083 }1089 }
@@ -1108,17 +1114,17 @@ func CommitRows(rows []CommitRow) templ.Component {
1108 templ_7745c5c3_Var56 = templ.NopComponent1114 templ_7745c5c3_Var56 = templ.NopComponent
1109 }1115 }
1110 ctx = templ.ClearChildren(ctx)1116 ctx = templ.ClearChildren(ctx)
1111- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>")1117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<table class=\"list commits\" aria-label=\"Commits\"><tbody data-rows>")
1112 if templ_7745c5c3_Err != nil {1118 if templ_7745c5c3_Err != nil {
1113 return templ_7745c5c3_Err1119 return templ_7745c5c3_Err
1114 }1120 }
1115 for _, c := range rows {1121 for _, c := range rows {
1116- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<tr")1122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<tr")
1117 if templ_7745c5c3_Err != nil {1123 if templ_7745c5c3_Err != nil {
1118 return templ_7745c5c3_Err1124 return templ_7745c5c3_Err
1119 }1125 }
1120 if c.ID != "" {1126 if c.ID != "" {
1121- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, " id=\"")1127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " id=\"")
1122 if templ_7745c5c3_Err != nil {1128 if templ_7745c5c3_Err != nil {
1123 return templ_7745c5c3_Err1129 return templ_7745c5c3_Err
1124 }1130 }
@@ -1131,18 +1137,18 @@ func CommitRows(rows []CommitRow) templ.Component {
1131 if templ_7745c5c3_Err != nil {1137 if templ_7745c5c3_Err != nil {
1132 return templ_7745c5c3_Err1138 return templ_7745c5c3_Err
1133 }1139 }
1134- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\"")1140+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"")
1135 if templ_7745c5c3_Err != nil {1141 if templ_7745c5c3_Err != nil {
1136 return templ_7745c5c3_Err1142 return templ_7745c5c3_Err
1137 }1143 }
1138 }1144 }
1139 if c.Cursor {1145 if c.Cursor {
1140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " data-cursor")1146+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " data-cursor")
1141 if templ_7745c5c3_Err != nil {1147 if templ_7745c5c3_Err != nil {
1142 return templ_7745c5c3_Err1148 return templ_7745c5c3_Err
1143 }1149 }
1144 }1150 }
1145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "><td class=\"c-sha mute\">")1151+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "><td class=\"c-sha mute\">")
1146 if templ_7745c5c3_Err != nil {1152 if templ_7745c5c3_Err != nil {
1147 return templ_7745c5c3_Err1153 return templ_7745c5c3_Err
1148 }1154 }
@@ -1155,7 +1161,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1155 if templ_7745c5c3_Err != nil {1161 if templ_7745c5c3_Err != nil {
1156 return templ_7745c5c3_Err1162 return templ_7745c5c3_Err
1157 }1163 }
1158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</td><td class=\"c-subject\"><a href=\"")1164+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "</td><td class=\"c-subject\"><a href=\"")
1159 if templ_7745c5c3_Err != nil {1165 if templ_7745c5c3_Err != nil {
1160 return templ_7745c5c3_Err1166 return templ_7745c5c3_Err
1161 }1167 }
@@ -1168,7 +1174,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1168 if templ_7745c5c3_Err != nil {1174 if templ_7745c5c3_Err != nil {
1169 return templ_7745c5c3_Err1175 return templ_7745c5c3_Err
1170 }1176 }
1171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "\">")1177+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "\">")
1172 if templ_7745c5c3_Err != nil {1178 if templ_7745c5c3_Err != nil {
1173 return templ_7745c5c3_Err1179 return templ_7745c5c3_Err
1174 }1180 }
@@ -1181,12 +1187,12 @@ func CommitRows(rows []CommitRow) templ.Component {
1181 if templ_7745c5c3_Err != nil {1187 if templ_7745c5c3_Err != nil {
1182 return templ_7745c5c3_Err1188 return templ_7745c5c3_Err
1183 }1189 }
1184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a> ")1190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</a> ")
1185 if templ_7745c5c3_Err != nil {1191 if templ_7745c5c3_Err != nil {
1186 return templ_7745c5c3_Err1192 return templ_7745c5c3_Err
1187 }1193 }
1188 if c.Merge {1194 if c.Merge {
1189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<span class=\"pill\">merge</span> ")1195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span class=\"pill\">merge</span> ")
1190 if templ_7745c5c3_Err != nil {1196 if templ_7745c5c3_Err != nil {
1191 return templ_7745c5c3_Err1197 return templ_7745c5c3_Err
1192 }1198 }
@@ -1198,7 +1204,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1198 }1204 }
1199 }1205 }
1200 for _, b := range c.Branches {1206 for _, b := range c.Branches {
1201- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span class=\"pill\">")1207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<span class=\"pill\">")
1202 if templ_7745c5c3_Err != nil {1208 if templ_7745c5c3_Err != nil {
1203 return templ_7745c5c3_Err1209 return templ_7745c5c3_Err
1204 }1210 }
@@ -1211,13 +1217,13 @@ func CommitRows(rows []CommitRow) templ.Component {
1211 if templ_7745c5c3_Err != nil {1217 if templ_7745c5c3_Err != nil {
1212 return templ_7745c5c3_Err1218 return templ_7745c5c3_Err
1213 }1219 }
1214- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</span> ")1220+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ")
1215 if templ_7745c5c3_Err != nil {1221 if templ_7745c5c3_Err != nil {
1216 return templ_7745c5c3_Err1222 return templ_7745c5c3_Err
1217 }1223 }
1218 }1224 }
1219 for _, t := range c.Tags {1225 for _, t := range c.Tags {
1220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<span class=\"pill tag\"><span class=\"sr\">tag </span>")1226+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<span class=\"pill tag\"><span class=\"sr\">tag </span>")
1221 if templ_7745c5c3_Err != nil {1227 if templ_7745c5c3_Err != nil {
1222 return templ_7745c5c3_Err1228 return templ_7745c5c3_Err
1223 }1229 }
@@ -1230,12 +1236,12 @@ func CommitRows(rows []CommitRow) templ.Component {
1230 if templ_7745c5c3_Err != nil {1236 if templ_7745c5c3_Err != nil {
1231 return templ_7745c5c3_Err1237 return templ_7745c5c3_Err
1232 }1238 }
1233- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</span>")1239+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span>")
1234 if templ_7745c5c3_Err != nil {1240 if templ_7745c5c3_Err != nil {
1235 return templ_7745c5c3_Err1241 return templ_7745c5c3_Err
1236 }1242 }
1237 }1243 }
1238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</td><td class=\"c-ck\">")1244+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</td><td class=\"c-ck\">")
1239 if templ_7745c5c3_Err != nil {1245 if templ_7745c5c3_Err != nil {
1240 return templ_7745c5c3_Err1246 return templ_7745c5c3_Err
1241 }1247 }
@@ -1243,7 +1249,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1243 if templ_7745c5c3_Err != nil {1249 if templ_7745c5c3_Err != nil {
1244 return templ_7745c5c3_Err1250 return templ_7745c5c3_Err
1245 }1251 }
1246- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</td><td class=\"c-who dim\">")1252+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</td><td class=\"c-who dim\">")
1247 if templ_7745c5c3_Err != nil {1253 if templ_7745c5c3_Err != nil {
1248 return templ_7745c5c3_Err1254 return templ_7745c5c3_Err
1249 }1255 }
@@ -1256,7 +1262,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1256 if templ_7745c5c3_Err != nil {1262 if templ_7745c5c3_Err != nil {
1257 return templ_7745c5c3_Err1263 return templ_7745c5c3_Err
1258 }1264 }
1259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</td><td class=\"c-when nowrap dim\"><time datetime=\"")1265+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</td><td class=\"c-when nowrap dim\"><time datetime=\"")
1260 if templ_7745c5c3_Err != nil {1266 if templ_7745c5c3_Err != nil {
1261 return templ_7745c5c3_Err1267 return templ_7745c5c3_Err
1262 }1268 }
@@ -1269,7 +1275,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1269 if templ_7745c5c3_Err != nil {1275 if templ_7745c5c3_Err != nil {
1270 return templ_7745c5c3_Err1276 return templ_7745c5c3_Err
1271 }1277 }
1272- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\" title=\"")1278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" title=\"")
1273 if templ_7745c5c3_Err != nil {1279 if templ_7745c5c3_Err != nil {
1274 return templ_7745c5c3_Err1280 return templ_7745c5c3_Err
1275 }1281 }
@@ -1282,7 +1288,7 @@ func CommitRows(rows []CommitRow) templ.Component {
1282 if templ_7745c5c3_Err != nil {1288 if templ_7745c5c3_Err != nil {
1283 return templ_7745c5c3_Err1289 return templ_7745c5c3_Err
1284 }1290 }
1285- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\">")1291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">")
1286 if templ_7745c5c3_Err != nil {1292 if templ_7745c5c3_Err != nil {
1287 return templ_7745c5c3_Err1293 return templ_7745c5c3_Err
1288 }1294 }
@@ -1295,12 +1301,12 @@ func CommitRows(rows []CommitRow) templ.Component {
1295 if templ_7745c5c3_Err != nil {1301 if templ_7745c5c3_Err != nil {
1296 return templ_7745c5c3_Err1302 return templ_7745c5c3_Err
1297 }1303 }
1298- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "</time></td></tr>")1304+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</time></td></tr>")
1299 if templ_7745c5c3_Err != nil {1305 if templ_7745c5c3_Err != nil {
1300 return templ_7745c5c3_Err1306 return templ_7745c5c3_Err
1301 }1307 }
1302 }1308 }
1303- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</tbody></table>")1309+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</tbody></table>")
1304 if templ_7745c5c3_Err != nil {1310 if templ_7745c5c3_Err != nil {
1305 return templ_7745c5c3_Err1311 return templ_7745c5c3_Err
1306 }1312 }
@@ -1336,7 +1342,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
1336 if templ_7745c5c3_Err != nil {1342 if templ_7745c5c3_Err != nil {
1337 return templ_7745c5c3_Err1343 return templ_7745c5c3_Err
1338 }1344 }
1339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"")1345+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span class=\"")
1340 if templ_7745c5c3_Err != nil {1346 if templ_7745c5c3_Err != nil {
1341 return templ_7745c5c3_Err1347 return templ_7745c5c3_Err
1342 }1348 }
@@ -1349,7 +1355,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
1349 if templ_7745c5c3_Err != nil {1355 if templ_7745c5c3_Err != nil {
1350 return templ_7745c5c3_Err1356 return templ_7745c5c3_Err
1351 }1357 }
1352- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\" title=\"")1358+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\" title=\"")
1353 if templ_7745c5c3_Err != nil {1359 if templ_7745c5c3_Err != nil {
1354 return templ_7745c5c3_Err1360 return templ_7745c5c3_Err
1355 }1361 }
@@ -1362,12 +1368,12 @@ func signaturePill(b *SignatureBadge) templ.Component {
1362 if templ_7745c5c3_Err != nil {1368 if templ_7745c5c3_Err != nil {
1363 return templ_7745c5c3_Err1369 return templ_7745c5c3_Err
1364 }1370 }
1365- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\">")1371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\">")
1366 if templ_7745c5c3_Err != nil {1372 if templ_7745c5c3_Err != nil {
1367 return templ_7745c5c3_Err1373 return templ_7745c5c3_Err
1368 }1374 }
1369 if b.Proto != "" {1375 if b.Proto != "" {
1370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<span class=\"sig-proto\">")1376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<span class=\"sig-proto\">")
1371 if templ_7745c5c3_Err != nil {1377 if templ_7745c5c3_Err != nil {
1372 return templ_7745c5c3_Err1378 return templ_7745c5c3_Err
1373 }1379 }
@@ -1380,7 +1386,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
1380 if templ_7745c5c3_Err != nil {1386 if templ_7745c5c3_Err != nil {
1381 return templ_7745c5c3_Err1387 return templ_7745c5c3_Err
1382 }1388 }
1383- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</span> ")1389+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span> ")
1384 if templ_7745c5c3_Err != nil {1390 if templ_7745c5c3_Err != nil {
1385 return templ_7745c5c3_Err1391 return templ_7745c5c3_Err
1386 }1392 }
@@ -1394,7 +1400,7 @@ func signaturePill(b *SignatureBadge) templ.Component {
1394 if templ_7745c5c3_Err != nil {1400 if templ_7745c5c3_Err != nil {
1395 return templ_7745c5c3_Err1401 return templ_7745c5c3_Err
1396 }1402 }
1397- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span>")1403+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</span>")
1398 if templ_7745c5c3_Err != nil {1404 if templ_7745c5c3_Err != nil {
1399 return templ_7745c5c3_Err1405 return templ_7745c5c3_Err
1400 }1406 }
M internal/adapters/web/events.go +2 -0
@@ -50,6 +50,8 @@ import (
5050 //
5151 // home-list data: <li id="repo-<name>">…</li>, or <li id="repo-<name>" data-gone></li>
5252 // for a repository that is deleted or no longer visible
53+// Activity rows carry data-activity: push, MR lifecycle/comment,
54+// or issue lifecycle/comment (never pending moderation or CI).
5355 // repo-last-push data: <dd id="last-push-<name>">…</dd>
5456 // refs-list data: <tbody id="ref-rows">…</tbody>, and again with
5557 // <dl id="refs-kv">…</dl>, the facts of the rail
@@ -50,6 +50,8 @@ import (
50 //50 //
51 // home-list data: <li id="repo-<name>">…</li>, or <li id="repo-<name>" data-gone></li>51 // home-list data: <li id="repo-<name>">…</li>, or <li id="repo-<name>" data-gone></li>
52 // for a repository that is deleted or no longer visible52 // for a repository that is deleted or no longer visible
53+// Activity rows carry data-activity: push, MR lifecycle/comment,
54+// or issue lifecycle/comment (never pending moderation or CI).
53 // repo-last-push data: <dd id="last-push-<name>">…</dd>55 // repo-last-push data: <dd id="last-push-<name>">…</dd>
54 // refs-list data: <tbody id="ref-rows">…</tbody>, and again with56 // refs-list data: <tbody id="ref-rows">…</tbody>, and again with
55 // <dl id="refs-kv">…</dl>, the facts of the rail57 // <dl id="refs-kv">…</dl>, the facts of the rail
M internal/adapters/web/fragments.go +18 -0
@@ -112,6 +112,24 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment,
112112 name = ev.Repo.String()
113113 case domain.PushEvent:
114114 name, fresh = ev.Repo.String(), true
115+ case domain.MergeRequestOpened:
116+ name, fresh = ev.MergeRequest.Repo.String(), true
117+ case domain.MergeRequestMerged:
118+ name, fresh = ev.MergeRequest.Repo.String(), true
119+ case domain.MergeRequestClosed:
120+ name, fresh = ev.MergeRequest.Repo.String(), true
121+ case domain.MergeRequestReady:
122+ name, fresh = ev.MergeRequest.Repo.String(), true
123+ case domain.CommentAdded:
124+ name, fresh = ev.Repo.String(), true
125+ case domain.IssueOpened:
126+ name, fresh = ev.Issue.Repo.String(), true
127+ case domain.IssueClosed:
128+ name, fresh = ev.Issue.Repo.String(), true
129+ case domain.IssueReopened:
130+ name, fresh = ev.Issue.Repo.String(), true
131+ case domain.IssueCommented:
132+ name, fresh = ev.Repo.String(), true
115133 case domain.RunUpdated:
116134 name = ev.Run.Repo.String() // the CI state of the row
117135 case domain.JobUpdated:
@@ -112,6 +112,24 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment,
112 name = ev.Repo.String()112 name = ev.Repo.String()
113 case domain.PushEvent:113 case domain.PushEvent:
114 name, fresh = ev.Repo.String(), true114 name, fresh = ev.Repo.String(), true
115+ case domain.MergeRequestOpened:
116+ name, fresh = ev.MergeRequest.Repo.String(), true
117+ case domain.MergeRequestMerged:
118+ name, fresh = ev.MergeRequest.Repo.String(), true
119+ case domain.MergeRequestClosed:
120+ name, fresh = ev.MergeRequest.Repo.String(), true
121+ case domain.MergeRequestReady:
122+ name, fresh = ev.MergeRequest.Repo.String(), true
123+ case domain.CommentAdded:
124+ name, fresh = ev.Repo.String(), true
125+ case domain.IssueOpened:
126+ name, fresh = ev.Issue.Repo.String(), true
127+ case domain.IssueClosed:
128+ name, fresh = ev.Issue.Repo.String(), true
129+ case domain.IssueReopened:
130+ name, fresh = ev.Issue.Repo.String(), true
131+ case domain.IssueCommented:
132+ name, fresh = ev.Repo.String(), true
115 case domain.RunUpdated:133 case domain.RunUpdated:
116 name = ev.Run.Repo.String() // the CI state of the row134 name = ev.Run.Repo.String() // the CI state of the row
117 case domain.JobUpdated:135 case domain.JobUpdated:
M internal/adapters/web/home.go +22 -11
@@ -56,12 +56,12 @@ func (s *Server) listRepos(ctx context.Context) ([]ports.RepoView, error) {
5656 // homeSorts are the orders the home list offers, in order. The query also
5757 // accepts "desc" and "branch", which the page does not link to.
5858 var homeSorts = []struct{ key, label string }{
59- {"push", "last push"},
59+ {"activity", "last activity"},
6060 {"name", "name"},
6161 }
6262
6363 // homeKeys are the keys of ?sort= that sortRepos knows.
64-var homeKeys = []string{"push", "name", "desc", "branch"}
64+var homeKeys = []string{"activity", "name", "desc", "branch"}
6565
6666 // sortRepos orders repos by the column key; ties keep the order by name.
6767 func sortRepos(repos []ports.RepoView, key string, desc bool) {
@@ -71,8 +71,8 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) {
7171 return strings.ToLower(a.Description) < strings.ToLower(b.Description)
7272 case "branch":
7373 return a.DefaultBranch < b.DefaultBranch
74- case "push":
75- return a.PushedAt.Before(b.PushedAt)
74+ case "activity":
75+ return repoActiveAt(a).Before(repoActiveAt(b))
7676 }
7777 return a.Name.String() < b.Name.String()
7878 }
@@ -84,17 +84,17 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) {
8484 })
8585 }
8686
87-// home lists the repositories: /?sort=<col>&dir=asc|desc, newest push first
87+// home lists the repositories: /?sort=<col>&dir=asc|desc, newest activity first
8888 // by default.
8989 func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) {
9090 repos, err := s.listRepos(ctx)
9191 if err != nil {
9292 return nil, 0, err
9393 }
94- key, desc := "push", true
94+ key, desc := "activity", true
9595 for _, k := range homeKeys {
9696 if r.URL.Query().Get("sort") == k {
97- key, desc = k, k == "push"
97+ key, desc = k, k == "activity"
9898 }
9999 }
100100 if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" {
@@ -103,11 +103,11 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in
103103 sortRepos(repos, key, desc)
104104
105105 now := s.cfg.Now()
106- p := HomePage{}
106+ p := HomePage{Sort: key, Desc: desc}
107107 for _, c := range homeSorts {
108108 sl := SortLink{Label: c.label}
109109 next := "asc"
110- if c.key == "push" {
110+ if c.key == "activity" {
111111 next = "desc"
112112 }
113113 if c.key == key {
@@ -267,8 +267,8 @@ func homeRow(r ports.RepoView, now time.Time, cursor, signed bool) HomeRow {
267267 Desc: r.Description, Topics: r.Topics, Cursor: cursor,
268268 Private: signed && r.Visibility == domain.VisibilityPrivate,
269269 }
270- if !r.PushedAt.IsZero() {
271- row.Active, row.ActiveAt = ago(now, r.PushedAt), rfc3339(r.PushedAt)
270+ if at := repoActiveAt(r); !at.IsZero() {
271+ row.Active, row.ActiveAt = ago(now, at), rfc3339(at)
272272 }
273273 return row
274274 }
@@ -284,3 +284,14 @@ func repoCmds(repos []ports.RepoView, except string) []CmdItem {
284284 }
285285 return out
286286 }
287+
288+// repoActiveAt falls back for repositories without recorded activity.
289+func repoActiveAt(r ports.RepoView) time.Time {
290+ if !r.ActiveAt.IsZero() {
291+ return r.ActiveAt
292+ }
293+ if r.PushedAt.After(r.CreatedAt) {
294+ return r.PushedAt
295+ }
296+ return r.CreatedAt
297+}
@@ -56,12 +56,12 @@ func (s *Server) listRepos(ctx context.Context) ([]ports.RepoView, error) {
56 // homeSorts are the orders the home list offers, in order. The query also56 // homeSorts are the orders the home list offers, in order. The query also
57 // accepts "desc" and "branch", which the page does not link to.57 // accepts "desc" and "branch", which the page does not link to.
58 var homeSorts = []struct{ key, label string }{58 var homeSorts = []struct{ key, label string }{
59- {"push", "last push"},59+ {"activity", "last activity"},
60 {"name", "name"},60 {"name", "name"},
61 }61 }
62 62
63 // homeKeys are the keys of ?sort= that sortRepos knows.63 // homeKeys are the keys of ?sort= that sortRepos knows.
64-var homeKeys = []string{"push", "name", "desc", "branch"}64+var homeKeys = []string{"activity", "name", "desc", "branch"}
65 65
66 // sortRepos orders repos by the column key; ties keep the order by name.66 // sortRepos orders repos by the column key; ties keep the order by name.
67 func sortRepos(repos []ports.RepoView, key string, desc bool) {67 func sortRepos(repos []ports.RepoView, key string, desc bool) {
@@ -71,8 +71,8 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) {
71 return strings.ToLower(a.Description) < strings.ToLower(b.Description)71 return strings.ToLower(a.Description) < strings.ToLower(b.Description)
72 case "branch":72 case "branch":
73 return a.DefaultBranch < b.DefaultBranch73 return a.DefaultBranch < b.DefaultBranch
74- case "push":74+ case "activity":
75- return a.PushedAt.Before(b.PushedAt)75+ return repoActiveAt(a).Before(repoActiveAt(b))
76 }76 }
77 return a.Name.String() < b.Name.String()77 return a.Name.String() < b.Name.String()
78 }78 }
@@ -84,17 +84,17 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) {
84 })84 })
85 }85 }
86 86
87-// home lists the repositories: /?sort=<col>&dir=asc|desc, newest push first87+// home lists the repositories: /?sort=<col>&dir=asc|desc, newest activity first
88 // by default.88 // by default.
89 func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) {89 func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) {
90 repos, err := s.listRepos(ctx)90 repos, err := s.listRepos(ctx)
91 if err != nil {91 if err != nil {
92 return nil, 0, err92 return nil, 0, err
93 }93 }
94- key, desc := "push", true94+ key, desc := "activity", true
95 for _, k := range homeKeys {95 for _, k := range homeKeys {
96 if r.URL.Query().Get("sort") == k {96 if r.URL.Query().Get("sort") == k {
97- key, desc = k, k == "push"97+ key, desc = k, k == "activity"
98 }98 }
99 }99 }
100 if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" {100 if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" {
@@ -103,11 +103,11 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in
103 sortRepos(repos, key, desc)103 sortRepos(repos, key, desc)
104 104
105 now := s.cfg.Now()105 now := s.cfg.Now()
106- p := HomePage{}106+ p := HomePage{Sort: key, Desc: desc}
107 for _, c := range homeSorts {107 for _, c := range homeSorts {
108 sl := SortLink{Label: c.label}108 sl := SortLink{Label: c.label}
109 next := "asc"109 next := "asc"
110- if c.key == "push" {110+ if c.key == "activity" {
111 next = "desc"111 next = "desc"
112 }112 }
113 if c.key == key {113 if c.key == key {
@@ -267,8 +267,8 @@ func homeRow(r ports.RepoView, now time.Time, cursor, signed bool) HomeRow {
267 Desc: r.Description, Topics: r.Topics, Cursor: cursor,267 Desc: r.Description, Topics: r.Topics, Cursor: cursor,
268 Private: signed && r.Visibility == domain.VisibilityPrivate,268 Private: signed && r.Visibility == domain.VisibilityPrivate,
269 }269 }
270- if !r.PushedAt.IsZero() {270+ if at := repoActiveAt(r); !at.IsZero() {
271- row.Active, row.ActiveAt = ago(now, r.PushedAt), rfc3339(r.PushedAt)271+ row.Active, row.ActiveAt = ago(now, at), rfc3339(at)
272 }272 }
273 return row273 return row
274 }274 }
@@ -284,3 +284,14 @@ func repoCmds(repos []ports.RepoView, except string) []CmdItem {
284 }284 }
285 return out285 return out
286 }286 }
287+
288+// repoActiveAt falls back for repositories without recorded activity.
289+func repoActiveAt(r ports.RepoView) time.Time {
290+ if !r.ActiveAt.IsZero() {
291+ return r.ActiveAt
292+ }
293+ if r.PushedAt.After(r.CreatedAt) {
294+ return r.PushedAt
295+ }
296+ return r.CreatedAt
297+}
M internal/adapters/web/home_test.go +49 -1
@@ -252,7 +252,7 @@ func TestRunUpdatedRefreshesTheHomeRow(t *testing.T) {
252252 assert.Equal(t, "home-list", got[0].event)
253253 assert.Contains(t, got[0].html, `id="repo-proj"`)
254254 assert.Contains(t, got[0].html, "passing")
255- assert.NotContains(t, got[0].html, "new push", "a run is not a push")
255+ assert.NotContains(t, got[0].html, "new activity", "a run is not a push")
256256
257257 // A job that ended refreshes the row too; one in progress does not.
258258 got, err = (&liveStream{s: srv}).render(t.Context(), ports.WatchEvent{Event: domain.JobUpdated{Job: domain.CIJob{Repo: repo, Phase: domain.CIPhaseRunning}}})
@@ -344,3 +344,51 @@ func TestHomeOffersTheLayouts(t *testing.T) {
344344 require.NotEqual(t, -1, store)
345345 assert.Less(t, store, app, "the store loads before the app script")
346346 }
347+
348+func TestHomeActivityOrderAndFallback(t *testing.T) {
349+ t.Parallel()
350+ now := time.Now().UTC()
351+ a, b, c := publicRepo(t, "a"), publicRepo(t, "b"), publicRepo(t, "c")
352+ a.PushedAt = now.Add(-time.Hour)
353+ a.ActiveAt = now
354+ b.PushedAt = now.Add(-time.Minute)
355+ c.CreatedAt = now.Add(-30 * time.Minute)
356+ repos := []ports.RepoView{b, c, a}
357+ sortRepos(repos, "activity", true)
358+ assert.Equal(t, []ports.RepoView{a, b, c}, repos)
359+ sortRepos(repos, "activity", false)
360+ assert.Equal(t, []ports.RepoView{c, b, a}, repos)
361+ assert.Equal(t, rfc3339(now), homeRow(a, now, false, false).ActiveAt)
362+ c.PushedAt = c.CreatedAt.Add(-time.Hour)
363+ assert.Equal(t, c.CreatedAt, repoActiveAt(c), "fallback is the later of creation and push")
364+}
365+
366+func TestActivityEventsRefreshTheHomeRow(t *testing.T) {
367+ t.Parallel()
368+ repo := publicRepo(t, "proj")
369+ srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: homeBrowse(t)})
370+ mr := domain.MergeRequest{Repo: repo.Name}
371+ issue := domain.Issue{Repo: repo.Name}
372+ for _, ev := range []domain.Event{
373+ domain.PushEvent{Repo: repo.Name},
374+ domain.MergeRequestOpened{MergeRequest: mr}, domain.MergeRequestMerged{MergeRequest: mr},
375+ domain.MergeRequestClosed{MergeRequest: mr}, domain.MergeRequestReady{MergeRequest: mr},
376+ domain.CommentAdded{Repo: repo.Name}, domain.IssueOpened{Issue: issue},
377+ domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name},
378+ } {
379+ t.Run(ev.EventKind(), func(t *testing.T) {
380+ got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev})
381+ require.NoError(t, err)
382+ require.Len(t, got, 1)
383+ assert.Equal(t, "home-list", got[0].event)
384+ assert.Contains(t, got[0].html, `id="repo-proj"`)
385+ assert.Contains(t, got[0].html, `data-activity`)
386+ assert.Contains(t, got[0].html, "new activity")
387+ })
388+ }
389+ for _, ev := range []domain.Event{domain.IssueSubmitted{Repo: repo.Name}, domain.IssuePendingRemoved{Repo: repo.Name}} {
390+ got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev})
391+ require.NoError(t, err)
392+ assert.Empty(t, got)
393+ }
394+}
@@ -252,7 +252,7 @@ func TestRunUpdatedRefreshesTheHomeRow(t *testing.T) {
252 assert.Equal(t, "home-list", got[0].event)252 assert.Equal(t, "home-list", got[0].event)
253 assert.Contains(t, got[0].html, `id="repo-proj"`)253 assert.Contains(t, got[0].html, `id="repo-proj"`)
254 assert.Contains(t, got[0].html, "passing")254 assert.Contains(t, got[0].html, "passing")
255- assert.NotContains(t, got[0].html, "new push", "a run is not a push")255+ assert.NotContains(t, got[0].html, "new activity", "a run is not a push")
256 256
257 // A job that ended refreshes the row too; one in progress does not.257 // A job that ended refreshes the row too; one in progress does not.
258 got, err = (&liveStream{s: srv}).render(t.Context(), ports.WatchEvent{Event: domain.JobUpdated{Job: domain.CIJob{Repo: repo, Phase: domain.CIPhaseRunning}}})258 got, err = (&liveStream{s: srv}).render(t.Context(), ports.WatchEvent{Event: domain.JobUpdated{Job: domain.CIJob{Repo: repo, Phase: domain.CIPhaseRunning}}})
@@ -344,3 +344,51 @@ func TestHomeOffersTheLayouts(t *testing.T) {
344 require.NotEqual(t, -1, store)344 require.NotEqual(t, -1, store)
345 assert.Less(t, store, app, "the store loads before the app script")345 assert.Less(t, store, app, "the store loads before the app script")
346 }346 }
347+
348+func TestHomeActivityOrderAndFallback(t *testing.T) {
349+ t.Parallel()
350+ now := time.Now().UTC()
351+ a, b, c := publicRepo(t, "a"), publicRepo(t, "b"), publicRepo(t, "c")
352+ a.PushedAt = now.Add(-time.Hour)
353+ a.ActiveAt = now
354+ b.PushedAt = now.Add(-time.Minute)
355+ c.CreatedAt = now.Add(-30 * time.Minute)
356+ repos := []ports.RepoView{b, c, a}
357+ sortRepos(repos, "activity", true)
358+ assert.Equal(t, []ports.RepoView{a, b, c}, repos)
359+ sortRepos(repos, "activity", false)
360+ assert.Equal(t, []ports.RepoView{c, b, a}, repos)
361+ assert.Equal(t, rfc3339(now), homeRow(a, now, false, false).ActiveAt)
362+ c.PushedAt = c.CreatedAt.Add(-time.Hour)
363+ assert.Equal(t, c.CreatedAt, repoActiveAt(c), "fallback is the later of creation and push")
364+}
365+
366+func TestActivityEventsRefreshTheHomeRow(t *testing.T) {
367+ t.Parallel()
368+ repo := publicRepo(t, "proj")
369+ srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: homeBrowse(t)})
370+ mr := domain.MergeRequest{Repo: repo.Name}
371+ issue := domain.Issue{Repo: repo.Name}
372+ for _, ev := range []domain.Event{
373+ domain.PushEvent{Repo: repo.Name},
374+ domain.MergeRequestOpened{MergeRequest: mr}, domain.MergeRequestMerged{MergeRequest: mr},
375+ domain.MergeRequestClosed{MergeRequest: mr}, domain.MergeRequestReady{MergeRequest: mr},
376+ domain.CommentAdded{Repo: repo.Name}, domain.IssueOpened{Issue: issue},
377+ domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name},
378+ } {
379+ t.Run(ev.EventKind(), func(t *testing.T) {
380+ got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev})
381+ require.NoError(t, err)
382+ require.Len(t, got, 1)
383+ assert.Equal(t, "home-list", got[0].event)
384+ assert.Contains(t, got[0].html, `id="repo-proj"`)
385+ assert.Contains(t, got[0].html, `data-activity`)
386+ assert.Contains(t, got[0].html, "new activity")
387+ })
388+ }
389+ for _, ev := range []domain.Event{domain.IssueSubmitted{Repo: repo.Name}, domain.IssuePendingRemoved{Repo: repo.Name}} {
390+ got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev})
391+ require.NoError(t, err)
392+ assert.Empty(t, got)
393+ }
394+}
M internal/adapters/web/jstest/a11y.test.mjs +25 -0
@@ -8,6 +8,7 @@ import { test } from 'node:test';
88 import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs';
99
1010 // The few things ermite.js uses that dom.mjs does not have.
11+El.prototype.prepend = function (...nodes) { this.nodes.unshift(...this.adopt(nodes)); };
1112 El.prototype.replaceWith = function (n) {
1213 const p = this.parent;
1314 const i = p.nodes.indexOf(this);
@@ -333,3 +334,27 @@ test('the preference is read from storage when the page loads', async () => {
333334 assert.deepEqual(dom.body.events, []);
334335 store.set('keys-off', '');
335336 });
337+
338+
339+test('activity moves a home row up only in newest activity order, preserving focus and cursor', () => {
340+ for (const [sort, desc, activity, moves] of [
341+ ['activity', 'true', true, true],
342+ ['activity', 'false', true, false],
343+ ['name', 'true', true, false],
344+ ['name', 'false', true, false],
345+ ['activity', 'true', false, false], // CI or metadata, not activity
346+ ]) {
347+ const r = rows();
348+ r.list.dataset.sort = sort;
349+ r.list.dataset.desc = desc;
350+ const old = r.list.children[1];
351+ old.setAttribute('data-cursor', '');
352+ active = old.children[0];
353+ const fresh = r.mk('repo-b', '/b');
354+ if (activity) fresh.setAttribute('data-activity', '');
355+ swapWith(`activity-${sort}-${desc}-${activity}`, fresh);
356+ assert.equal(r.list.children.indexOf(fresh), moves ? 0 : 1);
357+ assert.equal(active, fresh.children[0]);
358+ assert.ok(fresh.hasAttribute('data-cursor'));
359+ }
360+});
@@ -8,6 +8,7 @@ import { test } from 'node:test';
8 import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs';8 import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs';
9 9
10 // The few things ermite.js uses that dom.mjs does not have.10 // The few things ermite.js uses that dom.mjs does not have.
11+El.prototype.prepend = function (...nodes) { this.nodes.unshift(...this.adopt(nodes)); };
11 El.prototype.replaceWith = function (n) {12 El.prototype.replaceWith = function (n) {
12 const p = this.parent;13 const p = this.parent;
13 const i = p.nodes.indexOf(this);14 const i = p.nodes.indexOf(this);
@@ -333,3 +334,27 @@ test('the preference is read from storage when the page loads', async () => {
333 assert.deepEqual(dom.body.events, []);334 assert.deepEqual(dom.body.events, []);
334 store.set('keys-off', '');335 store.set('keys-off', '');
335 });336 });
337+
338+
339+test('activity moves a home row up only in newest activity order, preserving focus and cursor', () => {
340+ for (const [sort, desc, activity, moves] of [
341+ ['activity', 'true', true, true],
342+ ['activity', 'false', true, false],
343+ ['name', 'true', true, false],
344+ ['name', 'false', true, false],
345+ ['activity', 'true', false, false], // CI or metadata, not activity
346+ ]) {
347+ const r = rows();
348+ r.list.dataset.sort = sort;
349+ r.list.dataset.desc = desc;
350+ const old = r.list.children[1];
351+ old.setAttribute('data-cursor', '');
352+ active = old.children[0];
353+ const fresh = r.mk('repo-b', '/b');
354+ if (activity) fresh.setAttribute('data-activity', '');
355+ swapWith(`activity-${sort}-${desc}-${activity}`, fresh);
356+ assert.equal(r.list.children.indexOf(fresh), moves ? 0 : 1);
357+ assert.equal(active, fresh.children[0]);
358+ assert.ok(fresh.hasAttribute('data-cursor'));
359+ }
360+});
M internal/adapters/web/pages.templ +3 -1
@@ -2,6 +2,8 @@
22
33 package web
44
5+import "fmt"
6+
57 // Home is the repository list.
68 templ Home(l Layout, p HomePage) {
79 @Shell(l, nil) {
@@ -49,7 +51,7 @@ templ Home(l Layout, p HomePage) {
4951 </button>
5052 </div>
5153 </div>
52- <ul class="repos" id="repo-rows" data-rows>
54+ <ul class="repos" id="repo-rows" data-rows data-sort={ p.Sort } data-desc={ fmt.Sprint(p.Desc) }>
5355 for _, r := range p.Rows {
5456 @HomeRowView(r)
5557 }
@@ -2,6 +2,8 @@
2 2
3 package web3 package web
4 4
5+import "fmt"
6+
5 // Home is the repository list.7 // Home is the repository list.
6 templ Home(l Layout, p HomePage) {8 templ Home(l Layout, p HomePage) {
7 @Shell(l, nil) {9 @Shell(l, nil) {
@@ -49,7 +51,7 @@ templ Home(l Layout, p HomePage) {
49 </button>51 </button>
50 </div>52 </div>
51 </div>53 </div>
52- <ul class="repos" id="repo-rows" data-rows>54+ <ul class="repos" id="repo-rows" data-rows data-sort={ p.Sort } data-desc={ fmt.Sprint(p.Desc) }>
53 for _, r := range p.Rows {55 for _, r := range p.Rows {
54 @HomeRowView(r)56 @HomeRowView(r)
55 }57 }
M internal/adapters/web/pages_templ.go +370 -342 generated file: collapsed
M internal/adapters/web/static/js/ermite.js +5 -1
@@ -820,7 +820,8 @@
820820 // internal/adapters/web/events.go). A fragment is a top-level element with an
821821 // id: it replaces the element of the page that has the same id. A fragment
822822 // marked data-gone removes it; a row of a repository the home page does not
823-// list yet is added at the top of the list. A slot (data-slot: the place of a
823+// list yet is added at the top of the list. Activity updates also move existing
824+// home rows to the top when sorted by newest activity; CI updates stay put. A slot (data-slot: the place of a
824825 // banner, a persistent status region) keeps its element and only gets the
825826 // children of the fragment, so that screen readers announce the change. A gap,
826827 // which says the page may have missed events or that what it shows changed,
@@ -919,6 +920,9 @@ window.ermiteLive = (() => {
919920 }
920921 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
921922 old.replaceWith(el);
923+ const rows = document.getElementById('repo-rows');
924+ if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&
925+ rows.dataset.desc === 'true' && el.hasAttribute('data-activity')) rows.prepend(el);
922926 window.htmx?.process(el);
923927 fitIn(el);
924928 if (had) refocus(el, had);
@@ -820,7 +820,8 @@
820 // internal/adapters/web/events.go). A fragment is a top-level element with an820 // internal/adapters/web/events.go). A fragment is a top-level element with an
821 // id: it replaces the element of the page that has the same id. A fragment821 // id: it replaces the element of the page that has the same id. A fragment
822 // marked data-gone removes it; a row of a repository the home page does not822 // marked data-gone removes it; a row of a repository the home page does not
823-// list yet is added at the top of the list. A slot (data-slot: the place of a823+// list yet is added at the top of the list. Activity updates also move existing
824+// home rows to the top when sorted by newest activity; CI updates stay put. A slot (data-slot: the place of a
824 // banner, a persistent status region) keeps its element and only gets the825 // banner, a persistent status region) keeps its element and only gets the
825 // children of the fragment, so that screen readers announce the change. A gap,826 // children of the fragment, so that screen readers announce the change. A gap,
826 // which says the page may have missed events or that what it shows changed,827 // which says the page may have missed events or that what it shows changed,
@@ -919,6 +920,9 @@ window.ermiteLive = (() => {
919 }920 }
920 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');921 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
921 old.replaceWith(el);922 old.replaceWith(el);
923+ const rows = document.getElementById('repo-rows');
924+ if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&
925+ rows.dataset.desc === 'true' && el.hasAttribute('data-activity')) rows.prepend(el);
922 window.htmx?.process(el);926 window.htmx?.process(el);
923 fitIn(el);927 fitIn(el);
924 if (had) refocus(el, had);928 if (had) refocus(el, had);
M internal/adapters/web/view.go +3 -1
@@ -96,7 +96,7 @@ type HomeRow struct {
9696 Stats *HomeStats
9797 // Private marks a private repository; only set for a signed-in visitor.
9898 Private bool
99- // Fresh marks a row that a push just updated (live updates only).
99+ // Fresh marks a row that activity just updated (live updates only).
100100 Fresh bool
101101 // Commits are the newest of the default branch. NoCommits says there is
102102 // none (never pushed, or an empty branch); a failed read says nothing.
@@ -138,6 +138,8 @@ type SortLink struct {
138138
139139 // HomePage is the repository list.
140140 type HomePage struct {
141+ Sort string
142+ Desc bool
141143 Sorts []SortLink
142144 Rows []HomeRow
143145 }
@@ -96,7 +96,7 @@ type HomeRow struct {
96 Stats *HomeStats96 Stats *HomeStats
97 // Private marks a private repository; only set for a signed-in visitor.97 // Private marks a private repository; only set for a signed-in visitor.
98 Private bool98 Private bool
99- // Fresh marks a row that a push just updated (live updates only).99+ // Fresh marks a row that activity just updated (live updates only).
100 Fresh bool100 Fresh bool
101 // Commits are the newest of the default branch. NoCommits says there is101 // Commits are the newest of the default branch. NoCommits says there is
102 // none (never pushed, or an empty branch); a failed read says nothing.102 // none (never pushed, or an empty branch); a failed read says nothing.
@@ -138,6 +138,8 @@ type SortLink struct {
138 138
139 // HomePage is the repository list.139 // HomePage is the repository list.
140 type HomePage struct {140 type HomePage struct {
141+ Sort string
142+ Desc bool
141 Sorts []SortLink143 Sorts []SortLink
142 Rows []HomeRow144 Rows []HomeRow
143 }145 }
M internal/app/testdata/web/home.html +32 -30
@@ -61,7 +61,7 @@
6161 </div>
6262 <nav class="sort" aria-label="Sort repositories">
6363 <span class="mute">sort</span>
64-<a href="/?sort=push&amp;dir=asc" aria-current="true">last push <span aria-hidden="true">▼</span>
64+<a href="/?sort=activity&amp;dir=asc" aria-current="true">last activity <span aria-hidden="true">▼</span>
6565 <span class="sr">, descending, select for ascending</span>
6666 </a>
6767 <a href="/?sort=name&amp;dir=asc">name </a>
@@ -81,8 +81,31 @@
8181 </button>
8282 </div>
8383 </div>
84-<ul class="repos" id="repo-rows" data-rows>
85-<li id="repo-demo" class="repo" data-cursor>
84+<ul class="repos" id="repo-rows" data-rows data-sort="activity" data-desc="true">
85+<li id="repo-quiet" class="repo" data-cursor>
86+<div class="repo-main">
87+<h2 class="repo-name">
88+<a href="/quiet">quiet</a>
89+</h2>
90+<p class="repo-desc mute">no description</p>
91+<div class="repo-topics" aria-hidden="true">
92+</div>
93+<div class="repo-pulse" aria-hidden="true">
94+</div>
95+</div>
96+<div class="repo-head">
97+<dl class="repo-stats" aria-label="Figures of quiet">
98+<div class="rs-active">
99+<dt>last activity</dt>
100+<dd>
101+<time datetime="STAMP" title="STAMP">AGO</time>
102+</dd>
103+</div>
104+</dl>
105+</div>
106+<p class="repo-commits mute">no commits yet</p>
107+</li>
108+<li id="repo-demo" class="repo">
86109 <div class="repo-main">
87110 <h2 class="repo-name">
88111 <a href="/demo">demo</a>
@@ -148,27 +171,6 @@
148171 </li>
149172 </ul>
150173 </li>
151-<li id="repo-quiet" class="repo">
152-<div class="repo-main">
153-<h2 class="repo-name">
154-<a href="/quiet">quiet</a>
155-</h2>
156-<p class="repo-desc mute">no description</p>
157-<div class="repo-topics" aria-hidden="true">
158-</div>
159-<div class="repo-pulse" aria-hidden="true">
160-</div>
161-</div>
162-<div class="repo-head">
163-<dl class="repo-stats" aria-label="Figures of quiet">
164-<div class="rs-active">
165-<dt>last activity</dt>
166-<dd class="mute">never</dd>
167-</div>
168-</dl>
169-</div>
170-<p class="repo-commits mute">no commits yet</p>
171-</li>
172174 </ul>
173175 </section>
174176 </div>
@@ -343,15 +345,15 @@
343345 <span class="k">go</span>
344346 </a>
345347 </li>
346-<li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo">
347-<a href="/demo" tabindex="-1">
348-<span class="lbl">demo</span>
348+<li id="cmd-o1" role="option" aria-selected="false" data-k="repo quiet">
349+<a href="/quiet" tabindex="-1">
350+<span class="lbl">quiet</span>
349351 <span class="k">repo</span>
350352 </a>
351353 </li>
352-<li id="cmd-o2" role="option" aria-selected="false" data-k="repo quiet">
353-<a href="/quiet" tabindex="-1">
354-<span class="lbl">quiet</span>
354+<li id="cmd-o2" role="option" aria-selected="false" data-k="repo demo">
355+<a href="/demo" tabindex="-1">
356+<span class="lbl">demo</span>
355357 <span class="k">repo</span>
356358 </a>
357359 </li>
@@ -61,7 +61,7 @@
61 </div>61 </div>
62 <nav class="sort" aria-label="Sort repositories">62 <nav class="sort" aria-label="Sort repositories">
63 <span class="mute">sort</span>63 <span class="mute">sort</span>
64-<a href="/?sort=push&amp;dir=asc" aria-current="true">last push <span aria-hidden="true">▼</span>64+<a href="/?sort=activity&amp;dir=asc" aria-current="true">last activity <span aria-hidden="true">▼</span>
65 <span class="sr">, descending, select for ascending</span>65 <span class="sr">, descending, select for ascending</span>
66 </a>66 </a>
67 <a href="/?sort=name&amp;dir=asc">name </a>67 <a href="/?sort=name&amp;dir=asc">name </a>
@@ -81,8 +81,31 @@
81 </button>81 </button>
82 </div>82 </div>
83 </div>83 </div>
84-<ul class="repos" id="repo-rows" data-rows>84+<ul class="repos" id="repo-rows" data-rows data-sort="activity" data-desc="true">
85-<li id="repo-demo" class="repo" data-cursor>85+<li id="repo-quiet" class="repo" data-cursor>
86+<div class="repo-main">
87+<h2 class="repo-name">
88+<a href="/quiet">quiet</a>
89+</h2>
90+<p class="repo-desc mute">no description</p>
91+<div class="repo-topics" aria-hidden="true">
92+</div>
93+<div class="repo-pulse" aria-hidden="true">
94+</div>
95+</div>
96+<div class="repo-head">
97+<dl class="repo-stats" aria-label="Figures of quiet">
98+<div class="rs-active">
99+<dt>last activity</dt>
100+<dd>
101+<time datetime="STAMP" title="STAMP">AGO</time>
102+</dd>
103+</div>
104+</dl>
105+</div>
106+<p class="repo-commits mute">no commits yet</p>
107+</li>
108+<li id="repo-demo" class="repo">
86 <div class="repo-main">109 <div class="repo-main">
87 <h2 class="repo-name">110 <h2 class="repo-name">
88 <a href="/demo">demo</a>111 <a href="/demo">demo</a>
@@ -148,27 +171,6 @@
148 </li>171 </li>
149 </ul>172 </ul>
150 </li>173 </li>
151-<li id="repo-quiet" class="repo">
152-<div class="repo-main">
153-<h2 class="repo-name">
154-<a href="/quiet">quiet</a>
155-</h2>
156-<p class="repo-desc mute">no description</p>
157-<div class="repo-topics" aria-hidden="true">
158-</div>
159-<div class="repo-pulse" aria-hidden="true">
160-</div>
161-</div>
162-<div class="repo-head">
163-<dl class="repo-stats" aria-label="Figures of quiet">
164-<div class="rs-active">
165-<dt>last activity</dt>
166-<dd class="mute">never</dd>
167-</div>
168-</dl>
169-</div>
170-<p class="repo-commits mute">no commits yet</p>
171-</li>
172 </ul>174 </ul>
173 </section>175 </section>
174 </div>176 </div>
@@ -343,15 +345,15 @@
343 <span class="k">go</span>345 <span class="k">go</span>
344 </a>346 </a>
345 </li>347 </li>
346-<li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo">348+<li id="cmd-o1" role="option" aria-selected="false" data-k="repo quiet">
347-<a href="/demo" tabindex="-1">349+<a href="/quiet" tabindex="-1">
348-<span class="lbl">demo</span>350+<span class="lbl">quiet</span>
349 <span class="k">repo</span>351 <span class="k">repo</span>
350 </a>352 </a>
351 </li>353 </li>
352-<li id="cmd-o2" role="option" aria-selected="false" data-k="repo quiet">354+<li id="cmd-o2" role="option" aria-selected="false" data-k="repo demo">
353-<a href="/quiet" tabindex="-1">355+<a href="/demo" tabindex="-1">
354-<span class="lbl">quiet</span>356+<span class="lbl">demo</span>
355 <span class="k">repo</span>357 <span class="k">repo</span>
356 </a>358 </a>
357 </li>359 </li>
M internal/app/web_events_test.go +2 -2
@@ -123,7 +123,7 @@ func TestWebEventsHome(t *testing.T) {
123123 assert.Equal(t, "home-list", ev.name)
124124 assert.NotEmpty(t, ev.id)
125125 assert.Contains(t, ev.data, `<li id="repo-pub"`)
126- assert.Contains(t, ev.data, `new push`)
126+ assert.Contains(t, ev.data, `new activity`)
127127 assert.Contains(t, ev.data, `href="/pub"`)
128128 assert.Contains(t, ev.data, `<ul class="repo-commits" aria-label="Recent commits of pub">`, "the live row lists the commits too")
129129
@@ -134,7 +134,7 @@ func TestWebEventsHome(t *testing.T) {
134134 assert.Equal(t, "home-list", ev.name)
135135 assert.Contains(t, ev.data, `<li id="repo-fresh"`)
136136 assert.Contains(t, ev.data, "a &lt;b&gt;fresh&lt;/b&gt; one", "text is escaped")
137- assert.NotContains(t, ev.data, "new push")
137+ assert.NotContains(t, ev.data, "new activity")
138138
139139 // A repository that turns private is removed from the anonymous list.
140140 priv := domain.VisibilityPrivate
@@ -123,7 +123,7 @@ func TestWebEventsHome(t *testing.T) {
123 assert.Equal(t, "home-list", ev.name)123 assert.Equal(t, "home-list", ev.name)
124 assert.NotEmpty(t, ev.id)124 assert.NotEmpty(t, ev.id)
125 assert.Contains(t, ev.data, `<li id="repo-pub"`)125 assert.Contains(t, ev.data, `<li id="repo-pub"`)
126- assert.Contains(t, ev.data, `new push`)126+ assert.Contains(t, ev.data, `new activity`)
127 assert.Contains(t, ev.data, `href="/pub"`)127 assert.Contains(t, ev.data, `href="/pub"`)
128 assert.Contains(t, ev.data, `<ul class="repo-commits" aria-label="Recent commits of pub">`, "the live row lists the commits too")128 assert.Contains(t, ev.data, `<ul class="repo-commits" aria-label="Recent commits of pub">`, "the live row lists the commits too")
129 129
@@ -134,7 +134,7 @@ func TestWebEventsHome(t *testing.T) {
134 assert.Equal(t, "home-list", ev.name)134 assert.Equal(t, "home-list", ev.name)
135 assert.Contains(t, ev.data, `<li id="repo-fresh"`)135 assert.Contains(t, ev.data, `<li id="repo-fresh"`)
136 assert.Contains(t, ev.data, "a &lt;b&gt;fresh&lt;/b&gt; one", "text is escaped")136 assert.Contains(t, ev.data, "a &lt;b&gt;fresh&lt;/b&gt; one", "text is escaped")
137- assert.NotContains(t, ev.data, "new push")137+ assert.NotContains(t, ev.data, "new activity")
138 138
139 // A repository that turns private is removed from the anonymous list.139 // A repository that turns private is removed from the anonymous list.
140 priv := domain.VisibilityPrivate140 priv := domain.VisibilityPrivate
M internal/domain/repo.go +1 -0
@@ -42,6 +42,7 @@ type Repo struct {
4242 Visibility Visibility
4343 CreatedAt time.Time
4444 PushedAt time.Time // zero until the first push
45+ ActiveAt time.Time // latest push, merge request or issue activity
4546 // Settings are the merge settings; the stores always fill them in, with
4647 // the defaults for a repository that never stored any.
4748 Settings RepoSettings
@@ -42,6 +42,7 @@ type Repo struct {
42 Visibility Visibility42 Visibility Visibility
43 CreatedAt time.Time43 CreatedAt time.Time
44 PushedAt time.Time // zero until the first push44 PushedAt time.Time // zero until the first push
45+ ActiveAt time.Time // latest push, merge request or issue activity
45 // Settings are the merge settings; the stores always fill them in, with46 // Settings are the merge settings; the stores always fill them in, with
46 // the defaults for a repository that never stored any.47 // the defaults for a repository that never stored any.
47 Settings RepoSettings48 Settings RepoSettings
M internal/ports/dto.go +1 -1
@@ -21,7 +21,7 @@ type RepoPage struct {
2121
2222 // RepoView is a repository with the Git state that belongs in its summary.
2323 type RepoView struct {
24- domain.Repo
24+ domain.Repo // includes ActiveAt, the latest repository activity
2525 // DefaultBranch is the branch HEAD points to, even when it does not exist
2626 // yet; empty when HEAD is detached or the directory is missing.
2727 DefaultBranch string
@@ -21,7 +21,7 @@ type RepoPage struct {
21 21
22 // RepoView is a repository with the Git state that belongs in its summary.22 // RepoView is a repository with the Git state that belongs in its summary.
23 type RepoView struct {23 type RepoView struct {
24- domain.Repo24+ domain.Repo // includes ActiveAt, the latest repository activity
25 // DefaultBranch is the branch HEAD points to, even when it does not exist25 // DefaultBranch is the branch HEAD points to, even when it does not exist
26 // yet; empty when HEAD is detached or the directory is missing.26 // yet; empty when HEAD is detached or the directory is missing.
27 DefaultBranch string27 DefaultBranch string
A internal/usecase/activity.go +53 -0
@@ -0,0 +1,53 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package usecase
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "time"
10+
11+ "github.com/bnema/zerowrap"
12+
13+ "bnema.dev/ermite/internal/domain"
14+ "bnema.dev/ermite/internal/ports"
15+)
16+
17+// recordActivity is best-effort: a metadata failure never fails the action.
18+func recordActivity(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, at time.Time) {
19+ if _, err := touchActiveAt(ctx, meta, name, at); err != nil {
20+ log := zerowrap.FromCtx(ctx)
21+ log.Error().Err(err).Str("repo", name.String()).Msg("record repository activity")
22+ }
23+}
24+
25+// touchActiveAt preserves later activity and retries concurrent metadata writes.
26+// A repository deleted meanwhile is not an error.
27+func touchActiveAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) {
28+ for range pushRetries {
29+ repo, err := meta.Get(ctx, name)
30+ if errors.Is(err, domain.ErrNotFound) {
31+ return domain.Repo{}, nil
32+ }
33+ if err != nil {
34+ return domain.Repo{}, fmt.Errorf("get repository %q: %w", name, err)
35+ }
36+ if !now.After(repo.ActiveAt) {
37+ return repo, nil // a later activity already recorded
38+ }
39+ repo.ActiveAt = now
40+ err = meta.Update(ctx, repo)
41+ if errors.Is(err, domain.ErrConflict) {
42+ continue
43+ }
44+ if errors.Is(err, domain.ErrNotFound) {
45+ return domain.Repo{}, nil
46+ }
47+ if err != nil {
48+ return repo, fmt.Errorf("update repository %q: %w", name, err)
49+ }
50+ return repo, nil
51+ }
52+ return domain.Repo{}, fmt.Errorf("update active_at of %q: %w", name, domain.ErrConflict)
53+}
@@ -0,0 +1,53 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package usecase
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "time"
10+
11+ "github.com/bnema/zerowrap"
12+
13+ "bnema.dev/ermite/internal/domain"
14+ "bnema.dev/ermite/internal/ports"
15+)
16+
17+// recordActivity is best-effort: a metadata failure never fails the action.
18+func recordActivity(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, at time.Time) {
19+ if _, err := touchActiveAt(ctx, meta, name, at); err != nil {
20+ log := zerowrap.FromCtx(ctx)
21+ log.Error().Err(err).Str("repo", name.String()).Msg("record repository activity")
22+ }
23+}
24+
25+// touchActiveAt preserves later activity and retries concurrent metadata writes.
26+// A repository deleted meanwhile is not an error.
27+func touchActiveAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) {
28+ for range pushRetries {
29+ repo, err := meta.Get(ctx, name)
30+ if errors.Is(err, domain.ErrNotFound) {
31+ return domain.Repo{}, nil
32+ }
33+ if err != nil {
34+ return domain.Repo{}, fmt.Errorf("get repository %q: %w", name, err)
35+ }
36+ if !now.After(repo.ActiveAt) {
37+ return repo, nil // a later activity already recorded
38+ }
39+ repo.ActiveAt = now
40+ err = meta.Update(ctx, repo)
41+ if errors.Is(err, domain.ErrConflict) {
42+ continue
43+ }
44+ if errors.Is(err, domain.ErrNotFound) {
45+ return domain.Repo{}, nil
46+ }
47+ if err != nil {
48+ return repo, fmt.Errorf("update repository %q: %w", name, err)
49+ }
50+ return repo, nil
51+ }
52+ return domain.Repo{}, fmt.Errorf("update active_at of %q: %w", name, domain.ErrConflict)
53+}
A internal/usecase/activity_test.go +42 -0
@@ -0,0 +1,42 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package usecase
4+
5+import (
6+ "errors"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+ "bnema.dev/ermite/internal/ports/mocks"
15+)
16+
17+func TestTouchActiveAt(t *testing.T) {
18+ name, err := domain.ParseRepoName("proj")
19+ require.NoError(t, err)
20+ now := time.Now().UTC()
21+ t.Run("retries and preserves later activity", func(t *testing.T) {
22+ meta := mocks.NewRepoMetaStore(t)
23+ meta.EXPECT().Get(mock.Anything, name).Return(domain.Repo{Name: name}, nil).Once()
24+ meta.EXPECT().Update(mock.Anything, mock.MatchedBy(func(r domain.Repo) bool { return r.ActiveAt.Equal(now) && r.PushedAt.IsZero() })).Return(domain.ErrConflict).Once()
25+ later := domain.Repo{Name: name, ActiveAt: now.Add(time.Hour)}
26+ meta.EXPECT().Get(mock.Anything, name).Return(later, nil).Once()
27+ got, err := touchActiveAt(t.Context(), meta, name, now)
28+ require.NoError(t, err)
29+ require.Equal(t, later, got)
30+ })
31+ t.Run("deleted repository", func(t *testing.T) {
32+ meta := mocks.NewRepoMetaStore(t)
33+ meta.EXPECT().Get(mock.Anything, name).Return(domain.Repo{}, domain.ErrNotFound)
34+ _, err := touchActiveAt(t.Context(), meta, name, now)
35+ require.NoError(t, err)
36+ })
37+ t.Run("best effort failure", func(t *testing.T) {
38+ meta := mocks.NewRepoMetaStore(t)
39+ meta.EXPECT().Get(mock.Anything, name).Return(domain.Repo{}, errors.New("offline"))
40+ require.NotPanics(t, func() { recordActivity(t.Context(), meta, name, now) })
41+ })
42+}
@@ -0,0 +1,42 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package usecase
4+
5+import (
6+ "errors"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+ "bnema.dev/ermite/internal/ports/mocks"
15+)
16+
17+func TestTouchActiveAt(t *testing.T) {
18+ name, err := domain.ParseRepoName("proj")
19+ require.NoError(t, err)
20+ now := time.Now().UTC()
21+ t.Run("retries and preserves later activity", func(t *testing.T) {
22+ meta := mocks.NewRepoMetaStore(t)
23+ meta.EXPECT().Get(mock.Anything, name).Return(domain.Repo{Name: name}, nil).Once()
24+ meta.EXPECT().Update(mock.Anything, mock.MatchedBy(func(r domain.Repo) bool { return r.ActiveAt.Equal(now) && r.PushedAt.IsZero() })).Return(domain.ErrConflict).Once()
25+ later := domain.Repo{Name: name, ActiveAt: now.Add(time.Hour)}
26+ meta.EXPECT().Get(mock.Anything, name).Return(later, nil).Once()
27+ got, err := touchActiveAt(t.Context(), meta, name, now)
28+ require.NoError(t, err)
29+ require.Equal(t, later, got)
30+ })
31+ t.Run("deleted repository", func(t *testing.T) {
32+ meta := mocks.NewRepoMetaStore(t)
33+ meta.EXPECT().Get(mock.Anything, name).Return(domain.Repo{}, domain.ErrNotFound)
34+ _, err := touchActiveAt(t.Context(), meta, name, now)
35+ require.NoError(t, err)
36+ })
37+ t.Run("best effort failure", func(t *testing.T) {
38+ meta := mocks.NewRepoMetaStore(t)
39+ meta.EXPECT().Get(mock.Anything, name).Return(domain.Repo{}, errors.New("offline"))
40+ require.NotPanics(t, func() { recordActivity(t.Context(), meta, name, now) })
41+ })
42+}
M internal/usecase/issues.go +3 -0
@@ -124,6 +124,7 @@ func (i *Issues) Create(ctx context.Context, p domain.Principal, name domain.Rep
124124 log := zerowrap.FromCtx(ctx)
125125 log.Info().Str("repo", name.String()).Int64("number", issue.Number).
126126 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue opened")
127+ recordActivity(ctx, i.meta, repo.Name, now)
127128 i.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
128129 return issue, nil
129130 }
@@ -211,6 +212,7 @@ func (i *Issues) AddComment(ctx context.Context, p domain.Principal, name domain
211212 log := zerowrap.FromCtx(ctx)
212213 log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID).
213214 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue comment added")
215+ recordActivity(ctx, i.meta, name, c.CreatedAt)
214216 i.bus.Publish(ctx, domain.IssueCommented{
215217 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,
216218 })
@@ -260,6 +262,7 @@ func (i *Issues) transition(ctx context.Context, p domain.Principal, name domain
260262 log := zerowrap.FromCtx(ctx)
261263 log.Info().Str("repo", name.String()).Int64("number", number).
262264 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue " + verb + "d")
265+ recordActivity(ctx, i.meta, repo.Name, issue.UpdatedAt)
263266 if to == domain.IssueStateOpen {
264267 i.bus.Publish(ctx, domain.IssueReopened{Issue: issue, At: issue.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
265268 } else {
@@ -124,6 +124,7 @@ func (i *Issues) Create(ctx context.Context, p domain.Principal, name domain.Rep
124 log := zerowrap.FromCtx(ctx)124 log := zerowrap.FromCtx(ctx)
125 log.Info().Str("repo", name.String()).Int64("number", issue.Number).125 log.Info().Str("repo", name.String()).Int64("number", issue.Number).
126 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue opened")126 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue opened")
127+ recordActivity(ctx, i.meta, repo.Name, now)
127 i.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})128 i.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
128 return issue, nil129 return issue, nil
129 }130 }
@@ -211,6 +212,7 @@ func (i *Issues) AddComment(ctx context.Context, p domain.Principal, name domain
211 log := zerowrap.FromCtx(ctx)212 log := zerowrap.FromCtx(ctx)
212 log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID).213 log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID).
213 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue comment added")214 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue comment added")
215+ recordActivity(ctx, i.meta, name, c.CreatedAt)
214 i.bus.Publish(ctx, domain.IssueCommented{216 i.bus.Publish(ctx, domain.IssueCommented{
215 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,217 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,
216 })218 })
@@ -260,6 +262,7 @@ func (i *Issues) transition(ctx context.Context, p domain.Principal, name domain
260 log := zerowrap.FromCtx(ctx)262 log := zerowrap.FromCtx(ctx)
261 log.Info().Str("repo", name.String()).Int64("number", number).263 log.Info().Str("repo", name.String()).Int64("number", number).
262 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue " + verb + "d")264 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue " + verb + "d")
265+ recordActivity(ctx, i.meta, repo.Name, issue.UpdatedAt)
263 if to == domain.IssueStateOpen {266 if to == domain.IssueStateOpen {
264 i.bus.Publish(ctx, domain.IssueReopened{Issue: issue, At: issue.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})267 i.bus.Publish(ctx, domain.IssueReopened{Issue: issue, At: issue.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
265 } else {268 } else {
M internal/usecase/issues_integration_test.go +4 -0
@@ -46,6 +46,10 @@ func TestIssuesOverTheStore(t *testing.T) {
4646 require.NoError(t, err)
4747 assert.EqualValues(t, 2, issue.Number, "the number follows the merge request")
4848 assert.Equal(t, "bot", issue.Author)
49+ repo, err := store.Get(ctx, n)
50+ require.NoError(t, err)
51+ assert.True(t, repo.ActiveAt.Equal(now))
52+ assert.True(t, repo.PushedAt.IsZero())
4953
5054 kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1)
5155 require.NoError(t, err)
@@ -46,6 +46,10 @@ func TestIssuesOverTheStore(t *testing.T) {
46 require.NoError(t, err)46 require.NoError(t, err)
47 assert.EqualValues(t, 2, issue.Number, "the number follows the merge request")47 assert.EqualValues(t, 2, issue.Number, "the number follows the merge request")
48 assert.Equal(t, "bot", issue.Author)48 assert.Equal(t, "bot", issue.Author)
49+ repo, err := store.Get(ctx, n)
50+ require.NoError(t, err)
51+ assert.True(t, repo.ActiveAt.Equal(now))
52+ assert.True(t, repo.PushedAt.IsZero())
49 53
50 kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1)54 kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1)
51 require.NoError(t, err)55 require.NoError(t, err)
M internal/usecase/issues_test.go +2 -0
@@ -39,6 +39,7 @@ func newIssueUnit(t *testing.T, vis domain.Visibility, mode domain.IssuesMode) *
3939 clock := mocks.NewClock(t)
4040 clock.EXPECT().Now().Return(now).Maybe()
4141 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil).Maybe()
42+ u.meta.EXPECT().Update(mock.Anything, mock.MatchedBy(func(r domain.Repo) bool { return r.Name == u.repo.Name && r.ActiveAt.Equal(now) && r.PushedAt.IsZero() })).Return(nil).Maybe()
4243 u.issues = usecase.NewIssues(usecase.IssuesConfig{Meta: u.meta, Store: u.store, MergeRequests: u.mrs, Bus: u.bus, Clock: clock})
4344 return u
4445 }
@@ -116,6 +117,7 @@ func TestIssueCreateAndTransitions(t *testing.T) {
116117 got, err := u.issues.Create(ctx, p, u.repo.Name, ports.CreateIssueParams{Title: " Crash ", Body: "why"})
117118 require.NoError(t, err)
118119 assert.EqualValues(t, 7, got.Number)
120+ u.meta.AssertCalled(t, "Update", mock.Anything, mock.MatchedBy(func(r domain.Repo) bool { return r.ActiveAt.Equal(now) && r.PushedAt.IsZero() }))
119121 })
120122
121123 t.Run("invalid text stores nothing", func(t *testing.T) {
@@ -39,6 +39,7 @@ func newIssueUnit(t *testing.T, vis domain.Visibility, mode domain.IssuesMode) *
39 clock := mocks.NewClock(t)39 clock := mocks.NewClock(t)
40 clock.EXPECT().Now().Return(now).Maybe()40 clock.EXPECT().Now().Return(now).Maybe()
41 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil).Maybe()41 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil).Maybe()
42+ u.meta.EXPECT().Update(mock.Anything, mock.MatchedBy(func(r domain.Repo) bool { return r.Name == u.repo.Name && r.ActiveAt.Equal(now) && r.PushedAt.IsZero() })).Return(nil).Maybe()
42 u.issues = usecase.NewIssues(usecase.IssuesConfig{Meta: u.meta, Store: u.store, MergeRequests: u.mrs, Bus: u.bus, Clock: clock})43 u.issues = usecase.NewIssues(usecase.IssuesConfig{Meta: u.meta, Store: u.store, MergeRequests: u.mrs, Bus: u.bus, Clock: clock})
43 return u44 return u
44 }45 }
@@ -116,6 +117,7 @@ func TestIssueCreateAndTransitions(t *testing.T) {
116 got, err := u.issues.Create(ctx, p, u.repo.Name, ports.CreateIssueParams{Title: " Crash ", Body: "why"})117 got, err := u.issues.Create(ctx, p, u.repo.Name, ports.CreateIssueParams{Title: " Crash ", Body: "why"})
117 require.NoError(t, err)118 require.NoError(t, err)
118 assert.EqualValues(t, 7, got.Number)119 assert.EqualValues(t, 7, got.Number)
120+ u.meta.AssertCalled(t, "Update", mock.Anything, mock.MatchedBy(func(r domain.Repo) bool { return r.ActiveAt.Equal(now) && r.PushedAt.IsZero() }))
119 })121 })
120 122
121 t.Run("invalid text stores nothing", func(t *testing.T) {123 t.Run("invalid text stores nothing", func(t *testing.T) {
M internal/usecase/mergerequests.go +5 -0
@@ -254,6 +254,7 @@ func (m *MergeRequests) Create(ctx context.Context, p domain.Principal, name dom
254254 if err != nil {
255255 return domain.MergeRequest{}, err
256256 }
257+ recordActivity(ctx, m.meta, repo.Name, now)
257258 m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
258259 if m.pulls != nil {
259260 // The commits the opening is for are the ones of now.
@@ -377,6 +378,7 @@ func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name
377378 if err != nil {
378379 return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err)
379380 }
381+ recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)
380382 m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
381383 return m.withHead(ctx, mr)
382384 }
@@ -656,6 +658,7 @@ func (m *MergeRequests) AddComment(ctx context.Context, p domain.Principal, name
656658 log := zerowrap.FromCtx(ctx)
657659 log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID).
658660 Int64("reply_to", c.ReplyTo).Str("verdict", c.Verdict.String()).Bool("on_line", !general).Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("comment added")
661+ recordActivity(ctx, m.meta, name, c.CreatedAt)
659662 m.bus.Publish(ctx, domain.CommentAdded{
660663 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,
661664 })
@@ -796,6 +799,7 @@ func (m *MergeRequests) Close(ctx context.Context, p domain.Principal, name doma
796799 log := zerowrap.FromCtx(ctx)
797800 log.Info().Str("repo", name.String()).Int64("number", number).
798801 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request closed")
802+ recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)
799803 m.bus.Publish(ctx, domain.MergeRequestClosed{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
800804 if m.pulls != nil {
801805 m.pulls.OnPullRequest(ctx, name, ports.PullRequestEvent{Number: number, Action: domain.PullClosed, Head: mr.HeadID, Base: mr.BaseID}, p)
@@ -998,6 +1002,7 @@ func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name doma
9981002 }
9991003 }
10001004 at := mr.UpdatedAt
1005+ recordActivity(ctx, m.meta, repo.Name, at)
10011006 m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
10021007 m.bus.Publish(ctx, domain.PushEvent{
10031008 Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates,
@@ -254,6 +254,7 @@ func (m *MergeRequests) Create(ctx context.Context, p domain.Principal, name dom
254 if err != nil {254 if err != nil {
255 return domain.MergeRequest{}, err255 return domain.MergeRequest{}, err
256 }256 }
257+ recordActivity(ctx, m.meta, repo.Name, now)
257 m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})258 m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
258 if m.pulls != nil {259 if m.pulls != nil {
259 // The commits the opening is for are the ones of now.260 // The commits the opening is for are the ones of now.
@@ -377,6 +378,7 @@ func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name
377 if err != nil {378 if err != nil {
378 return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err)379 return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err)
379 }380 }
381+ recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)
380 m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})382 m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
381 return m.withHead(ctx, mr)383 return m.withHead(ctx, mr)
382 }384 }
@@ -656,6 +658,7 @@ func (m *MergeRequests) AddComment(ctx context.Context, p domain.Principal, name
656 log := zerowrap.FromCtx(ctx)658 log := zerowrap.FromCtx(ctx)
657 log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID).659 log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID).
658 Int64("reply_to", c.ReplyTo).Str("verdict", c.Verdict.String()).Bool("on_line", !general).Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("comment added")660 Int64("reply_to", c.ReplyTo).Str("verdict", c.Verdict.String()).Bool("on_line", !general).Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("comment added")
661+ recordActivity(ctx, m.meta, name, c.CreatedAt)
659 m.bus.Publish(ctx, domain.CommentAdded{662 m.bus.Publish(ctx, domain.CommentAdded{
660 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,663 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,
661 })664 })
@@ -796,6 +799,7 @@ func (m *MergeRequests) Close(ctx context.Context, p domain.Principal, name doma
796 log := zerowrap.FromCtx(ctx)799 log := zerowrap.FromCtx(ctx)
797 log.Info().Str("repo", name.String()).Int64("number", number).800 log.Info().Str("repo", name.String()).Int64("number", number).
798 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request closed")801 Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request closed")
802+ recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)
799 m.bus.Publish(ctx, domain.MergeRequestClosed{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})803 m.bus.Publish(ctx, domain.MergeRequestClosed{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
800 if m.pulls != nil {804 if m.pulls != nil {
801 m.pulls.OnPullRequest(ctx, name, ports.PullRequestEvent{Number: number, Action: domain.PullClosed, Head: mr.HeadID, Base: mr.BaseID}, p)805 m.pulls.OnPullRequest(ctx, name, ports.PullRequestEvent{Number: number, Action: domain.PullClosed, Head: mr.HeadID, Base: mr.BaseID}, p)
@@ -998,6 +1002,7 @@ func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name doma
998 }1002 }
999 }1003 }
1000 at := mr.UpdatedAt1004 at := mr.UpdatedAt
1005+ recordActivity(ctx, m.meta, repo.Name, at)
1001 m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})1006 m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
1002 m.bus.Publish(ctx, domain.PushEvent{1007 m.bus.Publish(ctx, domain.PushEvent{
1003 Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates,1008 Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates,
M internal/usecase/mergerequests_integration_test.go +11 -0
@@ -1448,3 +1448,14 @@ func TestNoteThreadsRangesAndVerdicts(t *testing.T) {
14481448 _, err = r.mrs.DeleteComment(r.ctx, root, r.repo, mr.Number, changes.ID)
14491449 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)
14501450 }
1451+
1452+func TestMergeRequestRecordsRepositoryActivity(t *testing.T) {
1453+ r := newMRRig(t)
1454+ r.seed()
1455+ r.feature("activity")
1456+ _, err := r.mrs.Create(r.ctx, root, r.repo, ports.CreateMergeRequestParams{Title: "Activity", Source: "activity", Target: "main"})
1457+ require.NoError(t, err)
1458+ repo, err := r.store.Get(r.ctx, r.repo)
1459+ require.NoError(t, err)
1460+ assert.True(t, repo.ActiveAt.Equal(now))
1461+}
@@ -1448,3 +1448,14 @@ func TestNoteThreadsRangesAndVerdicts(t *testing.T) {
1448 _, err = r.mrs.DeleteComment(r.ctx, root, r.repo, mr.Number, changes.ID)1448 _, err = r.mrs.DeleteComment(r.ctx, root, r.repo, mr.Number, changes.ID)
1449 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)1449 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)
1450 }1450 }
1451+
1452+func TestMergeRequestRecordsRepositoryActivity(t *testing.T) {
1453+ r := newMRRig(t)
1454+ r.seed()
1455+ r.feature("activity")
1456+ _, err := r.mrs.Create(r.ctx, root, r.repo, ports.CreateMergeRequestParams{Title: "Activity", Source: "activity", Target: "main"})
1457+ require.NoError(t, err)
1458+ repo, err := r.store.Get(r.ctx, r.repo)
1459+ require.NoError(t, err)
1460+ assert.True(t, repo.ActiveAt.Equal(now))
1461+}
M internal/usecase/pushes.go +10 -4
@@ -209,7 +209,7 @@ func (u *Pushes) checkWorkflows(ctx context.Context, name domain.RepoName, updat
209209 return nil
210210 }
211211
212-// Record is the post-receive step of an accepted push: it sets pushed_at,
212+// Record is the post-receive step of an accepted push: it sets pushed_at and active_at,
213213 // repairs HEAD when it points to a missing branch, tells the reader that the
214214 // repository changed, queues the workflows the push triggers, publishes a
215215 // PushEvent and schedules maintenance. Every
@@ -245,7 +245,8 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep
245245 return errors.Join(errs...)
246246 }
247247
248-// touchPushedAt stores pushed_at, retrying when another write got in between.
248+// touchPushedAt stores pushed_at and active_at, preserving later timestamps
249+// and retrying when another write got in between.
249250 // A repository deleted meanwhile is not an error. It returns the repository
250251 // as read, or the zero value when it is unknown. A push (Pushes.Record) and a
251252 // merge, which moves a branch too, both record through it.
@@ -258,10 +259,15 @@ func touchPushedAt(ctx context.Context, meta ports.RepoMetaStore, name domain.Re
258259 if err != nil {
259260 return domain.Repo{}, fmt.Errorf("get repository %q: %w", name, err)
260261 }
261- if !now.After(repo.PushedAt) {
262+ if !now.After(repo.PushedAt) && !now.After(repo.ActiveAt) {
262263 return repo, nil // a later push already recorded
263264 }
264- repo.PushedAt = now
265+ if now.After(repo.PushedAt) {
266+ repo.PushedAt = now
267+ }
268+ if now.After(repo.ActiveAt) {
269+ repo.ActiveAt = now
270+ }
265271 err = meta.Update(ctx, repo)
266272 if errors.Is(err, domain.ErrConflict) {
267273 continue
@@ -209,7 +209,7 @@ func (u *Pushes) checkWorkflows(ctx context.Context, name domain.RepoName, updat
209 return nil209 return nil
210 }210 }
211 211
212-// Record is the post-receive step of an accepted push: it sets pushed_at,212+// Record is the post-receive step of an accepted push: it sets pushed_at and active_at,
213 // repairs HEAD when it points to a missing branch, tells the reader that the213 // repairs HEAD when it points to a missing branch, tells the reader that the
214 // repository changed, queues the workflows the push triggers, publishes a214 // repository changed, queues the workflows the push triggers, publishes a
215 // PushEvent and schedules maintenance. Every215 // PushEvent and schedules maintenance. Every
@@ -245,7 +245,8 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep
245 return errors.Join(errs...)245 return errors.Join(errs...)
246 }246 }
247 247
248-// touchPushedAt stores pushed_at, retrying when another write got in between.248+// touchPushedAt stores pushed_at and active_at, preserving later timestamps
249+// and retrying when another write got in between.
249 // A repository deleted meanwhile is not an error. It returns the repository250 // A repository deleted meanwhile is not an error. It returns the repository
250 // as read, or the zero value when it is unknown. A push (Pushes.Record) and a251 // as read, or the zero value when it is unknown. A push (Pushes.Record) and a
251 // merge, which moves a branch too, both record through it.252 // merge, which moves a branch too, both record through it.
@@ -258,10 +259,15 @@ func touchPushedAt(ctx context.Context, meta ports.RepoMetaStore, name domain.Re
258 if err != nil {259 if err != nil {
259 return domain.Repo{}, fmt.Errorf("get repository %q: %w", name, err)260 return domain.Repo{}, fmt.Errorf("get repository %q: %w", name, err)
260 }261 }
261- if !now.After(repo.PushedAt) {262+ if !now.After(repo.PushedAt) && !now.After(repo.ActiveAt) {
262 return repo, nil // a later push already recorded263 return repo, nil // a later push already recorded
263 }264 }
264- repo.PushedAt = now265+ if now.After(repo.PushedAt) {
266+ repo.PushedAt = now
267+ }
268+ if now.After(repo.ActiveAt) {
269+ repo.ActiveAt = now
270+ }
265 err = meta.Update(ctx, repo)271 err = meta.Update(ctx, repo)
266 if errors.Is(err, domain.ErrConflict) {272 if errors.Is(err, domain.ErrConflict) {
267 continue273 continue
M internal/usecase/pushes_test.go +4 -4
@@ -306,7 +306,7 @@ func TestPushesRecord(t *testing.T) {
306306 f.sched.EXPECT().Schedule(ctx, n).Return().Once()
307307 repo := domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: now.Add(-time.Hour)}
308308 f.meta.EXPECT().Get(ctx, n).Return(repo, nil)
309- f.meta.EXPECT().Update(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: repo.CreatedAt, PushedAt: now}).Return(nil)
309+ f.meta.EXPECT().Update(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: repo.CreatedAt, PushedAt: now, ActiveAt: now}).Return(nil)
310310 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil)
311311 f.bus.EXPECT().Publish(ctx, domain.PushEvent{Repo: n, Updates: ups, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}).Return()
312312
@@ -334,7 +334,7 @@ func TestPushesRecord(t *testing.T) {
334334 repo := domain.Repo{Name: n}
335335 f.meta.EXPECT().Get(ctx, n).Return(repo, nil).Once()
336336 f.meta.EXPECT().Update(ctx, mock.Anything).Return(domain.ErrConflict).Once()
337- later := domain.Repo{Name: n, PushedAt: now.Add(time.Second)}
337+ later := domain.Repo{Name: n, PushedAt: now.Add(time.Second), ActiveAt: now.Add(time.Second)}
338338 f.meta.EXPECT().Get(ctx, n).Return(later, nil).Once() // a later push recorded meanwhile
339339 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil)
340340 f.bus.EXPECT().Publish(ctx, mock.Anything).Return()
@@ -414,7 +414,7 @@ func TestPushesDefaultBranchRule(t *testing.T) {
414414 t.Parallel()
415415 f := newPushFixture(t)
416416 f.sched.EXPECT().Schedule(ctx, n).Return().Once()
417- f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now}, nil)
417+ f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now, ActiveAt: now}, nil)
418418 f.git.EXPECT().DefaultBranch(ctx, n).Return(tt.head, tt.exists, nil)
419419 if tt.want != "" {
420420 f.git.EXPECT().SetDefaultBranch(ctx, n, tt.want).Return(nil)
@@ -429,7 +429,7 @@ func TestPushesDefaultBranchRule(t *testing.T) {
429429 f := newPushFixture(t)
430430 f.sched.EXPECT().Schedule(ctx, n).Return().Once()
431431 boom := errors.New("boom")
432- f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now}, nil)
432+ f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now, ActiveAt: now}, nil)
433433 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", false, nil)
434434 f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom)
435435 f.bus.EXPECT().Publish(ctx, mock.Anything).Return()
@@ -306,7 +306,7 @@ func TestPushesRecord(t *testing.T) {
306 f.sched.EXPECT().Schedule(ctx, n).Return().Once()306 f.sched.EXPECT().Schedule(ctx, n).Return().Once()
307 repo := domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: now.Add(-time.Hour)}307 repo := domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: now.Add(-time.Hour)}
308 f.meta.EXPECT().Get(ctx, n).Return(repo, nil)308 f.meta.EXPECT().Get(ctx, n).Return(repo, nil)
309- f.meta.EXPECT().Update(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: repo.CreatedAt, PushedAt: now}).Return(nil)309+ f.meta.EXPECT().Update(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: repo.CreatedAt, PushedAt: now, ActiveAt: now}).Return(nil)
310 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil)310 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil)
311 f.bus.EXPECT().Publish(ctx, domain.PushEvent{Repo: n, Updates: ups, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}).Return()311 f.bus.EXPECT().Publish(ctx, domain.PushEvent{Repo: n, Updates: ups, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}).Return()
312 312
@@ -334,7 +334,7 @@ func TestPushesRecord(t *testing.T) {
334 repo := domain.Repo{Name: n}334 repo := domain.Repo{Name: n}
335 f.meta.EXPECT().Get(ctx, n).Return(repo, nil).Once()335 f.meta.EXPECT().Get(ctx, n).Return(repo, nil).Once()
336 f.meta.EXPECT().Update(ctx, mock.Anything).Return(domain.ErrConflict).Once()336 f.meta.EXPECT().Update(ctx, mock.Anything).Return(domain.ErrConflict).Once()
337- later := domain.Repo{Name: n, PushedAt: now.Add(time.Second)}337+ later := domain.Repo{Name: n, PushedAt: now.Add(time.Second), ActiveAt: now.Add(time.Second)}
338 f.meta.EXPECT().Get(ctx, n).Return(later, nil).Once() // a later push recorded meanwhile338 f.meta.EXPECT().Get(ctx, n).Return(later, nil).Once() // a later push recorded meanwhile
339 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil)339 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil)
340 f.bus.EXPECT().Publish(ctx, mock.Anything).Return()340 f.bus.EXPECT().Publish(ctx, mock.Anything).Return()
@@ -414,7 +414,7 @@ func TestPushesDefaultBranchRule(t *testing.T) {
414 t.Parallel()414 t.Parallel()
415 f := newPushFixture(t)415 f := newPushFixture(t)
416 f.sched.EXPECT().Schedule(ctx, n).Return().Once()416 f.sched.EXPECT().Schedule(ctx, n).Return().Once()
417- f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now}, nil)417+ f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now, ActiveAt: now}, nil)
418 f.git.EXPECT().DefaultBranch(ctx, n).Return(tt.head, tt.exists, nil)418 f.git.EXPECT().DefaultBranch(ctx, n).Return(tt.head, tt.exists, nil)
419 if tt.want != "" {419 if tt.want != "" {
420 f.git.EXPECT().SetDefaultBranch(ctx, n, tt.want).Return(nil)420 f.git.EXPECT().SetDefaultBranch(ctx, n, tt.want).Return(nil)
@@ -429,7 +429,7 @@ func TestPushesDefaultBranchRule(t *testing.T) {
429 f := newPushFixture(t)429 f := newPushFixture(t)
430 f.sched.EXPECT().Schedule(ctx, n).Return().Once()430 f.sched.EXPECT().Schedule(ctx, n).Return().Once()
431 boom := errors.New("boom")431 boom := errors.New("boom")
432- f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now}, nil)432+ f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now, ActiveAt: now}, nil)
433 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", false, nil)433 f.git.EXPECT().DefaultBranch(ctx, n).Return("main", false, nil)
434 f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom)434 f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom)
435 f.bus.EXPECT().Publish(ctx, mock.Anything).Return()435 f.bus.EXPECT().Publish(ctx, mock.Anything).Return()
M proto/ermite/storage/repo.proto +2 -0
@@ -32,6 +32,8 @@ message Repo {
3232 RepoSettings settings = 6;
3333 // Normalized and sorted; empty on repositories stored before topics.
3434 repeated string topics = 7;
35+ // Latest push, merge request or issue activity; unset until recorded.
36+ google.protobuf.Timestamp active_at = 8;
3537 }
3638
3739 // SchemaVersion is stored under the key "meta/schema".
@@ -32,6 +32,8 @@ message Repo {
32 RepoSettings settings = 6;32 RepoSettings settings = 6;
33 // Normalized and sorted; empty on repositories stored before topics.33 // Normalized and sorted; empty on repositories stored before topics.
34 repeated string topics = 7;34 repeated string topics = 7;
35+ // Latest push, merge request or issue activity; unset until recorded.
36+ google.protobuf.Timestamp active_at = 8;
35 }37 }
36 38
37 // SchemaVersion is stored under the key "meta/schema".39 // SchemaVersion is stored under the key "meta/schema".

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 44

↑↓ moveenter openesc close