refactor: simplify PWA handlers and fix review findings

Address code review findings from reuse, quality, and efficiency agents:

- Cache manifest JSON and service worker JS at init (was rebuilt per
  request with allocations and JSON encoding on every hit)
- Add ImagePathsByUser store method for targeted image cleanup (was
  loading 100k full fave objects just to read image_path)
- Add missing aria-label on privacy toggle in fave_list.html (inline
  copy had drifted from the partial — accessibility bug)
- Fix comment/function name mismatch in pwa.go
- Remove redundant user nil-check in handleShare (requireLogin guards)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-04-07 10:47:13 +02:00
commit e379039fe8
5 changed files with 54 additions and 37 deletions

View file

@ -218,12 +218,10 @@ func (h *Handler) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request)
}
// Delete user's images from disk before database deletion.
faves, _, _ := h.deps.Faves.ListByUser(id, 100000, 0)
for _, f := range faves {
if f.ImagePath != "" {
if delErr := image.Delete(h.deps.Config.UploadDir, f.ImagePath); delErr != nil {
slog.Error("image delete error", "fave_id", f.ID, "error", delErr)
}
imagePaths, _ := h.deps.Faves.ImagePathsByUser(id)
for _, p := range imagePaths {
if delErr := image.Delete(h.deps.Config.UploadDir, p); delErr != nil {
slog.Error("image delete error", "error", delErr)
}
}
if user.AvatarPath != "" {

View file

@ -32,14 +32,20 @@ type Deps struct {
type Handler struct {
deps Deps
rateLimiter *middleware.RateLimiter
// Cached PWA responses (computed once at init, never change).
manifestJSON []byte
swJS []byte
}
// New creates a new Handler with the given dependencies.
func New(deps Deps) *Handler {
return &Handler{
h := &Handler{
deps: deps,
rateLimiter: middleware.NewRateLimiter(deps.Config.RateLimit),
}
h.initPWACache()
return h
}
// RateLimiterCleanupLoop periodically evicts stale rate limiter entries.

View file

@ -14,8 +14,9 @@ import (
"kode.naiv.no/olemd/favoritter/web"
)
// handleManifest serves the Web App Manifest with dynamic BasePath injection.
func (h *Handler) handleManifest(w http.ResponseWriter, r *http.Request) {
// initPWACache pre-computes the manifest JSON and service worker JS
// so they can be served without per-request allocations.
func (h *Handler) initPWACache() {
bp := h.deps.Config.BasePath
manifest := map[string]any{
@ -42,44 +43,33 @@ func (h *Handler) handleManifest(w http.ResponseWriter, r *http.Request) {
},
},
}
h.manifestJSON, _ = json.Marshal(manifest)
w.Header().Set("Content-Type", "application/manifest+json")
json.NewEncoder(w).Encode(manifest)
staticFS, err := fs.Sub(web.StaticFS, "static")
if err == nil {
if data, err := fs.ReadFile(staticFS, "sw.js"); err == nil {
h.swJS = []byte(strings.ReplaceAll(string(data), "{{BASE_PATH}}", bp))
}
}
}
// handleServiceWorker serves the service worker JS from root scope.
// BasePath is injected via placeholder replacement so the SW can
// cache the correct static asset paths.
// handleManifest serves the pre-computed Web App Manifest.
func (h *Handler) handleManifest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json")
w.Write(h.manifestJSON)
}
// handleServiceWorker serves the pre-computed service worker JS.
func (h *Handler) handleServiceWorker(w http.ResponseWriter, r *http.Request) {
staticFS, err := fs.Sub(web.StaticFS, "static")
if err != nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
data, err := fs.ReadFile(staticFS, "sw.js")
if err != nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
content := strings.ReplaceAll(string(data), "{{BASE_PATH}}", h.deps.Config.BasePath)
w.Header().Set("Content-Type", "application/javascript")
w.Header().Set("Cache-Control", "no-cache")
w.Write([]byte(content))
w.Write(h.swJS)
}
// handleShare receives Android share intents via the Web Share Target API
// and redirects to the new-fave form with pre-filled values.
// Uses GET to avoid CSRF issues (Android cannot provide CSRF tokens).
func (h *Handler) handleShare(w http.ResponseWriter, r *http.Request) {
user := middleware.UserFromContext(r.Context())
if user == nil {
http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther)
return
}
sharedURL := r.URL.Query().Get("url")
sharedTitle := r.URL.Query().Get("title")
sharedText := r.URL.Query().Get("text")
@ -88,7 +78,6 @@ func (h *Handler) handleShare(w http.ResponseWriter, r *http.Request) {
if sharedURL == "" && sharedText != "" {
sharedURL = extractURL(sharedText)
if sharedURL != "" {
// Remove the URL from text so it's not duplicated.
sharedText = strings.TrimSpace(strings.Replace(sharedText, sharedURL, "", 1))
}
}
@ -96,7 +85,7 @@ func (h *Handler) handleShare(w http.ResponseWriter, r *http.Request) {
description := sharedTitle
if description == "" && sharedText != "" {
description = sharedText
sharedText = "" // Don't duplicate into notes.
sharedText = ""
}
target := h.deps.Config.BasePath + "/faves/new?"
@ -114,7 +103,7 @@ func (h *Handler) handleShare(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target+params.Encode(), http.StatusSeeOther)
}
// handleFaveNewWithPreFill shows the new fave form, optionally pre-filled
// handleFaveNewPreFill shows the new fave form, optionally pre-filled
// from query parameters (used by share target and bookmarklets).
func (h *Handler) handleFaveNewPreFill(w http.ResponseWriter, r *http.Request) {
user := middleware.UserFromContext(r.Context())