Web UI: music channel, posted-to-Matrix glow, redesigned index, AVIF thumbnails

- Add music as a fourth channel (nav + theme color)
- Glowing themed border on cards that have been posted to Matrix
- Replace per-channel index sections with: "Pete just posted" strip,
  channel dashboard (last post, 24h count, totals), unified latest feed
- /img proxy: SSRF-guarded thumbnail re-encoder that resizes to 800px
  and runs avifenc -q 45, cached under data/img-cache
This commit is contained in:
prosolis
2026-05-24 23:07:17 -07:00
parent 0111a1b06d
commit bddd15f7d1
11 changed files with 438 additions and 47 deletions

View File

@@ -21,6 +21,8 @@ type StoryView struct {
ArticleURL string
Source string
SeenAt time.Time
Posted bool
Channel string // channel slug; also the theme key
}
func toView(s storage.Story) StoryView {
@@ -31,6 +33,8 @@ func toView(s storage.Story) StoryView {
ArticleURL: s.ArticleURL,
Source: s.Source,
SeenAt: time.Unix(s.SeenAt, 0),
Posted: s.Posted,
Channel: s.Channel,
}
}
@@ -54,12 +58,17 @@ type channelPage struct {
type indexPage struct {
pageData
Sections []indexSection
JustPosted []StoryView
Stats []channelStat
Latest []StoryView
}
type indexSection struct {
Channel Channel
Stories []StoryView // top 3 per channel
type channelStat struct {
Channel Channel
LastPostedAt time.Time // zero if never posted
HasLastPosted bool
PostsToday int
Total int
}
func (s *Server) base() pageData {
@@ -70,21 +79,56 @@ func (s *Server) base() pageData {
}
func (s *Server) handleIndex(w http.ResponseWriter, _ *http.Request) {
sections := make([]indexSection, 0, len(channels))
for _, ch := range channels {
rows, err := storage.ListClassifiedByChannel(ch.Slug, 4, 0)
if err != nil {
slog.Error("web: list failed", "channel", ch.Slug, "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
views := make([]StoryView, 0, len(rows))
for _, r := range rows {
views = append(views, toView(r))
}
sections = append(sections, indexSection{Channel: ch, Stories: views})
const (
justPostedLimit = 6
latestLimit = 16
)
postedRows, err := storage.ListRecentlyPosted(justPostedLimit)
if err != nil {
slog.Error("web: list recently posted failed", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
justPosted := make([]StoryView, 0, len(postedRows))
for _, r := range postedRows {
justPosted = append(justPosted, toView(r))
}
latestRows, err := storage.ListAllClassified(latestLimit, 0)
if err != nil {
slog.Error("web: list all classified failed", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
latest := make([]StoryView, 0, len(latestRows))
for _, r := range latestRows {
latest = append(latest, toView(r))
}
dayStart := time.Now().Add(-24 * time.Hour).Unix()
stats := make([]channelStat, 0, len(channels))
for _, ch := range channels {
total, _ := storage.CountClassifiedByChannel(ch.Slug)
lastTs := storage.GetLastPostTime(ch.Slug)
stat := channelStat{
Channel: ch,
PostsToday: storage.CountPostsInWindow(ch.Slug, dayStart),
Total: total,
}
if lastTs > 0 {
stat.LastPostedAt = time.Unix(lastTs, 0)
stat.HasLastPosted = true
}
stats = append(stats, stat)
}
data := indexPage{
pageData: s.base(),
JustPosted: justPosted,
Stats: stats,
Latest: latest,
}
data := indexPage{pageData: s.base(), Sections: sections}
s.render(w, "index", data)
}
@@ -141,6 +185,15 @@ func (s *Server) render(w http.ResponseWriter, page string, data any) {
}
var funcs = template.FuncMap{
"thumb": thumbURL,
"channel": func(slug string) Channel {
for _, ch := range channels {
if ch.Slug == slug {
return ch
}
}
return Channel{Slug: slug, Title: slug, Theme: slug}
},
"dict": func(pairs ...any) map[string]any {
m := make(map[string]any, len(pairs)/2)
for i := 0; i+1 < len(pairs); i += 2 {