nandi/gleanpublic Fork 0
867e4ef
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Remove read status from article recommendations and unify card templatesUnverified

Julien Robert committed 2026-05-17T23:29:01+02:00 Browse files
867e4ef parent: d2575c6
modified internal/cluster/scoring.go +1 -3
@@ -43,7 +43,6 @@ type ArticleRecommendation struct {
4343 Author string
4444 Summary string
4545 Published sql.NullTime
46- IsRead bool
4746 Score float64
4847 }
4948
@@ -488,7 +487,6 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
488487 SELECT a.id, a.title, COALESCE(a.url, ''), la.feed_url, COALESCE(f.title, ''),
489488 COALESCE(f.favicon_url, ''),
490489 COALESCE(a.author, ''), COALESCE(a.summary, ''), a.published,
491- COALESCE(rs.is_read, 0),
492490 COALESCE(la.like_signal, 0) * ?
493491 + COALESCE(sl.social, 0) * ?
494492 + COALESCE(cb.score, 0) * ?
@@ -520,7 +518,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
520518 for rows.Next() {
521519 rec := &ArticleRecommendation{}
522520 if err := rows.Scan(&rec.ArticleID, &rec.Title, &rec.URL, &rec.FeedURL, &rec.FeedTitle,
523- &rec.FaviconURL, &rec.Author, &rec.Summary, &rec.Published, &rec.IsRead, &rec.Score); err != nil {
521+ &rec.FaviconURL, &rec.Author, &rec.Summary, &rec.Published, &rec.Score); err != nil {
524522 return nil, err
525523 }
526524 recs = append(recs, rec)
@@ -43,7 +43,6 @@ type ArticleRecommendation struct {
43 Author string43 Author string
44 Summary string44 Summary string
45 Published sql.NullTime45 Published sql.NullTime
46- IsRead bool
47 Score float6446 Score float64
48 }47 }
49 48
@@ -488,7 +487,6 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
488 SELECT a.id, a.title, COALESCE(a.url, ''), la.feed_url, COALESCE(f.title, ''),487 SELECT a.id, a.title, COALESCE(a.url, ''), la.feed_url, COALESCE(f.title, ''),
489 COALESCE(f.favicon_url, ''),488 COALESCE(f.favicon_url, ''),
490 COALESCE(a.author, ''), COALESCE(a.summary, ''), a.published,489 COALESCE(a.author, ''), COALESCE(a.summary, ''), a.published,
491- COALESCE(rs.is_read, 0),
492 COALESCE(la.like_signal, 0) * ?490 COALESCE(la.like_signal, 0) * ?
493 + COALESCE(sl.social, 0) * ?491 + COALESCE(sl.social, 0) * ?
494 + COALESCE(cb.score, 0) * ?492 + COALESCE(cb.score, 0) * ?
@@ -520,7 +518,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
520 for rows.Next() {518 for rows.Next() {
521 rec := &ArticleRecommendation{}519 rec := &ArticleRecommendation{}
522 if err := rows.Scan(&rec.ArticleID, &rec.Title, &rec.URL, &rec.FeedURL, &rec.FeedTitle,520 if err := rows.Scan(&rec.ArticleID, &rec.Title, &rec.URL, &rec.FeedURL, &rec.FeedTitle,
523- &rec.FaviconURL, &rec.Author, &rec.Summary, &rec.Published, &rec.IsRead, &rec.Score); err != nil {521+ &rec.FaviconURL, &rec.Author, &rec.Summary, &rec.Published, &rec.Score); err != nil {
524 return nil, err522 return nil, err
525 }523 }
526 recs = append(recs, rec)524 recs = append(recs, rec)
modified internal/db/article.go +4 -0
@@ -42,6 +42,10 @@ type Article struct {
4242 // NavSuffix holds the query string appended to article detail links to preserve
4343 // listing context (feed scope, liked) for next-article navigation.
4444 NavSuffix string
45+ // DismissURL, when non-empty, shows a dismiss button that POSTs to this URL.
46+ DismissURL string
47+ DismissField string
48+ DismissValue string
4549 }
4650
4751 type ReadState struct {
@@ -42,6 +42,10 @@ type Article struct {
42 // NavSuffix holds the query string appended to article detail links to preserve42 // NavSuffix holds the query string appended to article detail links to preserve
43 // listing context (feed scope, liked) for next-article navigation.43 // listing context (feed scope, liked) for next-article navigation.
44 NavSuffix string44 NavSuffix string
45+ // DismissURL, when non-empty, shows a dismiss button that POSTs to this URL.
46+ DismissURL string
47+ DismissField string
48+ DismissValue string
45 }49 }
46 50
47 type ReadState struct {51 type ReadState struct {
modified internal/server/dashboard_handler.go +21 -1
@@ -2,6 +2,7 @@ package server
22
33 import (
44 "context"
5+ "database/sql"
56 "net/http"
67 "time"
78
@@ -103,6 +104,25 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
103104
104105 resolvePeopleHandles(ctx, peopleRecs)
105106
107+ articleRecArticles := make([]*db.Article, len(articleRecs))
108+ for i, rec := range articleRecs {
109+ articleRecArticles[i] = &db.Article{
110+ ID: rec.ArticleID,
111+ FeedURL: rec.FeedURL,
112+ FeedTitle: rec.FeedTitle,
113+ FeedFaviconURL: sql.NullString{String: rec.FaviconURL, Valid: rec.FaviconURL != ""},
114+ Title: rec.Title,
115+ URL: sql.NullString{String: rec.URL, Valid: rec.URL != ""},
116+ Author: sql.NullString{String: rec.Author, Valid: rec.Author != ""},
117+ Summary: sql.NullString{String: rec.Summary, Valid: rec.Summary != ""},
118+ Published: rec.Published,
119+ IsRead: sql.NullBool{Bool: false, Valid: true},
120+ DismissURL: "/recs/dismiss-article",
121+ DismissField: "article_url",
122+ DismissValue: rec.URL,
123+ }
124+ }
125+
106126 var impressions []feedback.Impression
107127 for _, rec := range articleRecs {
108128 impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL})
@@ -133,7 +153,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
133153 "SubscriptionCount": subCount,
134154 "UnreadCount": unreadCount,
135155 "Articles": articles,
136- "ArticleRecommendations": articleRecs,
156+ "ArticleRecommendations": articleRecArticles,
137157 "FeedRecommendations": feedRecs,
138158 "FollowedPeople": followedPeople,
139159 "DiscoverPeople": discoverPeople,
@@ -2,6 +2,7 @@ package server
2 2
3 import (3 import (
4 "context"4 "context"
5+ "database/sql"
5 "net/http"6 "net/http"
6 "time"7 "time"
7 8
@@ -103,6 +104,25 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
103 104
104 resolvePeopleHandles(ctx, peopleRecs)105 resolvePeopleHandles(ctx, peopleRecs)
105 106
107+ articleRecArticles := make([]*db.Article, len(articleRecs))
108+ for i, rec := range articleRecs {
109+ articleRecArticles[i] = &db.Article{
110+ ID: rec.ArticleID,
111+ FeedURL: rec.FeedURL,
112+ FeedTitle: rec.FeedTitle,
113+ FeedFaviconURL: sql.NullString{String: rec.FaviconURL, Valid: rec.FaviconURL != ""},
114+ Title: rec.Title,
115+ URL: sql.NullString{String: rec.URL, Valid: rec.URL != ""},
116+ Author: sql.NullString{String: rec.Author, Valid: rec.Author != ""},
117+ Summary: sql.NullString{String: rec.Summary, Valid: rec.Summary != ""},
118+ Published: rec.Published,
119+ IsRead: sql.NullBool{Bool: false, Valid: true},
120+ DismissURL: "/recs/dismiss-article",
121+ DismissField: "article_url",
122+ DismissValue: rec.URL,
123+ }
124+ }
125+
106 var impressions []feedback.Impression126 var impressions []feedback.Impression
107 for _, rec := range articleRecs {127 for _, rec := range articleRecs {
108 impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL})128 impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL})
@@ -133,7 +153,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
133 "SubscriptionCount": subCount,153 "SubscriptionCount": subCount,
134 "UnreadCount": unreadCount,154 "UnreadCount": unreadCount,
135 "Articles": articles,155 "Articles": articles,
136- "ArticleRecommendations": articleRecs,156+ "ArticleRecommendations": articleRecArticles,
137 "FeedRecommendations": feedRecs,157 "FeedRecommendations": feedRecs,
138 "FollowedPeople": followedPeople,158 "FollowedPeople": followedPeople,
139 "DiscoverPeople": discoverPeople,159 "DiscoverPeople": discoverPeople,
modified internal/tmpl/dashboard.html +2 -2
@@ -32,7 +32,7 @@
3232 <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2>
3333 <div class="space-y-3">
3434 {{range .ArticleRecommendations}}
35- {{template "recommendation-article-card.html" .}}
35+ {{template "article-card.html" .}}
3636 {{end}}
3737 </div>
3838 </div>
@@ -76,7 +76,7 @@
7676 <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2>
7777 <div class="space-y-3">
7878 {{range .ArticleRecommendations}}
79- {{template "recommendation-article-card.html" .}}
79+ {{template "article-card.html" .}}
8080 {{end}}
8181 </div>
8282 </div>
@@ -32,7 +32,7 @@
32 <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2>32 <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2>
33 <div class="space-y-3">33 <div class="space-y-3">
34 {{range .ArticleRecommendations}}34 {{range .ArticleRecommendations}}
35- {{template "recommendation-article-card.html" .}}35+ {{template "article-card.html" .}}
36 {{end}}36 {{end}}
37 </div>37 </div>
38 </div>38 </div>
@@ -76,7 +76,7 @@
76 <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2>76 <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2>
77 <div class="space-y-3">77 <div class="space-y-3">
78 {{range .ArticleRecommendations}}78 {{range .ArticleRecommendations}}
79- {{template "recommendation-article-card.html" .}}79+ {{template "article-card.html" .}}
80 {{end}}80 {{end}}
81 </div>81 </div>
82 </div>82 </div>
modified internal/tmpl/partials/article-card.html +8 -0
@@ -21,6 +21,14 @@
2121 <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
2222 <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span>
2323 </button>
24+ {{if .DismissURL}}
25+ <button hx-post="{{.DismissURL}}" hx-target="closest article" hx-swap="delete" title="Remove from recommendations"
26+ class="group inline-flex items-center justify-center gap-1 text-[10px] text-spot-text hover:text-spot-red hover:bg-spot-red/15 uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-hover transition w-full">
27+ <input type="hidden" name="{{.DismissField}}" value="{{.DismissValue}}">
28+ <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
29+ <span>Hide</span>
30+ </button>
31+ {{end}}
2432 </div>
2533 </div>
2634 <div class="annotate-form hidden mt-4 pt-4 border-t border-spot-divider">
@@ -21,6 +21,14 @@
21 <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>21 <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
22 <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span>22 <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span>
23 </button>23 </button>
24+ {{if .DismissURL}}
25+ <button hx-post="{{.DismissURL}}" hx-target="closest article" hx-swap="delete" title="Remove from recommendations"
26+ class="group inline-flex items-center justify-center gap-1 text-[10px] text-spot-text hover:text-spot-red hover:bg-spot-red/15 uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-hover transition w-full">
27+ <input type="hidden" name="{{.DismissField}}" value="{{.DismissValue}}">
28+ <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
29+ <span>Hide</span>
30+ </button>
31+ {{end}}
24 </div>32 </div>
25 </div>33 </div>
26 <div class="annotate-form hidden mt-4 pt-4 border-t border-spot-divider">34 <div class="annotate-form hidden mt-4 pt-4 border-t border-spot-divider">
deleted internal/tmpl/partials/recommendation-article-card.html +0 -27
deleted file mode 100644
@@ -1,27 +0,0 @@
1-{{define "recommendation-article-card.html"}}
2-<article data-article-url="{{.URL}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative {{if not .IsRead}}border-l border-spot-green/80{{end}}">
3- <div class="flex items-start justify-between gap-4">
4- <div class="min-w-0 flex-1">
5- <div class="flex items-start gap-2.5">
6- <a href="/articles/{{.ArticleID}}" class="{{if .IsRead}}font-semibold text-[15px]{{else}}font-bold text-[17px]{{end}} text-spot-text hover:text-spot-green transition leading-snug">{{.Title}}</a>
7- </div>
8- <div class="text-xs text-spot-secondary mt-2 flex items-center gap-1.5 flex-wrap">
9- {{if .FaviconURL}}{{template "favicon" dict "src" .FaviconURL "size" "w-3.5 h-3.5"}}{{end}}
10- <a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-text transition font-medium">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a>
11- {{if .Author}}<span class="text-spot-muted">&middot;</span><span>{{.Author}}</span>{{end}}
12- {{if .Published.Valid}}<span class="text-spot-muted">&middot;</span><span>{{.Published.Time.Format "Jan 2"}}</span>{{end}}
13- </div>
14- {{if .Summary}}<p class="text-sm text-spot-secondary mt-2.5 line-clamp-2 leading-relaxed">{{plainText .Summary}}</p>{{end}}
15- </div>
16- <div class="shrink-0 pt-0.5">
17- <button hx-post="/recs/dismiss-article" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}"
18- class="text-[10px] text-spot-muted hover:text-spot-text uppercase tracking-button transition flex items-center gap-1"
19- title="Not interested">
20- <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}">
21- <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
22- Dismiss
23- </button>
24- </div>
25- </div>
26-</article>
27-{{end}}
deleted file mode 100644
@@ -1,27 +0,0 @@
1-{{define "recommendation-article-card.html"}}
2-<article data-article-url="{{.URL}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative {{if not .IsRead}}border-l border-spot-green/80{{end}}">
3- <div class="flex items-start justify-between gap-4">
4- <div class="min-w-0 flex-1">
5- <div class="flex items-start gap-2.5">
6- <a href="/articles/{{.ArticleID}}" class="{{if .IsRead}}font-semibold text-[15px]{{else}}font-bold text-[17px]{{end}} text-spot-text hover:text-spot-green transition leading-snug">{{.Title}}</a>
7- </div>
8- <div class="text-xs text-spot-secondary mt-2 flex items-center gap-1.5 flex-wrap">
9- {{if .FaviconURL}}{{template "favicon" dict "src" .FaviconURL "size" "w-3.5 h-3.5"}}{{end}}
10- <a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-text transition font-medium">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a>
11- {{if .Author}}<span class="text-spot-muted">&middot;</span><span>{{.Author}}</span>{{end}}
12- {{if .Published.Valid}}<span class="text-spot-muted">&middot;</span><span>{{.Published.Time.Format "Jan 2"}}</span>{{end}}
13- </div>
14- {{if .Summary}}<p class="text-sm text-spot-secondary mt-2.5 line-clamp-2 leading-relaxed">{{plainText .Summary}}</p>{{end}}
15- </div>
16- <div class="shrink-0 pt-0.5">
17- <button hx-post="/recs/dismiss-article" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}"
18- class="text-[10px] text-spot-muted hover:text-spot-text uppercase tracking-button transition flex items-center gap-1"
19- title="Not interested">
20- <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}">
21- <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
22- Dismiss
23- </button>
24- </div>
25- </div>
26-</article>
27-{{end}}