+9
-2
.golangci.yml
+9
-2
.golangci.yml
···
1
1
# golangci-lint configuration for ATCR
2
2
# See: https://golangci-lint.run/usage/configuration/
3
3
version: "2"
4
+
5
+
issues:
6
+
fix: true
7
+
4
8
linters:
5
9
settings:
6
10
staticcheck:
···
25
29
linters:
26
30
- errcheck
27
31
28
-
# TODO: fix issues and remove these paths one by one
29
-
30
32
formatters:
31
33
enable:
32
34
- gofmt
33
35
- goimports
36
+
settings:
37
+
gofmt:
38
+
rewrite-rules:
39
+
- pattern: 'interface{}'
40
+
replacement: 'any'
+24
-7
cmd/credential-helper/main.go
+24
-7
cmd/credential-helper/main.go
···
11
11
"os/exec"
12
12
"path/filepath"
13
13
"runtime"
14
+
"strconv"
14
15
"strings"
15
16
"time"
16
17
)
···
385
386
}
386
387
387
388
var tokenResult DeviceTokenResponse
388
-
json.NewDecoder(tokenResp.Body).Decode(&tokenResult)
389
+
if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResult); err != nil {
390
+
fmt.Fprintf(os.Stderr, "\nFailed to decode response: %v\n", err)
391
+
tokenResp.Body.Close()
392
+
continue
393
+
}
389
394
tokenResp.Body.Close()
390
395
391
396
if tokenResult.Error == "authorization_pending" {
···
767
772
// Compare each part
768
773
for i := range min(len(newParts), len(curParts)) {
769
774
newNum := 0
775
+
if parsed, err := strconv.Atoi(newParts[i]); err == nil {
776
+
newNum = parsed
777
+
}
770
778
curNum := 0
771
-
fmt.Sscanf(newParts[i], "%d", &newNum)
772
-
fmt.Sscanf(curParts[i], "%d", &curNum)
779
+
if parsed, err := strconv.Atoi(curParts[i]); err == nil {
780
+
curNum = parsed
781
+
}
773
782
774
783
if newNum > curNum {
775
784
return true
···
881
890
// Install new binary
882
891
if err := copyFile(binaryPath, currentPath); err != nil {
883
892
// Try to restore backup
884
-
os.Rename(backupPath, currentPath)
893
+
if renameErr := os.Rename(backupPath, currentPath); renameErr != nil {
894
+
fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr)
895
+
}
885
896
return fmt.Errorf("failed to install new binary: %w", err)
886
897
}
887
898
···
889
900
if err := os.Chmod(currentPath, 0755); err != nil {
890
901
// Try to restore backup
891
902
os.Remove(currentPath)
892
-
os.Rename(backupPath, currentPath)
903
+
if renameErr := os.Rename(backupPath, currentPath); renameErr != nil {
904
+
fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr)
905
+
}
893
906
return fmt.Errorf("failed to set permissions: %w", err)
894
907
}
895
908
···
1047
1060
1048
1061
// Ensure directory exists
1049
1062
dir := filepath.Dir(path)
1050
-
os.MkdirAll(dir, 0700)
1063
+
if err := os.MkdirAll(dir, 0700); err != nil {
1064
+
return
1065
+
}
1051
1066
1052
-
os.WriteFile(path, data, 0600)
1067
+
if err := os.WriteFile(path, data, 0600); err != nil {
1068
+
return // Cache write failed, non-critical
1069
+
}
1053
1070
}
+15
-13
pkg/appview/handlers/manifest_health.go
+15
-13
pkg/appview/handlers/manifest_health.go
···
2
2
3
3
import (
4
4
"context"
5
+
"html/template"
6
+
"log/slog"
5
7
"net/http"
6
8
"net/url"
7
9
"time"
···
12
14
// ManifestHealthHandler handles HTMX polling for manifest health status
13
15
type ManifestHealthHandler struct {
14
16
HealthChecker *holdhealth.Checker
17
+
Templates *template.Template
15
18
}
16
19
17
20
func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···
61
64
func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) {
62
65
w.Header().Set("Content-Type", "text/html")
63
66
64
-
if pending {
65
-
// Still checking - render badge with HTMX retry after 3 seconds
66
-
retryURL := "/api/manifest-health?endpoint=" + url.QueryEscape(endpoint)
67
-
w.Write([]byte(`<span class="checking-badge"
68
-
hx-get="` + retryURL + `"
69
-
hx-trigger="load delay:3s"
70
-
hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span>`))
71
-
} else if !reachable {
72
-
// Unreachable - render offline badge
73
-
w.Write([]byte(`<span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span>`))
74
-
} else {
75
-
// Reachable - no badge (empty response)
76
-
w.Write([]byte(``))
67
+
data := struct {
68
+
Pending bool
69
+
Reachable bool
70
+
RetryURL string
71
+
}{
72
+
Pending: pending,
73
+
Reachable: reachable,
74
+
RetryURL: url.QueryEscape(endpoint),
75
+
}
76
+
77
+
if err := h.Templates.ExecuteTemplate(w, "health-badge", data); err != nil {
78
+
slog.Warn("Failed to render health badge", "error", err)
77
79
}
78
80
}
+1
-3
pkg/appview/handlers/opengraph.go
+1
-3
pkg/appview/handlers/opengraph.go
···
211
211
}
212
212
213
213
// Draw package icon with description-sized text
214
-
if err := card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted); err != nil {
215
-
slog.Warn("Failed to draw package icon", "error", err)
216
-
}
214
+
card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted)
217
215
card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false)
218
216
219
217
// ATCR branding (bottom right)
+8
-1
pkg/appview/handlers/settings.go
+8
-1
pkg/appview/handlers/settings.go
···
73
73
// UpdateDefaultHoldHandler handles updating the default hold
74
74
type UpdateDefaultHoldHandler struct {
75
75
Refresher *oauth.Refresher
76
+
Templates *template.Template
76
77
}
77
78
78
79
func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···
105
106
}
106
107
107
108
w.Header().Set("Content-Type", "text/html")
108
-
w.Write([]byte(`<div class="success"><i data-lucide="check"></i> Default hold updated successfully!</div>`))
109
+
if err := h.Templates.ExecuteTemplate(w, "alert", map[string]string{
110
+
"Class": "success",
111
+
"Icon": "check",
112
+
"Message": "Default hold updated successfully!",
113
+
}); err != nil {
114
+
slog.Warn("Failed to render alert", "error", err)
115
+
}
109
116
}
+7
-5
pkg/appview/ogcard/card.go
+7
-5
pkg/appview/ogcard/card.go
···
9
9
_ "image/jpeg" // Register JPEG decoder for image.Decode
10
10
"image/png"
11
11
"io"
12
+
"log/slog"
12
13
"net/http"
13
14
"time"
14
15
···
118
119
draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over)
119
120
}
120
121
121
-
// DrawText draws text at the specified position
122
-
func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color, align int, bold bool) error {
122
+
// DrawText draws text at the specified position.
123
+
func (c *Card) DrawText(text string, x, y float64, size float64, col color.Color, align int, bold bool) {
123
124
f := regularFont
124
125
if bold {
125
126
f = boldFont
126
127
}
127
128
if f == nil {
128
-
return nil // No font loaded
129
+
return // No font loaded
129
130
}
130
131
131
132
ctx := freetype.NewContext()
···
152
153
}
153
154
154
155
pt := freetype.Pt(int(x), int(y))
155
-
_, err := ctx.DrawString(text, pt)
156
-
return err
156
+
if _, err := ctx.DrawString(text, pt); err != nil {
157
+
slog.Warn("Failed to draw text", "text", text, "error", err)
158
+
}
157
159
}
158
160
159
161
// MeasureText returns the width of text in pixels
+7
-6
pkg/appview/ogcard/icons.go
+7
-6
pkg/appview/ogcard/icons.go
···
6
6
"image"
7
7
"image/color"
8
8
"image/draw"
9
+
"log/slog"
9
10
"strings"
10
11
11
12
"github.com/srwiley/oksvg"
···
28
29
"package": `<path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`,
29
30
}
30
31
31
-
// DrawIcon draws a Lucide icon at the specified position with the given size and color
32
-
func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) error {
32
+
// DrawIcon draws a Lucide icon at the specified position with the given size and color.
33
+
func (c *Card) DrawIcon(name string, x, y, size int, col color.Color) {
33
34
path, ok := iconPaths[name]
34
35
if !ok {
35
-
return fmt.Errorf("unknown icon: %s", name)
36
+
slog.Warn("Unknown icon", "name", name)
37
+
return
36
38
}
37
39
38
40
// Build full SVG with color
···
45
47
// Parse SVG
46
48
icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg)))
47
49
if err != nil {
48
-
return fmt.Errorf("failed to parse icon SVG: %w", err)
50
+
slog.Warn("Failed to parse icon SVG", "name", name, "error", err)
51
+
return
49
52
}
50
53
51
54
// Create target image for the icon
···
63
66
// Draw icon onto card
64
67
rect := image.Rect(x, y, x+size, y+size)
65
68
draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over)
66
-
67
-
return nil
68
69
}
+2
pkg/appview/routes/routes.go
+2
pkg/appview/routes/routes.go
···
146
146
// Manifest health check API endpoint (HTMX polling)
147
147
router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{
148
148
HealthChecker: deps.HealthChecker,
149
+
Templates: deps.Templates,
149
150
}).ServeHTTP)
150
151
151
152
router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)(
···
196
197
197
198
r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{
198
199
Refresher: deps.Refresher,
200
+
Templates: deps.Templates,
199
201
}).ServeHTTP)
200
202
201
203
r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{
+3
pkg/appview/templates/partials/alert.html
+3
pkg/appview/templates/partials/alert.html
+10
pkg/appview/templates/partials/health-badge.html
+10
pkg/appview/templates/partials/health-badge.html
···
1
+
{{ define "health-badge" }}
2
+
{{ if .Pending }}
3
+
<span class="checking-badge"
4
+
hx-get="/api/manifest-health?endpoint={{ .RetryURL }}"
5
+
hx-trigger="load delay:3s"
6
+
hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span>
7
+
{{ else if not .Reachable }}
8
+
<span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span>
9
+
{{ end }}
10
+
{{ end }}
+91
pkg/appview/ui_test.go
+91
pkg/appview/ui_test.go
···
551
551
"install.html",
552
552
"manifest-modal",
553
553
"push-list.html",
554
+
"health-badge",
555
+
"alert",
554
556
}
555
557
556
558
for _, name := range expectedTemplates {
···
680
682
t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
681
683
}
682
684
})
685
+
}
686
+
}
687
+
688
+
func TestTemplateExecution_HealthBadge(t *testing.T) {
689
+
tmpl, err := Templates()
690
+
if err != nil {
691
+
t.Fatalf("Templates() error = %v", err)
692
+
}
693
+
694
+
tests := []struct {
695
+
name string
696
+
data map[string]any
697
+
expectInOutput string
698
+
expectMissing string
699
+
}{
700
+
{
701
+
name: "pending state",
702
+
data: map[string]any{
703
+
"Pending": true,
704
+
"Reachable": false,
705
+
"RetryURL": "http%3A%2F%2Fexample.com",
706
+
},
707
+
expectInOutput: "checking-badge",
708
+
expectMissing: "offline-badge",
709
+
},
710
+
{
711
+
name: "offline state",
712
+
data: map[string]any{
713
+
"Pending": false,
714
+
"Reachable": false,
715
+
"RetryURL": "",
716
+
},
717
+
expectInOutput: "offline-badge",
718
+
expectMissing: "checking-badge",
719
+
},
720
+
{
721
+
name: "online state - empty output",
722
+
data: map[string]any{
723
+
"Pending": false,
724
+
"Reachable": true,
725
+
"RetryURL": "",
726
+
},
727
+
expectMissing: "badge",
728
+
},
729
+
}
730
+
731
+
for _, tt := range tests {
732
+
t.Run(tt.name, func(t *testing.T) {
733
+
buf := new(bytes.Buffer)
734
+
err := tmpl.ExecuteTemplate(buf, "health-badge", tt.data)
735
+
if err != nil {
736
+
t.Fatalf("Failed to execute template: %v", err)
737
+
}
738
+
739
+
output := buf.String()
740
+
if tt.expectInOutput != "" && !strings.Contains(output, tt.expectInOutput) {
741
+
t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
742
+
}
743
+
if tt.expectMissing != "" && strings.Contains(output, tt.expectMissing) {
744
+
t.Errorf("Template output %q should not contain %q", output, tt.expectMissing)
745
+
}
746
+
})
747
+
}
748
+
}
749
+
750
+
func TestTemplateExecution_Alert(t *testing.T) {
751
+
tmpl, err := Templates()
752
+
if err != nil {
753
+
t.Fatalf("Templates() error = %v", err)
754
+
}
755
+
756
+
data := map[string]string{
757
+
"Class": "success",
758
+
"Icon": "check",
759
+
"Message": "Operation completed!",
760
+
}
761
+
762
+
buf := new(bytes.Buffer)
763
+
err = tmpl.ExecuteTemplate(buf, "alert", data)
764
+
if err != nil {
765
+
t.Fatalf("Failed to execute template: %v", err)
766
+
}
767
+
768
+
output := buf.String()
769
+
expectedParts := []string{"success", "check", "Operation completed!"}
770
+
for _, expected := range expectedParts {
771
+
if !strings.Contains(output, expected) {
772
+
t.Errorf("Template output %q does not contain expected %q", output, expected)
773
+
}
683
774
}
684
775
}
685
776
+1
-1
pkg/hold/admin/auth.go
+1
-1
pkg/hold/admin/auth.go
···
87
87
}
88
88
89
89
// renderTemplate renders a template with the given data
90
-
func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) {
90
+
func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data any) {
91
91
w.Header().Set("Content-Type", "text/html; charset=utf-8")
92
92
93
93
if err := ui.templates.ExecuteTemplate(w, name, data); err != nil {