From c865748ca59b2020a627979319df1eb768861981 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Thu, 20 Aug 2026 18:01:42 +0530 Subject: [PATCH] switch to Geist and cache help center pages Geist replaces Instrument Sans and is served from static/ instead of Google Fonts, so it works on installs with no internet access. Public help center pages are cached in Redis via fastcache with ETags, and any admin write clears the group so edits show up on the next load. --- cmd/handlers.go | 40 ++--- cmd/helpcenter.go | 98 ++++++++++++- cmd/helpcenter_cache_test.go | 138 ++++++++++++++++++ cmd/init.go | 7 + cmd/main.go | 3 + frontend/DESIGN.md | 2 +- frontend/apps/main/index.html | 6 +- .../sidebar/ConversationSideBar.vue | 10 +- frontend/apps/widget/index.html | 6 +- frontend/shared-ui/assets/styles/main.scss | 2 +- frontend/tailwind.config.cjs | 4 + go.mod | 4 +- go.sum | 91 +++++++++++- internal/helpcenter/helpcenter.go | 8 + internal/helpcenter/queries.sql | 7 + static/email-templates/base.html | 6 +- static/public/static/fonts.css | 17 +++ .../static/fonts/Geist-Italic-Variable.woff2 | Bin 0 -> 73320 bytes .../public/static/fonts/Geist-Variable.woff2 | Bin 0 -> 69760 bytes static/public/static/fonts/LICENSE.txt | 92 ++++++++++++ static/public/static/help-center-classic.css | 2 +- static/public/static/help-center-docs.css | 2 +- static/public/static/style.css | 4 +- static/public/web-templates/csat-widget.html | 5 +- .../web-templates/help/shared/head.html | 5 +- static/public/web-templates/index.html | 2 + 26 files changed, 500 insertions(+), 61 deletions(-) create mode 100644 cmd/helpcenter_cache_test.go create mode 100644 static/public/static/fonts.css create mode 100644 static/public/static/fonts/Geist-Italic-Variable.woff2 create mode 100644 static/public/static/fonts/Geist-Variable.woff2 create mode 100644 static/public/static/fonts/LICENSE.txt diff --git a/cmd/handlers.go b/cmd/handlers.go index 002b2704..c117d080 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -37,7 +37,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { // Settings. g.GET("/api/v1/settings/general", auth(handleGetGeneralSettings)) - g.PUT("/api/v1/settings/general", perm(handleUpdateGeneralSettings, "general_settings:manage")) + g.PUT("/api/v1/settings/general", perm(clearsHelpCenterCache(handleUpdateGeneralSettings), "general_settings:manage")) g.GET("/api/v1/settings/notifications/email", perm(handleGetEmailNotificationSettings, "notification_settings:manage")) g.PUT("/api/v1/settings/notifications/email", perm(handleUpdateEmailNotificationSettings, "notification_settings:manage")) @@ -295,24 +295,24 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.GET("/api/v1/help-centers/locales", perm(handleGetHelpCenterLocales, "help_center:manage")) g.GET("/api/v1/help-centers/{id}", perm(handleGetHelpCenter, "help_center:manage")) g.GET("/api/v1/help-centers/{id}/tree", perm(handleGetHelpCenterTree, "help_center:manage")) - g.POST("/api/v1/help-centers", perm(handleCreateHelpCenter, "help_center:manage")) - g.PUT("/api/v1/help-centers/{id}", perm(handleUpdateHelpCenter, "help_center:manage")) + g.POST("/api/v1/help-centers", perm(clearsHelpCenterCache(handleCreateHelpCenter), "help_center:manage")) + g.PUT("/api/v1/help-centers/{id}", perm(clearsHelpCenterCache(handleUpdateHelpCenter), "help_center:manage")) g.POST("/api/v1/help-centers/{id}/preview", perm(handleHelpCenterPreview, "help_center:manage")) - g.PUT("/api/v1/help-centers/{id}/toggle", perm(handleToggleHelpCenterActive, "help_center:manage")) - g.DELETE("/api/v1/help-centers/{id}", perm(handleDeleteHelpCenter, "help_center:manage")) + g.PUT("/api/v1/help-centers/{id}/toggle", perm(clearsHelpCenterCache(handleToggleHelpCenterActive), "help_center:manage")) + g.DELETE("/api/v1/help-centers/{id}", perm(clearsHelpCenterCache(handleDeleteHelpCenter), "help_center:manage")) g.GET("/api/v1/help-centers/{hc_id}/collections", perm(handleGetCollections, "help_center:manage")) - g.POST("/api/v1/help-centers/{hc_id}/collections", perm(handleCreateCollection, "help_center:manage")) - g.PUT("/api/v1/help-centers/{hc_id}/collections/{id}", perm(handleUpdateCollection, "help_center:manage")) - g.DELETE("/api/v1/help-centers/{hc_id}/collections/{id}", perm(handleDeleteCollection, "help_center:manage")) - g.PUT("/api/v1/help-centers/{hc_id}/collection-sort-order", perm(handleUpdateCollectionSortOrders, "help_center:manage")) - g.PUT("/api/v1/collections/{id}/toggle", perm(handleToggleCollection, "help_center:manage")) - g.PUT("/api/v1/collections/{col_id}/article-sort-order", perm(handleUpdateArticleSortOrders, "help_center:manage")) + g.POST("/api/v1/help-centers/{hc_id}/collections", perm(clearsHelpCenterCache(handleCreateCollection), "help_center:manage")) + g.PUT("/api/v1/help-centers/{hc_id}/collections/{id}", perm(clearsHelpCenterCache(handleUpdateCollection), "help_center:manage")) + g.DELETE("/api/v1/help-centers/{hc_id}/collections/{id}", perm(clearsHelpCenterCache(handleDeleteCollection), "help_center:manage")) + g.PUT("/api/v1/help-centers/{hc_id}/collection-sort-order", perm(clearsHelpCenterCache(handleUpdateCollectionSortOrders), "help_center:manage")) + g.PUT("/api/v1/collections/{id}/toggle", perm(clearsHelpCenterCache(handleToggleCollection), "help_center:manage")) + g.PUT("/api/v1/collections/{col_id}/article-sort-order", perm(clearsHelpCenterCache(handleUpdateArticleSortOrders), "help_center:manage")) g.GET("/api/v1/collections/{col_id}/articles/{id}", perm(handleGetArticle, "help_center:manage")) - g.POST("/api/v1/collections/{col_id}/articles", perm(handleCreateArticle, "help_center:manage")) - g.DELETE("/api/v1/collections/{col_id}/articles/{id}", perm(handleDeleteArticle, "help_center:manage")) - g.PUT("/api/v1/articles/{id}", perm(handleUpdateArticle, "help_center:manage")) - g.PUT("/api/v1/articles/{id}/collection", perm(handleMoveArticle, "help_center:manage")) - g.PUT("/api/v1/articles/{id}/status", perm(handleUpdateArticleStatus, "help_center:manage")) + g.POST("/api/v1/collections/{col_id}/articles", perm(clearsHelpCenterCache(handleCreateArticle), "help_center:manage")) + g.DELETE("/api/v1/collections/{col_id}/articles/{id}", perm(clearsHelpCenterCache(handleDeleteArticle), "help_center:manage")) + g.PUT("/api/v1/articles/{id}", perm(clearsHelpCenterCache(handleUpdateArticle), "help_center:manage")) + g.PUT("/api/v1/articles/{id}/collection", perm(clearsHelpCenterCache(handleMoveArticle), "help_center:manage")) + g.PUT("/api/v1/articles/{id}/status", perm(clearsHelpCenterCache(handleUpdateArticleStatus), "help_center:manage")) g.GET("/api/v1/help-centers/{id}/insights", perm(handleGetHelpCenterInsights, "help_center:manage")) // Public help center JSON API. @@ -393,11 +393,11 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { getAndHead("/robots.txt", rateLimit(handleRobotsTxt, "public")) getAndHead("/sitemap.xml", rateLimit(handleSitemapIndex, "public")) getAndHead("/hc/{slug}", rateLimit(handleRedirectHelpCenterHome, "public")) - getAndHead("/hc/{slug}/{locale}", rateLimit(handleShowHelpCenterHome, "public")) + getAndHead("/hc/{slug}/{locale}", rateLimit(cachedHelpCenterPage(handleShowHelpCenterHome), "public")) getAndHead("/hc/{slug}/{locale}/sitemap.xml", rateLimit(handleHelpCenterSitemap, "public")) - getAndHead("/hc/{slug}/{locale}/search", rateLimit(handleHelpCenterSearch, "public")) - getAndHead("/hc/{slug}/{locale}/collections/{collection_slug}", rateLimit(handleShowHelpCenterCollection, "public")) - getAndHead("/hc/{slug}/{locale}/articles/{article_slug}", rateLimit(handleShowHelpCenterArticle, "public")) + getAndHead("/hc/{slug}/{locale}/search", rateLimit(cachedHelpCenterPage(handleHelpCenterSearch), "public")) + getAndHead("/hc/{slug}/{locale}/collections/{collection_slug}", rateLimit(cachedHelpCenterPage(handleShowHelpCenterCollection), "public")) + getAndHead("/hc/{slug}/{locale}/articles/{article_slug}", rateLimit(countArticleView(cachedHelpCenterPage(handleShowHelpCenterArticle)), "public")) g.GET("/csat/{uuid}", rateLimit(handleShowCSAT, "public")) g.GET("/csat/{uuid}/widget", rateLimit(handleShowCSATWidget, "public")) diff --git a/cmd/helpcenter.go b/cmd/helpcenter.go index 3d1e0d74..cadf4434 100644 --- a/cmd/helpcenter.go +++ b/cmd/helpcenter.go @@ -6,6 +6,7 @@ import ( "fmt" "hash/fnv" "html/template" + "io" "log" "math" "net" @@ -25,6 +26,7 @@ import ( realip "github.com/ferluci/fast-realip" "github.com/knadh/stuffbin" "github.com/valyala/fasthttp" + "github.com/zerodha/fastcache/v4" "github.com/zerodha/fastglue" ) @@ -43,7 +45,15 @@ const ( sitemapDate = "2006-01-02" schemaOrgContext = "https://schema.org" - helpCenterCacheControl = "public, max-age=300, stale-while-revalidate=3600" + helpCenterCacheControl = "public, no-cache" + + fastCachePrefix = "libredesk:cache:" + + helpCenterCacheTTL = 24 * time.Hour + + helpCenterCacheGroup = "helpcenter" + + helpCenterXMLCacheControl = "public, max-age=300, stale-while-revalidate=3600" // articleFeedbackDedupTTL is how long a reader IP's vote on an article suppresses further votes. articleFeedbackDedupTTL = 24 * time.Hour @@ -60,6 +70,16 @@ const ( ) var ( + // Logger is set because fastcache writes to the field when it finds it nil, racing across requests. + helpCenterCacheOpts = &fastcache.Options{ + NamespaceKey: "slug", + ETag: true, + IncludeQueryString: true, + // Backstop for keys nothing invalidates, such as a deleted help center's. + TTL: helpCenterCacheTTL, + Logger: log.New(io.Discard, "", 0), + } + // crawlerUARe matches search and preview bots, whose hits must not count as reader views. crawlerUARe = regexp.MustCompile(`(?i)bot\b|bot/|crawler|spider|crawling|slurp|facebookexternalhit|preview|headlesschrome|lighthouse|feedfetcher|python-requests|curl/|wget/`) @@ -689,9 +709,6 @@ func handleShowHelpCenterArticle(r *fastglue.Request) error { } // The JSON-LD embeds the author too, not just the byline. hideArticleAuthor(helpCenterTheme(helpCenter), &article) - if !isCrawler(r) { - app.helpcenter.IncrementArticleViewCount(article.ID) - } if markdown { r.RequestCtx.Response.Header.Set("X-Robots-Tag", noIndexHeader) @@ -1055,11 +1072,13 @@ func handleUpdateArticle(r *fastglue.Request) error { // no-store default that RenderWebPage applies for the app's authenticated pages. func renderHelpCenterPage(r *fastglue.Request, name string, data map[string]interface{}) error { app := r.Context.(*App) - err := app.tmpl.RenderWebPage(r.RequestCtx, name, data) + if err := app.tmpl.RenderWebPage(r.RequestCtx, name, data); err != nil { + return err + } r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) r.RequestCtx.Response.Header.Del("Pragma") r.RequestCtx.Response.Header.Del("Expires") - return err + return nil } // sendXML writes v as an XML document. @@ -1069,12 +1088,77 @@ func sendXML(r *fastglue.Request, v any) error { return sendErrorEnvelope(r, err) } r.RequestCtx.SetContentType("application/xml; charset=utf-8") - r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) + r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterXMLCacheControl) fmt.Fprint(r.RequestCtx, xml.Header) r.RequestCtx.Write(out) return nil } +// countArticleView records a view after the page is served, counting cache hits too. +func countArticleView(h fastglue.FastRequestHandler) fastglue.FastRequestHandler { + return func(r *fastglue.Request) error { + err := h(r) + switch r.RequestCtx.Response.StatusCode() { + case fasthttp.StatusOK, fasthttp.StatusNotModified: + if !isCrawler(r) { + app := r.Context.(*App) + slug, _ := r.RequestCtx.UserValue("slug").(string) + articleSlug, _ := r.RequestCtx.UserValue("article_slug").(string) + locale, _ := r.RequestCtx.UserValue("locale").(string) + app.helpcenter.IncrementPublishedArticleView(slug, strings.TrimSuffix(articleSlug, markdownSlugExtension), locale) + } + } + return err + } +} + +// cachedHelpCenterPage serves a public help center page from the response cache, which +// stores the body alone, so the caching headers are re-applied on the hit and 304 paths. +func cachedHelpCenterPage(h fastglue.FastRequestHandler) fastglue.FastRequestHandler { + return func(r *fastglue.Request) error { + app := r.Context.(*App) + err := app.fastCache.Cached(h, helpCenterCacheOpts, helpCenterCacheGroup)(r) + switch r.RequestCtx.Response.StatusCode() { + case fasthttp.StatusOK, fasthttp.StatusNotModified: + r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) + r.RequestCtx.Response.Header.Del("Pragma") + r.RequestCtx.Response.Header.Del("Expires") + } + return err + } +} + +// clearsHelpCenterCache drops the cached public pages after a successful write. +func clearsHelpCenterCache(h fastglue.FastRequestHandler) fastglue.FastRequestHandler { + return func(r *fastglue.Request) error { + err := h(r) + if r.RequestCtx.Response.StatusCode() < fasthttp.StatusMultipleChoices { + clearHelpCenterCache(r.Context.(*App), "") + } + return err + } +} + +// clearHelpCenterCache drops every cached public page for slug, or for all when slug is empty. +func clearHelpCenterCache(app *App, slug string) { + if slug != "" { + if err := app.fastCache.DelGroup(slug, helpCenterCacheGroup); err != nil { + app.lo.Error("error clearing help center cache", "slug", slug, "error", err) + } + return + } + helpCenters, err := app.helpcenter.GetAllHelpCenters() + if err != nil { + app.lo.Error("error listing help centers to clear cache", "error", err) + return + } + for _, hc := range helpCenters { + if err := app.fastCache.DelGroup(hc.Slug, helpCenterCacheGroup); err != nil { + app.lo.Error("error clearing help center cache", "slug", hc.Slug, "error", err) + } + } +} + // isCrawler reports whether the request came from a bot rather than a reader; HEAD probes never count as readers. func isCrawler(r *fastglue.Request) bool { return r.RequestCtx.IsHead() || crawlerUARe.Match(r.RequestCtx.Request.Header.UserAgent()) diff --git a/cmd/helpcenter_cache_test.go b/cmd/helpcenter_cache_test.go new file mode 100644 index 00000000..874dfef2 --- /dev/null +++ b/cmd/helpcenter_cache_test.go @@ -0,0 +1,138 @@ +package main + +import ( + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/redis/go-redis/v9" + "github.com/valyala/fasthttp" + goredis "github.com/zerodha/fastcache/stores/goredis/v9" + "github.com/zerodha/fastcache/v4" + "github.com/zerodha/fastglue" +) + +func TestHelpCenterPageCacheServesAndInvalidates(t *testing.T) { + fc := newTestFastCache(t) + + renders := 0 + body := "

original

" + handler := func(r *fastglue.Request) error { + renders++ + r.RequestCtx.SetContentType("text/html; charset=utf-8") + r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) + r.RequestCtx.Response.SetBodyString(body) + return nil + } + cached := fc.Cached(handler, helpCenterCacheOpts, helpCenterCacheGroup) + + const uri = "/hc/support/en/articles/refunds" + + first := request(uri, "support", "") + if err := cached(first); err != nil { + t.Fatalf("first request: %v", err) + } + if renders != 1 { + t.Fatalf("first request should render, renders = %d", renders) + } + etag := string(first.RequestCtx.Response.Header.Peek("ETag")) + if etag == "" { + t.Fatal("expected an ETag on the rendered page") + } + + second := request(uri, "support", "") + if err := cached(second); err != nil { + t.Fatalf("second request: %v", err) + } + if renders != 1 { + t.Errorf("second request re-rendered instead of using the cache, renders = %d", renders) + } + if got := string(second.RequestCtx.Response.Body()); got != body { + t.Errorf("cached body = %q, want %q", got, body) + } + + revalidate := request(uri, "support", etag) + if err := cached(revalidate); err != nil { + t.Fatalf("revalidation: %v", err) + } + if got := revalidate.RequestCtx.Response.StatusCode(); got != fasthttp.StatusNotModified { + t.Errorf("matching ETag returned %d, want 304", got) + } + if got := revalidate.RequestCtx.Response.Body(); len(got) != 0 { + t.Errorf("304 carried a body of %d bytes", len(got)) + } + + // An admin write clears the group, so the next read must re-render the edit. + if err := fc.DelGroup("support", helpCenterCacheGroup); err != nil { + t.Fatalf("clearing the group: %v", err) + } + body = "

edited

" + afterEdit := request(uri, "support", "") + if err := cached(afterEdit); err != nil { + t.Fatalf("request after edit: %v", err) + } + if renders != 2 { + t.Errorf("cleared cache did not re-render, renders = %d", renders) + } + if got := string(afterEdit.RequestCtx.Response.Body()); got != body { + t.Errorf("stale body served after an edit: got %q, want %q", got, body) + } + if newETag := string(afterEdit.RequestCtx.Response.Header.Peek("ETag")); newETag == etag { + t.Error("ETag survived an edit, readers holding the old copy would never refetch") + } +} + +func TestHelpCenterPageCacheIsolatesHelpCentersAndQueries(t *testing.T) { + fc := newTestFastCache(t) + + served := map[string]int{} + handler := func(r *fastglue.Request) error { + uri := string(r.RequestCtx.URI().RequestURI()) + served[uri]++ + r.RequestCtx.Response.SetBodyString(uri) + return nil + } + cached := fc.Cached(handler, helpCenterCacheOpts, helpCenterCacheGroup) + + for _, req := range []*fastglue.Request{ + request("/hc/support/en/search?q=refund", "support", ""), + request("/hc/support/en/search?q=billing", "support", ""), + request("/hc/docs/en/search?q=refund", "docs", ""), + } { + if err := cached(req); err != nil { + t.Fatalf("request: %v", err) + } + } + if len(served) != 3 { + t.Fatalf("expected 3 distinct cache entries, got %d: %v", len(served), served) + } + + // Clearing one help center must not touch another's pages. + if err := fc.DelGroup("support", helpCenterCacheGroup); err != nil { + t.Fatalf("clearing the group: %v", err) + } + if err := cached(request("/hc/docs/en/search?q=refund", "docs", "")); err != nil { + t.Fatalf("request: %v", err) + } + if got := served["/hc/docs/en/search?q=refund"]; got != 1 { + t.Errorf("clearing 'support' evicted 'docs', renders = %d", got) + } +} + +// newTestFastCache returns a cache backed by an in-memory redis. +func newTestFastCache(t *testing.T) *fastcache.FastCache { + t.Helper() + mr := miniredis.RunT(t) + return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, redis.NewClient(&redis.Options{Addr: mr.Addr()}))) +} + +// request builds a GET for the given URI with the slug namespace the options read. +func request(uri, slug, ifNoneMatch string) *fastglue.Request { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI(uri) + ctx.SetUserValue("slug", slug) + if ifNoneMatch != "" { + ctx.Request.Header.Set("If-None-Match", ifNoneMatch) + } + return &fastglue.Request{RequestCtx: ctx} +} diff --git a/cmd/init.go b/cmd/init.go index d4e21772..d717ac78 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -70,6 +70,8 @@ import ( "github.com/redis/go-redis/v9" flag "github.com/spf13/pflag" "github.com/volatiletech/null/v9" + goredis "github.com/zerodha/fastcache/stores/goredis/v9" + "github.com/zerodha/fastcache/v4" "github.com/zerodha/logf" ) @@ -940,6 +942,11 @@ func initRedis() *redis.Client { }) } +// initFastCache inits the shared HTTP response cache. +func initFastCache(rdb *redis.Client) *fastcache.FastCache { + return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, rdb)) +} + // initRedis inits postgres DB. func initDB() *sqlx.DB { dsn := fmt.Sprintf( diff --git a/cmd/main.go b/cmd/main.go index 95f5a1ac..98520aee 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -57,6 +57,7 @@ import ( "github.com/knadh/koanf/v2" "github.com/knadh/stuffbin" "github.com/valyala/fasthttp" + "github.com/zerodha/fastcache/v4" "github.com/zerodha/fastglue" "github.com/zerodha/logf" ) @@ -128,6 +129,7 @@ type App struct { contextLink *contextlink.Manager rateLimit *ratelimit.Limiter redis *redis.Client + fastCache *fastcache.FastCache importer *importer.Importer wsHub *ws.Hub @@ -322,6 +324,7 @@ func main() { contextLink: initContextLink(db, i18n), rateLimit: rateLimiter, redis: rdb, + fastCache: initFastCache(rdb), userNotification: userNotification, wsHub: wsHub, } diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 036e7cfa..06e10dbb 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -92,7 +92,7 @@ In light mode `card` is pure white against a near-white background. In dark mode ## 3. Typography -Font: Instrument Sans. Sizes: `text-xs` 12 · `text-sm` 14 · `text-base` 16 · `text-lg` 18 · +Font: Geist. Sizes: `text-xs` 12 · `text-sm` 14 · `text-base` 16 · `text-lg` 18 · `text-xl` 20 · `text-2xl` 24 · `text-3xl` 30. Weights: 400 body · 500 labels · 600 headings · 700 rare emphasis. diff --git a/frontend/apps/main/index.html b/frontend/apps/main/index.html index 9361f948..781f17db 100644 --- a/frontend/apps/main/index.html +++ b/frontend/apps/main/index.html @@ -5,10 +5,8 @@ - - - + + diff --git a/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue b/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue index 5df27d1c..52c5fa83 100644 --- a/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue +++ b/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue @@ -18,7 +18,7 @@
{ const priorityOptions = computed(() => conversationStore.priorityOptions) +const agentOptions = computed(() => { + const none = { value: 'none', label: t('globals.terms.none') } + const isMe = (option) => String(option.value) === String(userStore.userID) + const me = usersStore.options.find(isMe) + if (!me) return [none, ...usersStore.options] + return [none, me, ...usersStore.options.filter((option) => !isMe(option))] +}) + const fetchTags = async () => { await tagStore.fetchTags() tags.value = tagStore.tags.map((item) => item.name) diff --git a/frontend/apps/widget/index.html b/frontend/apps/widget/index.html index 59fe1f2b..66ac7efe 100644 --- a/frontend/apps/widget/index.html +++ b/frontend/apps/widget/index.html @@ -6,10 +6,8 @@ libredesk Chat Widget - - - + + diff --git a/frontend/shared-ui/assets/styles/main.scss b/frontend/shared-ui/assets/styles/main.scss index db785c53..125a12e2 100644 --- a/frontend/shared-ui/assets/styles/main.scss +++ b/frontend/shared-ui/assets/styles/main.scss @@ -22,7 +22,7 @@ @layer base { html, body { - font-family: 'Instrument Sans', sans-serif; + @apply font-sans; min-height: 100%; overflow: hidden; margin: 0; diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index f0da9695..3c8aef7a 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -1,6 +1,7 @@ const animate = require("tailwindcss-animate") const typography = require("@tailwindcss/typography") const plugin = require("tailwindcss/plugin") +const defaultTheme = require("tailwindcss/defaultTheme") const canHover = plugin(({ addVariant }) => addVariant("can-hover", "@media (hover: hover)")) @@ -26,6 +27,9 @@ module.exports = { } }, extend: { + fontFamily: { + sans: ['Geist', ...defaultTheme.fontFamily.sans] + }, height: { screen: '100dvh' }, diff --git a/go.mod b/go.mod index efcd19f1..1855f052 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/abhinavxd/ssrfguard v0.1.0 - github.com/alicebob/miniredis/v2 v2.32.1 + github.com/alicebob/miniredis/v2 v2.33.0 github.com/coreos/go-oidc/v3 v3.11.0 github.com/disintegration/imaging v1.6.2 github.com/emersion/go-imap/v2 v2.0.0-beta.3 @@ -42,6 +42,8 @@ require ( github.com/valyala/fasthttp v1.62.0 github.com/volatiletech/null/v9 v9.0.0 github.com/yuin/goldmark v1.8.4 + github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0 + github.com/zerodha/fastcache/v4 v4.2.1 github.com/zerodha/fastglue v1.8.0 github.com/zerodha/logf v0.5.5 github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0 diff --git a/go.sum b/go.sum index 8a694217..ad4cc84a 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,19 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/abhinavxd/ssrfguard v0.1.0 h1:Ns/llAQ63uGFehxSvhCd+WGDKmBEEmIH+E1AW1CGgWM= github.com/abhinavxd/ssrfguard v0.1.0/go.mod h1:eNVubb+m/r3KrKWYdG6hxzeAfj+t2ZmZss4V/x7D6Ws= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= -github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw= +github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= +github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -19,19 +24,26 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -39,8 +51,16 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emersion/go-imap/v2 v2.0.0-beta.3 h1:z0TLMfYnDsFupXLhzRXgOzXenD3uPvNniQSu5fN1teg= github.com/emersion/go-imap/v2 v2.0.0-beta.3/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= @@ -54,6 +74,8 @@ github.com/fasthttp/websocket v1.5.9 h1:9deGuzYcCRKjk940kNwSN6Hd14hk4zYwropm4UsU github.com/fasthttp/websocket v1.5.9/go.mod h1:NLzHBFur260OMuZHohOfYQwMTpR7sfSpUnuqKxMpgKA= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferluci/fast-realip v1.0.1 h1:zPi0iv7zgOOlM/qJt9mozLz5IjVRAezaqHJoQ0JJ/yI= github.com/ferluci/fast-realip v1.0.1/go.mod h1:Ag7xdRQ9GOCL/pwbDe4zJv6SlfYdROArc8O+qIKhRc4= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -62,6 +84,12 @@ github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -71,6 +99,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -131,8 +161,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mackee/go-readability v0.3.1 h1:DUwcwlhNLPtrBkGyJPcKp51oOKBvZvvMDPPFFLUIcKc= github.com/mackee/go-readability v0.3.1/go.mod h1:lfyLr0PJ+fQ+z6r6IBrexFxP4AoVsaDJAGvMcoJ4UAM= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= @@ -148,6 +182,18 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-karan/balance v0.0.0-20250317053523-d32c6ade6cf1 h1:YYLFUQMdeCyUVUAxE/IkPbkDfOVkUl9mbBdwkzU4UbE= github.com/mr-karan/balance v0.0.0-20250317053523-d32c6ade6cf1/go.mod h1:YMjMm+2l1ye+v1MeuUJ1QPxXKzWp+x8iqg4vWuKB3Ao= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -158,6 +204,10 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -168,6 +218,8 @@ github.com/pkoukk/tiktoken-go-loader v0.0.2 h1:LUKws63GV3pVHwH1srkBplBv+7URgmOmh github.com/pkoukk/tiktoken-go-loader v0.0.2/go.mod h1:4mIkYyZooFlnenDlormIo6cd5wrlUKNr97wp9nGgEKo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rhnvrm/simples3 v0.10.1 h1:V/EbG6tC3F5MS7Vw5D02qIbNJom2jU+FVtGdrmsD67o= @@ -175,6 +227,12 @@ github.com/rhnvrm/simples3 v0.10.1/go.mod h1:c2xW30bukipkBlWNnXG1wDjq3gykQ6ww2AB github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= @@ -185,6 +243,12 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= @@ -203,6 +267,12 @@ github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0 h1:DtP3AflFjGTyHjXgd3UxuFfvGqk0rmfzm9PRbuiYwjU= +github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0/go.mod h1:Pds6hwIUaduCzChTTS3JWKdXLhpdvjvF3r8rgB0huYk= +github.com/zerodha/fastcache/v4 v4.2.1 h1:MbkZv/lwSgAqoCnP+b023wxNlqHch0CU/B6RXaLgZKI= +github.com/zerodha/fastcache/v4 v4.2.1/go.mod h1:jfFLkiuyMIO8u7KlojdeLGaVyPuVXES0PRGxS7byr6s= github.com/zerodha/fastglue v1.8.0 h1:yCfb8YwZLoFrzHiojRcie19olLDT48vjuinVn1Ge5Uc= github.com/zerodha/fastglue v1.8.0/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= github.com/zerodha/logf v0.5.5 h1:AhxHlixHNYwhFjvlgTv6uO4VBKYKxx2I6SbHoHtWLBk= @@ -211,6 +281,14 @@ github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0 h1:wkFp+/IM1cXjmidprZw8 github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0/go.mod h1:HxUlesaeO/JuymHHoPQ+7GKVjrkCwEYiM/0+oyiaaDo= github.com/zerodha/simplesessions/v3 v3.0.0 h1:seHwxVNnlCbp5nG8GFxSsRUdiHnfb39QdEW3J536O9Y= github.com/zerodha/simplesessions/v3 v3.0.0/go.mod h1:lAK+CJmZRlbvfq+OnkB8Iyf6LWgjzvUuWYKX1XA51P0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -240,7 +318,6 @@ golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/helpcenter/helpcenter.go b/internal/helpcenter/helpcenter.go index 784e1440..a721faa7 100644 --- a/internal/helpcenter/helpcenter.go +++ b/internal/helpcenter/helpcenter.go @@ -203,6 +203,7 @@ type queries struct { GetPublishedArticlesByCollection *sqlx.Stmt `query:"get-published-articles-by-collection"` SearchPublishedArticles *sqlx.Stmt `query:"search-published-articles"` IncrementArticleViewCount *sqlx.Stmt `query:"increment-article-view-count"` + IncrementPublishedArticleView *sqlx.Stmt `query:"increment-published-article-view-count"` InsertArticleFeedback *sqlx.Stmt `query:"insert-article-feedback"` InsertSearchQuery *sqlx.Stmt `query:"insert-search-query"` @@ -957,6 +958,13 @@ func (m *Manager) IncrementArticleViewCount(id int) { } } +// IncrementPublishedArticleView increments a published article's view count by its URL parts. +func (m *Manager) IncrementPublishedArticleView(helpCenterSlug, articleSlug, locale string) { + if _, err := m.q.IncrementPublishedArticleView.Exec(helpCenterSlug, articleSlug, locale); err != nil { + m.lo.Error("error incrementing article view count", "error", err, "slug", articleSlug) + } +} + // RecordArticleFeedback stores a reader's helpful/not-helpful vote for a published article. func (m *Manager) RecordArticleFeedback(articleID int, isHelpful bool) error { if _, err := m.q.InsertArticleFeedback.Exec(articleID, isHelpful); err != nil { diff --git a/internal/helpcenter/queries.sql b/internal/helpcenter/queries.sql index 8aad9891..5469796f 100644 --- a/internal/helpcenter/queries.sql +++ b/internal/helpcenter/queries.sql @@ -424,6 +424,13 @@ UPDATE help_articles SET view_count = view_count + 1 WHERE id = $1; +-- name: increment-published-article-view-count +UPDATE help_articles a +SET view_count = view_count + 1 +FROM article_collections c, help_centers h +WHERE a.collection_id = c.id AND c.help_center_id = h.id + AND h.slug = $1 AND a.slug = $2 AND a.locale = $3 AND a.status = 'published'; + -- name: insert-article-feedback INSERT INTO help_article_feedback (article_id, is_helpful) SELECT $1, $2 diff --git a/static/email-templates/base.html b/static/email-templates/base.html index c22e6c47..5e8e40be 100644 --- a/static/email-templates/base.html +++ b/static/email-templates/base.html @@ -6,14 +6,10 @@ - - -