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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user