+9
-2
.golangci.yml
+9
-2
.golangci.yml
···
1
# golangci-lint configuration for ATCR
2
# See: https://golangci-lint.run/usage/configuration/
3
version: "2"
4
linters:
5
settings:
6
staticcheck:
···
25
linters:
26
- errcheck
27
28
-
# TODO: fix issues and remove these paths one by one
29
-
30
formatters:
31
enable:
32
- gofmt
33
- goimports
···
1
# golangci-lint configuration for ATCR
2
# See: https://golangci-lint.run/usage/configuration/
3
version: "2"
4
+
5
+
issues:
6
+
fix: true
7
+
8
linters:
9
settings:
10
staticcheck:
···
29
linters:
30
- errcheck
31
32
formatters:
33
enable:
34
- gofmt
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
"os/exec"
12
"path/filepath"
13
"runtime"
14
"strings"
15
"time"
16
)
···
385
}
386
387
var tokenResult DeviceTokenResponse
388
-
json.NewDecoder(tokenResp.Body).Decode(&tokenResult)
389
tokenResp.Body.Close()
390
391
if tokenResult.Error == "authorization_pending" {
···
767
// Compare each part
768
for i := range min(len(newParts), len(curParts)) {
769
newNum := 0
770
curNum := 0
771
-
fmt.Sscanf(newParts[i], "%d", &newNum)
772
-
fmt.Sscanf(curParts[i], "%d", &curNum)
773
774
if newNum > curNum {
775
return true
···
881
// Install new binary
882
if err := copyFile(binaryPath, currentPath); err != nil {
883
// Try to restore backup
884
-
os.Rename(backupPath, currentPath)
885
return fmt.Errorf("failed to install new binary: %w", err)
886
}
887
···
889
if err := os.Chmod(currentPath, 0755); err != nil {
890
// Try to restore backup
891
os.Remove(currentPath)
892
-
os.Rename(backupPath, currentPath)
893
return fmt.Errorf("failed to set permissions: %w", err)
894
}
895
···
1047
1048
// Ensure directory exists
1049
dir := filepath.Dir(path)
1050
-
os.MkdirAll(dir, 0700)
1051
1052
-
os.WriteFile(path, data, 0600)
1053
}
···
11
"os/exec"
12
"path/filepath"
13
"runtime"
14
+
"strconv"
15
"strings"
16
"time"
17
)
···
386
}
387
388
var tokenResult DeviceTokenResponse
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
+
}
394
tokenResp.Body.Close()
395
396
if tokenResult.Error == "authorization_pending" {
···
772
// Compare each part
773
for i := range min(len(newParts), len(curParts)) {
774
newNum := 0
775
+
if parsed, err := strconv.Atoi(newParts[i]); err == nil {
776
+
newNum = parsed
777
+
}
778
curNum := 0
779
+
if parsed, err := strconv.Atoi(curParts[i]); err == nil {
780
+
curNum = parsed
781
+
}
782
783
if newNum > curNum {
784
return true
···
890
// Install new binary
891
if err := copyFile(binaryPath, currentPath); err != nil {
892
// Try to restore backup
893
+
if renameErr := os.Rename(backupPath, currentPath); renameErr != nil {
894
+
fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr)
895
+
}
896
return fmt.Errorf("failed to install new binary: %w", err)
897
}
898
···
900
if err := os.Chmod(currentPath, 0755); err != nil {
901
// Try to restore backup
902
os.Remove(currentPath)
903
+
if renameErr := os.Rename(backupPath, currentPath); renameErr != nil {
904
+
fmt.Fprintf(os.Stderr, "Warning: failed to restore backup: %v\n", renameErr)
905
+
}
906
return fmt.Errorf("failed to set permissions: %w", err)
907
}
908
···
1060
1061
// Ensure directory exists
1062
dir := filepath.Dir(path)
1063
+
if err := os.MkdirAll(dir, 0700); err != nil {
1064
+
return
1065
+
}
1066
1067
+
if err := os.WriteFile(path, data, 0600); err != nil {
1068
+
return // Cache write failed, non-critical
1069
+
}
1070
}
+15
-13
pkg/appview/handlers/manifest_health.go
+15
-13
pkg/appview/handlers/manifest_health.go
···
2
3
import (
4
"context"
5
"net/http"
6
"net/url"
7
"time"
···
12
// ManifestHealthHandler handles HTMX polling for manifest health status
13
type ManifestHealthHandler struct {
14
HealthChecker *holdhealth.Checker
15
}
16
17
func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···
61
func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) {
62
w.Header().Set("Content-Type", "text/html")
63
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(``))
77
}
78
}
···
2
3
import (
4
"context"
5
+
"html/template"
6
+
"log/slog"
7
"net/http"
8
"net/url"
9
"time"
···
14
// ManifestHealthHandler handles HTMX polling for manifest health status
15
type ManifestHealthHandler struct {
16
HealthChecker *holdhealth.Checker
17
+
Templates *template.Template
18
}
19
20
func (h *ManifestHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···
64
func (h *ManifestHealthHandler) renderBadge(w http.ResponseWriter, endpoint string, reachable, pending bool) {
65
w.Header().Set("Content-Type", "text/html")
66
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)
79
}
80
}
+1
-3
pkg/appview/handlers/opengraph.go
+1
-3
pkg/appview/handlers/opengraph.go
···
211
}
212
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
-
}
217
card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false)
218
219
// ATCR branding (bottom right)
···
211
}
212
213
// Draw package icon with description-sized text
214
+
card.DrawIcon("package", int(layout.TextX), int(textY)-int(ogcard.FontDescription), int(ogcard.FontDescription), ogcard.ColorMuted)
215
card.DrawText(repoText, layout.TextX+42, textY, ogcard.FontDescription, ogcard.ColorMuted, ogcard.AlignLeft, false)
216
217
// ATCR branding (bottom right)
+8
-1
pkg/appview/handlers/settings.go
+8
-1
pkg/appview/handlers/settings.go
···
73
// UpdateDefaultHoldHandler handles updating the default hold
74
type UpdateDefaultHoldHandler struct {
75
Refresher *oauth.Refresher
76
}
77
78
func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···
105
}
106
107
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
}
···
73
// UpdateDefaultHoldHandler handles updating the default hold
74
type UpdateDefaultHoldHandler struct {
75
Refresher *oauth.Refresher
76
+
Templates *template.Template
77
}
78
79
func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···
106
}
107
108
w.Header().Set("Content-Type", "text/html")
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
+
}
116
}
+7
-5
pkg/appview/ogcard/card.go
+7
-5
pkg/appview/ogcard/card.go
···
9
_ "image/jpeg" // Register JPEG decoder for image.Decode
10
"image/png"
11
"io"
12
"net/http"
13
"time"
14
···
118
draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over)
119
}
120
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 {
123
f := regularFont
124
if bold {
125
f = boldFont
126
}
127
if f == nil {
128
-
return nil // No font loaded
129
}
130
131
ctx := freetype.NewContext()
···
152
}
153
154
pt := freetype.Pt(int(x), int(y))
155
-
_, err := ctx.DrawString(text, pt)
156
-
return err
157
}
158
159
// MeasureText returns the width of text in pixels
···
9
_ "image/jpeg" // Register JPEG decoder for image.Decode
10
"image/png"
11
"io"
12
+
"log/slog"
13
"net/http"
14
"time"
15
···
119
draw.Draw(c.img, rect, &image.Uniform{col}, image.Point{}, draw.Over)
120
}
121
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) {
124
f := regularFont
125
if bold {
126
f = boldFont
127
}
128
if f == nil {
129
+
return // No font loaded
130
}
131
132
ctx := freetype.NewContext()
···
153
}
154
155
pt := freetype.Pt(int(x), int(y))
156
+
if _, err := ctx.DrawString(text, pt); err != nil {
157
+
slog.Warn("Failed to draw text", "text", text, "error", err)
158
+
}
159
}
160
161
// MeasureText returns the width of text in pixels
+7
-6
pkg/appview/ogcard/icons.go
+7
-6
pkg/appview/ogcard/icons.go
···
6
"image"
7
"image/color"
8
"image/draw"
9
"strings"
10
11
"github.com/srwiley/oksvg"
···
28
"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
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 {
33
path, ok := iconPaths[name]
34
if !ok {
35
-
return fmt.Errorf("unknown icon: %s", name)
36
}
37
38
// Build full SVG with color
···
45
// Parse SVG
46
icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg)))
47
if err != nil {
48
-
return fmt.Errorf("failed to parse icon SVG: %w", err)
49
}
50
51
// Create target image for the icon
···
63
// Draw icon onto card
64
rect := image.Rect(x, y, x+size, y+size)
65
draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over)
66
-
67
-
return nil
68
}
···
6
"image"
7
"image/color"
8
"image/draw"
9
+
"log/slog"
10
"strings"
11
12
"github.com/srwiley/oksvg"
···
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"/>`,
30
}
31
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) {
34
path, ok := iconPaths[name]
35
if !ok {
36
+
slog.Warn("Unknown icon", "name", name)
37
+
return
38
}
39
40
// Build full SVG with color
···
47
// Parse SVG
48
icon, err := oksvg.ReadIconStream(bytes.NewReader([]byte(svg)))
49
if err != nil {
50
+
slog.Warn("Failed to parse icon SVG", "name", name, "error", err)
51
+
return
52
}
53
54
// Create target image for the icon
···
66
// Draw icon onto card
67
rect := image.Rect(x, y, x+size, y+size)
68
draw.Draw(c.img, rect, iconImg, image.Point{}, draw.Over)
69
}
+2
pkg/appview/routes/routes.go
+2
pkg/appview/routes/routes.go
···
146
// Manifest health check API endpoint (HTMX polling)
147
router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{
148
HealthChecker: deps.HealthChecker,
149
}).ServeHTTP)
150
151
router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)(
···
196
197
r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{
198
Refresher: deps.Refresher,
199
}).ServeHTTP)
200
201
r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{
···
146
// Manifest health check API endpoint (HTMX polling)
147
router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{
148
HealthChecker: deps.HealthChecker,
149
+
Templates: deps.Templates,
150
}).ServeHTTP)
151
152
router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)(
···
197
198
r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{
199
Refresher: deps.Refresher,
200
+
Templates: deps.Templates,
201
}).ServeHTTP)
202
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
"install.html",
552
"manifest-modal",
553
"push-list.html",
554
+
"health-badge",
555
+
"alert",
556
}
557
558
for _, name := range expectedTemplates {
···
682
t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
683
}
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
+
}
774
}
775
}
776
+1
-1
pkg/hold/admin/auth.go
+1
-1
pkg/hold/admin/auth.go
···
87
}
88
89
// renderTemplate renders a template with the given data
90
-
func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) {
91
w.Header().Set("Content-Type", "text/html; charset=utf-8")
92
93
if err := ui.templates.ExecuteTemplate(w, name, data); err != nil {