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 {