feat: display git-bug identity avatars in web UI

Amolith and Crush created

Add avatar display functionality to the bug tracking interface.
Integrates with git-bug's AvatarUrl() method to fetch and render
identity avatars safely in both bugs list and detail views.

- Add AuthorAvatar fields to BugListItem and TimelineItem structs
- Fetch avatar URLs using snap.Author.AvatarUrl() in backend
- Create responsive avatar CSS styling with fallbacks
- Update templates to display avatars before usernames
- Implement lazy loading and referrer policies for security

Implements: bug-d42c94f
Co-Authored-By: Crush <crush@charm.land>

Change summary

pkg/web/static/overrides.css | 17 ++++++++++
pkg/web/templates/base.html  |  5 +-
pkg/web/templates/bug.html   | 10 +++---
pkg/web/templates/bugs.html  |  2 
pkg/web/webui_bugs.go        | 62 +++++++++++++++++++++----------------
5 files changed, 61 insertions(+), 35 deletions(-)

Detailed changes

pkg/web/static/overrides.css 🔗

@@ -184,3 +184,20 @@ article > p:only-child {
 	border: 2px solid color-mix(in srgb, var(--label-color) 40%, var(--pico-card-background-color));
 	color: var(--pico-color);
 }
+
+.avatar {
+	display: inline-block;
+	width: 1.5rem;
+	height: 1.5rem;
+	border-radius: 50%;
+	vertical-align: middle;
+	margin-right: 0.375rem;
+	object-fit: cover;
+	background-color: var(--pico-secondary-background);
+}
+
+.avatar-timeline {
+	width: 2rem;
+	height: 2rem;
+	margin-right: 0.5rem;
+}

pkg/web/templates/base.html 🔗

@@ -13,10 +13,11 @@
   <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍦</text></svg>">
 
   <link rel="preload" href="/static/pico-2.1.1-pink.min.css" as="style">
-  <link rel="preload" href="/static/overrides.css?v=1" as="style">
+  <link rel="preload" href="/static/overrides.css?v=2" as="style">
 
   <link rel="stylesheet" href="/static/pico-2.1.1-pink.min.css">
-  <link rel="stylesheet" href="/static/overrides.css?v=1">
+  <link rel="stylesheet" href="/static/overrides.css?v=2">
+  <link rel="stylesheet" href="/static/syntax.css?v=1">
 
   {{block "page-styles" .}}{{end}}
 </head>

pkg/web/templates/bug.html 🔗

@@ -35,7 +35,7 @@
   {{if eq .Type "create"}}
   <article id="{{.ID}}">
     <header>
-      <p><strong>{{.Author}}</strong> opened <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time>{{if $.Edited}} <em>(edited)</em>{{end}}</p>
+      <p>{{if .AuthorAvatar}}<img src="{{.AuthorAvatar}}" alt="" class="avatar avatar-timeline" loading="lazy" referrerpolicy="no-referrer">{{end}}<strong>{{.Author}}</strong> opened <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time>{{if $.Edited}} <em>(edited)</em>{{end}}</p>
     </header>
     {{if .Message}}
     {{.Message}}
@@ -47,7 +47,7 @@
   {{else if eq .Type "comment"}}
   <article id="{{.ID}}">
     <header>
-      <p><strong>{{.Author}}</strong> commented <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time>{{if .Edited}} <em>(edited)</em>{{end}}</p>
+      <p>{{if .AuthorAvatar}}<img src="{{.AuthorAvatar}}" alt="" class="avatar avatar-timeline" loading="lazy" referrerpolicy="no-referrer">{{end}}<strong>{{.Author}}</strong> commented <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time>{{if .Edited}} <em>(edited)</em>{{end}}</p>
     </header>
     {{if .Message}}
     {{.Message}}
@@ -58,18 +58,18 @@
   
   {{else if eq .Type "title"}}
   <article id="{{.ID}}">
-    <p><strong>{{.Author}}</strong> changed the title to <strong>{{.Title}}</strong> <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time></p>
+    <p>{{if .AuthorAvatar}}<img src="{{.AuthorAvatar}}" alt="" class="avatar avatar-timeline" loading="lazy" referrerpolicy="no-referrer">{{end}}<strong>{{.Author}}</strong> changed the title to <strong>{{.Title}}</strong> <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time></p>
   </article>
   
   {{else if eq .Type "status"}}
   <article id="{{.ID}}">
-    <p><strong>{{.Author}}</strong> {{.Status}} the bug <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time></p>
+    <p>{{if .AuthorAvatar}}<img src="{{.AuthorAvatar}}" alt="" class="avatar avatar-timeline" loading="lazy" referrerpolicy="no-referrer">{{end}}<strong>{{.Author}}</strong> {{.Status}} the bug <time datetime="{{.Timestamp | rfc3339}}" data-tooltip="{{.Timestamp | formatDate}}">{{.Timestamp | relativeTime}}</time></p>
   </article>
   
   {{else if eq .Type "labels"}}
   <article id="{{.ID}}">
     <p>
-      <strong>{{.Author}}</strong>
+      {{if .AuthorAvatar}}<img src="{{.AuthorAvatar}}" alt="" class="avatar avatar-timeline" loading="lazy" referrerpolicy="no-referrer">{{end}}<strong>{{.Author}}</strong>
       {{if .AddedLabels}}
       added label{{if gt (len .AddedLabels) 1}}s{{end}}
       {{range $i, $label := .AddedLabels}}{{if $i}}, {{end}}<code>{{$label}}</code>{{end}}

