diff --git a/.golangci.yml b/.golangci.yml index 93a4d15..ee329ba 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,10 @@ # golangci-lint configuration for ATCR # See: https://golangci-lint.run/usage/configuration/ version: "2" + +issues: + fix: true + linters: settings: staticcheck: @@ -25,9 +29,12 @@ linters: linters: - errcheck - # TODO: fix issues and remove these paths one by one - formatters: enable: - gofmt - goimports + settings: + gofmt: + rewrite-rules: + - pattern: 'interface{}' + replacement: 'any' diff --git a/cmd/credential-helper/main.go b/cmd/credential-helper/main.go index 91c6559..e68e0ed 100644 --- a/cmd/credential-helper/main.go +++ b/cmd/credential-helper/main.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strconv" "strings" "time" ) @@ -385,7 +386,11 @@ func authorizeDevice(serverURL string) (*DeviceConfig, error) { } var tokenResult DeviceTokenResponse - json.NewDecoder(tokenResp.Body).Decode(&tokenResult) + if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResult); err != nil { + fmt.Fprintf(os.Stderr, "\nFailed to decode response: %v\n", err) + tokenResp.Body.Close() + continue + } tokenResp.Body.Close() if tokenResult.Error == "authorization_pending" { @@ -767,9 +772,13 @@ func isNewerVersion(newVersion, currentVersion string) bool { // Compare each part for i := range min(len(newParts), len(curParts)) { newNum := 0 + if parsed, err := strconv.Atoi(newParts[i]); err == nil { + newNum = parsed + } curNum := 0 - fmt.Sscanf(newParts[i], "%d", &newNum) - fmt.Sscanf(curParts[i], "%d", &curNum) + if parsed, err := strconv.Atoi(curParts[i]); err == nil { + curNum = parsed + } if newNum > curNum { return true @@ -881,7 +890,9 @@ func performUpdate(versionInfo *VersionAPIResponse) error { // Install new binary if err := copyFile(binaryPath, currentPath); err != nil { // Try to restore backup - os.Rename(backupPath, currentPath) + if renameErr := os.Rename(backupPath, currentPath); renameErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr) + } return fmt.Errorf("failed to install new binary: %w", err) } @@ -889,7 +900,9 @@ func performUpdate(versionInfo *VersionAPIResponse) error { if err := os.Chmod(currentPath, 0755); err != nil { // Try to restore backup os.Remove(currentPath) - os.Rename(backupPath, currentPath) + if renameErr := os.Rename(backupPath, currentPath); renameErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr) + } return fmt.Errorf("failed to set permissions: %w", err) } @@ -1047,7 +1060,11 @@ func saveUpdateCheckCache(cache *UpdateCheckCache) { // Ensure directory exists dir := filepath.Dir(path) - os.MkdirAll(dir, 0700) + if err := os.MkdirAll(dir, 0700); err != nil { + return + } - os.WriteFile(path, data, 0600) + if err := os.WriteFile(path, data, 0600); err != nil { + return // Cache write failed, non-critical + } } diff --git a/pkg/appview/handlers/manifest_health.go b/pkg/appview/handlers/manifest_health.go index c2b0850..808643f 100644 --- a/pkg/appview/handlers/manifest_health.go +++ b/pkg/appview/handlers/manifest_health.go @@ -2,6 +2,8 @@ package handlers import ( "context" + "html/template" + "log/slog" "net/http" "net/url" "time" @@ -12,6 +14,7 @@ import ( // ManifestHealthHandler handles HTMX polling for manifest health status type ManifestHealthHandler struct { HealthChecker *holdhealth.Checker + Templates *template.Template } func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -61,18 +64,17 @@ func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) { w.Header().Set("Content-Type", "text/html") - if pending { - // Still checking - render badge with HTMX retry after 3 seconds - retryURL := "/api/manifest-health?endpoint=" + url.QueryEscape(endpoint) - w.Write([]byte(` Checking...`)) - } else if !reachable { - // Unreachable - render offline badge - w.Write([]byte(` Offline`)) - } else { - // Reachable - no badge (empty response) - w.Write([]byte(``)) + data := struct { + Pending bool + Reachable bool + RetryURL string + }{ + Pending: pending, + Reachable: reachable, + RetryURL: url.QueryEscape(endpoint), + } + + if err := h.Templates.ExecuteTemplate(w, "health-badge", data); err != nil { + slog.Warn("Failed to render health badge", "error", err) } } diff --git a/pkg/appview/handlers/opengraph.go b/pkg/appview/handlers/opengraph.go index 68b8010..3d4d164 100644 --- a/pkg/appview/handlers/opengraph.go +++ b/pkg/appview/handlers/opengraph.go @@ -211,9 +211,7 @@ func (h *UserOGHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Draw package icon with description-sized text - if err := card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted); err != nil { - slog.Warn("Failed to draw package icon", "error", err) - } + card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted) card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false) // ATCR branding (bottom right) diff --git a/pkg/appview/handlers/settings.go b/pkg/appview/handlers/settings.go index 99f426e..81ef581 100644 --- a/pkg/appview/handlers/settings.go +++ b/pkg/appview/handlers/settings.go @@ -73,6 +73,7 @@ func (h *SettingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // UpdateDefaultHoldHandler handles updating the default hold type UpdateDefaultHoldHandler struct { Refresher *oauth.Refresher + Templates *template.Template } func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -105,5 +106,11 @@ func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ } w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`
Default hold updated successfully!
`)) + if err := h.Templates.ExecuteTemplate(w, "alert", map[string]string{ + "Class": "success", + "Icon": "check", + "Message": "Default hold updated successfully!", + }); err != nil { + slog.Warn("Failed to render alert", "error", err) + } } diff --git a/pkg/appview/ogcard/card.go b/pkg/appview/ogcard/card.go index dfc81f0..ac6e89c 100644 --- a/pkg/appview/ogcard/card.go +++ b/pkg/appview/ogcard/card.go @@ -9,6 +9,7 @@ import ( _ "image/jpeg" // Register JPEG decoder for image.Decode "image/png" "io" + "log/slog" "net/http" "time" @@ -118,14 +119,14 @@ func (c *Card) DrawRect(x, y, w, h int, col color.Color) { draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over) } -// DrawText draws text at the specified position -func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color, align int, bold bool) error { +// DrawText draws text at the specified position. +func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color, align int, bold bool) { f := regularFont if bold { f = boldFont } if f == nil { - return nil // No font loaded + return // No font loaded } ctx := freetype.NewContext() @@ -152,8 +153,9 @@ func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color } pt := freetype.Pt(int(x), int(y)) - _, err := ctx.DrawString(text, pt) - return err + if _, err := ctx.DrawString(text, pt); err != nil { + slog.Warn("Failed to draw text", "text", text, "error", err) + } } // MeasureText returns the width of text in pixels diff --git a/pkg/appview/ogcard/icons.go b/pkg/appview/ogcard/icons.go index 64d7f23..94d5f74 100644 --- a/pkg/appview/ogcard/icons.go +++ b/pkg/appview/ogcard/icons.go @@ -6,6 +6,7 @@ import ( "image" "image/color" "image/draw" + "log/slog" "strings" "github.com/srwiley/oksvg" @@ -28,11 +29,12 @@ var iconPaths = map[string]string{ "package": ``, } -// DrawIcon draws a Lucide icon at the specified position with the given size and color -func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) error { +// DrawIcon draws a Lucide icon at the specified position with the given size and color. +func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) { path, ok := iconPaths[name] if !ok { - return fmt.Errorf("unknown icon: %s", name) + slog.Warn("Unknown icon", "name", name) + return } // Build full SVG with color @@ -45,7 +47,8 @@ func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) error { // Parse SVG icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg))) if err != nil { - return fmt.Errorf("failed to parse icon SVG: %w", err) + slog.Warn("Failed to parse icon SVG", "name", name, "error", err) + return } // Create target image for the icon @@ -63,6 +66,4 @@ func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) error { // Draw icon onto card rect := image.Rect(x, y, x+size, y+size) draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over) - - return nil } diff --git a/pkg/appview/routes/routes.go b/pkg/appview/routes/routes.go index d56efec..6ebdce1 100644 --- a/pkg/appview/routes/routes.go +++ b/pkg/appview/routes/routes.go @@ -146,6 +146,7 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) { // Manifest health check API endpoint (HTMX polling) router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{ HealthChecker: deps.HealthChecker, + Templates: deps.Templates, }).ServeHTTP) router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( @@ -196,6 +197,7 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) { r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{ Refresher: deps.Refresher, + Templates: deps.Templates, }).ServeHTTP) r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{ diff --git a/pkg/appview/templates/partials/alert.html b/pkg/appview/templates/partials/alert.html new file mode 100644 index 0000000..49a3c3f --- /dev/null +++ b/pkg/appview/templates/partials/alert.html @@ -0,0 +1,3 @@ +{{ define "alert" }} +
{{ .Message }}
+{{ end }} diff --git a/pkg/appview/templates/partials/health-badge.html b/pkg/appview/templates/partials/health-badge.html new file mode 100644 index 0000000..94af249 --- /dev/null +++ b/pkg/appview/templates/partials/health-badge.html @@ -0,0 +1,10 @@ +{{ define "health-badge" }} +{{ if .Pending }} + Checking... +{{ else if not .Reachable }} + Offline +{{ end }} +{{ end }} diff --git a/pkg/appview/ui_test.go b/pkg/appview/ui_test.go index 191c422..eabee29 100644 --- a/pkg/appview/ui_test.go +++ b/pkg/appview/ui_test.go @@ -551,6 +551,8 @@ func TestTemplates(t *testing.T) { "install.html", "manifest-modal", "push-list.html", + "health-badge", + "alert", } for _, name := range expectedTemplates { @@ -683,6 +685,95 @@ func TestTemplateExecution_WithFuncMap(t *testing.T) { } } +func TestTemplateExecution_HealthBadge(t *testing.T) { + tmpl, err := Templates() + if err != nil { + t.Fatalf("Templates() error = %v", err) + } + + tests := []struct { + name string + data map[string]any + expectInOutput string + expectMissing string + }{ + { + name: "pending state", + data: map[string]any{ + "Pending": true, + "Reachable": false, + "RetryURL": "http%3A%2F%2Fexample.com", + }, + expectInOutput: "checking-badge", + expectMissing: "offline-badge", + }, + { + name: "offline state", + data: map[string]any{ + "Pending": false, + "Reachable": false, + "RetryURL": "", + }, + expectInOutput: "offline-badge", + expectMissing: "checking-badge", + }, + { + name: "online state - empty output", + data: map[string]any{ + "Pending": false, + "Reachable": true, + "RetryURL": "", + }, + expectMissing: "badge", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := new(bytes.Buffer) + err := tmpl.ExecuteTemplate(buf, "health-badge", tt.data) + if err != nil { + t.Fatalf("Failed to execute template: %v", err) + } + + output := buf.String() + if tt.expectInOutput != "" && !strings.Contains(output, tt.expectInOutput) { + t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput) + } + if tt.expectMissing != "" && strings.Contains(output, tt.expectMissing) { + t.Errorf("Template output %q should not contain %q", output, tt.expectMissing) + } + }) + } +} + +func TestTemplateExecution_Alert(t *testing.T) { + tmpl, err := Templates() + if err != nil { + t.Fatalf("Templates() error = %v", err) + } + + data := map[string]string{ + "Class": "success", + "Icon": "check", + "Message": "Operation completed!", + } + + buf := new(bytes.Buffer) + err = tmpl.ExecuteTemplate(buf, "alert", data) + if err != nil { + t.Fatalf("Failed to execute template: %v", err) + } + + output := buf.String() + expectedParts := []string{"success", "check", "Operation completed!"} + for _, expected := range expectedParts { + if !strings.Contains(output, expected) { + t.Errorf("Template output %q does not contain expected %q", output, expected) + } + } +} + func TestStaticHandler(t *testing.T) { handler := StaticHandler() if handler == nil { diff --git a/pkg/hold/admin/auth.go b/pkg/hold/admin/auth.go index e741157..49b1bd4 100644 --- a/pkg/hold/admin/auth.go +++ b/pkg/hold/admin/auth.go @@ -87,7 +87,7 @@ func (ui *AdminUI) newPageData(r *http.Request, title, activePage string) PageDa } // renderTemplate renders a template with the given data -func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) { +func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := ui.templates.ExecuteTemplate(w, name, data); err != nil {