diff --git a/.tangled/workflows/lint.yaml b/.tangled/workflows/lint.yaml index 24af4c9..4271cef 100644 --- a/.tangled/workflows/lint.yaml +++ b/.tangled/workflows/lint.yaml @@ -5,7 +5,7 @@ when: branch: ["main"] engine: kubernetes -image: golang:1.25-trixie +image: golang:1.26-trixie architecture: amd64 steps: diff --git a/.tangled/workflows/release-credential-helper.yml b/.tangled/workflows/release-credential-helper.yml index 456be2c..be2149d 100644 --- a/.tangled/workflows/release-credential-helper.yml +++ b/.tangled/workflows/release-credential-helper.yml @@ -12,7 +12,7 @@ when: tag: ["v*"] engine: kubernetes -image: golang:1.25-trixie +image: golang:1.26-trixie architecture: amd64 environment: diff --git a/.tangled/workflows/tests.yml b/.tangled/workflows/tests.yml index a17423f..18124af 100644 --- a/.tangled/workflows/tests.yml +++ b/.tangled/workflows/tests.yml @@ -5,7 +5,7 @@ when: branch: ["main"] engine: kubernetes -image: golang:1.25-trixie +image: golang:1.26-trixie architecture: amd64 steps: diff --git a/Dockerfile.appview b/Dockerfile.appview index abdf3b9..f437055 100644 --- a/Dockerfile.appview +++ b/Dockerfile.appview @@ -18,8 +18,13 @@ COPY . . RUN npm ci RUN go generate ./... +# Legal "Last updated" dates — pass from host (see Makefile docker-appview +# target). Empty falls back to the hardcoded default in legal.go. +ARG PRIVACY_DATE="" +ARG TERMS_DATE="" + RUN CGO_ENABLED=1 go build \ - -ldflags="-s -w -linkmode external -extldflags '-static'" \ + -ldflags="-s -w -linkmode external -extldflags '-static' -X 'atcr.io/pkg/appview/handlers.privacyLastUpdated=${PRIVACY_DATE}' -X 'atcr.io/pkg/appview/handlers.termsLastUpdated=${TERMS_DATE}'" \ -tags sqlite_omit_load_extension \ -trimpath \ -o atcr-appview ./cmd/appview diff --git a/Makefile b/Makefile index 86fd734..8ed691a 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,18 @@ $(GENERATED_ASSETS): build: build-appview build-hold build-credential-helper ## Build all binaries +# Legal page "Last updated" dates come from the git commit date of the page +# templates. Empty values (e.g., Docker builds without .git) fall back to the +# hardcoded default in legal.go. +LEGAL_PKG := atcr.io/pkg/appview/handlers +PRIVACY_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/privacy.html 2>/dev/null) +TERMS_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/terms.html 2>/dev/null) +APPVIEW_LDFLAGS := -X '$(LEGAL_PKG).privacyLastUpdated=$(PRIVACY_DATE)' -X '$(LEGAL_PKG).termsLastUpdated=$(TERMS_DATE)' + build-appview: $(GENERATED_ASSETS) ## Build appview binary only @echo "→ Building appview..." @mkdir -p bin - go build -o bin/atcr-appview ./cmd/appview + go build -ldflags="$(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview build-hold: $(GENERATED_ASSETS) ## Build hold binary only @echo "→ Building hold..." @@ -69,7 +77,19 @@ test-verbose: ## Run tests with verbose output .PHONY: check-golangci-lint check-golangci-lint: - @which golangci-lint > /dev/null || (echo "→ Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) + @LINT_PKG=github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \ + CUR_GO=$$(go version | grep -oE 'go[0-9]+\.[0-9]+' | head -1 | sed 's/^go//'); \ + if ! command -v golangci-lint > /dev/null 2>&1; then \ + echo "→ Installing golangci-lint..."; \ + go install $$LINT_PKG; \ + else \ + LINT_GO=$$(golangci-lint --version 2>&1 | grep -oE 'built with go[0-9]+\.[0-9]+' | head -1 | sed 's/^built with go//'); \ + if [ -n "$$LINT_GO" ] && [ "$$LINT_GO" != "$$CUR_GO" ] && \ + [ "$$(printf '%s\n%s\n' $$LINT_GO $$CUR_GO | sort -V | head -1)" = "$$LINT_GO" ]; then \ + echo "→ golangci-lint built with go$$LINT_GO but project targets go$$CUR_GO — reinstalling..."; \ + go install $$LINT_PKG; \ + fi; \ + fi lint: check-golangci-lint ## Run golangci-lint @echo "→ Running golangci-lint..." @@ -97,7 +117,10 @@ docker: docker-appview docker-hold docker-scanner ## Build all Docker images docker-appview: ## Build appview Docker image @echo "→ Building appview Docker image..." - docker build -f Dockerfile.appview -t atcr.io/atcr.io/appview:latest . + docker build -f Dockerfile.appview \ + --build-arg PRIVACY_DATE=$(PRIVACY_DATE) \ + --build-arg TERMS_DATE=$(TERMS_DATE) \ + -t atcr.io/atcr.io/appview:latest . docker-hold: ## Build hold Docker image @echo "→ Building hold Docker image..." diff --git a/config-appview.example.yaml b/config-appview.example.yaml index 6391938..115219e 100644 --- a/config-appview.example.yaml +++ b/config-appview.example.yaml @@ -52,6 +52,8 @@ ui: libsql_auth_token: "" # How often to sync with remote libSQL server. Default: 60s. libsql_sync_interval: 1m0s + # Source code URL displayed in the footer "Source" link. Defaults to the upstream ATCR project. + source_url: https://tangled.org/evan.jarrett.net/at-container-registry # Health check and cache settings. health: # How long to cache hold health check results. @@ -74,7 +76,6 @@ jetstream: relay_endpoints: - https://relay1.us-east.bsky.network - https://relay1.us-west.bsky.network - - https://relay.waow.tech # JWT authentication settings. auth: # RSA private key for signing registry JWTs issued to Docker clients. @@ -100,9 +101,9 @@ billing: # ISO 4217 currency code (e.g. "usd"). currency: usd # Redirect URL after successful checkout. Use {base_url} placeholder. - success_url: '{base_url}/settings#billing' + success_url: '{base_url}/settings/billing' # Redirect URL after cancelled checkout. Use {base_url} placeholder. - cancel_url: '{base_url}/settings#billing' + cancel_url: '{base_url}/settings/billing' # Subscription tiers ordered by rank (lowest to highest). tiers: - # Tier name. Position in list determines rank (0-based). diff --git a/docs/BILLING_REFACTOR.md b/docs/BILLING_REFACTOR.md index 6a26754..6adaff0 100644 --- a/docs/BILLING_REFACTOR.md +++ b/docs/BILLING_REFACTOR.md @@ -206,8 +206,8 @@ server: billing: enabled: true currency: usd - success_url: "{base_url}/settings#storage" - cancel_url: "{base_url}/settings#storage" + success_url: "{base_url}/settings/billing" + cancel_url: "{base_url}/settings/billing" tiers: - name: "Free" # No stripe_price = free tier diff --git a/pkg/appview/config.go b/pkg/appview/config.go index d52bf28..93c5be1 100644 --- a/pkg/appview/config.go +++ b/pkg/appview/config.go @@ -83,6 +83,9 @@ type UIConfig struct { // How often to sync with the remote libSQL server. LibsqlSyncInterval time.Duration `yaml:"libsql_sync_interval" comment:"How often to sync with remote libSQL server. Default: 60s."` + + // Source code URL displayed in the footer "Source" link. + SourceURL string `yaml:"source_url" comment:"Source code URL displayed in the footer \"Source\" link. Defaults to the upstream ATCR project."` } // HealthConfig defines health check and cache settings @@ -162,6 +165,7 @@ func setDefaults(v *viper.Viper) { v.SetDefault("ui.libsql_sync_url", "") v.SetDefault("ui.libsql_auth_token", "") v.SetDefault("ui.libsql_sync_interval", "60s") + v.SetDefault("ui.source_url", "https://tangled.org/evan.jarrett.net/at-container-registry") // Health defaults v.SetDefault("health.cache_ttl", "15m") @@ -216,8 +220,8 @@ func ExampleYAML() ([]byte, error) { // Populate example billing tiers so operators see the structure cfg.Billing.Currency = "usd" - cfg.Billing.SuccessURL = "{base_url}/settings#billing" - cfg.Billing.CancelURL = "{base_url}/settings#billing" + cfg.Billing.SuccessURL = "{base_url}/settings/billing" + cfg.Billing.CancelURL = "{base_url}/settings/billing" cfg.Billing.OwnerBadge = true cfg.Billing.Tiers = []billing.BillingTierConfig{ {Name: "deckhand", Description: "Get started with basic storage", MaxWebhooks: 1}, diff --git a/pkg/appview/handlers/base.go b/pkg/appview/handlers/base.go index 8df3b8f..6db53d4 100644 --- a/pkg/appview/handlers/base.go +++ b/pkg/appview/handlers/base.go @@ -46,4 +46,5 @@ type BaseUIHandler struct { ClientName string // Full name: "AT Container Registry" ClientShortName string // Short name: "ATCR" AIAdvisorEnabled bool // True when Claude API key is configured + SourceURL string // Source code URL for the footer "Source" link } diff --git a/pkg/appview/handlers/common.go b/pkg/appview/handlers/common.go index 18f2759..4a11b38 100644 --- a/pkg/appview/handlers/common.go +++ b/pkg/appview/handlers/common.go @@ -18,6 +18,8 @@ type PageData struct { ClientShortName string // Brand name for templates (e.g., "ATCR") OciClient string // Preferred OCI client for pull commands (e.g., "docker", "podman") AIAdvisorEnabled bool // True when AI Image Advisor is available + SourceURL string // Source code URL for the footer "Source" link + CurrentPath string // Request path (used for OAuth return_to) } // NewPageData creates a PageData struct with common fields populated from the request @@ -36,6 +38,8 @@ func NewPageData(r *http.Request, h *BaseUIHandler) PageData { ClientShortName: h.ClientShortName, OciClient: ociClient, AIAdvisorEnabled: h.AIAdvisorEnabled, + SourceURL: h.SourceURL, + CurrentPath: r.URL.RequestURI(), } } diff --git a/pkg/appview/handlers/device.go b/pkg/appview/handlers/device.go index e72e8b2..b4d372c 100644 --- a/pkg/appview/handlers/device.go +++ b/pkg/appview/handlers/device.go @@ -527,7 +527,7 @@ const deviceSuccessTemplate = `
Device {{.DeviceName}} has been successfully authorized.
You can now close this window and return to your terminal.
- +