pkg/web/templates/bugs.html 🔗

@@ -30,7 +30,7 @@
       {{.Title}}
     </h3>
      <footer>
-       Opened <time datetime="{{.CreatedAt | rfc3339}}" data-tooltip="{{.CreatedAt | formatDate}}">{{.CreatedAt | relativeTime}}</time> by <strong>{{.Author}}</strong>
+       Opened <time datetime="{{.CreatedAt | rfc3339}}" data-tooltip="{{.CreatedAt | formatDate}}">{{.CreatedAt | relativeTime}}</time> by {{if .AuthorAvatar}}<img src="{{.AuthorAvatar}}" alt="" class="avatar" loading="lazy" referrerpolicy="no-referrer">{{end}}<strong>{{.Author}}</strong>
        {{if gt .CommentCount 0}}
        • {{.CommentCount}} comments
        {{end}}

pkg/web/webui_bugs.go 🔗

@@ -43,6 +43,7 @@ type BugListItem struct {
 	FullID       string
 	Title        string
 	Author       string
+	AuthorAvatar string
 	Status       string
 	CreatedAt    time.Time
 	LastActivity time.Time
@@ -68,11 +69,12 @@ type BugData struct {
 }
 
 type TimelineItem struct {
-	Type      string
-	ID        string
-	Author    string
-	Timestamp time.Time
-	Edited    bool
+	Type         string
+	ID           string
+	Author       string
+	AuthorAvatar string
+	Timestamp    time.Time
+	Edited       bool
 
 	Message       template.HTML
 	Title         string
@@ -137,6 +139,7 @@ func getBugsList(rc *cache.RepoCache, status string) ([]BugListItem, error) {
 			FullID:       snap.Id().String(),
 			Title:        snap.Title,
 			Author:       snap.Author.DisplayName(),
+			AuthorAvatar: snap.Author.AvatarUrl(),
 			Status:       snap.Status.String(),
 			CreatedAt:    snap.CreateTime,
 			LastActivity: getLastActivity(snap),
@@ -206,12 +209,13 @@ func buildTimelineItems(snap *bug.Snapshot) []TimelineItem {
 			}
 
 			items = append(items, TimelineItem{
-				Type:      "create",
-				ID:        op.CombinedId().String(),
-				Author:    op.Author.DisplayName(),
-				Timestamp: op.CreatedAt.Time(),
-				Edited:    op.Edited(),
-				Message:   messageHTML,
+				Type:         "create",
+				ID:           op.CombinedId().String(),
+				Author:       op.Author.DisplayName(),
+				AuthorAvatar: op.Author.AvatarUrl(),
+				Timestamp:    op.CreatedAt.Time(),
+				Edited:       op.Edited(),
+				Message:      messageHTML,
 			})
 
 		case *bug.AddCommentTimelineItem:
@@ -226,30 +230,33 @@ func buildTimelineItems(snap *bug.Snapshot) []TimelineItem {
 			}
 
 			items = append(items, TimelineItem{
-				Type:      "comment",
-				ID:        op.CombinedId().String(),
-				Author:    op.Author.DisplayName(),
-				Timestamp: op.CreatedAt.Time(),
-				Edited:    op.Edited(),
-				Message:   messageHTML,
+				Type:         "comment",
+				ID:           op.CombinedId().String(),
+				Author:       op.Author.DisplayName(),
+				AuthorAvatar: op.Author.AvatarUrl(),
+				Timestamp:    op.CreatedAt.Time(),
+				Edited:       op.Edited(),
+				Message:      messageHTML,
 			})
 
 		case *bug.SetTitleTimelineItem:
 			items = append(items, TimelineItem{
-				Type:      "title",
-				ID:        op.CombinedId().String(),
-				Author:    op.Author.DisplayName(),
-				Timestamp: op.UnixTime.Time(),
-				Title:     op.Title,
+				Type:         "title",
+				ID:           op.CombinedId().String(),
+				Author:       op.Author.DisplayName(),
+				AuthorAvatar: op.Author.AvatarUrl(),
+				Timestamp:    op.UnixTime.Time(),
+				Title:        op.Title,
 			})
 
 		case *bug.SetStatusTimelineItem:
 			items = append(items, TimelineItem{
-				Type:      "status",
-				ID:        op.CombinedId().String(),
-				Author:    op.Author.DisplayName(),
-				Timestamp: op.UnixTime.Time(),
-				Status:    op.Status.Action(),
+				Type:         "status",
+				ID:           op.CombinedId().String(),
+				Author:       op.Author.DisplayName(),
+				AuthorAvatar: op.Author.AvatarUrl(),
+				Timestamp:    op.UnixTime.Time(),
+				Status:       op.Status.Action(),
 			})
 
 		case *bug.LabelChangeTimelineItem:
@@ -267,6 +274,7 @@ func buildTimelineItems(snap *bug.Snapshot) []TimelineItem {
 				Type:          "labels",
 				ID:            op.CombinedId().String(),
 				Author:        op.Author.DisplayName(),
+				AuthorAvatar:  op.Author.AvatarUrl(),
 				Timestamp:     op.UnixTime.Time(),
 				AddedLabels:   added,
 				RemovedLabels: removed,